「网络」- 网络性能优化思路

  CREATED BY JENKINSBOT

确定优化目标

网络性能优化的目标是什么?即观察到的网络性能指标,要达到多少才合适呢?整体目标是降低网络延迟(如 RTT)和提高吞吐量(如 BPS 和 PPS),但具体到不同应用中,每个指标的优化标准可能会不同,优先级顺序也大相径庭。因此网络性能优化的目标需要依应用程序而定。

第一步、我们首先应该明确优化的标准,即要针对特定系统和应用程序进行基准测试,得到网络协议栈各层的基准性能。

对于 NAT 网关,通常需要达到或接近线性转发,也就是说, PPS 是最主要的性能目标。

对于 数据库、缓存等等 系统,快速完成网络收发,即低延迟是主要的性能目标。

对于 Web 服务,需要同时兼顾吞吐量和延迟

第二步、在进行基准测试时,我们就可以按照协议栈的每一层来测试。底层性能决定高层性能(底层性能是极限性能),我们从下到上进行测试。

首先是网络接口层和网络层,每秒可处理的网络包数 PPS,就是它们最重要的性能指标(特别是在小包的情况下)。可以用内核自带的发包工具 pktgen 来测试 PPS 的性能。

再向上到传输层的 TCP 和 UDP,它们主要负责网络传输。对它们而言,吞吐量(BPS)、连接数、延迟,就是最重要的性能指标。可以用 iperf 或 netperf 来测试传输层的性能。不过要注意,网络包的大小会直接影响这些指标的值。所以需要测试一系列不同大小网络包的性能。

应用层,最需要关注的是吞吐量(BPS)、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。

注意,测试场景要尽量模拟生产环境,这样的测试才更有价值。比如,你可以到生产环境中,录制实际的请求情况,再到测试中回放。

网络性能工具

从网络性能指标出发

吞吐量(BPS):sar nethogs iftop
分别可以查看网络接口、进程以及IP地址的网络吞吐量

PPS:sar /proc/net/dev
查看网络接口的PPS

连接数:netstat ss
查看网络连接数

延迟:ping hping3
通过 ICMP、TCP 等测试网络延迟

连接跟踪数:conntrack
查看和管理连接跟踪状况

路由:mtr route traceroute
查看路由并测试链路信息

DNS:dig nslookup
排查DNS解析问题

防火墙和NAT:iptables
配置和管理防火墙及NAT规则

网卡功能:ethtool
查看和配置网络接口的功能

抓包:tcpdump Wireshark
抓包分析网络流量

内核协议栈跟踪:bcc systemt ap
动态跟踪内核协议栈的行为

从性能工具出发

ifconfg ip
配置和查看网络接口

ss
查看网络连接数

sar /proc/net/dev/sys/class/net/eth0/statistics/
查看网络接口的网络收发情况

nethogs
查看进程的网络收发情况

iftop
查看IP的网络收发情况

ethool
查看和配置网络接口

conntrack
查看和管理连接跟踪状况

nslookup dig
排查DNS解析问题

mtr route traceroute
查看路由并测试链路信息

ping hping3
测试网络延迟

tcpdump
网络抓包工具

Wireshark
网络抓包和图形界面分析工具

iptables
配置和管理防火墙及NAT规则

perf
剖析内核协议栈的性能

systemtap bcc
动态追踪内核协议栈的行为

网络性能优化

总的来说:1)先要获得网络基准测试报告,2)然后通过相关性能工具定位出网络性能瓶颈,3)再接下来的优化工作。

要优化网络性能,肯定离不开 Linux 系统的网络协议栈和网络收发流程的辅助,要熟记网络接收和发送流程。

应用程序

应用程序,通常通过套接字接口进行网络操作。由于网络收发通常比较耗时,所以应用程序的优化主要就是对 网络 I/O进程自身的工作模型 的优化。

从网络 I/O 的角度来说:
第一种)是最常用的 I/O 多路复用技术 epoll,主要用来取代 select 和 poll。这其实是解决 C10K 问题的关键,也是目前很多网络应用默认使用的机制。
第二种)是使用异步 I/O(Asynchronous I/O,AIO)。AIO 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。等到 I/O 完成后,系统会用事件通知的方式,告诉应用程序结果。不过,AIO 的使用比较复杂,你需要小心处理很多边缘情况。

从进程的工作模型来说:
第一种)主进程 + 多个 worker 子进程。其中,主进程负责管理网络连接,而子进程负责实际的业务处理。这也是最常用的一种模型。
第二种)监听到相同端口的多进程模型。在这种模型下,所有进程都会监听相同接口,并且开启 SO_REUSEPORT 选项,由内核负责,把请求负载均衡到这些监听进程中去。

应用层的网络协议优化:
1)使用长连接取代短连接,可以显著降低 TCP 建立连接的成本。在每秒请求次数较多时,这样做的效果非常明显。
2)使用内存等方式,来缓存不常变化的数据,可以降低网络 I/O 次数,同时加快应用程序的响应速度。
3)使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可以提高应用程序的吞吐。
4)使用 DNS 缓存、预取、HTTPDNS 等方式,减少 DNS 解析的延迟,也可以提升网络 I/O 的整体速度

套接字

套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口

每个套接字,都有一个读写缓冲区:
1)读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据
2)写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞

为了提高网络的吞吐量,你通常需要调整这些缓冲区的大小:
1)增大每个套接字的缓冲区大小 net.core.optmem_max;
2)增大套接字接收缓冲区大小(net.core.rmem_max)和发送缓冲区大小(net.core.wmem_max);
3)增大 TCP 接收缓冲区大小(net.ipv4.tcp_rmem)和发送缓冲区大小(net.ipv4.tcp_wmem);

