47讲案例篇:服务器总是时不时丢包,我该怎么办(上)


上⼀节,我们梳理了,应⽤程序容器化后性能下降的分析⽅法。⼀起先简单回顾下。
 
容器利⽤ Linux 内核提供的命名空间技术,将不同应⽤程序的运⾏隔离起来,并⽤统⼀的镜像,来管理应⽤程序的依赖环境。
这为应⽤程序的管理和维护,带来了极⼤的便捷性,并进⼀步催⽣了微服务、云原⽣等新⼀代技术架构。
 
不过,虽说有很多优势,但容器化也会对应⽤程序的性能带来⼀定影响。⽐如,上⼀节我们⼀起分析的 Java 应⽤,就容易发
⽣启动过慢、运⾏⼀段时间后 OOM 退出等问题。当你碰到这种问题时,不要慌,我们前⾯四⼤基础模块中的各种思路,都依
然适⽤。
 
实际上,我们专栏中的很多案例都在容器中运⾏。容器化后,应⽤程序会通过命名空间进⾏隔离。所以,你在分析时,不要忘
了结合命名空间、cgroups、iptables 等来综合分析。⽐如:
cgroups 会影响容器应⽤的运⾏;
iptables 中的 NAT,会影响容器的⽹络性能;
叠加⽂件系统,会影响应⽤的 I/O 性能等。
关于 NAT 的影响,我在⽹络模块的 如何优化NAT性能 ⽂章中,已经为你介绍了很多优化思路。今天,我们⼀起来看另⼀种情
况,也就是丢包的分析⽅法。
 
所谓丢包,是指在⽹络数据的收发过程中,由于种种原因,数据包还没传输到应⽤程序中,就被丢弃了。这些被丢弃包的数
量,除以总的传输包数,也就是我们常说的丢包率。丢包率是⽹络性能中最核⼼的指标之⼀。
 
丢包通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着⽹络拥塞和重传,进⽽还会导致⽹络延迟增⼤、吞吐
降低。接下来,我就以最常⽤的反向代理服务器 Nginx 为例,带你⼀起看看,如何分析⽹络丢包的问题。由于内容⽐较多,这个案
例将分为上下两篇来讲解,今天我们先看第⼀部分内容。
 
案例准备
今天的案例需要⽤到两台虚拟机,还是基于 Ubuntu 18.04,同样适⽤于其他的 Linux 系统。我使⽤的案例环境如下所示:
机器配置:2 CPU,8GB 内存。
 
预先安装 docker、curl、hping3 等⼯具,如 apt install docker.io curl hping3。
这些⼯具,我们在前⾯的案例中已经多次使⽤,这⾥就不再重复介绍。
现在,打开两个终端,分别登录到这两台虚拟机中,并安装上述⼯具。
注意,以下所有命令都默认以 root ⽤户运⾏,如果你⽤普通⽤户身份登陆系统,请运⾏ sudo su root 命令,切换到 root ⽤
户。
 
如果安装过程有问题,你可以先上⽹搜索解决,实在解决不了的,记得在留⾔区向我提问。
到这⾥,准备⼯作就完成了。接下来,我们正式进⼊操作环节。
案例分析
我们今天要分析的案例是⼀个 Nginx 应⽤,如下图所示,hping3 和 curl 是 Nginx 的客户端。

为了⽅便你运⾏,我已经把它打包成了⼀个 Docker 镜像,并推送到 Docker Hub 中。你可以直接按照下⾯的步骤来运⾏它。

在终端⼀中执⾏下⾯的命令,启动 Nginx 应⽤,并在80端⼝监听。如果⼀切正常,你应该可以看到如下的输出:
$ docker run --name nginx --hostname nginx --privileged -p 80:80 -itd feisky/nginx:drop 
dae0202cc27e5082b282a6aeeb1398fcec423c642e63322da2a97b9ebd7538e0
然后,执⾏ docker ps 命令,查询容器的状态,你会发现容器已经处于运⾏状态(Up)了:
$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dae0202cc27e feisky/nginx:drop "/start.sh" 4 minutes ago Up 4 minutes 0.0.0.0:80->80
不过,从 docker ps 的输出,我们只能知道容器处于运⾏状态,⾄于 Nginx 是否可以正常处理外部请求,还需要进⼀步的确
认。
接着,我们切换到终端⼆中,执⾏下⾯的 hping3 命令,进⼀步验证 Nginx 是不是真的可以正常访问了。注意,这⾥我没有使
⽤ ping,是因为 ping 基于 ICMP 协议,⽽ Nginx 使⽤的是 TCP 协议。
# -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
从 hping3 的输出中,我们可以发现,发送了 10 个请求包,却只收到了 5 个回复,50% 的包都丢了。再观察每个请求的 RTT
可以发现,RTT 也有⾮常⼤的波动变化,⼩的时候只有 3ms,⽽⼤的时候则有 3s。
根据这些输出,我们基本能判断,已经发⽣了丢包现象。可以猜测,3s 的 RTT ,很可能是因为丢包后重传导致的。那到底是
哪⾥发⽣了丢包呢?
 
