Featured image of post mtu配置不当导致的网络通信问题

mtu配置不当导致的网络通信问题

docker

问题现象

  • 宿主机可以正常访问到接口地址并且拿到数据

  • 容器内不带参数访问接口,服务器可以正常访问400错误(证明响应了)

  • 容器内带参数访问接口,会不响应直到被远端返回RST包,响应为curl: (56) OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 104。

排查过程

开始怀疑是ssl/tls交换协议存在问题,查了容器内和容器外,确实有点差异:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@10-210-112-69 myadmin.fs]# curl -V
curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.29.0 NSS/3.36 zlib/1.2.7 libidn/1.28 libssh2/1.4.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smtp smtps telnet tftp 
Features: AsynchDNS GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz unix-sockets 
[root@10-210-112-69 myadmin.fs]# docker exec -it 603 bash
[root@603d0decc5d6 /]# curl -V
curl 7.61.1 (x86_64-redhat-linux-gnu) libcurl/7.61.1 OpenSSL/1.1.1g zlib/1.2.11 nghttp2/1.33.0
Release-Date: 2018-09-05
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp 
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy Metalink

这个结果导致我一度怀疑是宿主机linux kernel版本太低了,容器中的库用到了什么内核新的特性导致的。

直到我拿到了http协议的地址,依然现象如故。 很明显这个时候已经剥离了ssl/tls了,更大的可能是网络传输出了问题,遂在宿主机对docker0网卡进行抓包。

正常请求抓包结果:

序号 时间 目标 协议 长度 信息
1 0.000000 10.210.112.69 19.130.176.63 TCP 74 35806 → 8310 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM TSval=1495514762 TSecr=0 WS=128
2 0.005118 19.130.176.63 10.210.112.69 TCP 74 8310 → 35806 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM TSval=783766239 TSecr=1495514762 WS=512
3 0.005172 10.210.112.69 19.130.176.63 TCP 66 35806 → 8310 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=1495514767 TSecr=783766239
4 0.005267 10.210.112.69 19.130.176.63 HTTP 173 GET /service/api/getPopulation HTTP/1.1
5 0.010290 19.130.176.63 10.210.112.69 HTTP/JSON 424 HTTP/1.1 400 Bad Request , JSON (application/json)
6 0.010343 10.210.112.69 19.130.176.63 TCP 66 35806 → 8310 [ACK] Seq=108 Ack=359 Win=30336 Len=0 TSval=1495514773 TSecr=783766244
7 0.010438 10.210.112.69 19.130.176.63 TCP 66 35806 → 8310 [FIN, ACK] Seq=108 Ack=359 Win=30336 Len=0 TSval=1495514773 TSecr=783766244
8 0.011468 19.130.176.63 10.210.112.69 TCP 66 8310 → 35806 [FIN, ACK] Seq=359 Ack=109 Win=29184 Len=0 TSval=783766246 TSecr=1495514773
9 0.011579 10.210.112.69 19.130.176.63 TCP 66 35806 → 8310 [ACK] Seq=109 Ack=360 Win=30336 Len=0 TSval=1495514774 TSecr=783766246

异常请求抓包结果:

序号 时间 目标 协议 长度 信息
1 0.000000 10.210.112.69 19.130.176.63 TCP 74 33662 → 8310 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM TSval=1494493375 TSecr=0 WS=128
2 0.001855 19.130.176.63 10.210.112.69 TCP 74 8310 → 33662 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM TSval=782744858 TSecr=1494493375 WS=512
3 0.001940 10.210.112.69 19.130.176.63 TCP 66 33662 → 8310 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=1494493377 TSecr=782744858
4 0.002037 10.210.112.69 19.130.176.63 HTTP 200 GET /service/api/getPopulation?pageNumber=1&pageSize=1000 HTTP/1.1
5 0.042302 19.130.176.63 10.210.112.69 TCP 66 8310 → 33662 [ACK] Seq=1 Ack=135 Win=30208 Len=0 TSval=782744899 TSecr=1494493377
34 60.224484 10.210.112.69 19.130.176.63 TCP 66 [TCP Keep-Alive] 33662 → 8310 [ACK] Seq=134 Ack=1 Win=29312 Len=0 TSval=1494553600 TSecr=782744899
35 60.225605 19.130.176.63 10.210.112.69 TCP 66 [TCP Previous segment not captured] 8310 → 33662 [ACK] Seq=15929 Ack=135 Win=30208 Len=0 TSval=782805081 TSecr=1494493377
72 120.384547 10.210.112.69 19.130.176.63 TCP 66 [TCP Keep-Alive] 33662 → 8310 [ACK] Seq=134 Ack=1 Win=29312 Len=0 TSval=1494613760 TSecr=782744899
73 120.387179 19.130.176.63 10.210.112.69 TCP 54 8310 → 33662 [RST] Seq=1 Win=0 Len=0

其中在不带参数时我们能够明显看到,整个正常的请求和响应过程。但是在异常的部分我们能看到:

  • 前3个包显示TCP三次握手正常完成。
  • 第4个包是HTTP GET请求,发送成功。
  • 第5个包显示服务器确认收到了请求。
  • 之后出现了较长时间的空闲(约60秒)。
  • 第34和35包显示客户端发送了Keep-Alive包,服务器正常响应。
  • 又经过约60秒后,客户端再次发送Keep-Alive包(第72包)。
  • 服务器以RST包响应(第73包),表明连接被重置。

问题分析如下:

  • TCP连接建立成功,HTTP请求也发送成功。
  • 服务器似乎没有及时响应HTTP请求(但是实际上服务器响应很快,所以可能发生了丢包或者其他的问题)。
  • 客户端发送Keep-Alive包以维持连接。
  • 最终,服务器因响应超时而选择重置连接。

很明显是第二步包传输中出问题了,因为宿主机没有这个问题,所以我们完全有理由怀疑是容器网络转换把这个问题搞出来了,那就回头来看dockerd,查遍了dockerd的日志一切正常,没有响应任何错误。(后面想起来真蠢,2层的错误dockerd当然不会知道)

又想起同一个地址不带参数和带参数会导致不同的现象,这里面最大的不同不就是响应体的大小吗?想到这里,赶快看了下宿主机eth0的配置:

1
2
3
4
5
6
7
$ ip addr
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc fq_codel state UP group default qlen 1000
    link/ether fc:aa:14:ff:f7:54 brd ff:ff:ff:ff:ff:ff
    inet 192.168.30.100/23 brd 192.168.31.255 scope global noprefixroute enp2s0
       valid_lft forever preferred_lft forever
    inet6 fe80::feaa:14ff:feff:f754/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

我突然悟了,宿主机MTU有人改小了(1460)。docker默认的mtu是1500,这玩意比宿主机大,当 docker0 MTU大于宿主机MTU(如 本次的1460)时,frame需要再次分片,如果传输中有不允许分片或者其他类似的标志,那就会导致网卡丢弃数据包,就会出现「小包可通,大包不通」的情况。

那就好办了,修改下/etc/docker/daemon.json

1
2
3
{
    "mtu": 1400
}

重启就解决了。

最佳实践

  • 在使用容器或虚拟化技术时,应确保网络各层的MTU设置一致。
  • 可以考虑使用路径MTU发现(PMTUD)技术来动态调整MTU大小。
  • 在网络配置中,始终选择较小的MTU值可以避免类似问题。