这篇文章上次修改于 954 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

1 现象

# -c 表示发送 10 个请求,-S 表示使用 TCP SYN,-p 指定端口为 80
$ hping3 -c 10 -S -p 80 192.168.0.30
HPING 192.168.0.30 (eth0 192.168.0.30): S set, 40 headers + 0 data bytes
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=3 win=5120 rtt=7.5 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=4 win=5120 rtt=7.4 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=3.3 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=7 win=5120 rtt=3.0 ms
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=3027.2 ms

--- 192.168.0.30 hping statistic ---
10 packets transmitted, 5 packets received, 50% packet loss
round-trip min/avg/max = 3.0/609.7/3027.2 ms

发送了 10 个请求包,却只收到了 5 个回复,50% 的包都丢了。

2 排查

可能丢包的原因:

  • 两台机器连接之间,可能会发生传输失败的错误,比如网络拥塞、线路错误等;
  • 网卡收包后,环形缓冲区可能会因为溢出而丢包;
  • 链路层,可能会因为网络帧校验失败、QoS 等而丢包;
  • IP 层,可能会因为路由失败、组包大小超过 MTU 等而丢包;
  • 传输层,可能会因为端口未监听、资源占用超过内核限制等而丢包;
  • 套接字层,可能会因为套接字缓冲区溢出而丢包;
  • 应用层,可能会因为应用程序异常而丢包;
  • 如果配置了 iptables 规则,可能因为 iptables 过滤规则而丢包。

2.1 链路层

当缓冲区溢出等原因导致网卡丢包时,Linux 会在网卡收发数据的统计信息中,记录下收发错误的次数。

查看网卡的丢包记录:

root@nginx:/# netstat -i
Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0       100       31      0      0 0             8      0      0      0 BMRU
lo       65536        0      0      0 0             0      0      0      0 LRU

没有发现任何错误,说明容器的虚拟网卡没有丢包。不过要注意,如果用 tc 等工具配置了 QoS,那么 tc 规则导致的丢包,就不会包含在网卡的统计信息中。

检查一下 eth0 上是否配置了 tc 规则,是否丢包:

root@nginx:/# tc -s qdisc show dev eth0
qdisc netem 800d: root refcnt 2 limit 1000 loss 30%
 Sent 432 bytes 8 pkt (dropped 4, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0

eth0 上面配置了一个网络模拟排队规则(qdisc netem),并且配置了丢包率为 30%(loss 30%)。再看后面的统计信息,发送了 8 个包,但是丢了 4 个。应该是 netem 模块丢弃了 Nginx 的响应包。

删除 tc 中的 netem 模块:

root@nginx:/# tc qdisc del dev eth0 root netem loss 30%

重新执行 hping3 命令后,发现问题没有解决。

2.2 网络层和传输层

查看协议收发汇总:

root@nginx:/# netstat -s
Ip:
    Forwarding: 1          //开启转发
    31 total packets received    //总收包数
    0 forwarded            //转发包数
    0 incoming packets discarded  //接收丢包数
    25 incoming packets delivered  //接收的数据包数
    15 requests sent out      //发出的数据包数
Icmp:
    0 ICMP messages received    //收到的ICMP包数
    0 input ICMP message failed    //收到ICMP失败数
    ICMP input histogram:
    0 ICMP messages sent      //ICMP发送数
    0 ICMP messages failed      //ICMP失败数
    ICMP output histogram:
Tcp:
    0 active connection openings  //主动连接数
    0 passive connection openings  //被动连接数
    11 failed connection attempts  //失败连接尝试数
    0 connection resets received  //接收的连接重置数
    0 connections established    //建立连接数
    25 segments received      //已接收报文数
    21 segments sent out      //已发送报文数
    4 segments retransmitted    //重传报文数
    0 bad segments received      //错误报文数
    0 resets sent          //发出的连接重置数
Udp:
    0 packets received
    ...
TcpExt:
    11 resets received for embryonic SYN_RECV sockets  //半连接重置数
    0 packet headers predicted
    TCPTimeouts: 7    //超时数
    TCPSynRetrans: 4  //SYN重传数
  ...

只有 TCP 协议发生了丢包和重传,且主要错误是半连接重置。换句话说,主要的失败,都是三次握手失败。但是失败的根源还未找到。

2.3 iptables

iptables 和内核的连接跟踪机制也可能会导致丢包。要确认是不是连接跟踪导致的问题,其实只需要对比当前的连接跟踪数和最大连接跟踪数即可。

连接跟踪在 Linux 内核中是全局的(不属于网络命名空间)。查看连接跟踪数:

$ sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144
$ sysctl net.netfilter.nf_conntrack_count
net.netfilter.nf_conntrack_count = 182

连接数小于最大连接数,所以不是连接跟踪导致的。

执行 iptables 命令,查看 filter 表的统计数据:

root@nginx:/# iptables -t filter -nvL
Chain INPUT (policy ACCEPT 25 packets, 1000 bytes)
 pkts bytes target     prot opt in     out     source               destination
    6   240 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 15 packets, 660 bytes)
 pkts bytes target     prot opt in     out     source               destination
    6   264 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            statistic mode random probability 0.29999999981

