2026-08-25 23:03:00
问题是这样,我们有 2 组服务器,A 组和 B 组,一部分用户从 A 组下载文件,一部分从 B 组下载文件。分成 2 组的初衷是,B 组用户下载量通常比较大,并且是离线服务,意味着可以慢一些,但不能影响 A 组用户。现在的问题是,A 组 和 B 组最终下载的来源都是 S3,依然存在共享资源,会出现问题:在下载量比较大的时候,占用全部的 S3 上传带宽,导致 A 组用户收到影响。
我们希望限制 B 到 S3 的带宽,比如 10Gbps.
有几个限制:
这个问题的难点在于:流量整形一般都是做在 egress 的,因为 egress 天然有一个 queue,发送端可以决定在什么时候发出去 packet,从而把发送速率可以稳定控制在一个想要的比例。
但是我们这里的场景是 ingress,对于 ingress 是没有 queue 的,流量整形一般也无法作用在 ingress ——因为包已经被物理网卡收到,DMA 完成,作为 ingress 侧是无法决定这个包什么时候到达的,只能决定怎么处理这个包。所以 ingress 侧一般是 classification, action,即 drop。
基于 Drop 的 policing(真形象的名字,超速开罚单的警察) 就有一个问题:由于 TCP 的拥塞控制机制,发送的带宽会逐渐上涨,最终会超过 10Gb。限流开始生效时,sender 瞬时发送速率超过 10 Gbit/s,并持续消耗 policer 的 token;当 token 不足时,后续 packet 被判定为 exceed 并丢弃。
大量包被丢弃导致 TCP 的发送端认为发生了拥塞,会大幅下降 cwnd。实际测试,如果用 policing,实际能跑的速度大概在 5-7Gb,而且不稳定。
以下是一个模拟环境,使用 policing 限速在 1Gbps,其 throughput 如下图所示:
再看 window scaling,可以看到 outstanding bytes 的上包络线呈现出明显的 AIMD/CUBIC loss-response 特征;结合 sender 侧的 ss -ti 可以确认 cwnd 在 loss 后也发生了类似下降——在上升到一定的大小之后,就会因为超过 policing 的限速而被 drop 一些,TCP 在发现有丢包之后,就降低 cwnd,从而降低发送速度,符合 AIMD。(这个环境用的拥塞算法是 cubic,默认的参数 cat /sys/module/tcp_cubic/parameters/beta 是 717,下降的比例是 717/1024 ≈ 0.7,和图中相符,图中是从 500kb 下降到 350kb)。
这样用户就不高兴了:为什么实际只能用到 5Gb 左右,不是说好了 10Gb 的吗?
如果要做到规整的 10Gb,这里就需要用到流量整形了,traffic shaping。
Linux 有一个特殊的驱动:ifb,全称是 Intermediate Functional Block,是一个虚拟网络设备,主要用来把原本的 ingress 流量重定向到一个“可做 egress qdisc”的设备上,从而实现 ingress shaping。
本质上,就是在收包链路上加一个网卡绕一下,这个网卡会调用 egress 的代码,这样我们就可以使用 tc 原来的逻辑做 egress 的 traffic shaping 了。
加上 ifb 虚拟网卡之后,包的路线如下:

在 ifb_xmit() 的时候,看起来是一个负责发送包的代码,实际上它的逻辑是:修改 skb 的 dev 为原来真实的进来的那个 dev1,(而不是 ifb 这个虚拟的),然后进入到 netif_receive_skb() kernel 网络栈里面。
这样,我们就可以用这个 egress 的 qdisc 做 shaping 了。
实现的代码如下:
第一步,创建 ifb 虚拟接口,并且把流量从物理网卡 redirect 到 ifb:
modprobe ifb
ip link add ifb0 type ifb
ip link set ifb0 up
tc qdisc add dev bond0 handle ffff: ingress
tc filter add dev bond0 parent ffff: protocol ip u32 \
match u32 0 0 \
action mirred egress redirect dev ifb0第二步,创建一个 HTB,有 2 个 class,一个有限速 10Gb,另一个没有,可以跑到物理线速。
tc qdisc add dev ifb0 root handle 1: htb default 20
tc class add dev ifb0 parent 1: classid 1:1 \
htb rate 50gbit ceil 50gbit
tc class add dev ifb0 parent 1:1 classid 1:10 \
htb rate 10gbit ceil 10gbit
tc class add dev ifb0 parent 1:1 classid 1:20 \
htb rate 50gbit ceil 50gbit第三步,把要限速的 IP 放到 1:10 class:
tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 \
match ip src $ip/32 \
flowid 1:10然后测试吞吐,证明实际的 throughput 稳定在 956 Mbits/sec。


