《动手测试单机百万连接》实验记录(一)
最近参考动手测试单机百万连接这篇文章的方案二做实验,中间碰到一些坑,记录如下:
实验介绍
- 机器A:8核4G的虚拟机,ip是192.168.2.44,centos 7,内核3.10.0
- 机器B:8核8G的物理机,ip是192.168.2.120,ubuntu 22.04,内核5.15.0
- 机器B启20个tcp server,监听端口范围是8100~8119
- 机器A启20个客户端进程,各自选择一个端口向机器B发起5万连接
- 最终目标:机器A最终向机器B建立100万个tcp连接
问题一
此时两个机器的内核参数均做了如下修改:
sysctl -w net.ipv4.ip_local_port_range="5000 65000"
sysctl -w fs.file-max=1100000
sysctl -w fs.nr_open=1100000
sysctl -w net.core.somaxconn=40960
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_orphans=1000000
sysctl -w net.ipv4.tcp_max_syn_backlog=40960
ulimit -n 1000000
echo -e "* hard nofile 1000000\n* soft nofile 1000000" >> /etc/security/limits.conf
失败现象:建立了约17577个连接后就卡住了,机器B上用dmesg|tail
查到如下错误:
nf_conntrack: nf_conntrack: table full, dropping packet
此时查看机器B的连接跟踪数conntrack -C
是65536,机器B的内核配置net.netfilter.nf_conntrack_max
也是这个数
解决办法:把net.netfilter.nf_conntrack_max设置成6553600
问题二
解决问题一后,再次启动实验,发现可以轻松的很快建立10w多连接,但是随后达到20w连接的时候速度就变慢了,差不多每秒建立150连接,达到40w的时候每秒只能建立20个连接了,似乎遇到了性能瓶颈。我也曾经等待了一个晚上,最终也只能达到60多w的样子。
这个时候,我尝试在机器A上抓包tcpdump -i ens33 -n -nn host 192.168.2.120 and port 8100
,看到的现象是机器A发个机器B的syn包,有的能立即收到syn+ack响应,有的需要发送若干次syn包才能收到syn+ack响应,有的在尝试发送6次syn包后宣告失败。在机器B上抓包tcpdump -n -nn host 192.168.2.44 and port 8100
,现象同机器A。那么问题是为什么有时候机器B在收到syn包的时候会没有回应呢?
我尝试使用bpftrace工具来捕获丢包的场景,运行bpftrace kfree_skb.bt
,kfree_skb.bt的源码如下:
kprobe:kfree_skb
{
$skb = (struct sk_buff*) arg0;
$ip_header = ((struct iphdr *) ($skb->head + $skb->network_header));
// 这里只捕获 daddr 是 192.168.2.120 的包
if ($ip_header->daddr == 0x7802a8c0)
{
$tcp_header= ((struct tcphdr *) ($skb->head + $skb->network_header + ($ip_header->ihl << 2)));
$sport = $tcp_header->source;
$dport = $tcp_header->dest;
$dport = ($dport >> 8) | (($dport << 8) & 0xff00);
$sport = ($sport >> 8) | (($sport << 8) & 0xff00);
// 这里只捕获 dport 是 8100 的包
if ($dport == 8100) {
time("%H:%M:%S ");
printf("%s:%d > %s:%d\n", ntop($ip_header->saddr), $sport, ntop($ip_header->daddr), $dport);
}
}
}
此时可以观察到,在内核在调用kfree_skb丢包的时候,saddr和sport正好是tcpdump上没有收到回应的syn包。
再用bpftrace -e 'kprobe:kfree_skb { @[kstack(15)] = count(); }'
获取kfree_skb的上游调用链,得到结果如下:
kfree_skb+1
ip_local_deliver+209
ip_sublist_rcv_finish+103
ip_sublist_rcv+385
ip_list_rcv+245
__netif_receive_skb_list_core+531
netif_receive_skb_list_internal+398
napi_complete_done+122
e1000e_poll+203
__napi po1l+51
net_rx_action+294
再用./funcgraph ip_local_deliver
获取ip_local_deliver下游调用链。funcgraph是基于内核ftrace技术的工具。当发生丢包时刻的调用栈如下:
5) | ip_local_deliver() {
5) 0.897 us | irq_enter_rcu();
5) | __sysvec_irq_work() {
5) | __wake_up() {
5) | __wake_up_common_lock() {}
5) + 46.952 us | }
5) + 48.697 us | }
5) | irq_exit_rcu() {
5) 0.788 us | idle_cpu();
5) 2.309 us | }
5) | nf_hook_slow() {
5) | nf_nat_ipv4_local_in [nf_nat]() {
5) | nf_nat_inet_fn [nf_nat]() {
5) | __nf_nat_alloc_null_binding [nf_nat]() {
5) | nf_nat_setup_info [nf_nat]() {
5) 0.732 us | nf_ct_invert_tuple [nf_conntrack]();
5) | get_unique_tuple [nf_nat]() {
5) 0.880 us | in_range [nf_nat]();
5) 0.764 us | nf_ct_invert_tuple [nf_conntrack]();
5) | nf_conntrack_tuple_taken [nf_conntrack]() {
5) 0.922 us | hash_conntrack_raw [nf_conntrack]();
5) 0.750 us | rcu_read_unlock_strict();
5) 4.408 us | }
5) 9.114 us | }
5) 0.937 us | hash_by_src [nf_nat]();
5) 0.816 us | _raw_spin_lock_bh();
5) | _raw_spin_unlock_bh() {
5) 0.660 us | __local_bh_enable_ip();
5) 2.132 us | }
5) 0.741 us | irq_enter_rcu();
5) | __sysvec_irq_work() {
5) | __wake_up() {
5) | __wake_up_common_lock() {
5) 0.603 us | _raw_spin_lock_irqsave();
5) | __wake_up_common() {
5) | autoremove_wake_function() {
5) | default_wake_function() {
5) | try_to_wake_up() {
5) 0.538 us | _raw_spin_lock_irqsave();
5) 0.507 us | _raw_spin_unlock_irqrestore();
5) 2.521 us | }
5) 3.532 us | }
5) 4.772 us | }
5) 6.077 us | }
5) 0.476 us | _raw_spin_unlock_irqrestore();
5) 9.408 us | }
5) + 10.557 us | }
5) + 12.095 us | }
5) | irq_exit_rcu() {
5) 0.812 us | idle_cpu();
5) 2.285 us | }
5) + 38.743 us | }
5) + 40.207 us | }
5) + 41.882 us | }
5) + 43.329 us | }
5) | ipv4_confirm [nf_conntrack]() {
5) | nf_confirm [nf_conntrack]() {
5) | __nf_conntrack_confirm [nf_conntrack]() {
5) 1.009 us | hash_conntrack_raw [nf_conntrack]();
5) | nf_conntrack_double_lock.constprop.0 [nf_conntrack]() {
5) | nf_conntrack_lock [nf_conntrack]() {
5) 0.787 us | _raw_spin_lock();
5) 2.157 us | }
5) 0.673 us | _raw_spin_lock();
5) 4.664 us | }
5) | nf_ct_del_from_dying_or_unconfirmed_list [nf_conntrack]() {
5) 0.551 us | _raw_spin_lock();
5) 1.716 us | }
5) | nf_ct_add_to_dying_list [nf_conntrack]() {
5) 0.723 us | _raw_spin_lock();
5) 2.199 us | }
5) 0.641 us | __local_bh_enable_ip();
5) + 15.020 us | }
5) + 16.478 us | }
5) + 17.824 us | }
5) | kfree_skb() {
5) | skb_release_head_state() {
5) | nf_conntrack_destroy() {
5) | nf_ct_destroy [nf_conntrack]() {
5) 0.638 us | nf_ct_remove_expectations [nf_conntrack]();
5) | nf_ct_del_from_dying_or_unconfirmed_list [nf_conntrack]() {
5) 0.632 us | _raw_spin_lock();
5) 1.965 us | }
5) 0.758 us | __local_bh_enable_ip();
5) | nf_conntrack_free [nf_conntrack]() {
5) | nf_ct_ext_destroy [nf_conntrack]() {
5) 0.694 us | rcu_read_unlock_strict();
5) | nf_nat_cleanup_conntrack [nf_nat]() {
5) 0.844 us | hash_by_src [nf_nat]();
5) 0.557 us | _raw_spin_lock_bh();
5) | _raw_spin_unlock_bh() {
5) 0.735 us | __local_bh_enable_ip();
5) 2.115 us | }
5) 6.347 us | }
5) 0.713 us | rcu_read_unlock_strict();
5) 0.533 us | rcu_read_unlock_strict();
5) 0.537 us | rcu_read_unlock_strict();
5) 0.714 us | rcu_read_unlock_strict();
5) 0.685 us | rcu_read_unlock_strict();
5) 0.759 us | rcu_read_unlock_strict();
5) 0.715 us | rcu_read_unlock_strict();
5) 0.690 us | rcu_read_unlock_strict();
5) 1.190 us | kfree();
5) + 22.548 us | }
5) 1.096 us | kmem_cache_free();
5) 0.665 us | rcu_read_unlock_strict();
5) + 27.145 us | }
5) + 34.083 us | }
5) 0.710 us | rcu_read_unlock_strict();
5) + 37.040 us | }
5) + 38.558 us | }
5) | skb_release_data() {
5) | skb_free_head() {
5) 0.994 us | kfree();
5) 3.123 us | }
5) 4.551 us | }
5) | kfree_skbmem() {
5) 1.068 us | kmem_cache_free();
5) 2.555 us | }
5) + 48.565 us | }
5) ! 113.116 us | }
5) 0.811 us | rcu_read_unlock_strict();
5) ! 175.328 us | }
在确定了丢包时刻的内核调用链后,可以看到在kfree_skb之前,有进行一些conntrack连接跟踪相关的逻辑处理,而要进一步查找原因,则需要耐心的分析这个过程的源码了。功力不够,我看了一段时间没有头绪,只有放弃了。但是这里可以确定是conntrack的逻辑导致的丢包。
实验最后
在网上找个一个解决办法,即建立连接时去掉conntrack的逻辑,conntrack是内核netfilter的一部分,如果我们的业务并不需要NAT,那么完全可以去掉它,还能换来一定的性能提升。去掉逻辑也很简单:
// 添加PREROUTING规则:去掉目标ip是192.168.2.120的连接跟踪
iptables -t raw -A PREROUTING -p tcp -d 192.168.2.120 -j NOTRACK
// 查看当前的PREROUTING规则
iptables -t raw -L PREROUTING -n --line-numbers
// 删除PREROUTING规则,1表示的是规则行号
iptables -t raw -D PREROUTING 1
再次运行实验,可以最终建立100万的连接了。
本作品采用《CC 协议》,转载必须注明作者和本文链接