大带宽服务器上的Docker:别让默认bridge和conntrack锁吃掉你的10Gbps
开场:你以为容器吃满带宽,其实内核在替你扛雷
在大带宽服务器上跑Docker,最讽刺的事情是:你花大价钱买了10Gbps独享带宽,但容器默认网络路径上的iptables、conntrack和netfilter锁,会在你跑到3-4Gbps时就开始教你做人。CPU软中断飙到100%,`/var/log/messages`里全是“nf_conntrack: table full”,或者干脆ping通但TCP连接半死不活。
这篇文章不聊K8s,不做CNI选型,就说单机Docker在大带宽场景下的三个最隐蔽的坑,以及怎么用macvlan/ipvlan把Docker默认的NAT那一层烂逻辑直接拆掉。
坑一:Docker0桥的MTU,和你的物理网卡“貌合神离”
大带宽服务器通常配万兆网卡,MTU默认1500。但如果你物理网卡开了Jumbo Frame(MTU 9000),或者上游运营商链路支持巨帧,而Docker0还死守着1500,数据包就需要在桥接处分片/重组——这是纯CPU运算,直接摧毁你的PPS性能。
更坑的是,Docker默认桥接的MTU,经常在创建容器时从物理网卡自动继承,而docker0本身的MTU却可能是1500。查一下你的真实情况:
# 物理网卡MTU
ip link show eth0 | grep mtu
# docker0 MTU
ip link show docker0 | grep mtu
# 某个已运行容器的eth0 MTU
docker exec my_container cat /sys/class/net/eth0/mtu
三个值不一致,别犹豫,直接改造Docker守护进程的MTU。在/etc/docker/daemon.json里加:
{
"mtu": 9000
}
然后systemctl restart docker。注意:改了MTU后,必须删除旧的自定义网络并重新创建,否则容器还是老的MTU:
docker network rm my_net
docker network create --driver bridge --opt com.docker.network.driver.mtu=9000 my_net
别指望iptables帮你做透明分片,它只会把被丢弃的包默默计入没有统计的会计账本里。先把MTU对齐,这是最便宜的性能提升。
坑二:conntrack表满——大带宽服务器上的“静默丢包之王”
Docker默认创建ingress/egress规则,每个转发包都要过conntrack。即使你设置了`-p tcp -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT`,但在大流量下,conntrack表项以每秒数万条的速度膨胀。一旦表满,新连接直接DROP——不是REJECT,是静默丢包。
排查命令,先用它看当前状态:
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count
# 丢包统计(重点看insert_failed/drop)
cat /proc/net/stat/nf_conntrack | awk '{print $22, $24}'
你大概率会看到`insert_failed`疯狂上涨,说明conntrack已经满了。网上的通用调优会让你直接把`nf_conntrack_max`调到几百万——这最多拖延半小时。真正的根治是:大带宽业务不要让Docker走NAT。如果你必须保留bridge模式,至少把超时时间缩短:
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=10
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_syn_recv=10
sysctl -w net.netfilter.nf_conntrack_udp_timeout=10
但这只是缓兵之计。真正能打满万兆的方案,是彻底绕开conntrack。也就是下一条。
坑三:默认bridge NAT + iptables锁,是万兆带宽的隐形天花板
Docker默认桥接的网络拓扑是:容器eth0 → docker0 → 宿主机路由判断 → POSTROUTING链做SNAT → 物理网卡。每一次包转发都要穿过PREROUTING、FORWARD、POSTROUTING三条链,还要查conntrack。如果你是纯出方向流量(比如爬虫、音视频分发),这还能忍;但如果是入方向大流量业务(游戏TCP接入、视频上传、大文件上传),入站包还要经过DNAT,所有条件都在netfilter的同一把自旋锁下串行处理。多核CPU再多,netfilter只让你跑一个核心——这就是你10Gbps网卡只能跑到3Gbps的根本原因。
验证方法:用`iperf3`分别测宿主机和容器,观察下面这个指标:
# 在宿主机上跑服务端
iperf3 -s
# 在另一台机器上测宿主机IP
iperf3 -c 宿主机IP
# 再在容器里跑服务端
docker run --rm -p 5201:5201 networkstatic/iperf3 -s
# 测容器映射的端口
iperf3 -c 宿主机IP -p 5201
如果容器端口的吞吐量只有宿主机端口的60%-70%,同时`top`里softirq占了满核——恭喜你,撞上netfilter锁了。这时候你加机器没用,换CPU没用,唯一的解法是让容器直接挂到物理网卡上,跳过Docker的NAT桥。
破局:用macvlan或ipvlan直接暴露容器,把Docker从数据路径里摘掉
在单机大带宽场景,最激进也最有效的方案,是用Docker的macvlan驱动,让每个容器直接拥有一个宿主机同网段的IP,走物理网卡收发数据。Docker在这里退化成单纯“拉进程”的管理工具,网络路径上完全不存在iptables和conntrack。
# 创建macvlan网络,绑定物理网卡eth0,指定子网和网关
docker network create -d macvlan \
--subnet=203.0.113.0/24 \
--gateway=203.0.113.1 \
-o parent=eth0 \
macvlan_public
启动容器时直接指定这个网络,不需要-P/-p端口映射:
docker run --network=macvlan_public --ip=203.0.113.10 -it nginx
宿主机防火墙规则、`tc` qdisc、路由规则全部还在物理网卡上生效,但Docker不再插入任何netfilter钩子。你可以用`ethtool -S eth0`验证,容器流量不再经过`docker0`和`FORWARD`链。
注意macvlan的硬性限制:普通macvlan模式,容器与宿主机之间不能互通(除shim网卡)。如果你有“容器访问宿主服务”的需求,改用ipvlan L2模式,性能几乎一样:
docker network create -d ipvlan \
--subnet=203.0.113.0/24 \
--gateway=203.0.113.1 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
ipvlan_public
用ipvlan,容器可以访问宿主机IP,而且不会像macvlan那样在交换侧学到一堆假MAC地址。
进阶:大带宽容器网络调优的其余死角
1. RPS/RFS——把软中断拆到多核
即使不用Docker NAT,单条高吞吐TCP连接也只会在一颗CPU上处理收包。排查是否是单核瓶颈:
# 查看各CPU软中断分布
cat /proc/softirqs | grep -E 'NET_RX|CPU'
# 这会显示所有CPU的NET_RX数量,如果集中在一个CPU编号上...
把物理网卡的队列中断分散到多核,用RPS代替RSS(因为云上虚拟网卡可能不支持多队列):
echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
同时开启flow steering,让同一个TCP流的包保持在同一核处理,避免乱序:
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 1 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
2. ring buffer别默认,直接拉满
ethtool -G eth0 rx 4096 tx 4096
如果报错,说明虚拟网卡不支持,跳过。但物理裸机大带宽服务器一般没问题。
3. Docker自身的iptables策略——关掉不需要的互访
如果终究无法脱离bridge(例如需要容器名解析),至少把Docker默认的跨容器互通关掉,减少iptables规则数量:
# 在daemon.json里加
{
"iptables": true,
"icc": false
}
同时把`default-address-pools`配好,避免Docker自动创建一堆大网段,让路由表膨胀。大带宽服务器上,路由表太大会拖慢三层的查表效率。
排错现场:一张图判断你的容器流量被谁吃了
# 1. 首先看容器到宿主机带宽
iperf3 -c 容器IP
# 2. 容器到网关(跨物理机)
iperf3 -c 网关IP
# 3. 容器到公网
# 如果2正常3明显下降,跑一下
tcpdump -ni eth0 -c 1000 host 你的IP
# 观察TCP重传比例、是否有乱序
典型诊断结论:容器的流量重传率>2%,先查网卡队列的丢包和物理链路协商速度;如果软中断全集中在CPU0-1,优先做RPS;如果`ethtool -S eth0`里`rx_missed`持续增长,说明ring buffer太小或CPU来不及取包。
这套思路,我在轻云互联的万兆大带宽服务器上验证过。他们的红旗物理机直接支持macvlan透传,物理网卡队列完整暴露,没有虚拟化层在中间捣乱。容器从bridge切到macvlan之后,同一台机器跑满10Gbps,load从4.2降到0.8。注意我这不是给轻云互联打广告——是他们的裸机参数确实没有隐藏掉`ethtool`的队列信息,这一点对Docker底层调优太重要了。
最后避坑清单(直接抄)
- 上线前先统一三层MTU:物理网卡、docker0、容器eth0必须一致。巨帧环境用9000,普通环境用1500。
- 不要盯着nf_conntrack_max调参:那是给你留擦屁股的时间,不是解决方案。大吞吐业务直接上macvlan/ipvlan。
- macvlan宿主机不能访问容器IP:如果要跑“宿主机nginx反代容器”的场景,改用ipvlan或加个shim网卡。
- 检查软中断分布:单核NET_RX超过80%,就是没开RPS/RSS。这不是可选项,是必选项。
- 不用Docker时,干脆别用默认bridge:新建网络时直接指定
--opt com.docker.network.driver.mtu,并禁用--icc。 - 不要在生产环境直接改daemon.json的mtu:先备份网络定义,离线测试容器重启后网络是否恢复,否则你会被运维同事追杀。
Docker在大带宽服务器上的最大敌人,不是容器本身,而是它默认给你裹的那层NAT逻辑。扔掉这层逻辑,你才能看到物理网卡真正的速度。相信你的网卡,而不是相信Docker的网络魔法。