这样就达到了我们的目的:这条链路的速度稳定地使用限速 1Gbps 左右。
有几个小问题值得讨论。
为什么是 950 Mbits/sec 而不是 1Gbits/sec?
tc 做限制的时候,计算的是 skb 的 size,1500 Bytes,而这张图(以及使用的 iperf3 的显示)是计算的 TCP 的 body,即 MSS,1448 Bytes。1000 × 1448 / 1500 ≈ 965 Mbit/s。
在 cwnd 的图里面,刚开始的时候有一些丢包,为什么?
TCP 启动时通常先经过 slow start,cwnd 快速增长;进入 congestion avoidance 后,CUBIC 按三次函数继续探测可用带宽。发生 loss 后,它记录此前的窗口位置并降低 cwnd,随后重新逼近旧的 Wmax。
在这个图里面,只有一开始的时候有 loss,后续几乎没有 loss,贴着 throughput 发,说明 traffic shaping 做的不错。
按照 TCP 的拥塞控制算法,AIMD,即使是 traffic shaping 不也应该在最大窗口附近震荡吗?为什么这个图看起来这么平滑?
一个原因是,在逐渐逼近 1Gbps 的时候,limit 已经不是 cwnd 了,这种情况下 cubic 不会再激进地增加 cwnd2。通过 ss -ti 可以看到,大部分的时间卡在 sndbuf_limited:4688ms(29.6%)。
ESTAB 0 3646016 [::ffff:10.10.10.2]:5201 [::ffff:10.10.10.1]:59444
cubic wscale:13,13 rto:228 rtt:24.801/0.155 ato:40 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:2918 ssthresh:1717 bytes_sent:1893410080 bytes_acked:1889764624 byt es_received:37 segs_out:1307809 segs_in:652610 data_segs_out:1307808 data_segs_in:1 send 1.36Gbps lastrcv:15848 pacing_rate 1.64Gbps delivery_rate 961Mbps delivered:1305291 app_limited busy:15848ms rwnd_limited:12ms(0.1%) sndbuf_limited:4688ms(29.6%) unacked:2518 rcv_space:14600 rcv_ssthresh:42230 notsent:560 minrtt:3.024如果调整一下参数,扩大 wmem,cwnd 实际也会震荡的。
最后一个问题也是最关键的问题,为什么在 traffic shaping 之后,cwnd 的变化速度变得这么平滑了呢?都是一样的 cubic 算法,为什么不像 policing 那样剧烈下降?以及为什么这里 send buffer 出现了瓶颈?
使用 policing 时,包进入 ingress 只有 2 中 action:
使用 ifb 的 traffic shaping 之后,ingress 的包进入物理网卡,被 mirror 到 ifb,然后在这里经过 queue 排队。经过流量整形之后,包到达 kernel 的网络栈,然后发出 ack。这里的关键是,tc 添加了一个漏斗,大量的包(当然,buffer 需要内存的,内存是有限的,如果大到一定的程度就只能 drop 了)进来之后都存起来,然后用固定的流速放出去。只有被放出去的包才进入网络栈,只有进入了网络栈,kernel 才会给 sender 发回去 ack,此时,sender 的 cwnd 才有了新的空闲,新的包得以发送。
再换一个角度解释一下,从 sender 的角度,假设 cwnd 是 4,sender 发送了 4 个包,在收到 ACK 之前,无法再发送新的包了。这时候,receiver 的 shaper 每秒通过一个包,即每秒会发回来一个 ack,这样,后续的每一秒,比如5,6,7,8,sender 都可以再发送一个包到网络上。
send buffer 是另一层限制。TCP 已经发送但尚未被 ACK 的数据仍然需要保留在 send buffer 中,以便发生丢包时重传。因此 IFB shaping 不仅延迟 ACK、限制 cwnd 中空间的释放,也会延迟 TCP send buffer 中已发送数据的释放。当 send buffer 不够大时,sender 甚至可能在 cwnd 尚未用满之前就受到 send buffer 限制。
所以 IFB 以固定速率释放数据后,ACK 也以相应的节奏返回。ACK 一方面释放 cwnd 中的 outstanding 空间,另一方面使已经确认的数据可以从 send buffer 中清除。两者共同使 sender 的发送节奏逐渐跟随 shaper 的速率。
即,网络上最多可以飞多少,是 cwnd/rwnd 等窗口决定;什么时候可以继续注入新 packet,则由 ACK clock 驱动。(而 ACK 也会让总的 cwnd 提高)。
以上就叫做 TCP 的 self-clocking3,ACK 本身就像“时钟脉冲”,会决定发送速度。
这个机制有一个关键就是包没有被丢弃,只是 ack 的时间晚了一些,如果包被丢了(比如 policing),那么 cwnd 会下降,无法匀速发送。
︎
if (!tcp_is_cwnd_limited(sk)) return; https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_cubic.c?utm_source=chatgpt.com#L326
︎
︎
2026-08-19 22:27:00
ICMP 一直是我最喜欢的协议,在没有机器/设备的登录权限的时候,ICMP (ping) 可以让我在 debug 的时候获得很多关键的信息:网络通不通,延迟多少,有没有环路,经过了哪些设备,等等。其中比较关键的机制,一个是 ping 工具,即 echo/reply 机制;另一个是在 TTL 耗尽的时候发回来 ICMP 错误信息1,这是 traceroute 工具实现的基础原理。
在网上经常有人贴出来一些奇奇怪怪的 traceroute 结果2,有人找到很长的 traceroute 会很兴奋。所以…… 我想我们能不能用系统的方法找到 traceroute 比较长的 IP 呢?
虽然这个问题没有什么实际的意义,但是本文在探索这个问题的时候会遇到一些常见的技术,还是挺有意思的。
实验 traceroute 的起点,我们设置在新加坡。
新加坡是一个重要的交通枢纽。这是在写这篇文章的时候,新加坡附近起降的飞机(PS,新加坡的樟宜机场是我最喜欢的机场,如果你路过新加坡的话,建议给樟宜机场预留多 2 个小时的时间逛一逛)。

也是一个重要的海上枢纽。

在网络方面,新加坡也有丰富的海底光缆。
所以,预期最长的 hop 不会太高,现在世界的互联网越来越扁平了,一般一个互联网 IP 可以在 10-20 个 hop 触达。我用了一台 DigitalOcean 的机器来做这个实验,用它来 traceroute 1.1.1.1 只需要 8 跳,延迟在 1ms 左右。

