Linux系统
Linux物理层
LSI Raid 阵列日常操作
MegaCLI基本使用指南
重要参数含义说明
Raid的增删改
Linux系统层
Linux 系统启动过程流程
timedatectl命令时间时区操作
sar命令用法
Linux 性能调优工具9张图
Linux 特殊权限说明
Linux系统三级等保整改脚本
CentOS 7 停止维护(EOL)后的仓库变动
Linux查看主板内存槽与内存信息
安装麒麟Kylin-v10 Arm64版本到阿里云
CentOS7 多网卡单网关利用策略路由实现源进源出
初始化Linux数据盘(parted)
解决CentOS7下yum命令的异常
EXSI虚机mount出现‘unknown filesystem type 'LVM2_member'’
Linux虚机网卡单队列导致压测CPU无法满载的问题
Linux网络性能优化建议
Linux 修改系统语言环境
LInux文件系统中的默认保留空间 Ext4 vs. XFS
Linux CPU占用率原理与精确度分析
中标麒麟安装Nvidia显卡驱动
Linux主机双网卡同网段同网关配置
Linux 服务层
编译Expat 2.6.2的rpm包并升级
Linux主机挂载共享samba出现普通用户没有写权限的问题
编译OpenSSH 9.3p1的rpm包并升级
CentOS 7.x通过rpm升级OpenSSH到 8.5p1版本
Linux日志切割Logrotate原理和配置详解
systemd下配置sshd监听端口
编译NTP 4.2.8p17的rpm包并升级
编译OpenSSL 1.1.1w的rpm包并升级
linux命令集
磁盘工具集
Linux du 命令
fpsync数据迁移工具
字符处理集
Linux sed 命令
Linux命令输出重定向到变量
使用 paste 合并文件内容
常用调试指令集
编译cmake 3.5.2版本
网络工具集
MTR探测主机间丢包
Linux性能测试
甲骨文主机测试
本文档使用 MrDoc 发布
-
+
home page
Linux网络性能优化建议
每一种性能优化方法都有它适用或者不适用的应用场景。你应当根据你当前的项目现状灵活来选择用或者不用。 # 1、网络请求优化 ## 减少不必要的网络 IO 当发送一个网络包,首先得从用户态切换到内核态,花费一次系统调用的开销。进入到内核以后,又得经过冗长的协议栈,这会花费不少的 CPU周期,最后进入环回设备的“驱动程序”。接收端在软中断中花费不少的 CPU 周期后又需经过接收协议栈的处理,最后唤醒或者通知用户进程来处理。当服务端处理完以后,还需重复上述流程来返回结果,最后进程才能收到结果。另外多进程协作来完成一项工作时会引入更多的进程上下文切 换,这些开销从开发视角来看,做的其实都是无用功。 上述分析的只是本机网络 IO,如果是跨机器的还需双方网卡的 DMA 拷贝过程,以及两端之间的网络RTT 耗时延迟 ## 尽量合并网络请求 尽可能地把多次的网络请求合并到一次,这样既节约了双端的 CPU 开销,也能降低多次 RTT 导致的耗时。 举例说明: 如有一个 redis,里面存了每一个 App 的信息(应用名、包名、版本、截图等)。 现需根据用户安装应用列表来查询数据库中有哪些应用比用户的版本更新,如果有则提醒用户更新。 错误代码示例: ```php <?php for(安装列表 as 包名){ redis>get(包名) ... } ``` 上面这段代码功能上实现上没问题,问题在于性能。现代用户平均安装 App 的数量在 60 个左右。 这段代码在运行的时候,每当用户来请求一次,服务器就需要和 redis 之间进行 60 次网络请求。总耗时最少是 60个 RTT 起。 更好的方法是应该使用 redis 中提供的批量获取命令,如 hmget、pipeline等,经过一次网络 IO 就获取到所有想要的数据,  图1、网络请求合并 ## 主机通信路由尽可能短 在握手一切正常的情况下, TCP 握手的时间基本取决于两台机器之间的 RTT 耗时。虽然无法彻底去掉这个耗时,但却可降低 RTT,把客户端和服务器放的足够地近一些。尽量把每个机房内部的数据请求都在本地机房解决,减少跨地网络传输。 ## 内网调用用内网域名 为什么要这么做,原因有以下几点 - 1)外网接口慢。 - 2)带宽成本高。 - 3)NAT单点瓶颈。 # 2、接收过程优化 ## 调整网卡 RingBuffer 大小 当网线中的数据帧到达网卡后,第一站就是 RingBuffer。网卡在 RingBuffer 中寻找可用的内存位置,找到后 DMA引擎会把数据 DMA 到 RingBuffer 内存里。因此第一个要监控和调优的就是网卡的 RingBuffer,可使用ethtool 来查看 Ringbuffer 的大小。 ```bash > ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 512 RX Mini: 0 RX Jumbo: 0 TX: 512 ``` 这里的网卡设置 RingBuffer 最大允许设置到 4096 ,目前的实际设置是 512。 这里有一个小细节,ethtool 查看到的是实际是 Rx bd 的大小。Rx bd 位于网卡中,相当于一个指针。 RingBuffer 在内存中,Rx bd 指向 RingBuffer。Rx bd 和 RingBuffer 中的元素是一一对应的关系。在网卡启动的时候,内核会为网卡的 Rx bd 在内存中分配RingBuffer,并设置好对应关系。 在 Linux 的整个网络栈中,RingBuffer 起到一个任务的收发中转站的角色。对于接收过程来讲,网卡负责往RingBuffer 中写入收到的数据帧,ksoftirqd 内核线程负责从中取走处理。只要 ksoftirqd 线程工作的足够快,RingBuffer 这个中转站就不会出现问题。但假如某一时刻,瞬间来了特别多的包,而 ksoftirqd处理不过来了,这时 RingBuffer 可能瞬间就被填满了,后面再来的包网卡直接就会丢弃,不做任何处理!  图2 RingBuffer 溢出 查看服务器是否因这个原因导致丢包 ``` [root@localhost ~]# ifconfig eth0 eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.1.1 netmask 255.255.255.0 broadcast 192.168.1.255 ether fa:16:00:00:13:28 txqueuelen 1000 (Ethernet) RX packets 141295598 bytes 368745747933 (343.4 GiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 114344384 bytes 346048160127 (322.2 GiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 [root@localhost ~]# ``` 在 ifconfig 中如果 overruns 指标增长,表示有包因 RingBuffer 装不下而被丢弃。 加大 RingBuffer 可解决问题 ``` ethtool -G eth1 rx 4096 tx 4096 ``` 这样网卡会被分配更大一点的“中转站”,可以解决偶发的瞬时的丢包。 >引发的问题: 排队的包过多会增加处理网络包的延时。所以应该让内核处理网络包的速度更快一些,而不是让网络包在RingBuffer 中排队。  RSS 可以让更多的核来参与网络包接收。 ## 多队列网卡 RSS 调优 硬中断的情况可以通过内核提供的伪文件 /proc/interrupts 来进行查看。  截图包含非常丰富的信息。 - 网卡的输入队列 virtio1-input.0 的中断号是 27 - 总的中断次数是 1109986815 - 27 号中断都是由 CPU3 来处理的。 输入队列的中断都在 CPU3 上是因为内核的一个中断亲和性配置。 ``` > cat /proc/irq/27/smp_affinity 8 ``` smp_affinity 里是CPU的亲和性的绑定,8 是二进制的 1000, 第4位为 1。代表的就是当前的第 27 号中断的都由第 4 个 CPU 核心 - CPU3 来处理。 现在的主流网卡基本上都是支持多队列的。通过 ethtool 工具可以查看网卡的队列情况。 ```bash > ethtool -l eth0 Channel parameters for eth0: Pre-set maximums: RX: 0 TX: 0 Other: 1 Combined: 63 Current hardware settings: RX: 0 TX: 0 Other: 1 Combined: 8 ``` 上述结果表示当前网卡支持的最大队列数是 63 ,当前开启的队列数是 8 。这样当有数据到达的时候,可以将接收进来的包分散到多个队列里。另外每一个队列都有自己的中断号。  截图显示virtio 这块虚拟网卡上有四个输入队列,其硬中断号分别是 27、29、31 和 33。有独立的中断号就可以独立向某个 CPU 核心发起硬中断请求,让对应 CPU 来 poll 包。 中断和 CPU 的对应关系是通过 `cat/proc/irq/{中断号}/smp_affinity` 来查看。通过将不同队列的 CPU 亲和性打散到多个 CPU 核上,就可以让多核同时并行处理接收到的包了。这个特性叫做 RSS(Receive Side Scaling,接收端扩展),如图所示。这是加快 Linux内核处理网络包的速度非常有用的一个优化手段。  在网卡支持多队列的服务器上,想提高内核收包的能力,直接简单加大队列数就可以了,这比加大 RingBuffer 更为有用。 加大 RingBuffer 只是给个更大的空间让网络帧能继续排队,而加大队列数则能让包更早地被内核处理。 ethtool 修改队列数量方法如下: ``` ethtool -L eth0 combined 32 ``` 不过在一般情况下,队列中断号和 CPU 之间的亲和性并不需要手工维护,有一个irqbalance的服务来自动管理。通过 ps 命令可以查看到这个进程。 ```bash > ps -ef | grep irqb root 29805 1 0 18:57 ? 00:00:00 /usr/sbin/irqbalance foreground ``` Irqbalance 会根据系统中断负载的情况,自动维护和迁移各个中断的 CPU 亲和性,以保持各个 CPU 之间的中断开销均衡。如果有必要,irqbalance 也会自动把中断从一个 CPU 迁移到另一个 CPU 上。如果确实想自己维护亲和性,需先关掉 irqbalance,然后再修改中断号对应的 smp_affinity。 ```bash > service irqbalance stop > echo 2 > /proc/irq/30/smp_affinity ``` ## 硬中断合并 当网络包接收到 RingBuffer 后,接下来通过硬中断通知 CPU。那么你觉得从整体效率上来讲,是有包到达就发起中断好呢,还是攒一些数据包再通知 CPU 更好。 对于CPU来讲也是一样,CPU要做一件新的事情之前,要加载该进程的地址空间,load进程代码,读取进程数据,各级别 cache 要慢慢热身。因此如果能适当降低中断的频率,多攒几个包一起发出中断,对提升 CPU 的整体工作效率是有帮助的。所以,网卡允许我们对硬中断进行合并。 现在我们来看一下网卡的硬中断合并配置。 # ethtool c eth0 Coalesce parameters for eth0: Adaptive RX: off TX: off ...... rxusecs: 1 rxframes: 0 rxusecsirq: 0 rxframesirq: 0 ...... 我们来说一下上述结果的大致含义 Adaptive RX::自适应中断合并,网卡驱动自己判断啥时候该合并啥时候不合并 rx-usecs:当过这么长时间过后,一个 RX interrupt 就会被产生 rx-frames:当累计接收到这么多个帧后,一个 RX interrupt 就会被产生 如果你想好了修改其中的某一个参数了的话,直接使用 ethtool -C 就可以,例如: # ethtool C eth0 adaptiverx on 不过需要注意的是,减少中断数量虽然能使得 Linux 整体网络包吞吐更高,不过一些包的延迟也会增大,所以用的时候得适当注意。 建议4:软中断 budget 调整 再举个日常工作相关的例子,不知道你有没有听说过番茄工作法这种高效工作方法。它的大致意思就是你在工作的时候,要有一整段的不被打扰的时间,集中精力处理某一项工作。这一整段时间时长被建议是 25 分钟。对于我们的Linux的处理软中断的 ksoftirqd 来说,它也和番茄工作法思路类似。一旦它被硬中断触发开始了工作,它会集中精力处理一波儿网络包(绝不只是1个),然后再去做别的事情。 我们说的处理一波儿是多少呢,策略略复杂。我们只说其中一个比较容易理解的,那就是net.core.netdev_budget 内核参数。 # sysctl a | grep net.core.netdev_budget = 300 这个的意思说的是,ksoftirqd 一次最多处理300个包,处理够了就会把 CPU 主动让出来,以便 Linux 上其它的任务可以得到处理。那么假如说,我们现在就是想提高内核处理网络包的效率。那就可以让 ksoftirqd 进程多干一会儿网络包的接收,再让出 CPU。至于怎么提高,直接修改这个参数的值就好了。 #sysctl w net.core.netdev_budget=600 如果要保证重启仍然生效,需要将这个配置写到/etc/sysctl.conf 建议5:接收处理合并 硬中断合并是指的攒一堆数据包后再通知一次 CPU,不过数据包仍然是分开的。Lro(Large Receive Offload) /Gro(Generic Receive Offload) 还能把数据包合并起来后再往上层传递。 如果应用中是大文件的传输,大部分包都是一段数据,不用 LRO / GRO 的话,会每次都将一个小包传送到协议栈(IP接收函数、TCP接收)函数中进行处理。开启了的话,内核或者网卡会进行包的合并,之后将一个大包传给协议处理函数,如图 2.4。这样 CPU 的效率也就提高了。  图2.4 接收处理合并 Lro 和 Gro 的区别是合并包的位置不同。Lro 是在网卡上就把合并的事情给做了,因此要求网卡硬件必须支持才行。而 Gso 是在内核源码中用软件的方式实现的,更加通用,不依赖硬件。 那么如何查看你的系统内是否打开了 LRO / GRO 呢? # ethtool k eth0 genericreceiveoffload: on largereceiveoffload: on ... 如果你的网卡驱动没有打开 GRO 的话,可以通过如下方式打开。 # ethtool K eth0 gro on # ethtool K eth0 lro on 3、发送过程优化 建议1:控制数据包大小 在第四章中我们看到,在发送协议栈执行的过程中到了 IP 层如果要发送的数据大于 MTU 的话,会被分片。这个分片会有哪些影响呢?首先就是在分片的过程中我们看到多了一次的内存拷贝。其次就是分片越多,在网络传输的过程中出现丢包的风险也越大。当丢包重传出现的时候,重传定时器的工作时间单位是秒,也就是说最快 1 秒以后才能开始重传。所以,如果在你的应用程序里可能的话,可以尝试将数据大小控制在一个 MTU 内部来极致地提高性能。我所知道的是在早期的 QQ 后台服务中应用过这个技巧,不知道现在还有没有在用。 建议2:减少内存拷贝 假如你要发送一个文件给另外一台机器上,那么比较基础的做法是先调用 read 把文件读出来,再调用 send 把数据把数据发出去。这样数据需要频繁地在内核态内存和用户态内存之间拷贝,如图 3.1。  图3.1 read + write 发送文件 目前减少内存拷贝主要有两种方法,分别是使用 mmap 和 sendfile 两个系统调用。使用 mmap 系统调用的话,映射进来的这段地址空间的内存在用户态和内核态都是可以使用的。如果你发送数据是发的是 mmap 映射进来的数据,则内核直接就可以从地址空间中读取,如图 3.2,这样就节约了一次从内核态到用户态的拷贝过程。  图3.2 mmap + write 发送文件 不过在 mmap 发送文件的方式里,系统调用的开销并没有减少,还是发生两次内核态和用户态的上下文切换。如果你只是想把一个文件发送出去,而不关心它的内容,则可以调用另外一个做的更极致的系统调用 - sendfile。在这个系统调用里,彻底把读文件和发送文件给合并起来了,系统调用的开销又省了一次。再配合绝大多数网卡都支持的"分散-收集"(Scatter-gather)DMA 功能。可以直接从 PageCache 缓存区中 DMA 拷贝到网卡中,如图3.3。这样绝大部分的 CPU 拷贝操作就都省去了。  图3.3 sendfile 发送文件 建议3:发送处理合并 在建议 1 中我们说到过发送过程在 IP 层如果要发送的数据大于 MTU 的话,会被分片。但其实是有一个例外情况,那就是开启了 TSO(TCP Segmentation Offload)/ GSO(Generic Segmentation Offload)。我们来回顾和跟进 一下发送过程中的相关源码: //file: net/ipv4/ip_output.c static int ip_finish_output(struct sk_buff *skb) { ...... //大于 mtu 的话就要进行分片了 if (skb>len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb)) return ip_fragment(skb, ip_finish_output2); else return ip_finish_output2(skb); } ip_finish_output 是协议层中的函数。skb_is_gso 判断是否使用 gso,如果使用了的话,就可以把分片过程推迟到更下面的设备层去做。 //file: net/core/dev.c int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq) { ...... if (netif_needs_gso(skb, features)) { if (unlikely(dev_gso_segment(skb, features))) goto out_kfree_skb; if (skb>next) goto gso; } } dev_hard_start_xmit 位于设备层,和物理网卡离得更近了。netif_needs_gso 来判断是否需要进行 GSO 切分。在这个函数里会判断网卡硬件是不是支持 TSO,如果支持则不进行 GSO 切分,将大包直接传给网卡驱动,切分工作推迟到网卡硬件中去做。如果硬件不支持,则调用 dev_gso_segment 开始切分。 推迟分片的好处是可以省去大量包的协议头的计算工作量,减轻 CPU 的负担。  图3.4 发送处理合并 使用 ethtool 工具可以查看当前 tso 和 gso 的开启状况。 # ethtool k eth0 tcpsegmentationoffload: on txtcpsegmentation: on txtcpecnsegmentation: off [fixed] txtcp6segmentation: on udpfragmentationoffload: off [fixed] genericsegmentationoffload: off 如果没有开启,可以使用 ethtool 打开。 # ethtool K eth0 tso on # ethtool K eth0 gso on 建议4:多队列网卡 XPS 调优 在第四章的发送过程中 4.4.5 小节,我们看到在 __netdev_pick_tx 函数中,要选择一个发送队列出来。如果存在XPS (Transmit Packet Steering)配置,就以 XPS 配置为准。过程是根据当前 CPU 的 id 号去到 XPS 中查看是要用哪个发送队列,来看下源码。 //file: net/core/flow_dissector.c static inline int get_xps_queue(struct net_device *dev, struct sk_buff *skb) { //获取 xps 配置 dev_maps = rcu_dereference(dev>xps_maps); if (dev_maps) { map = rcu_dereference(map = rcu_dereference( //raw_smp_processor_id() 是获取当前 cpu id dev_maps>cpu_map[raw_smp_processor_id()]); if (map) { if (map>len == 1) queue_index = map>queues[0]; ... } 源码中 raw_smp_processor_id 是在获取当前执行的 CPU id。用该 CPU 号查看对应的 CPU 核是否有配置。XPS配置在 /sys/class/net//queues/tx-/xps_cpus 这个伪文件里。例如对于我手头的一台服务器来说,配置是这样的。 # cat /sys/class/net/eth0/queues/tx0/xps_cpus 00000001 # cat /sys/class/net/eth0/queues/tx1/xps_cpus 00000002 # cat /sys/class/net/eth0/queues/tx2/xps_cpus 00000004 # cat /sys/class/net/eth0/queues/tx3/xps_cpus 00000008 ...... 上述结果中 xps_cpus 是一个 CPU 掩码,表示当前队列对应的 CPU 号。从上面输出看对于 eth0 网卡 下的 tx-0 队列来说,是和 CPU0 绑定的。00000001 表示 CPU0,00000002 表示 CPU1,...,以此类推。假如当前 CPU 核是CPU0,那么找到的队列就是 eth0 网卡 下的 tx-0。  图3.5 多队列网卡发送 那么通过 XPS 指定了当前 CPU 要使用的发送队列有什么好处呢。好处大致是有两个: 第一,因为更少的 CPU 争用同一个队列,所以设备队列锁上的冲突大大减少。如果进一步配置成每个 CPU都有自己独立的队列用,则会完全消除队列锁的开销。 第二,CPU 和发送队列一对一绑定以后能提高传输结构的局部性,从而进一步提升效率。 关于 RSS、RPS、RFS、aRFS、XPS 等网络包收发过程中的优化手段可用参考源码中 Documentation/networking/scaling.txt 这个文档。里面有关于这些技术的详细官方说明。 建议5:使用 eBPF 绕开协议栈的本机 IO 如果你的业务中涉及到大量的本机网络 IO 可以考虑这个优化方案。 在第 5 章中我们看到,本机网络 IO 和跨机 IO 比较起来,确实是节约了驱动上的一些开销。发送数据不需要进RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。但是在内核其它组件上,可是一点都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走 了一个遍。连“驱动”程序都走了(虽然对于回环设备来说这个驱动只是一个纯软件的虚拟出来的东东)。 如果想用本机网络 IO,但是又不想频繁地在协议栈中绕来绕去。那么你可以试试 eBPF。使用 eBPF 的 sockmap和 sk redirect 可以绕过 TCP/IP 协议栈,而被直接发送给接收端的 socket,业界已经有公司在这么做了。 4、内核与进程协作优化 建议1:尽量少用 recvfrom 等进程阻塞的方式 在 3.3 节我们看到,在使用了 recvfrom 阻塞方式来接收 socket 上数据的时候。每次一个进程专⻔为了等一个socket 上的数据就得被从 CPU 上拿下来。然后再换上另一个 进程。等到数据 ready 了,睡眠的进程又会被唤醒。总共两次进程上下文切换开销。如果我们服务器上需要有大量的用户请求需要处理,那就需要有很多的进程存在,而且不停地切换来切换去。这样的缺点有如下这么几个: 因为每个进程只能同时等待一条连接,所以需要大量的进程。 进程之间互相切换的时候需要消耗很多 CPU 周期,一次切换大约是 3 - 5 us 左右。 频繁的切换导致 L1、L2、L3 等高速缓存的效果大打折扣 大家可能以为这种网络 IO 模型很少见了。但其实在很多传统的客户端 SDK 中,比如 mysql、redis 和 kafka 仍然是沿用了这种方式。 建议2:使用成熟的网络库 使用 epoll 可以高效地管理海量的 socket。在服务器端。我们有各种成熟的网络库进行使用。这些网络库都对epoll 使用了不同程度的封装。 首先第一个要给大家参考的是 Redis。老版本的 Redis 里单进程高效地使用 epoll 就能支持每秒数万 QPS 的高性能。如果你的服务是单进程的,可以参考 Redis 在网络 IO 这块的源码。 如果是多线程的,线程之间的分工有很多种模式。那么哪个线程负责等待读 IO 事件,那个线程负责处理用户请求,哪个线程又负责给用户写返回。根据分工的不同,又衍生出单 Reactor、多 Reactor、以及 Proactor 等多种模式。大家也不必头疼,只要理解了这些原理之后选择一个性能不错的网络库就可以了。比如 PHP 中的 Swoole、Golang 的 net 包、Java 中的 netty 、C++ 中的 Sogou Workflow 都封装的非常的不错。 建议3:使用 Kernel-ByPass 新技术 如果你的服务对网络要求确实特别特特别的高,而且各种优化措施也都用过了,那么现在还有终极优化大招 --Kernel-ByPass 技术。在本书我们看到了内核在接收网络包的时候要经过很⻓的收发路径。在这期间牵涉到很多内核组件之间的协同、协议栈的处理、以及内核态和用户态的拷贝和切换。Kernel-ByPass 这类的技术方案就是绕开内核协议栈,自己在用户态来实现网络包的收发。这样不但避开了繁杂的内核协议栈处理,也减少了频繁了内核态用户态之间的拷贝和切换,性能将发挥到极致! 目前我所知道的方案有 SOLARFLARE 的软硬件方案、DPDK 等等。如果大家感兴趣,可以多去了解一下! 5、握手挥手过程优化 建议1:配置充足的端口范围 客户端在调用 connect 系统调用发起连接的时候,需要先选择一个可用的端口。内核在选用端口的时候,是采用从可用端口范围中某一个随机位置开始遍历的方式。如果端口不充足的话,内核可能需要循环撞很多次才能选上一个可用的。这也会导致花费更多的 CPU 周期在内部的哈希表查找以及可能的自旋锁等待上。因此不要等到端口用尽报错了才开始加大端口范围,而且应该一开始的时候就保持一个比较充足的值。 # vi /etc/sysctl.conf net.ipv4.ip_local_port_range = 5000 65000 # sysctl p //使配置生效 如果端口加大了仍然不够用,那么可以考虑开启端口 reuse 和 recycle。这样端口在连接断开的时候就不需要等待2MSL 的时间了,可以快速回收。开启这个参数之前需要保证 tcp_timestamps 是开启的。 # vi /etc/sysctl.conf net.ipv4.tcp_timestamps = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tw_recycle = 1 # sysctl p 建议2:客户端最好不要使用 bind 如果不是业务有要求,建议客户端不要使用 bind。因为我们在 6.3 节看到过,connect 系统调用在选择端口的时候,即使一个端口已经被用过了,只要和已经有的连接四元组不完全一致,那这个端口仍然可以被用于建立新连接。但是 bind 函数会破坏 connect 的这段端口选择逻辑,直接绑定一个端口,而且一个端口只能被绑定一次。如果使用了 bind,则一个端口只能用于发起一条连接上。总体上来看,你的机器的最大并发连接数就真的受限于65535 了。 建议3:小心连接队列溢出 服务器端使用了两个连接队列来响应来自客户端的握手请求。这两个队列的长度是在服务器 listen 的时候就确定好了的。如果发生溢出,很可能会丢包。所以如果你的业务使用的是短连接且流量比较大,那么一定得学会观察这两个队列是否存在溢出的情况。因为一旦出现因为连接队列导致的握手问题,那么 TCP 连接耗时都是秒级以上了。 对于半连接队列, 有个简单的办法。那就是只要保证 tcp_syncookies 这个内核参数是 1 就能保证不会有因为半连接队列满而发生的丢包。对于全连接队列来说,可以通过 netstat -s 来观察。netstat -s 可查看到当前系统全连接队列满导致的丢包统计。但该数字记录的是总丢包数,所以你需要再借助 watch 命令动态监控。 # watch 'netstat -s | grep overflowed' 160 times the listen queue of a socket overflowed //全连接队列满导致的丢包 如果输出的数字在你监控的过程中变了,那说明当前服务器有因为全连接队列满而产生的丢包。你就需要加大你的全连接队列的⻓度了。全连接队列是应用程序调用 listen时传入的 backlog 以及内核参数 net.core.somaxconn 二者之中较小的那个。如果需要加大,可能两个参数都需要改。如果你手头并没有服务器的权限,只是发现自己的客户端机连接某个 server 出现耗时长,想定位一下是否是因为握手队列的问题。那也有间接的办法,可以 tcpdump 抓包查看是否有 SYN 的 TCP Retransmission。如果有偶发的 TCP Retransmission, 那就说明对应的服务端连接队列可能有问题了。 建议4:减少握手重试 在 6.5 节我们看到如果握手发生异常,客户端或者服务端就会启动超时重传机制。这个超时重试的时间间隔是翻倍地增长的,1 秒、3 秒、7 秒、15 秒、31 秒、63 秒 ......。对于我们提供给用户直接访问的接口来说,重试第一次耗时 1 秒多已经是严重影响用户体验了。如果重试到第三次以后,很有可能某一个环节已经报错返回 504 了。所以在这种应用场景下,维护这么多的超时次数其实没有任何意义。倒不如把他们设置的小一些,尽早放弃。其中客户端的 syn 重传次数由 tcp_syn_retries 控制,服务器半连接队列中的超时次数是由 tcp_synack_retries 来控制。把它们两个调成你想要的值。 建议5:打开 TFO( TCP Fast Open) 我们第 6 章的时候没有介绍一个细节,那就是 fastopen 功能。在客户端和服务器端都支持该功能的前提下,客户端的第三次握手 ack 包就可以携带要发送给服务器的数据。这样就会节约一个 RTT 的时间开销。如果支持,可以尝试启用。 # vi /etc/sysctl.conf net.ipv4.tcp_fastopen = 3 //服务器和客户端两种角色都启用 # sysctl -p 建议6:保持充足的文件描述符上限 在 Linux 下一切皆是文件,包括我们网络连接中的 socket。如果你的服务进程需要支持海量的并发连接。那么调整和加大文件描述符上限是很关键的。否则你的线上服务将会收到 “Too many open files”这个错误。 相关的限制机制请参考 8.2 节,这里我们给出一套推荐的修改方法。例如你的服务需要在单进程支持 100 W 条并发,那么建议: # vi /etc/sysctl.conf fs.filemax=1100000 //系统级别设置成 110 W,多留点 buffer。 fs.nr_open=1100000 //进程级别也设置成 110 W,因为要保证比 hard nofile 大 # sysctl -p # vi /etc/security/limits.conf //用户进程级别都设置成 100W * soft nofile 1000000 * hard nofile 1000000 建议7:如果请求频繁,请弃用短连接改用长连接 如果你的服务器频繁请求某个 server,比如 redis 缓存。和建议 1 比起来,一个更好一点的方法是使用长连接。这样的好处有 1)节约了握手开销。短连接中每次请求都需要服务和缓存之间进行握手,这样每次都得让用户多等一个握手的时间开销。 2)规避了队列满的问题。前面我们看到当全连接或者半连接队列溢出的时候,服务器直接丢包。而客户端呢并不知情,所以傻傻地等 3 秒才会重试。要知道 tcp 本身并不是专门为互联网服务设计的。这个 3 秒的超时对于互联网用户的体验影响是致命的。 3)端口数不容易出问题。端连接中,在释放连接的时候,客户端使用的端口需要进入 TIME_WAIT 状态,等待 2MSL的时间才能释放。所以如果连接频繁,端口数量很容易不够用。而长连接就固定使用那么几十上百个端口就够用了。 建议8:TIME_WAIT 的优化 很多线上服务如果使用了短连接的情况下,就会出现大量的 TIME_WAIT。 首先,我想说的是没有必要见到两三万个 TIME_WAIT 就恐慌的不行。从内存的⻆度来考虑,一条 TIME_WAIT 状态的连接仅仅是 0.5 KB 的内存而已。从端口占用的角度来说,确实是消耗掉了一个端口。但假如你下次再连接的是不同的 Server 的话,该端口仍然可以使用。只有在所有 TIME_WAIT 都聚集在和一个 Server 的连接上的时候才会有问题。 那怎么解决呢? 其实办法有很多。第一个办法是按上面建议 1 中的开启端口 reuse 和 recycle。第二个办法是限制TIME_WAIT 状态的连接的最大数量。 # vi /etc/sysctl.conf net.ipv4.tcp_max_tw_buckets = 32768 # sysctl -p 如果再彻底一些,也可以干脆采用建议 7 ,直接用⻓连接代替频繁的短连接。连接频率大大降低以后,自然也就没有 TIME_WAIT 的问题了。
Nathan
June 27, 2023, 4:22 p.m.
转发文档
Collection documents
Last
Next
手机扫码
Copy link
手机扫一扫转发分享
Copy link
Markdown文件
PDF文件
Docx文件
share
link
type
password
Update password