两条 DROP 规则的统计数值不是 0,它们分别在 INPUT 和 OUTPUT 链中。这两条规则实际上是一样的,指的是使用 statistic 模块,进行随机 30% 的丢包。

删除这两条 DROP 规则:

root@nginx:/# iptables -t filter -D INPUT -m statistic --mode random --probability 0.30 -j DROP
root@nginx:/# iptables -t filter -D OUTPUT -m statistic --mode random --probability 0.30 -j DROP

重新执行 hping3,发现丢包问题解决了。

检查 Nginx 对 HTTP 请求的响应:

$ curl --max-time 3 http://192.168.0.30
curl: (28) Operation timed out after 3000 milliseconds with 0 bytes received

发现连接超时了。继续执行 hping3 命令,发现还是正常的,这时候需要抓包了。

2.4 tcpdump

在终端一中执行 tcpdump 命令:

root@nginx:/# tcpdump -i eth0 -nn port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

在终端二再次执行前面的 curl 命令。然后在终端一会看到如下输出:

14:40:00.589235 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [S], seq 332257715, win 29200, options [mss 1418,sackOK,TS val 486800541 ecr 0,nop,wscale 7], length 0
14:40:00.589277 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [S.], seq 1630206251, ack 332257716, win 4880, options [mss 256,sackOK,TS val 2509376001 ecr 486800541,nop,wscale 7], length 0
14:40:00.589894 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 486800541 ecr 2509376001], length 0
14:40:03.589352 IP 10.255.255.5.39058 > 172.17.0.2.80: Flags [F.], seq 76, ack 1, win 229, options [nop,nop,TS val 486803541 ecr 2509376001], length 0
14:40:03.589417 IP 172.17.0.2.80 > 10.255.255.5.39058: Flags [.], ack 1, win 40, options [nop,nop,TS val 2509379001 ecr 486800541,nop,nop,sack 1 {76:77}], length 0

前三个包是正常的三次握手,第四个包确在 3s 后才到,还是客户端发来的 FIN 包,说明是客户端的连接关闭了。我们并没有抓到 curl 发来的 HTTP Get 请求,究竟是网卡丢包了,还是客户端根本没有发来呢?

查看网卡有没有丢包:

root@nginx:/# netstat -i
Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0       100      157      0    344 0            94      0      0      0 BMRU
lo       65536        0      0      0 0             0      0      0      0 LRU

接收包丢包数是 344,看来是在网卡接收时丢包了。但为什么 hping3 时不丢包,但是 GET 会丢包呢?对比下 hping3 和 curl 两个工具:

  • hping3 只发送了 SYN 包。
  • curl 在发送了 SYN 包后,还会发送 HTTP GET 请求包。HTTP GET 请求本质上也是 TCP 包,但是还携带了 HTTP GET 数据。

可能是 MTU 配置错误导致的。继续查看网卡的输出,eth0 的 MTU 只有 100,太小了,修改成 1500:

root@nginx:/# ifconfig eth0 mtu 1500

再次执行 curl 命令,发现收到了正常的响应。

参考

倪鹏飞. Linux 性能优化实战.