回到本文的问题:找到一个 traceroute 最长的 IP,直观的方法就是 traceroute 每一个互联网的 IP。(本文只讨论 IPv4)
traceroute 对每一跳发 3 个包,超时时间 5s,最长 hop 尝试 30 跳。假设平均 15 跳完成,15 跳 × 3 探针 × ~100ms RTT = 5s,如果 IP ping 不通,那么 traceroute 可能要花 2min 以上。即使按照 5s 来算,我们 traceroute 整个互联网也需要:4,294,967,296 × 5 秒 = 21,474,836,480 秒 ≈ 680 年。如果开 1000 个并发,也需要 680 年 ÷ 1000 = 0.68 年 ≈ 248 天,大约8个月的时间。
怎样加快呢?
先来看一下现在的时间花在哪里了。traceroute 的逻辑是:对于目标 IP,发送 TTL=1 的包,等待回复,然后再重复 2 次;接下来换 TTL=2 的包,发送,等待回复,重复 3 次……
这样有两个问题:
对于本文的这个项目,我们是想找出来 traceroute 最长的一个 IP,而并不关心具体的路径。
要求高性能,我们换一个方式:不再使用发送——等待,我们设计两个程序,一个给所有的 IP 一起发送包,另一个监听收到的回应,如果是 ICMP 的 reply 包,就记录此 IP 可以 ping 通,如果收到其他的 ICMP 类型的包,直接丢弃即可。这样就完全没有等待时间了,而且之后两个程序,上下文切换的问题也解决了。
但是这就有了一个新的问题:我们怎么知道收到的包对应的 TTL 是多少呢?仅通过收到的 ICMP Time Exceeded 包是无法知道 TTL 消耗了多少的,怎么找到最长的 TTL 呢?
为了区分出来 TTL,我们分多轮进行扫描,先对所有的 IP 发送 TTL=1 的包,接收程序如果收到了 ICMP reply 的回应,说明这些 IP 在 TTL=1 的时候就能 ping 通,由于我们要找的是 TTL 越长越好,所以这些 IP可以直接淘汰了。接下来我们把 TTL=1 不能 ping 通的包,用 TTL=2 再发送一轮,如果能收到 ICMP reply,那么也可以淘汰了…… 假设我们在 TTL=30 的时候有一些 IP 能 ping 通,但是在 TTL=31 以及之后的时候没有任何 IP 可以 ping 通,那就说明这些 TTL=30 的 IP 就是胜者。
这样需要多久呢?
DigitalOcean 页面解释:All other Droplets have a maximum network throughput limit of 2 Gbps3. 每一个 ICMP 包的大小是:Ethernet 14 + IP 20 + ICMP 8 + payload 32 = 74 bytes,所以,理论上我们可以跑到:
2 Gbps ÷ (74 bytes × 8 bits) = 2,000,000,000 ÷ 592 ≈ 3,378,378 pps ≈ 3.4M pps
Ping 一次整个互联网只需要:
4,294,967,296 (2^32,所有的 IPv4 数量) ÷ 3,400,000 ≈ 1263 秒 ≈ 21 分钟
但是为了避免 overload 接受端(大量网段在同一个区域),以及中间设备可能存在的 conntrack,我们把速度限制在 100K pps,这样,只用了全速的 2.7%,ping 一次需要的时间是:
4,294,967,296 ÷ 100,000 ≈ 42,950 秒 ≈ 11.9 小时
对于我们的场景来说,也足够了。
使用我们自己的方式来发送 ping 包并且能跳过不需要的 conntrack 功能,就需要使用 kernel bypass 技术:
在收包程序上,直接看这个 IP 是不是一个合法的 ICMP reply,如果是,就记录 ping 通,如果不是就放通或丢弃。
但是 XDP 是运行在 kernel 的程序,如何把 ICMP reply 里面的 IP 信息记录到文件中呢?
Ring buffer4 在网络领域是一个非常常用的数据结构,它本质上是一个 buffer,生产方可以往里面写,消费方从里面读,是两个指针。它天然适合网络的原因是,buffer 的 head 和 tail 是相接的,自然而然就可以实现「如果生产方生产的速度太快,丢弃(覆盖)最早到达并且还没有处理的包」。(对于 BPF ring buffer,如果用户态消费得不够快、buffer 没有剩余空间,新的记录会写入失败。)
使用 BPF ring buffer (BPF_MAP_TYPE_RINGBUF),作为 kernel space 和 user space 的桥梁——kernel 往这个 ring buffer 里面不断写入可以 ping 通的 IP,用户态读出来这个 IP 记录到文件中。
接下来我们看用户态的程序。怎么存储这些 IP 呢?
我们可以使用一个 txt 文件不断 append IP,但是这样查找起来的话就是 O(n) 了。(不过我们只需要最后的几个赢家,大部分情况不需要查找,所以……还好啦)。另一个方案是使用一个数据库,比如 sqlite,查找快,不过大量写入的时候就有瓶颈。
看起来用最简单的文本好一些。
如果按照一个 IP 一行的格式,xxx.xxx.xxx.xxx\n,一共是 16 bytes,16 bytes * 2^32 就是 64GiB (最坏的情况)。
ping 一次就需要 64GiB!这也太多了,作为一个 hobby project,我们的目标是使用一个 $5 的 DigitalOcean VPS 来完成这个任务,磁盘只有 25GiB,远远不够。
有哪些字符可以简化呢?乍一看,首先是每一个 IP 的一个点,以及,IP 的每一个段实际有效数据是 0-255,但是可以表示的容量是 0-999,所以有很大一部分空间浪费了。
诶等等,IP 不就是 4 个 bytes 吗?这样的话我们可以把 IP 转化成一个 32位的 int:
>>> import ipaddress; int(ipaddress.IPv4Address('192.168.0.1'))
3232235521这样的话每 4 bytes 是一个 IP 总共是 16GiB,缩小了 4 倍!而且,4 bytes 里面每一个组合都是一个合法的 IP ,完全没有空间的浪费。
等等,「每一个组合都是一个合法的 IP」——那是不是就意味着,我们从 0 数到 2^32,每一个位置都有一个 IP?这样的话,可以让每一个位置的 bit 位,如果是 1,表示这个 IP 能够 ping 通,如果是 0,表示 ping 不通(默认,文件初始化为 0)。而这个 bit 所在的 index(比如,是文件的第 3232235521 比特位,就表示 192.168.0.1 这个 IP),就表示 IP。
恭喜我们自己!我们刚刚发明了 bitmap!
但是每次 seek 过去,把要写入的 bit 组合成一个 byte,调用 syscall write,这也太麻烦了!
mmap 可以帮我们把文件映射到内存地址空间,我们只要操作内存就可以了,操作系统会在后台帮我们同步——定期把内存的修改写入到文件中。这可太方便了!
这样,我们 ping 一轮,就有一个 512 MiB(Exactly 512 MiB!) 的文件,里面存储了这一轮 ping 通的 IP。然后我们拿 bit 是 0 的 index 作为 IP,做下一轮的 ping,能通的 IP 会越来越少,直到得到一个冠军。
其实互联网大部分 IP 是 ping 不通的,尤其是我们刚开始使用比较小的 TTL。所以我们的文件的大部分内容都是 0,只有一小部分是 1,而且 1 的部分通常是连续的。因为 IP 是按连续的段分配给不通的组织,一般来说,一个段要么都可以通,要么都不通。
那么我们可以省略中间的 0 的部分吗?这样的话可以节省一大部分磁盘。
答案是可以!这叫做 Sparse file,或者叫 file hole。比如我可以创建一个 10PB 的超大文件:
$ truncate -s 10240T super.big.file
$ ls -hl super.big.file
-rw-r--r-- 1 xintao.lai staff 10P Aug 19 18:17 super.big.file然后实际占用的空间,使用 du 查看,是 0:
$ du -sh super.big.file
0 super.big.file使用非常简单,seek 到 EOF 以后的位置,随后再 write,中间没有写过的区域形成 hole (注意,不是 write 0)。
我们可以先用 ftruncate 调用,创建一个 sparse file,然后用 mmap 只修改需要写为 1 的部分,这样,其实只有 1 占用空间了。
实际运行还有很多问题,比如 IP 不稳定,有时候回复 ping 有时候不回复有时候回复;有些 IP 使用另一个 IP 回复 ping;有些 IP 不减 TTL 导致产生无限环路,等等。
最后我找到 34 跳的就放弃了:

代码放在这里了:https://github.com/laixintao/traceroute-the-world 有兴趣的读者可以自己跑跑看。
代码是 AI 写的,但是这篇博客是我自己纯手写的。这年头能读的博客不多了,但是可以放心的是,这个博客不会有大批量 AI 生成的文字。
︎
︎
︎
︎
2026-07-10 12:34:30
by Kahlil Gibran
Your children are not your children.
They are the sons and daughters of Life's longing for itself.
They come through you but not from you,
And though they are with you, yet they belong not to you.
You may give them your love but not your thoughts.
For they have their own thoughts.
You may house their bodies but not their souls,
For their souls dwell in the house of tomorrow, which you cannot visit, not even in your dreams.
You may strive to be like them, but seek not to make them like you.
For life goes not backward nor tarries with yesterday.
You are the bows from which your children as living arrows are sent forth.
The archer sees the mark upon the path of the infinite,
and He bends you with His might that His arrows may go swift and far.
Let your bending in the archer's hand be for gladness;
For even as he loves the arrow that flies, so He loves also the bow that is stable.
2026-06-29 22:55:30
时间过得真快,Rick and Morty 已经播出 13 年了。
在试播集的时候,Rick 说的这句话——「你、你的人生还长着呢,而且你的肛门现在还又紧又有弹性」——我就不理解,为什么肛门不会紧?
“Y-y-you’ve got your whole life ahead of you, and your anal cavity is still taught, yet malleable”
但 13 年之后,我理解了 Rick……
由于肛门不再紧致的问题,所以我上厕所不会带手机玩。厕所里有一块太太买的硅藻泥脚垫(非常好用),上面有一圈比较「正能量」的文字,如下:
由于已经使用半年了,看着比较脏。读者可能也不会有我这么多的时间去转着圈读。所以我把这段话抄下来:
Life comes in a package.
This package includes happiness and sorrow,
failure and success,
hope and despair.
Life is a learning process.
Experiences in life teach us new lessons and make us a better person.
With each passing day we learn to handle various situations.
平平无奇的一段话。但是我读过一百遍了,在某一天突然发现,这说的真有道理。
—— Life comes in a package.
如果没有失败,成功就没有意义。正是因为有失败的几率,成功才变得值得庆祝。
如果没有悲伤,幸福也容易被忽视。
读过一些讲如何处理情绪的书,其中共同提到的一个技巧是,不要与情绪对抗,要观察情绪,就如同对待天气,认识到情绪的变化,就如同天气会变化,让情绪过去。
但很难做到,不知道如何操作。
我海藻泥开悟之后,一旦想,Life comes in a package, 就非常容易理解情绪的变化了。那些焦虑,是因为关爱和担心;伤心,也是因为爱。
生活是一个过程,而不是一个结果。当人老了,回顾自己的一生的时候,看到的不会是最后到达的点,而是曾经走过的所有的路。
这样一想,路上无论遇到什么,都不会是遗憾了,无论遇到什么,都可以勇敢地去面对。
2026-06-10 19:52:00
这篇文章写一下服务器的网络方面的调优经验。如果网络的带宽使用在 1GE,其实没有必要做调优,默认的参数基本没有问题;如果有 10G 以上的网络带宽要求,就需要做一些优化了,比如三层网关,四层负载均衡,七层负载均衡,大流量的 Redis 服务器这种场景,在 25GE, 100GE 甚至 400GE 的网卡上,如果不做调优,可能无法跑满带宽。
本文专注于系统参数调优,不涉及代码方面的优化。(当然代码方面的优化也是非常重要的,在高性能网络方面,通过优化代码减少指令数,提高缓存命中率,性能提升也是非常显著的。)
本文提供优化思路,具体的性能差别需要设计实验来得出,虽然我们做了大量的性能测试,但是全贴出来就显得文章太长没有重点,所以实验参数就不贴了。
这是一个 TCP 的 ingress 大体的流程图。