排查之前,我们可以回忆⼀下 Linux 的⽹络收发流程,先从理论上分析,哪⾥有可能会发⽣丢包。你不妨拿出⼿边的笔和纸,
边回忆边在纸上梳理,思考清楚再继续下⾯的内容。
在这⾥,为了帮你理解⽹络丢包的原理,我画了⼀张图,你可以保存并打印出来使⽤:
从 hping3 的输出中,我们可以发现,发送了 10 个请求包,却只收到了 5 个回复,50% 的包都丢了。再观察每个请求的 RTT
可以发现,RTT 也有⾮常⼤的波动变化,⼩的时候只有 3ms,⽽⼤的时候则有 3s。
根据这些输出,我们基本能判断,已经发⽣了丢包现象。可以猜测,3s 的 RTT ,很可能是因为丢包后重传导致的。那到底是
哪⾥发⽣了丢包呢?
排查之前,我们可以回忆⼀下 Linux 的⽹络收发流程,先从理论上分析,哪⾥有可能会发⽣丢包。你不妨拿出⼿边的笔和纸,
边回忆边在纸上梳理,思考清楚再继续下⾯的内容。
在这⾥,为了帮你理解⽹络丢包的原理,我画了⼀张图,你可以保存并打印出来使⽤:

 

 从图中你可以看出,可能发⽣丢包的位置,实际上贯穿了整个⽹络协议栈。换句话说,全程都有丢包的可能。⽐如我们从下往

上看:
在两台 VM 连接之间,可能会发⽣传输失败的错误,⽐如⽹络拥塞、线路错误等;
在⽹卡收包后,环形缓冲区可能会因为溢出⽽丢包;
在链路层,可能会因为⽹络帧校验失败、QoS 等⽽丢包;
在 IP 层,可能会因为路由失败、组包⼤⼩超过 MTU 等⽽丢包;
在传输层,可能会因为端⼝未监听、资源占⽤超过内核限制等⽽丢包;
在套接字层,可能会因为套接字缓冲区溢出⽽丢包;
在应⽤层,可能会因为应⽤程序异常⽽丢包;
此外,如果配置了 iptables 规则,这些⽹络包也可能因为 iptables 过滤规则⽽丢包。
当然,上⾯这些问题,还有可能同时发⽣在通信的两台机器中。不过,由于我们没对 VM2 做任何修改,并且 VM2 也只运⾏
了⼀个最简单的 hping3 命令,这⼉不妨假设它是没有问题的。
 
为了简化整个排查过程,我们还可以进⼀步假设, VM1 的⽹络和内核配置也没问题。这样⼀来,有可能发⽣问题的位置,就
都在容器内部了。
现在我们切换回终端⼀,执⾏下⾯的命令,进⼊容器的终端中:
$ docker exec -it nginx bash 
root@nginx:/#
在这⾥简单说明⼀下,接下来的所有分析,前⾯带有 root@nginx:/# 的操作,都表示在容器中进⾏。
注意:实际环境中,容器内部和外部都有可能发⽣问题。不过不要担⼼,容器内、外部的分析步骤和思路都是⼀样的,只不
过要花更多的时间⽽已。
那么, 接下来,我们就可以从协议栈中,逐层排查丢包问题。
 
链路层
⾸先,来看最底下的链路层。当缓冲区溢出等原因导致⽹卡丢包时,Linux 会在⽹卡收发数据的统计信息中,记录下收发错误
的次数。
你可以通过 ethtool 或者 netstat ,来查看⽹卡的丢包记录。⽐如,可以在容器中执⾏下⾯的命令,查看丢包情况:
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
输出中的 RX-OK、RX-ERR、RX-DRP、RX-OVR ,分别表示接收时的总包数、总错误数、进⼊Ring Buffer 后因其他原因
(如内存不⾜)导致的丢包数以及 Ring Buffer 溢出导致的丢包数。
TX-OK、TX-ERR、TX-DRP、TX-OVR 也代表类似的含义,只不过是指发送时对应的各个指标。
注意,由于 Docker 容器的虚拟⽹卡,实际上是⼀对 veth pair,⼀端接⼊容器中⽤作 eth0,另⼀端在主机中接⼊ docker0
⽹桥中。veth 驱动并没有实现⽹络统计的功能,所以使⽤ ethtool -S 命令,⽆法得到⽹卡收发数据的汇总信息。
从这个输出中,我们没有发现任何错误,说明容器的虚拟⽹卡没有丢包。不过要注意,如果⽤ tc 等⼯具配置了 QoS,那么 tc
规则导致的丢包,就不会包含在⽹卡的统计信息中。
 
