网络性能优化之套路篇

此篇文章为我个人学习笔记,出自倪朋飞老师的Linux性能优化实战,强烈建议购买:https://time.geekbang.org/column/intro/140

Linux 网络收发流程

网络包的接收流程

当一个网络帧到达网卡后,网卡会通过 DMA 方式,把这个网络包放到收包队列中;然后通过硬中断,告诉中断处理程序已经收到了网络包。

接着,网卡中断处理程序会为网络帧分配内核数据结构(sk_buff),并将其拷贝到 sk_buff 缓冲区中;然后再通过软中断,通知内核收到了新的网络帧。

接下来,内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧。

比如,

  • 在链路层检查报文的合法性,找出上层协议的类型(比如 IPv4 还是 IPv6),再去掉帧头、帧尾,然后交给网络层。
  • 网络层取出 IP 头,判断网络包下一步的走向,比如是交给上层处理还是转发。当网络层确认这个包是要发送到本机后,就会取出上层协议的类型(比如 TCP 还是 UDP),去掉 IP 头,再交给传输层处理。
  • 传输层取出 TCP 头或者 UDP 头后,根据 < 源 IP、源端口、目的 IP、目的端口 > 四元组作为标识,找出对应的 Socket,并把数据拷贝到 Socket 的接收缓存中。

最后,应用程序就可以使用 Socket 接口,读取到新接收到的数据了。

网络包的发送流程

了解网络包的接收流程后,就很容易理解网络包的发送流程。网络包的发送流程就是上图的右半部分,很容易发现,网络包的发送方向,正好跟接收方向相反。

首先,应用程序调用 Socket API(比如 sendmsg)发送网络包。

由于这是一个系统调用,所以会陷入到内核态的套接字层中。套接字层会把数据包放到 Socket 发送缓冲区中。

接下来,网络协议栈从 Socket 发送缓冲区中,取出数据包;再按照 TCP/IP 栈,从上到下逐层处理。比如,传输层和网络层,分别为其增加 TCP 头和 IP 头,执行路由查找确认下一跳的 IP,并按照 MTU 大小进行分片。

分片后的网络包,再送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后添加帧头和帧尾,放到发包队列中。这一切完成后,会有软中断通知驱动程序:发包队列中有新的网络帧需要发送。

最后,驱动程序通过 DMA ,从发包队列中读出网络帧,并通过物理网卡把它发送出去。

缓冲区

Linux 网络收发的流程涉及到了多个队列和缓冲区,包括:

  • 网卡收发网络包时,通过 DMA 方式交互的环形缓冲区
  • 网卡中断处理程序为网络帧分配的,内核数据结构 sk_buff 缓冲区
  • 应用程序通过套接字接口,与网络协议栈交互时的套接字缓冲区。

其中,环形缓冲区,由于需要 DMA 与网卡交互,理应属于网卡设备驱动的范围。

sk_buff 缓冲区,是一个维护网络帧结构的双向链表,链表中的每一个元素都是一个网络帧(Packet)。虽然 TCP/IP 协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制。

套接字缓冲区,则允许应用程序,给每个套接字配置不同大小的接收或发送缓冲区。应用程序发送数据,实际上就是将数据写入缓冲区;而接收数据,其实就是从缓冲区中读取。至于缓冲区中数据的进一步处理,则由传输层的 TCP 或 UDP 协议来完成。

整个数据包交互过程图如下:

img

网络性能指标以及测试方法

性能指标

通常用带宽、吞吐量、延时、PPS(Packet Per Second)等指标衡量网络的性能。

  • 带宽,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。
  • 吞吐量,表示没有丢包时的最大数据传输速率,单位通常为 b/s (比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽的限制,吞吐量 / 带宽也就是该网络链路的使用率。
  • 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。
  • PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。

除了这些指标,网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)等也是常用的性能指标。

怎么样评估的网络性能呢?不同的业务有关不同的测试方法,基于 HTTP 或者 HTTPS 的 Web 应用程序,评估的网络性能应该在应用层;而对大多数游戏服务器来说,为了支持更大的同时在线人数,通常会基于 TCP 或 UDP ,与客户端进行交互,这时就需要我们测试 TCP/UDP 的性能;还有一些场景,是把 Linux 作为一个软交换机或者路由器来用的。这种情况下,你更关注网络包的处理能力(即 PPS),重点关注网络层的转发性能。

各协议层的性能测试

转发性能