Linux 网络栈的收包流程大致上可以分成这么几步:
我们按照这个路线讨论需要调优的地方。
首先,网卡是通过 PCIe 接口插在主板上,如果 PCIe 的带宽小于网卡的带宽,那么在第 1 步就出现瓶颈了。
可以用以下命令检查 PCIe 的实际带宽。
先找到网卡所在的 PCIe 地址:
$ ethtool -i eth2
driver: mlx5_core
version: 24.04-0.7.0
firmware-version: 26.43.2026 (MT_0000000531)
expansion-rom-version:
bus-info: 0000:31:00.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: yes然后用 lspci 查看这个地址的信息:
$ lspci -vv -s 0000:31:00.0
31:00.0 Ethernet controller: Mellanox Technologies MT2894 Family [ConnectX-6 Lx]
Subsystem: Mellanox Technologies MT2894 Family [ConnectX-6 Lx]
Physical Slot: 5
Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr+ Stepping- SERR+ FastB2B- DisINTx+
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0, Cache Line Size: 32 bytes
Interrupt: pin A routed to IRQ 18
NUMA node: 0
Region 0: Memory at 202ffc000000 (64-bit, prefetchable) [size=32M]
Expansion ROM at b0e00000 [disabled] [size=1M]
Capabilities: [60] Express (v2) Endpoint, MSI 00
DevCap: MaxPayload 512 bytes, PhantFunc 0, Latency L0s unlimited, L1 unlimited
ExtTag+ AttnBtn- AttnInd- PwrInd- RBE+ FLReset+ SlotPowerLimit 75W
DevCtl: CorrErr+ NonFatalErr+ FatalErr+ UnsupReq-
RlxdOrd+ ExtTag+ PhantFunc- AuxPwr- NoSnoop+ FLReset-
MaxPayload 512 bytes, MaxReadReq 4096 bytes
DevSta: CorrErr+ NonFatalErr- FatalErr- UnsupReq+ AuxPwr- TransPend-
LnkCap: Port #0, Speed 16GT/s, Width x8, ASPM not supported
ClockPM- Surprise- LLActRep- BwNot- ASPMOptComp+
LnkCtl: ASPM Disabled; RCB 64 bytes, Disabled- CommClk+
ExtSynch- ClockPM- AutWidDis- BWInt- AutBWInt-
LnkSta: Speed 16GT/s, Width x8
TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
DevCap2: Completion Timeout: Range ABC, TimeoutDis+ NROPrPrP- LTR-
10BitTagComp+ 10BitTagReq- OBFF Not Supported, ExtFmt- EETLPPrefix-
EmergencyPowerReduction Not Supported, EmergencyPowerReductionInit-
FRS- TPHComp- ExtTPHComp-
AtomicOpsCap: 32bit- 64bit- 128bitCAS-
DevCtl2: Completion Timeout: 260ms to 900ms, TimeoutDis- LTR- 10BitTagReq- OBFF Disabled,
AtomicOpsCtl: ReqEn+
LnkCap2: Supported Link Speeds: 2.5-16GT/s, Crosslink- Retimer+ 2Retimers+ DRS-
LnkCtl2: Target Link Speed: 16GT/s, EnterCompliance- SpeedDis-
Transmit Margin: Normal Operating Range, EnterModifiedCompliance- ComplianceSOS-
Compliance Preset/De-emphasis: -6dB de-emphasis, 0dB preshoot
LnkSta2: Current De-emphasis Level: -6dB, EqualizationComplete+ EqualizationPhase1+
EqualizationPhase2+ EqualizationPhase3+ LinkEqualizationRequest-
Retimer- 2Retimers- CrosslinkRes: unsupported
...关键信息是第 23 行显示 LnkSta: Speed 16GT/s, Width x8。
16 GT/s 是 PCIe 的单通道的速度,表示 Giga Transfers per second,PCIe Lane 每秒传输多少个符号(symbol),PCIe Gen 4 每 130 bit 有 2 bit 的编码开销,所以实际带宽是:
128b / 130b Encoding * 16G ≈ 15.754 Gbps
一共有 8 lane,15.754 Gbps * 8 = 126 Gbps.
上面的这张网卡是 25GE,所以 PCIe 的带宽是绰绰有余了。如果是 200 GE 的网卡,就必须插在 PCIe Gen 4 x 16 或者 PCIe Gen 5 x 8 以上才行了。
这里需要格外注意的一点是,我这张网卡虽然是 25GE,但是上面是有 2 个 25GE 的接口的,做了 bonding1,所以实际这张网卡的带宽是 50G。以此类推,如果是 100GE 双网口的网卡,PCIe Gen 4 x 8 是不够的。
可以用 ethtool 查看在用网卡的实际 PCI bus 地址,都是 31:00,说明是同一个 bus,同一个 device。
$ ethtool -i eth2
driver: mlx5_core
version: 24.04-0.7.0
firmware-version: 26.43.2026 (MT_0000000531)
expansion-rom-version:
bus-info: 0000:31:00.0
supports-statistics: yes
supports-test: yes
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: yes
$ ethtool -i eth3
driver: mlx5_core
version: 24.04-0.7.0
firmware-version: 26.43.2026 (MT_0000000531)
expansion-rom-version:
bus-info: 0000:31:00.1
supports-statistics: yes
supports-test: yes
supports-eeprom-access: no
supports-register-dump: no
supports-priv-flags: yes我们再来看第 2 步,网卡把数据包的信息 DMA 到内存。
现代网卡得益于 DMA 机制,在网卡收到包到写入内存的时候完全不需要 CPU 参与工作。
——其实这也是一个优化的基本思路,CPU 本身就不适合处理单一的、大批量的任务,应该尽量减少 CPU 的工作。
Linux 里面每一个进程都有一个虚拟的,独立的内存空间,CPU 读取内存的时候,需要 MMU 把虚拟地址转换成物理内存地址。
PCIe 设备直接 DMA 内存也是一样,不过用的是是 IOMMU。简单来说,设备 DMA 的路径如下:
NIC -> IOVA -> IOMMU Page Table -> Physical Address
IOMMU 在这里负责将设备访问的 I/O 地址(IOVA)转换为物理地址,并限制设备只能访问被授权的内存。但是这里也有一层虚拟地址到物理地址的映射,会带来 overhead。
如果关闭,就可以节省这层 overhead,让设备直接写物理内存。但是这样会带来风险——如果网卡存在 bug,就可以写到其他内存地址,是比较危险的。
也可以使用 pass though 模式,让 IOMMU 使用直通模式运行,这样也没有每次内存写入的翻译成本,但是对内存有保护。配置方式是 GRUB 的启动命令添加 intel_iommu=on iommu=pt. 在 pt 模式下,设备通过 IOMMU 的开销很小,而且只能访问特定范围的内存,不能越界。
如果每次 DMA 的时候都申请内存,会有不小的开销。所以比较好的方式是事先规划好一部分内存,这部分内存就不回收了,直接给网卡 DMA 用。
这个功能不需要配置,但是需要网卡驱动和 kernel 支持。
可以通过 debugfs 查看实际网卡有没有使用 page pool:cat /sys/kernel/debug/page_pool/1-0x00000000abcd1234/stats。
(如果没有,也不一定就是每次分配内存,可能用了别的内存优化机制)
网卡把包写入哪一块内存也有说法。到这里就不得不说一下现代服务器的架构。
现代的服务器一般是双路服务器,里面有 2 颗 CPU,也叫双子星服务器。注意,这里说的是 2 颗 CPU,2 Socket,而不是双核 CPU。
下面是浪潮 NF5180M6 服务器的物理俯视图:
厂商一般会提供服务器的手册2,里面重要的一部分就是逻辑图。
图中的一个重要信息是:内存也是由两部分组成,一半连接 cpu0,另一半连接 cpu1. 这意味着,物理上 cpu0 访问左边的内存比较快,如果要访问右边的内存,就要走 UPI 去另一个 CPU,比较慢。对于 cpu1 来说反之。
用 numactl 可以看到内存布局:
$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62
node 0 size: 128069 MB
node 0 free: 119511 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63
node 1 size: 128965 MB
node 1 free: 115027 MB
node distances:
node 0 1
0: 10 21
1: 21 10NUMA 的意思是 Non-Uniform Memory Access,意思是 CPU 访问不同的内存,速度是不一样的。从上面的输出中可以得知,逻辑的 CPU 0,2,4,6…62 号访问 node 0 的内存较快,访问 node 1 的内存较慢,CPU 1,3,5…63 号访问 node 1 的内存较快,访问 node 0 的内存较慢。
这就启发我们,所有内存访问,都应该尽量走 local,避免走 remote:
这样性能才是最优的。
从逻辑图可以看出,一个 PCI 设备只连接到一个 CPU。我们可以用 sysfs 来查看这个 PCI 设备连接的 CPU 所在的 NUMA 节点:
$ cat /sys/class/net/eth2/device/numa_node
0注意:这个文件的信息本质上是 ACPI 提供的,数据来源于硬件的上报,但是有些不守规矩的厂商可能没有上报正确的数据。最准确的信息其实是上面厂商提供的逻辑图。3
即 eth2 这个网卡是连接到 node 0 上的。那么网卡驱动在初始化的时候,应该从 node 0 申请内存,这样,后续的 DMA 都使用 node 0 的内存。这部分一般不需要调试,驱动的内存初始化一般都是 NUMA aware 的。
网卡 DMA 到内存之后,接着是 CPU 的 softirq 来处理这个包,我们希望 node 0 的 CPU 来处理,而不是 node 1 的 CPU 来处理。
首先,找到中断号。(这里,我们以其中一个 queue 1 为例,多 queue 我们后面继续讨论)
cat /proc/interrupts | grep mlx5_comp1@
115: 0 0 998453167 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI 30932994-edge mlx5_comp1@pci:0000:3b:00.0
180: 0 0 43 892854366 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI 30935042-edge mlx5_comp1@pci:0000:3b:00.1由于这个网卡有 2 个接口,每一个接口都有一个 1 号 queue,所以 grep 出来了 2 个。以第一个为例,它的中断号码是 115 号。我们就可以对 115 中断进行 CPU affinity 绑定:对 /proc/irq/115/smp_affinity_list 写入 2,就表示由 cpu2 来处理这个中断,即 cpu2 来处理网卡 1号 queue 的数据包。
数据包最后会通过 socket API 来给用户程序消费。(如果是 XDP 实现的 L3 或者 L4 网关,就没有这一步了),用户程序尽量放在 node 0 来执行,性能会更高。用户态程序绑定 NUMA 的方式很多,比如可以用 numactl --cpunodebind=0 --membind=0 ./app 来让程序尽量使用 node 0 的 cpu 和内存;或者在程序内部使用 pthread_setaffinity_np() 和 sched_setaffinity() 绑定 CPU。
在 200G/400G 的网卡上,内存带宽依然可能成为瓶颈,可以用 pcm-memory 和 perf 来检查瓶颈是否在内存。
说到了 CPU,我们接下来继续聊一聊 CPU 有关的优化。
首先是网卡 queue 的数量。有一次我们发现服务器的带宽使用上不去,流量一高就会有丢包。一个 CPU 的使用率 100%,其他的 CPU 空闲。这是一个比较常见的问题,原因是网卡只开了一个 queue。网卡把所有收到的包都 DMA 到这个 queue 的 ring buffer,所以只有一个 CPU 来处理这个 ring buffer 的包。一个 CPU 是无法处理 10GE 的网卡带来的流量的。
这种情况就如下图所示:
解决的办法就是让更多的 CPU 来干活。
但是我们不能让更多的 CPU 来处理这个 queue 的数据,原因是:
那就只能开更多的 queue,每一个 cpu 处理一个 queue。
这样,我们就需要网卡支持把收到的流量放到多个 queue,但是依然要保持 flow -> queue hash 的一致性。这个功能叫做 RSS,Receive Side Scaling,是在网卡里实现的。虽然 CPU 也支持对收到的包分流(RPS/XPS),但是这个工作也要在一个 CPU 上执行,而且太慢了,一般不用。
开多个 queue 很简单,可以通过 ethtool -l eth2 查看当前 queue 的数量。
通过 ethtool -L eth2 combined 8 修改 queue 的数量为 8。
Queue 的数量越多越好吗?也不是,我们的目标是尽可能利用多的 CPU,如果 queue 的数量超过了 CPU 的数量其实也没有意义。
以下面我这台机器为例:
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 64
On-line CPU(s) list: 0-63
Vendor ID: GenuineIntel
BIOS Vendor ID: Intel
Model name: Intel(R) Xeon(R) Silver 4216 CPU @ 2.10GHz
BIOS Model name: Intel(R) Xeon(R) Silver 4216 CPU @ 2.10GHz CPU @ 2.1GHz
BIOS CPU family: 179
CPU family: 6
Model: 85
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 2
Stepping: 7
Frequency boost: disabled
CPU(s) scaling MHz: 100%
CPU max MHz: 2101.0000
CPU min MHz: 800.0000
BogoMIPS: 4200.00
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr
sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good
nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 s
sse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsa
ve avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single intel_pp
in ssbd mba ibrs ibpb stibp ibrs_enhanced tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase ts
c_adjust bmi1 avx2 smep bmi2 erms invpcid cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushop
t clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cq
m_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke
avx512_vnni md_clear flush_l1d arch_capabilities
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 1 MiB (32 instances)
L1i: 1 MiB (32 instances)
L2: 32 MiB (32 instances)
L3: 44 MiB (2 instances)
NUMA:
NUMA node(s): 2
NUMA node0 CPU(s): 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62
NUMA node1 CPU(s): 1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63在 node0 显示有 32 个 CPU,那么应该开 32 个 queue 绑定到这些 CPU 吗?
这就错了,其实最优的方案是用 16 个 CPU。因为 Thread(s) per core: 2,即这是一个 Hyper-Threading 的 CPU,每一个物理 core 有 2 个 thread。
如果看下 CPU 的分布,会发现 cpu0 和 cpu 32 是同一个物理 core,2 和 34 是同一个,以此类推。实际每一个 socket 只有 16 个物理 core,算上超线程才是 32 个 core。
# lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ MINMHZ MHZ
0 0 0 0 0:0:0:0 yes 2101.0000 800.0000 2100.0000
...
2 0 0 2 7:7:7:0 yes 2101.0000 800.0000 2100.0000
...
32 0 0 0 0:0:0:0 yes 2101.0000 800.0000 2099.9990
...
34 0 0 2 7:7:7:0 yes 2101.0000 800.0000 2099.99900 和 32,2 和 34,它们的 node, socket, core 都是一模一样。CPU 的计算单元是 share 的。做网络处理,我们的瓶颈不在等待内存,而是在 cpu 的计算。超线程并不会增加 cpu 的计算能力。(不要被 Intel 骗了!)
相反,如果使用了这些 cpu,反而会造成 cache 命中率下降,网络处理极度依赖 cache 提前把下一个要处理的包 load 进来,cache 命中率下降意味着吞吐大幅度下降。
所以我对 IRQ 绑核的时候,只绑 2 个超线程核心中的一个。
或者在 GRUB 启动的时候添加 nosmt,这样超线程的功能直接被关闭了,系统启动之后只能看到物理 core。这样比开启 SMT 但是我们只用超线程中的一个逻辑 CPU 而言,会节省 5% 左右的 SMT 上下文管理成本。
话说,如果只使用 numa node0 的 CPU,那 node1 的 CPU 不是都浪费了吗?是的。
如果要低延迟的网络,推荐只用网卡的 PCIe 所在的 node 的 cpu。但是如果要更多的带宽处理能力,可以多开 16 个 queue,绑定到 node 1 的 cpu。这里是带宽和延迟的 trade off,在网络调优方面,很多地方都都是在对吞吐(带宽)和延迟做取舍。
网卡收到包之后,会先通过包的 5元组 得到一个 hash 值,然后根据 RETA(Redirection Table)选择一个 queue。
通过 ethtool -x 可以查看 RETA:
# ethtool -x enp59s0f0np0
RX flow hash indirection table for enp59s0f0np0 with 16 RX ring(s):
0: 1 2 3 4 5 6 7 8
8: 9 10 11 12 13 14 15 1
16: 2 3 4 5 6 7 8 9
24: 10 11 12 13 14 15 1 2
32: 3 4 5 6 7 8 9 10
40: 11 12 13 14 15 1 2 3
48: 4 5 6 7 8 9 10 11
56: 12 13 14 15 1 2 3 4
64: 5 6 7 8 9 10 11 12
72: 13 14 15 1 2 3 4 5
80: 6 7 8 9 10 11 12 13
88: 14 15 1 2 3 4 5 6
96: 7 8 9 10 11 12 13 14
104: 15 1 2 3 4 5 6 7
112: 8 9 10 11 12 13 14 15
120: 1 2 3 4 5 6 7 8
128: 9 10 11 12 13 14 15 1
136: 2 3 4 5 6 7 8 9
144: 10 11 12 13 14 15 1 2
152: 3 4 5 6 7 8 9 10
160: 11 12 13 14 15 1 2 3
168: 4 5 6 7 8 9 10 11
176: 12 13 14 15 1 2 3 4
184: 5 6 7 8 9 10 11 12
192: 13 14 15 1 2 3 4 5
200: 6 7 8 9 10 11 12 13
208: 14 15 1 2 3 4 5 6
216: 7 8 9 10 11 12 13 14
224: 15 1 2 3 4 5 6 7
232: 8 9 10 11 12 13 14 15
240: 1 2 3 4 5 6 7 8
248: 9 10 11 12 13 14 15 1
RSS hash key:
21:9a:d6:8d:f0:54:bb:c4:6c:5a:b6:d2:2f:b4:01:f1:51:c1:d0:79:7d:cb:7b:02:ee:c9:63:78:75:2a:51:cf:af:69:3d:d3:fb:6a:c6:1e
RSS hash function:
toeplitz: on
xor: off
crc32: off可以看出 hash 的方法是 toeplitz,一个有 255 个 bucket。举例来说,如果 hash 的 value 是 9,那么选择的 queue 就是第 2 行的第 2 个,即 10 号 queue。
RETA 也可以通过 ethtool -X eth2 equal 16 (平均分到 16 个 queue)指定。
可以发现,即使 ethtool -l 显示有 32 个 queue,也不是说实际使用的就是 32,如果我们在 RETA 里面平均分配到 16 个 queue 的话,实际只有 16 个 queue 会有流量 (IRQ)。
选中一个 queue 之后,NIC 会 DMA 到这个 queue 对应的内存。接下来我们要绑定这个 queue 对应的 CPU。
通过上面提到过的 /proc/interrupts 文件,可以查看这个 queue 对应的中断号。然后通过 /proc/irq/115/smp_affinity_list 来绑定 cpu 号。要对所有使用的 queue 都进行 cpu 绑定。同时,要注意只绑定物理 core,不要让一个物理 core 去处理多个 irq。
我用 Intel,博通,和 Mellanox 的网卡的 RSS 都没有遇到问题,RPS/XPS 可以关闭了。这个功能可以理解是 Linux 软件实现的 RSS,ingress 和 egress 方向。我们已经设置了 RSS,就不希望内核代码再次调度 cpu,造成 cache miss。
关闭 RPS 很简单,把每个 RX Queue 的 rps_cpus 设置为 0 即可。
for f in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
echo "$f: $(cat $f)"
done开启了多个 queue,每一个 queue 都有一个 ringbuffer,这个 buffer 的大小可以用 ethtool -g eth0 来查看。也可以用 ethtool 来调整。
这个值其实不会提升性能,因为它不影响处理速度。但是把它增大可以提升稳定性,在流量突增的时候,有更多 buffer 空间,可以减少丢包。
但是 buffer 越大就越好吗?肯定不是。
首先大的 buffer 会带来延迟。比如在 10Gbps 下,buffer 是 8196,如果 buffer 满了,流量稳定 10Gbps,那么每一个包进来之后都要在 buffer 中排队约 8196 才能被处理,latency 高达几 ms。如果 buffer 只有 512,那么徒增流量会直接被丢弃,但是每一个被处理的包都只有不到 1ms 的延迟。
另外,512 的 ringbuffer 很小,所有的 descriptor 都可以放到 CPU 的 L1 cache 里面,大大提高处理速度。
这是稳定性与延迟之间的 trade off。
为了让用户程序不占用处理 irq 的 CPU,造成延迟不稳定,我们可以让用户程序只在特定的 CPU 执行,跑 irq 的 CPU 不跑用户程序。
方式可以是通过 taskset 设置每一个程序的 CPU 绑定,也可以在 GRUB 设置启动参数,内核参数添加 isolcpus=0-19,这样普通程序默认就不使用 0-19 CPU 了。
这样做对一些管理程序也有好处。比如 LACP,ARP,SSH 不进入隔离的 CPU,即使网卡流量打满了,SSH 依然可以工作。(当然,更稳的方式是用带外管理)
CPU 有 scaling governor 设置,目的是在 CPU 空闲的时候可以进入低频模式运行来省电,对我们来说性能和稳定性更重要,所以可以改为 performance 模式。
CPU boost 需要管理。临时的性能提升不如稳定的性能,boost 可能会升高温度带来降频。
我们可以设置 softirq 的处理预算,让一次 softirq 处理更多的包。让 CPU 花在更多的时间在处理 softirq 上。
net.core.dev_weight=600 设置一次 NAPI 最多处理 600 个包,默认 64;net.core.netdev_budget=3000 设置一次 softirq 触发最多处理多少个包(与上面的区别是,如果 3 个 queue 对应到一个 cpu,那么这两个的设置是,一次 softirq 触发之后,每一个 queue 最多处理 600 个包,总共处理不超过 3000 个包。)net.core.netdev_budget_usecs=10000 让 softirq 一次最多运行 10ms,默认是 2000;echo 2 > /sys/class/net/<iface>/napi_defer_hard_irqs 设置 NAPI 在连续 2 次空转之后再退出。默认情况下 NAPI poll 到没有包处理就退出了。这个设置让它空转 2 次。本质上也是让 cpu 多花时间在处理网络上;echo 200000 > /sys/class/net/<iface>/gro_flush_timeout 这个让 GRO 等待更多的时间再 flush,这样可以把更多的小包合成一个大包,节省后续网络栈处理的包,提高 PPS。(但是对于 XDP转发 程序来说作用不到,XDP 工作在 driver,在内核的 GRO 前面,不会执行到 GRO)。最后来到了操作系统的 IRQ 处理网络栈。
如果每次来一个包,都触发一次 CPU 中断,这样中断的数量就太多了。
我们可以告诉网卡:收到包之后,先不要中断给 CPU,如果一共攒了 64 个包,或者时间等了 32 usecs,再发送一次中断:
$ ethtool -c eth2
Coalesce parameters for eth2:
Adaptive RX: on TX: on
stats-block-usecs: n/a
sample-interval: n/a
pkt-rate-low: n/a
pkt-rate-high: n/a
rx-usecs: 32
rx-frames: 64
rx-usecs-irq: n/a
rx-frames-irq: n/a
tx-usecs: 8
tx-frames: 128
tx-usecs-irq: n/a
tx-frames-irq: n/a这就是中断合并。
这个值的设置也是 trade-off——如果设置得太大,那么延迟会升高,但是可以节省 CPU 资源,吞吐增加,适合存储类服务;如果设置地太小,延迟就很低,但是资源占用就比较高,适合低延迟网络,如量化交易,游戏等等。
——如果我们让网卡在包比较少的情况下使用低的中断合并,在包多的时候使用高的中断合并,不就完美了吗?
这就是自适应的中断合并功能,在上面的网卡设置中,可以看到 Adaptive RX: on TX: on,网卡就会根据实际的网络流量来自动调节中断合并的参数。
CPU 的资源很宝贵,所以硬件优化的思路就是尽可能的把工作交给网卡来做,网卡的 ASIC 芯片很适合重复劳动。
比如:
网卡支持的 offloading 越来越多,根据使用的协议可以看网卡是否支持。
参数调优,首先要明确调优的目标:低延迟优先,高吞吐优先,稳定不丢包优先,还是成本最小(比如省电)优先?所有参数调整都是在做 trade off,不然的话这些参数没道理默认不是最优的。
提高性能的思路无非就是:
可观测性也很重要,无法观测每一个路径的成本,也就无法优化。所以不要盲目优化,先找到瓶颈在哪里,再去解决瓶颈。