所以接下来,我们还要检查⼀下 eth0 上是否配置了 tc 规则,并查看有没有丢包。我们继续容器终端中,执⾏下⾯的 tc 命
令,不过这次注意添加 -s 选项,以输出统计信息:
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
从 tc 的输出中可以看到, eth0 上⾯配置了⼀个⽹络模拟排队规则(qdisc netem),并且配置了丢包率为 30%(loss
30%)。再看后⾯的统计信息,发送了 8 个包,但是丢了 4 个。
看来,应该就是这⾥,导致 Nginx 回复的响应包,被 netem 模块给丢了。
既然发现了问题,解决⽅法也就很简单了,直接删掉 netem 模块就可以了。我们可以继续在容器终端中,执⾏下⾯的命令,
删除 tc 中的 netem 模块:
root@nginx:/# tc qdisc del dev eth0 root netem loss 30%
删除后,问题到底解决了没?我们切换到终端⼆中,重新执⾏刚才的 hping3 命令,看看现在还有没有问题:
$ 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=0 win=5120 rtt=7.9 ms 
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=2 win=5120 rtt=1003.8 ms 
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=5 win=5120 rtt=7.6 ms 
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=6 win=5120 rtt=7.4 ms 
len=44 ip=192.168.0.30 ttl=63 DF id=0 sport=80 flags=SA seq=9 win=5120 rtt=3.0 ms 
--- 192.168.0.30 hping statistic --- 
10 packets transmitted, 5 packets received, 50% packet loss 
round-trip min/avg/max = 3.0/205.9/1003.8 ms
不幸的是,从 hping3 的输出中,我们可以看到,跟前⾯现象⼀样,还是 50% 的丢包;RTT 的波动也仍旧很⼤,从 3ms 到
1s。
显然,问题还是没解决,丢包还在继续发⽣。不过,既然链路层已经排查完了,我们就继续向上层分析,看看⽹络层和传输层
有没有问题。
 
⽹络层和传输层
我们知道,在⽹络层和传输层中,引发丢包的因素⾮常多。不过,其实想确认是否丢包,是⾮常简单的事,因为 Linux 已经为
我们提供了各个协议的收发汇总情况。
我们继续在容器终端中,执⾏下⾯的 netstat -s 命令,就可以看到协议的收发汇总,以及错误信息了:
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重传数 
...
netstat 汇总了 IP、ICMP、TCP、UDP 等各种协议的收发统计信息。不过,我们的⽬的是排查丢包问题,所以这⾥主要观察
的是错误数、丢包数以及重传数。
根据上⾯的输出,你可以看到,只有 TCP 协议发⽣了丢包和重传,分别是:
11 次连接失败重试(11 failed connection attempts)
4 次重传(4 segments retransmitted)
11 次半连接重置(11 resets received for embryonic SYN_RECV sockets)
4 次 SYN 重传(TCPSynRetrans)
7 次超时(TCPTimeouts)
这个结果告诉我们,TCP 协议有多次超时和失败重试,并且主要错误是半连接重置。换句话说,主要的失败,都是三次握⼿
失败。
不过,虽然在这⼉看到了这么多失败,但具体失败的根源还是⽆法确定。所以,我们还需要继续顺着协议栈来分析。接下来的
⼏层⼜该如何分析呢?你不妨⾃⼰先来思考操作⼀下,下⼀节我们继续来⼀起探讨。
 
⼩结
⽹络丢包,通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着⽹络拥塞和重传,进⼀步还会导致⽹络延迟增
⼤、吞吐降低。
今天的这个案例,我们学会了如何从链路层、⽹络层和传输层等⼊⼿,分析⽹络丢包的问题。不过,案例最后,我们还没有找
出最终的性能瓶颈,下⼀节,我将继续为你讲解。
 
思考
最后,给你留⼀个思考题,也是案例最后提到的问题。
今天我们只分析了链路层、⽹络层以及传输层等。⽽根据 TCP/IP 协议栈和 Linux ⽹络收发原理,还有很多我们没分析到的地
⽅。那么,接下来,我们⼜该如何分析,才能破获这个案例,找出“真凶”呢?


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM