Linux-性能优化-网络篇

查看网络基本信息

ip a
第一,网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
第二,MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。第三,网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
第四,网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。
其中:errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
collisions 表示碰撞数据包数。
ss -ntlp
ss -s 查看协议栈信息
sar -n DEV 1 查看网络统计信息
rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
%ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s
ethtool eth0 | grep Speed

压测

iperf 和 netperf 都是最常用的网络性能测试工具,测试 TCP 和 UDP 的吞吐量
yum install iperf3
目标机器上启动 iperf 服务端:
-s表示启动服务端,-i表示汇报间隔,-p表示监听端口
$ iperf3 -s -i 1 -p 10000
接着,在另一台机器上运行 iperf 客户端,运行测试:
-c表示启动客户端,192.168.0.30为目标服务器的IP# -b表示目标带宽(单位是bits/s)# -t表示测试时间# -P表示并发数,-p表示目标服务器监听端口
$ iperf3 -c 192.168.0.30 -b 1G -t 15 -P 2 -p 10000

为了得到应用程序的实际性能,就要求性能工具本身可以模拟用户的请求负载,而 iperf、ab 这类工具就无能为力了。幸运的是,我们还可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。以 wrk 为例,它是一个 HTTP 性能测试工具,内置了 LuaJIT,方便你根据实际需求,生成所需的请求负载,或者自定义响应的处理方法。
wrk -c 1000 -t 2 http://192.168.0.30/
wrk 最大的优势,是其内置的 LuaJIT,可以用来实现复杂场景的性能测试。wrk 在调用 Lua 脚本时,可以将 HTTP 请求分为三个阶段,即 setup、running、done
wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/

在应用层,你可以使用 wrk、Jmeter 等模拟用户的负载,测试应用程序的每秒请求数、处理延迟、错误数等;
而在传输层,则可以使用 iperf 等工具,测试 TCP 的吞吐情况;
再向下,你还可以用 Linux 内核自带的 pktgen ,测试服务器的 PPS。

内核参数优化

默认的半连接容量只有 256:
$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
另外,连接每个 SYN_RECV 时,如果失败的话,内核还会自动重试,并且默认的重试次数是 5 次。你可以执行下面的命令,将其减小为 1 次:
$ sysctl -w net.ipv4.tcp_synack_retries=1
TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法。SYN Cookies 基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为 cookie。然后,这个 cookie 就被用作序列号,来应答 SYN+ACK 包,并释放连接状态。当客户端发送完三次握手的最后一次 ACK 后,服务器就会再次计算这个哈希值,确认是上次返回的 SYN+ACK 的返回包,才会进入 TCP 的连接状态。因而,开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。
注意,开启 TCP syncookies 后,内核选项 net.ipv4.tcp_max_syn_backlog 也就无效了。
开启 TCP SYN Cookies:
$ sysctl -w net.ipv4.tcp_syncookies=1

40ms TCP 延迟确认(Delayed ACK)的最小超时时间。这是针对 TCP ACK 的一种优化机制,也就是说,不用每次请求都发送一个 ACK,而是先等一会儿(比如 40ms),看看有没有“顺风车”。如果这段时间内,正好有其他包需要发送,那就捎带着 ACK 一起发送过去。当然,如果一直等不到其他包,那就超时后单独发送 ACK。

Nagle 算法,是 TCP 协议中用于减少小包发送数量的一种优化算法,目的是为了提高实际带宽的利用率。
举个例子,当有效负载只有 1 字节时,再加上 TCP 头部和 IP 头部分别占用的 20 字节,整个网络包就是 41 字节,这样实际带宽的利用率只有 2.4%(1/41)。往大了说,如果整个网络带宽都被这种小包占满,那整个网络的有效利用率就太低了。Nagle 算法正是为了解决这个问题。它通过合并 TCP 小包,提高网络带宽的利用率。
Nagle 算法规定,一个 TCP 连接上,最多只能有一个未被确认的未完成分组;在收到这个分组的 ACK 前,不发送其他分组。这些小分组会被组合起来,并在收到 ACK 后,用同一个分组发送出去。显然,Nagle 算法本身的想法还是挺好的,但是知道 Linux 默认的延迟确认机制后,你应该就不这么想了。因为它们一起使用时,网络延迟会明显
当 Sever 发送了第一个分组后,由于 Client 开启了延迟确认,就需要等待 40ms 后才会回复 ACK。同时,由于 Server 端开启了 Nagle,而这时还没收到第一个分组的 ACK,Server 也会在这里一直等着。直到 40ms 超时后,Client 才会回复 ACK,然后,Server 才会继续发送第二个分组。