网络接口层和网络层,它们主要负责网络包的封装、寻址、路由以及发送和接收。在这两个网络协议层中,每秒可处理的网络包数 PPS,就是最重要的性能指标。特别是 64B 小包的处理能力,值得我们特别关注。可以使用hping3,或者使用Linux 内核自带的高性能网络测试工具 pktgen。pktgen 支持丰富的自定义选项,方便你根据实际需要构造所需网络包,从而更准确地测试出目标服务器的性能。

hping3的主要用法如下,模拟 DoS 攻击:

1
2
3
4
5
6
7
8
9
# -S 参数表示设置 TCP 协议的 SYN(同步序列号),-p 表示目的端口为 80
# -i u10 表示每隔 10 微秒发送一个网络帧
$ hping3 -S -p 80 -i u10 192.168.0.30

## 防止DDOS的方法之一:
# 限制 syn 并发数为每秒 1 次
$ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
# 限制单个 IP 在 60 秒新建立的连接数为 10
$ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT

pktgen的安装参考:https://blog.csdn.net/minxihou/article/details/84024314

TCP/UDP性能

iperf 和 netperf 都是最常用的网络性能测试工具,测试 TCP 和 UDP 的吞吐量。它们都以客户端和服务器通信的方式,测试一段时间内的平均吞吐量。下面使用iperf3来测试:

在server端运行:

1
2
3
4
5
# -s 表示启动服务端,-i 表示汇报间隔,-p 表示监听...
[root@server ~]# iperf3 -s -i 1 -p 10000
-----------------------------------------------------------
Server listening on 10000
-----------------------------------------------------------

在client运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -c 表示启动客户端,192.168.153.132 为目标服务器的 IP
# -b 表示目标带宽 (单位是 bits/s)
# -t 表示测试时间
# -P 表示并发数,-p 表示目标服务器监听端口
[root@bogon ~]# iperf3 -c 192.168.153.132 -p 10000 -t 20 -b 1G
Connecting to host 192.168.153.132, port 10000
[ 4] local 192.168.153.200 port 43220 connected to 192.168.153.132 port 10000
[ ID] Interval Transfer Bandwidth Retr Cwnd
[ 4] 0.00-1.00 sec 108 MBytes 904 Mbits/sec 0 525 KBytes
[ 4] 1.00-2.00 sec 125 MBytes 1.05 Gbits/sec 0 755 KBytes
....
[ 4] 19.00-20.00 sec 170 MBytes 1.43 Gbits/sec 0 1.02 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bandwidth Retr
[ 4] 0.00-20.00 sec 2.33 GBytes 999 Mbits/sec 2 sender
[ 4] 0.00-20.00 sec 2.33 GBytes 999 Mbits/sec receiver

iperf Done.

在client以及server都可以看到结果,在最后都输出了测试的汇总结果,包括测试时间、数据传输量以及带宽等。按照发送和接收,这一部分又分为了 sender 和 receiver 两行。可以看到吞吐量达到999M。

HTTP性能

要测试 HTTP 的性能,也有大量的工具可以使用,比如 ab、webbench 等,都是常用的 HTTP 压力测试工具。其中,ab 是 Apache 自带的 HTTP 压测工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。

先在server上运行docker run --name nginx -p 80:80 -itd nginx;然后在client上运行ab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -c 表示并发请求数为 5000,-n 表示总的请求数为 10 万
# -r 表示套接字接收错误时仍然继续执行,-s 表示设置每个请求的超时时间为 2s
[root@bogon ~]# ab -n 10000 -c 1000 http://192.168.153.132/

Benchmarking 192.168.153.132 (be patient)
Completed 1000 requests
。。。
Finished 10000 requests