套接字的内核选项,及参考设置(具体设置需要根据实际的网络状况来确定):
增大每个套接字的缓冲区大小 net .core.optmem_max 81920
增大套接字接收缓冲区大小 net.core.rmem_max 513920
增大套接字发送缓冲区大小 net.core.wmem_max 513920
增大TCP接收缓冲区范围 net.ipv4.tcp_rmem 4096 87380 16777216
增大TCP发送缓冲区范围 net.ipv4.tcp_wmem 4096 65536 16777216
增大UDP缓冲区范围 net.ipv4.udp_mem 188562 251418 377124

有几点需要注意:
1)tcp_rmem 和 tcp_wmem 的三个数值分别是 min,default,max,系统会根据这些设置,自动调整 TCP 接收 / 发送缓冲区的大小
2)udp_mem 的三个数值分别是 min,pressure,max,系统会根据这些设置,自动调整 UDP 发送缓冲区的大小

传输层(TCP/UDP)

首先来看 TCP 协议的优化,我们首先要掌握 TCP 协议的基本原理,比如流量控制、慢启动、拥塞避免、延迟确认以及状态流图(如下图所示)等:

优化 TCP 的几类情况:

第一类,在请求数比较大的场景下,你可能会看到大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施:

1)增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。

2)减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放它们所占用的资源。

3)开启端口复用 net.ipv4.tcp_tw_reuse。这样,被 TIME_WAIT 状态占用的端口,还能用到新建的连接中。

4)增大本地端口的范围 net.ipv4.ip_local_port_range 。这样就可以支持更多连接,提高整体的并发能力。

5)增加最大文件描述符的数量。你可以使用 fs.nr_open 和 fs.file-max ,分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中,配置 LimitNOFILE ,设置应用程序的最大文件描述符数。

第二类,为了缓解 SYN FLOOD 等,利用 TCP 协议特点进行攻击而引发的性能问题,你可以考虑优化与 SYN 状态相关的内核选项,比如采取下面几种措施

1)增大 TCP 半连接的最大数量 net.ipv4.tcp_max_syn_backlog ,或者开启 TCP SYN Cookies net.ipv4.tcp_syncookies ,来绕开半连接数量限制的问题(注意,这两个选项不可同时使用)。

2)减少 SYN_RECV 状态的连接重传 SYN+ACK 包的次数 net.ipv4.tcp_synack_retries。

第三类,在长连接的场景中,通常使用 Keepalive 来检测 TCP 连接的状态,以便对端连接断开后,可以自动回收。但是,系统默认的 Keepalive 探测间隔和重试次数,一般都无法满足应用程序的性能要求。所以,这时候你需要优化与 Keepalive 相关的内核选项,比如:

1)缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time;

2)缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl;

3)减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。

TCP 优化,及参考设置:
增大处于 TIME_WAIT 状态的连接数量 net.ipv4.top_max_tw_buckets 1048576
增大连接跟踪表的大小 net.netfilter.nf_conntrack.max 1048576
缩短处于 TIME_WAIT 状态的超时时间 net.ipv4.tcp_fin_timeout 15
缩短连接跟踪表中处于 TIME_ WAIT 状态连接的超时时间 net.netfilter.nf_conntrack_tcp_timeout_time_wait 30
允许TIME _WAIT状态占用的端口还可以用到新建的连接中 net.ipv4.tcp_tw_reuse 1
增大本地端口号的范围 net.ipv4.ip_local_port_range 10000 65000
增加系统和应用程序的最大文件描述符数 fs.nr_open (系统), systemd 配置文件中的 LimitNOFILE (应用程序) 1048576
增加半连接的最大数量 net.ipv4.tcp_max.syn_backlog 16384
开启 SYN Cookies 功能 net.ipv4.tcp_syncookies 1
缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl 30
减少 Keepalive 探测失败后通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes 3
缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time 600

注意不同优化方法带来的冲突:
1)服务器端开启 Nagle 算法,而客户端开启延迟确认机制,就很容易导致网络延迟增大。
2)在使用 NAT 的服务器上,如果开启 net.ipv4.tcp_tw_recycle ,就很容易导致各种连接失败。实际上,由于坑太多,这个选项在内核的 4.1 版本中已经删除了

再来看 UDP 的优化,UDP 提供面向数据报的网络协议,它不需要网络连接,也不提供可靠性保障。所以 UDP 优化,相对于 TCP 来说,要简单得多。

以下常见的几种优化方案:
1)跟上篇套接字部分提到的一样,增大套接字缓冲区大小以及 UDP 缓冲区范围;
2)跟前面 TCP 部分提到的一样,增大本地端口号的范围;
3)根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生。

网络层

在网络层,最主要的优化,其实就是对路由、IP 分片、ICMP 等进行调优。

第一种,从路由和转发的角度出发,你可以调整下面的内核选项:

1)在需要转发的服务器中,比如用作 NAT 网关的服务器或者使用 Docker 容器时,开启 IP 转发,即设置 net.ipv4.ip_forward = 1。

2)调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。注意,增大该值会降低系统性能。

3)开启数据包的反向地址校验,比如设置 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

链路层

链路层负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。自然,链路层的优化,也是围绕这些基本功能进行的。接下来,我们从不同的几个方面分别来看:

由于网卡收包后调用的中断处理程序(特别是软中断),需要消耗大量的 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。

网络协议栈的冗长流程

可以用两种方式来优化:

第一种,使用 DPDK 技术,跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时,再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。

第二种,使用内核自带的 XDP 技术,在网络包进入内核协议栈前,就对其进行处理,这样也可以实现很好的性能。

参考文献

43 | 套路篇:网络性能优化的几个思路(上)