很多用Docker Compose搭服务的开发者,肯定碰到过这种情况:两个容器之间传数据,测出来的吞吐量比直接在宿主机上低好多,排查了半天应用代码,改了序列化方式、接口调用逻辑,甚至加了缓存,结果网速还是没上来。其实这种情况大部分时候,问题根本不在应用里,而是在容器的网络底层——Docker选的网络驱动不对,或者宿主机的内核参数没配好,这些隐形的坑才是影响性能的关键。
一、容器间网络性能拉胯的常见排查误区
1.1 别上来就死磕应用代码
大部分开发者遇到性能问题的第一反应,都是去改应用的逻辑,比如觉得是接口调用的序列化太占时间,或者是应用里的缓存策略不对。但容器间的网络传输,除了应用本身的处理,底层的网络转发、数据包处理、硬件资源占用都会影响速度。举个实际的例子:公司内部用Docker Compose部署了两个微服务,一个负责文件提取,一个负责文件存储,原本裸金属环境下,两个服务之间传100MB的文件只需要1秒,用Docker Compose部署后,同样的文件传了10秒,排查应用代码时,把JSON改成了MessagePack,又优化了接口的超时时间,结果只快了1秒,这时候就该换个思路,去查网络层的配置了。
二、Docker网络驱动对容器间吞吐量的影响
2.1 常用Docker网络驱动的差异
Docker容器的网络驱动,就是管容器怎么接入网络的“规则”,不同的驱动,转发性能差很多。常用的有bridge、host、overlay这三种:bridge是Docker默认的驱动,适合单主机部署,它会给每个容器分配虚拟网卡,通过iptables做端口转发;host驱动是让容器共享宿主机的网络栈,相当于容器和宿主机用同一个网卡,没有转发开销;overlay驱动适合多主机集群,能让跨主机的容器互相通信,但性能开销比bridge还大。 举个测试的例子,用iperf3工具测两个容器之间的TCP吞吐量,分别用bridge和host驱动,bridge驱动下的吞吐量只有22MB/s,host驱动下的吞吐量达到94MB/s,差距很明显,这说明驱动的选择对性能影响很大。
2.2 bridge驱动的性能瓶颈点
虽然bridge驱动隔离性好,配置简单,但它有两个隐藏的性能问题:一是所有容器的进出包都要经过Docker的iptables规则链,iptables的过滤和转发会增加CPU开销;二是默认的MTU(最大传输单元)是1500,如果宿主机或者物理网络支持更大的MTU(比如9000,叫巨帧),而容器的MTU没匹配,传大文件时会被拆成小数据包,拆包和拼包会浪费大量时间,这也是导致吞吐量低的常见原因。
三、宿主机内核参数对转发性能的影响
3.1 关键内核参数的作用
宿主机的内核是容器网络转发的底层支撑,有几个参数直接影响网络传输的速度:第一个是TCP的接收缓冲区最大值(net.core.rmem_max),这个参数决定了TCP能缓存的最大接收数据量,默认值一般只有212992字节,也就是200KB左右,对于大文件传输来说太小,会导致应用反复等待确认包,拖慢速度;第二个是TCP的发送缓冲区最大值(net.core.wmem_max),和接收缓冲区同理;第三个是TIME_WAIT端口复用(net.ipv4.tcp_tw_reuse),这个参数能让处于TIME_WAIT状态的端口被新连接复用,避免频繁创建和销毁连接的开销;还有IP转发开关(net.ipv4.ip_forward),容器之间通信需要开启这个参数,否则会被内核拦截。 查看这些参数的命令很简单,用sysctl工具就能查,比如:
# 查看宿主机当前的TCP接收缓冲区最大值
sysctl net.core.rmem_max
# 查看是否开启了IP转发,容器间通信需要
sysctl net.ipv4.ip_forward
3.2 内核参数的优化示例
如果查到的参数不符合要求,比如rmem_max只有200KB,就可以修改它。临时修改的话,重启宿主机就会失效,适合测试用;永久修改要改宿主机的/etc/sysctl.conf文件。举个例子,把TCP的接收和发送缓冲区改成16MB,开启端口复用,打开IP转发,操作如下:
# 临时修改TCP缓冲区和端口复用,重启后失效
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.ip_forward=1
永久修改的话,在/etc/sysctl.conf文件末尾加上这些参数,然后执行sysctl -p让配置生效,这样重启宿主机后参数也不会变。
四、具体排查与优化步骤
4.1 实际场景重现
之前遇到的微服务传文件慢的问题,就是Docker Compose部署的,当时测下来吞吐量只有12MB/s,裸金属环境是100MB/s。先排查了Docker的网络驱动,发现用的是默认的bridge驱动,然后查了宿主机的内核参数,发现rmem_max确实是200KB左右,这显然不够;再查MTU,宿主机的MTU是9000(因为公司用了巨帧优化),但Docker容器的MTU默认是1500,不匹配,导致大文件被拆成很多小包,速度上不去。
4.2 优化Docker网络的MTU
解决MTU不匹配的问题,只需要在docker-compose.yml里自定义网络,设置MTU为9000,这样容器的网络就会和宿主机的巨帧匹配,减少拆包开销。示例的技术栈是Docker Compose v2.x,配置如下: 【技术栈:Docker Compose v2.x】
version: '3.8'
# 自定义网络,设置MTU为9000,匹配宿主机的巨帧配置,减少大文件传输的拆包次数
networks:
app-network:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 9000
services:
# iperf服务器,用来接收测试流量,验证网络性能
iperf-server:
image: networkstatic/iperf3
command: -s
networks:
- app-network
# iperf客户端,并发4个连接,测试持续30秒,模拟实际的多连接传输场景
iperf-client:
image: networkstatic/iperf3
command: -c iperf-server -t 30 -i 5 -P 4
networks:
- app-network
这个配置把容器的网络MTU改成了9000,和宿主机一致,避免了拆包的额外开销。
4.3 优化后的效果
改完内核参数和MTU之后,再测两个微服务之间的文件传输速度,发现吞吐量涨到了88MB/s,又微调了TCP缓冲区,把rmem_max改成32MB,最后吞吐量稳定在95MB/s,接近裸金属的水平,解决了之前的性能问题。
五、技术优缺点与注意事项
5.1 不同网络驱动的优缺点
bridge驱动的优点是隔离性好,配置简单,适合本地开发和单主机部署;缺点是需要经过iptables转发,性能不如host驱动,多节点集群的话性能会更差。host驱动的优点是性能和宿主机完全一致,没有转发开销;缺点是容器和宿主机共享网络栈,隔离性差,安全性低,同一端口不能运行多个容器,适合对性能要求高的单主机场景。overlay驱动的优点是支持跨主机的容器通信,适合集群部署;缺点是性能开销大,配置复杂,不适合小型项目。
5.2 优化的注意事项
调整Docker网络和宿主机内核参数时,有几个注意点:一是巨帧MTU要确保整个网络都支持,包括宿主机、交换机、物理网络,否则会出现大包丢包的问题,反而更慢;二是内核参数不要随便改陌生的参数,比如tcp_syncookies是安全参数,不熟悉的话不要动,避免影响系统安全;三是用host驱动时要注意端口冲突,多个容器不能用同一个端口,除非做端口映射。
六、总结
容器间的网络吞吐量问题,很多时候不是应用代码的问题,而是底层的网络驱动和宿主机内核参数没配好。遇到性能问题时,先排查Docker的网络驱动是否合适,再检查MTU是否匹配,最后调整宿主机的内核参数,这三个步骤就能解决大部分的容器网络性能问题。另外,用iperf之类的工具测试网络吞吐量,能快速验证优化的效果,避免盲目的调整应用代码,节省排查时间。
评论
围绕“Docker Compose 部署的容器间网络吞吐量始终上不去,排查时不能只盯着应用代码,还要考虑网络驱动模式与宿主机内核参数对转发性能的影响”参与讨论