Concurrency Level: 1000
Time taken for tests: 9.742 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 8450000 bytes
HTML transferred: 6120000 bytes
Requests per second: 1026.46 [#/sec] (mean)
Time per request: 974.223 [ms] (mean)
Time per request: 0.974 [ms] (mean, across all concurrent requests)
Transfer rate: 847.03 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 214 512.5 1 3010
Processing: 32 402 1035.5 92 7011
Waiting: 0 401 1035.6 91 7011
Total: 55 615 1281.3 98 9524

Percentage of the requests served within a certain time (ms)
50% 98
66% 280
75% 406
80% 1059
90% 1519
95% 2840
98% 6986
99% 7583
100% 9524 (longest request)

ab 的测试结果分为三个部分,分别是请求汇总、连接时间汇总还有请求延迟汇总。

先看请求汇总:

  • Requests per second表示每秒请求数,为1026个,即server每秒处理了1026个请求
  • Time per request有2列,表示每个请求的延迟,第一行表示平均延迟,包括了线程运行的调度时间和网络请求响应时间,而第二行则表示实际请求的响应时间
  • Transfer rate表示吞吐量

连接时间汇总部分,则是分别展示了建立连接、请求、等待以及汇总等的各类时间,包括最小、最大、平均以及中值处理时间。

最后的请求延迟汇总部分,则给出了不同时间段内处理请求的百分比,比如, 90% 的请求,都可以在 1519ms 内完成。

注:ab测试时,会出现报错apr_pollset_poll: The timeout specified has expired (70007),这是由于内核参数的问题,设置如下,即可规避:

1
2
3
sysctl -w net.ipv4.tcp_syncookies=0
sysctl -w net.ipv4.tcp_max_tw_buckets=20000
sysctl -w net.ipv4.tcp_max_syn_backlog=2048

应用负载性能

为了得到应用程序的实际性能,就要求性能工具本身可以模拟用户的请求负载,而 iperf、ab 这类工具就无能为力了。幸运的是,我们还可以用 wrk、TCPCopy、Jmeter 或者 LoadRunner 等实现这个目标。

wrk 为例,它是一个 HTTP 性能测试工具,内置了 LuaJIT,方便你根据实际需求,生成所需的请求负载,或者自定义响应的处理方法。安装方法如下:

1
2
3
4
5
wget https://github.com/wg/wrk/archive/master.zip
unzip master.zip
cd master
make
cp wrk /usr/local/sbin

wrk的参数也很少,-c 表示并发连接数,-t 表示线程数:

1
2
3
4
5
6
7
8
9
10
[root@server ~]# wrk -c 1000 -t 2 http://192.168.153.132/
Running 10s test @ http://192.168.153.132/
2 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 143.82ms 51.94ms 1.98s 93.60%
Req/Sec 3.47k 0.89k 4.96k 68.25%
67905 requests in 10.08s, 55.05MB read
Socket errors: connect 0, read 0, write 0, timeout 19
Requests/sec: 6737.07
Transfer/sec: 5.46MB

每秒请求数是6737,吞吐量为5.46M,这比ab测试出来的数据好太多了。wrk跟ab命令相比,其wrk是支持Lua脚本,扩展性更好。

另外,nginx配置里面有tcp_nopush、tcp_nodelay,一般是开启的,我们来测试下关闭这2个选项的情况下,性能相差:

1
2
3
4
docker run --name nginx -p 80:80 -itd nginx
docker cp nginx:/etc/nginx/nginx.conf .
vi nginx.conf #将tcp_nopush、tcp_nodelay都配置为off
docker run -itd --name nginx-delay -v /root/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 nginx

使用wrk来测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[root@bogon ~]# wrk -t 2 -c 100 --latency http://192.168.153.132/
Running 10s test @ http://192.168.153.132/
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 38.70ms 73.45ms 819.57ms 96.12%
Req/Sec 1.91k 476.68 4.03k 82.74%
Latency Distribution
50% 24.19ms
75% 25.84ms
90% 45.15ms
99% 463.76ms
37534 requests in 10.07s, 30.42MB read
Requests/sec: 3727.46
Transfer/sec: 3.02MB
[root@bogon ~]#
[root@bogon ~]#
[root@bogon ~]# wrk -t 2 -c 100 --latency http://192.168.153.132:8080/
Running 10s test @ http://192.168.153.132:8080/
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 57.90ms 49.48ms 445.88ms 93.14%
Req/Sec 1.03k 272.81 1.50k 71.28%
Latency Distribution
50% 42.96ms
75% 50.31ms
90% 84.55ms
99% 337.39ms
19735 requests in 10.09s, 16.02MB read
Requests/sec: 1955.37
Transfer/sec: 1.59MB

在关闭tcp_nopush、tcp_nodelay的情况下,每秒请求数为1955,而在开启的情况下,每秒请求数是3727,相差一倍左右。 这是因为设置了tcp_nodelay之后,Nagle算法就会禁用所导致的。注:Nagle 算法,是 TCP 协议中用于减少小包发送数量的一种优化算法,目的是为了提高实际带宽的利用率。

总结一下:网络接口层和网络层,性能指标是每秒可处理的网络包数 PPS,用内核自带的发包工具 pktgen ,来测试 PPS 的性能; TCP 和 UDP的性能指标是吞吐量(BPS)、连接数以及延迟,使用用 iperf 或 netperf 测试;应用层,最需要关注的是吞吐量(BPS)、每秒请求数以及延迟等指标。你可以用 wrk、ab 等工具,来测试应用程序的性能。

网络性能优化方法

确定优化目标

虽然网络性能优化的整体目标,是降低网络延迟(如 RTT)和提高吞吐量(如 BPS 和 PPS),但具体到不同应用中,每个指标的优化标准可能会不同,优先级顺序也大相径庭。NAT 网关通常需要达到或接近线性转发,也就是说, PPS 是最主要的性能目标。再如,对于数据库、缓存等系统,快速完成网络收发,即低延迟,是主要的性能目标。而对于我们经常访问的 Web 服务来说,则需要同时兼顾吞吐量和延迟。

所以,为了更客观合理地评估优化效果,我们首先应该明确优化的标准,即要对系统和应用程序进行基准测试,得到网络协议栈各层的基准性能。

网络性能优化

想要优化,先要获得网络基准测试报告,然后通过相关性能工具,定位出网络性能瓶颈。

接下来,我们就可以从应用程序、套接字、传输层、网络层以及链路层等几个角度,分别来看网络性能优化的基本思路。

应用程序

应用程序,通常通过套接字接口进行网络操作。由于网络收发通常比较耗时,所以应用程序的优化,主要就是对网络 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 选项,由内核负责,把请求负载均衡到这些监听进程中去。

除了网络 I/O 和进程的工作模型外,应用层的网络协议优化,也是至关重要的一点。我总结了常见的几种优化方法。

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

套接字

套接字可以屏蔽掉 Linux 内核中不同协议的差异,为应用程序提供统一的访问接口。每个套接字,都有一个读写缓冲区。

  • 读缓冲区,缓存了远端发过来的数据。如果读缓冲区已满,就不能再接收新的数据。
  • 写缓冲区,缓存了要发出去的数据。如果写缓冲区已满,应用程序的写操作就会被阻塞。

所以,为了提高网络的吞吐量,你通常需要调整这些缓冲区的大小。比如:

  • 增大每个套接字的缓冲区大小 net.core.optmem_max
  • 增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max
  • 增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem

除此之外,套接字接口还提供了一些配置选项,用来修改网络连接的行为:

  • 为 TCP 连接设置 TCP_NODELAY 后,就可以禁用 Nagle 算法;
  • 为 TCP 连接开启 TCP_CORK 后,可以让小包聚合成大包后再发送(注意会阻塞小包的发送);
  • 使用 SO_SNDBUF 和 SO_RCVBUF ,可以分别调整套接字发送缓冲区和接收缓冲区的大小。

传输层

TCP协议优化

TCP 提供了面向连接的可靠传输服务。要优化 TCP,我们首先要掌握 TCP 协议的基本原理,比如流量控制、慢启动、拥塞避免、延迟确认以及状态流图。

掌握这些原理后,你就可以在不破坏 TCP 正常工作的基础上,对它进行优化。下面,我分几类情况详细说明。

第一类,在请求数比较大的场景下,你可能会看到大量处于 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.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

UDP优化

UDP 提供了面向数据报的网络协议,它不需要网络连接,也不提供可靠性保障。所以,UDP 优化,相对于 TCP 来说,要简单得多。这里我也总结了常见的几种优化方案。

  • 跟上篇套接字部分提到的一样,增大套接字缓冲区大小以及 UDP 缓冲区范围;
  • 跟前面 TCP 部分提到的一样,增大本地端口号的范围;
  • 根据 MTU 大小,调整 UDP 数据包的大小,减少或者避免分片的发生。

网络层

网络层,负责网络包的封装、寻址和路由,包括 IP、ICMP 等常见协议。在网络层,最主要的优化,其实就是对路由、 IP 分片以及 ICMP 等进行调优。

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

  • 在需要转发的服务器中,比如用作 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。

第三种,从 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。

到这里,我就从应用程序、套接字、传输层、网络层,再到链路层,分别介绍了相应的网络性能优化方法。通过这些方法的优化后,网络性能就可以满足绝大部分场景了。

C10M优化方案

在单机并发 1000 万的场景中,对 Linux 网络协议栈进行的各种优化策略,基本都没有太大效果。因为这种情况下,网络协议栈的冗长流程,其实才是最主要的性能负担。

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

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

0%