只有设置了 TCP_NODELAY 后,Nagle 算法才会禁用。nginx中开启tcp_nodelay

NAT
由于 NAT 基于 Linux 内核的连接跟踪机制来实现。所以,在分析 NAT 性能问题时,我们可以先从 conntrack 角度来分析,比如用 systemtap、perf 等,分析内核中 conntrack 的行文;然后,通过调整 netfilter 内核选项的参数,来进行优化。其实,Linux 这种通过连接跟踪机制实现的 NAT,也常被称为有状态的 NAT,而维护状态,也带来了很高的性能成本。所以,除了调整内核行为外,在不需要状态跟踪的场景下(比如只需要按预定的 IP 和端口进行映射,而不需要动态映射),我们也可以使用无状态的 NAT (比如用 tc 或基于 DPDK 开发),来进一步提升性能。
NAT优化
net.netfilter.nf_conntrack_count,表示当前连接跟踪数;
net.netfilter.nf_conntrack_max,表示最大连接跟踪数;
net.netfilter.nf_conntrack_buckets,表示连接跟踪表的大小
net.netfilter.nf_conntrack_tcp_timeout_time_wait TIME_WAIT超时时间,默认是120秒,可适当减小
nf_conntrack文档 https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt

小结:优化思路

应用程序角度
从网络 I/O 的角度来说,主要有下面两种优化思路。第一种是最常用的 I/O 多路复用技术 epoll,主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。第二种是使用异步 I/O(Asynchronous I/O,AIO)。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O 完成后,系统会用事件通知的方式,告诉应用程序结果。不过,AIO 的使用比较复杂,你需要小心处理很多边缘情况。而从进程的工作模型来说,也有两种不同的模型用来优化。第一种,主进程 + 多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。第二种,监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点。我总结了常见的几种优化方法。使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度。

套接字
读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。所以,为了提高网络的吞吐量,你通常需要调整这些缓冲区的大小。比如:增大每个套接字的缓冲区大小 net.core.optmem_max;增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max;增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem。
注意:tcp_rmem 和 tcp_wmem 的三个数值分别是 min,default,max,系统会根据这些设置,自动调整 TCP 接收 / 发送缓冲区的大小。udp_mem 的三个数值分别是 min,pressure,max,系统会根据这些设置,自动调整 UDP 发送缓冲区的大小。
为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;
为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);
使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。

传输层
第一类,在请求数比较大的场景下,你可能会看到大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施。增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。开启端口复用 net.ipv4.tcp_tw_reuse。这样,被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符数。
第二类,为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,你可以考虑优化与 SYN 状态相关的内核选项,比如采取下面几种措施。增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries。第三类,在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候你需要优化与 Keepalive 相关的内核选项,比如:缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time;缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl;减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。

网络层
第一种,从路由和转发的角度出发,你可以调整下面的内核选项。在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 1。调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。注意,增大该值会降低系统性能。开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。这样可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。
第二种,从分片的角度出发,最主要的是调整 MTU(Maximum Transmission Unit)的大小。通常,MTU 的大小应该根据以太网的标准来设置。以太网标准规定,一个网络帧最大为 1518B,那么去掉以太网头部的 18B 后,剩余的 1500 就是以太网 MTU 的大小。在使用 VXLAN、GRE 等叠加网络技术时,要注意,网络叠加会使原来的网络包变大,导致 MTU 也需要调整。比如,就以 VXLAN 为例,它在原来报文的基础上,增加了 14B 的以太网头部、 8B 的 VXLAN 头部、8B 的 UDP 头部以及 20B 的 IP 头部。换句话说,每个包比原来增大了 50B。所以,我们就需要把交换机、路由器等的 MTU,增大到 1550, 或者把 VXLAN 封包前(比如虚拟化环境中的虚拟网卡)的 MTU 减小为 1450。另外,现在很多网络设备都支持巨帧,如果是这种环境,你还可以把 MTU 调大为 9000,以提高网络吞吐量。
第三种,从 ICMP 的角度出发,为了避免 ICMP 主机探测、ICMP Flood 等各种网络问题,你可以通过内核选项,来限制 ICMP 的行为。比如,你可以禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1。这样,外部主机就无法通过 ICMP 来探测主机。或者,你还可以禁止广播 ICMP,即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。

链路层
由于网卡收包后调用的中断处理程序(特别是软中断),需要消耗大量的 CPU。所以,将这些中断处理程序调度到不同的 CPU 上执行,就可以显著提高网络吞吐量。这通常可以采用下面两种方法。比如,你可以为网卡硬中断配置 CPU 亲和性(smp_affinity),或者开启 irqbalance 服务。再如,你可以开启 RPS(Receive Packet Steering)和 RFS(Receive Flow Steering),将应用程序和软中断的处理,调度到相同 CPU 上,这样就可以增加 CPU 缓存命中率,减少网络延迟。
另外,现在的网卡都有很丰富的功能,原来在内核中通过软件处理的功能,可以卸载到网卡中,通过硬件来执行。TSO(TCP Segmentation Offload)和 UFO(UDP Fragmentation Offload):在 TCP/UDP 协议中直接发送大包;而 TCP 包的分段(按照 MSS 分段)和 UDP 的分片(按照 MTU 分片)功能,由网卡来完成 。GSO(Generic Segmentation Offload):在网卡不支持 TSO/UFO 时,将 TCP/UDP 包的分段,延迟到进入网卡前再执行。这样,不仅可以减少 CPU 的消耗,还可以在发生丢包时只重传分段后的包。LRO(Large Receive Offload):在接收 TCP 分段包时,由网卡将其组装合并后,再交给上层网络处理。不过要注意,在需要 IP 转发的情况下,不能开启 LRO,因为如果多个包的头部信息不一致,LRO 合并会导致网络包的校验错误。GRO(Generic Receive Offload):GRO 修复了 LRO 的缺陷,并且更为通用,同时支持 TCP 和 UDP。RSS(Receive Side Scaling):也称为多队列接收,它基于硬件的多个接收队列,来分配网络接收进程,这样可以让多个 CPU 来处理接收到的网络包。VXLAN 卸载:也就是让网卡来完成 VXLAN 的组包功能。
最后,对于网络接口本身,也有很多方法,可以优化网络的吞吐量。比如,你可以开启网络接口的多队列功能。这样,每个队列就可以用不同的中断号,调度到不同 CPU 上执行,从而提升网络的吞吐量。再如,你可以增大网络接口的缓冲区大小,以及队列长度等,提升网络传输的吞吐量(注意,这可能导致延迟增大)。你还可以使用 Traffic Control 工具,为不同网络流量配置 QoS。
单机并发 1000 万的场景中,对 Linux 网络协议栈进行的各种优化策略,基本都没有太大效果。因为这种情况下,网络协议栈的冗长流程,其实才是最主要的性能负担。这时,我们可以用两种方式来优化。第一种,使用 DPDK 技术,跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时,再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。第二种,使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理,这样也可以实现很好的性能。

在应用程序中,主要是优化 I/O 模型、工作模型以及应用层的网络协议;
在套接字层中,主要是优化套接字的缓冲区大小;
在传输层中,主要是优化 TCP 和 UDP 协议;
在网络层中,主要是优化路由、转发、分片以及 ICMP 协议;
最后,在链路层中,主要是优化网络包的收发、网络功能卸载以及网卡选项。
如果这些方法依然不能满足你的要求,那就可以考虑,使用 DPDK 等用户态方式,绕过内核协议栈;
或者,使用 XDP,在网络包进入内核协议栈前进行处理。

坚持原创技术分享,您的支持将鼓励我继续创作!