
1. 为什么光看 docker stats 不够容器网络排查总差最后一公里容器网络排查有个特别常见的状态业务告警说服务慢或者运维平台显示某台机器的带宽被打满了。拿起docker stats一看某个容器 NET I/O 已经飙到了几百 MBCPU 和内存却很正常。这时候你会下意识想到底是谁在连它它又在往哪儿发数据我遇到过好多次这种情况。最典型的一次是数据库容器从docker stats的界面里只能看到 rx/tx 的累计量在疯涨但根本不知道是哪个客户端 IP、哪条连接在跑大流量。你开docker exec进容器想看连接容器里又往往没有ss、netstat这些命令。折腾一圈下来问题还在原地。所以这篇文章想解决的就是这件事如何查看 docker 容器的 TCP 连接状态以及如何把连接和带宽占用对应起来。内容不是什么高深原理都是我在真实环境下排障时反复用的方法适合正在做容器运维、或者刚上手 docker 没多久、遇到“容器网卡流量异常但不知道去哪了”这类问题的同学参考。在展开命令之前值得先花点时间说清一个前提docker stats给到你的 NET I/O究竟是什么维度。1.1 docker stats 里的 NET I/O 到底在统计什么docker stats输出的 NET I/O本质上来自容器网络接口对应的虚拟网卡累计收发字节数。默认 bridge 模式下每个容器会有一个eth0宿主机上有一条对应的vethXXX虚拟网线。容器发出多少数据、接收多少数据会计在eth0的 rx/tx 计数器上也会同步体现在vethXXX这一侧。问题在于这个数字是聚合值。它只告诉你容器一共收发了多少字节不区分 TCP 还是 UDP不区分是 80 端口还是 3306 端口也不区分流量到底来自哪个远端地址。更麻烦的是它看不出速率。同一个容器上午 10 点跑了 3GB 流量下午 2 点又跑了 3GB 流量docker stats显示出来的 NET I/O 可能刚好因为采样窗口不同而完全不一样。也就是说docker stats只能帮你把怀疑范围从“整台机器”缩小到“某一个容器”。再往下走一步比如容器里维持了多少条 TCP 连接这些连接分别处于什么状态ESTABLISHED、TIME_WAIT、SYN_SENT哪条连接正在以多少速率传送数据对端是什么 IP、什么端口这些docker stats一概回答不了。它顶多说一句“你这个容器的网卡很忙”至于忙在哪儿你得自己去看。1.2 哪些场景下需要“看连接看带宽”而不是只看 stats起码有四类场景光看docker stats是完全不够的第一类是带宽异常占满。比如宿主机网卡流量跑到 900Mbpsdocker stats显示某个容器 rx 数值特别高。你需要立刻知道是不是有某个客户端在大量拉取数据这时候就必须看连接级信息。第二类是连接数异常。容器的连接数到达上限或者出现大量 TIME_WAIT请求变慢。这类问题跟带宽无关纯粹是 TCP 连接状态的问题。不看连接列表连排查入口都找不到。第三类是安全排查。比如怀疑某个端口被扫描、被爆破你需要快速列出当前所有 ESTABLISHED 连接的对端 IP、来源端口然后判断哪些是异常来源。第四类是容量规划。你想知道某个业务容器平均每秒产生多少连接、单连接带宽大概什么水平这需要持续采样连接信息和带宽变化而不是简单看一眼 docker stats 就能得出结论。理解了这一点接下来所有操作才有意义先拿到连接列表再结合网卡速率看谁在跑大流量最后定位到具体进程。2. 查连接前先避坑精简镜像里没有 ss先搞清楚该在哪个命名空间执行命令一个很常见的错误姿势是docker exec进容器然后敲ss -tnp结果提示ss: command not found。不少同学卡在这一步就放弃了觉得容器里工具不全就查不了。实际上这是对 Linux 网络命名空间的理解问题。容器和宿主机共享同一个内核但各自有独立的路由表、iptables 规则、socket 列表等“网络视图”。你执行docker exec进入容器时只是进入了一个新的进程和挂载空间同时带着容器对应的 network namespace。如果容器镜像里没有安装 iproute2那自然没有ss命令这不代表内核里拿不到 socket 信息只是缺少了那个用来“读内核信息”的小工具。2.1 最省事的做法把宿主机的工具通过 nsenter 借给容器 namespace与其进容器装 iproute2我更喜欢在宿主机上直接使用 nsenter把自己“切入”容器的 network namespace然后执行宿主机的ss。原理很简单nsenter 可以指定目标进程并进入该进程所在的各类 namespace。这里只需要网络 namespace因此用-n参数即可# 获取容器主进程在宿主机上的 PID PID$(docker inspect -f {{.State.Pid}} 容器名) # 进入该容器的网络命名空间并执行 ss nsenter -t $PID -n ss -tnp执行完这条命令后你看到的 TCP socket 列表就完全是容器内的视角。宿主机上装了 iproute2 就能用不需要在容器里安装任何东西。对比一下两种做法的优劣做法优点缺点docker exec 进容器临时安装 iproute2直接得到容器内环境后续排查可能比较顺手需要 apt/apk 可用容器重启后所有工具都会丢失生产环境不一定允许你安装软件包nsenter 用宿主机 ss不需要改动容器宿主机工具链完整速度快需要 root 权限要理解-t与-n的含义我自己几乎只在一种情况下用第一种容器内部需要反复抓包宿主机上不方便装 tcpdump我才会临时进容器装。其余时间一律 nsenter。2.2 ss 命令参数怎么选不写错才能输出真正有用的连接列表容器内执行ss时参数缩写的含义要特别留意否则容易得到一堆没用的数据。推荐组合是这样的nsenter -t $PID -n ss -tnp-t表示只看 TCP。-n表示不解析服务名和主机名直接显示 IP 和端口。在排查带宽场景下建议必须加否则一条连接会被解析成localhost:http之类反而影响效率。-p表示显示使用该 socket 的进程信息。这个参数在排查“哪个进程在跑大流量”时非常关键但加了你需要拥有足够权限否则可能看不到进程名。如果只想看已建立的连接可以加状态过滤nsenter -t $PID -n ss -tnp state established只看某个特定端口的连接比如 3306nsenter -t $PID -n ss -tnp sport :3306如果想知道哪些连接处于 TIME_WAIT把状态替换成time-wait即可。值得提醒的是在 Linux 系统中如果你没有 root 权限-p拿到的进程信息很可能是空的。绝大多数 docker 管理命令默认需要高权限执行所以实际使用时这一层限制通常不存在但遇到权限不足时不要慌乱先确认当前用户是不是 root、有没有CAP_SYS_PTRACE之类的容器安全限制。2.3 进入容器 network namespace 后宿主机原有连接会有影响吗不会影响。nsenter 改变的是你当前 shell 所在的 network namespace不影响宿主机上其他进程也不影响宿主机自身的网络连接。这条命令结束之后你的 shell 就回到宿主机的网络环境中一切如常。理解了这个机制很多看似奇怪的现象就解释得通了。比如你在容器里启动一个服务监听某个端口然后回到宿主机上用ss -tnlp看发现压根没有这个监听。不是服务没起来而是它监听在容器的 network namespace 里宿主机的ss默认看不到。这也能解释为什么有些人直接在宿主机上ss | grep 容器IP往往查不到东西——因为一条 TCP 连接在容器内和宿主机上是两个完全不同维度的开销宿主机的ss看到的是宿主机网络栈里的连接不是容器里的那一份。3. 把连接数和带宽对上账从 ss 到 veth 再到 iftop 的完整工具链进入容器 network namespace 后ss能给你一份完整的 TCP 连接列表包括本地地址、远端地址、收发队列、连接状态、进程名甚至可以显示出定时器和拥塞窗口信息。但有一个关键数据ss给不了你这条连接当前实际跑多少带宽。原因在于ss展示的是内核中 socket 的瞬时状态它没有持续的字节计数能力。想拿到每条连接的实时速率需要另一组工具和方法。我的推荐链路是先看ss的连接列表找到“有哪些活跃连接”然后到宿主机找到对应的 veth 接口最后用 iftop 或 tcpdump 观察这些连接/接口的流量速率。3.1 先找容器对应的 veth 网卡名这是后面所有操作的基础bridge 网络模式下容器 eth0 的另一端对应宿主机上一张虚拟网卡通常叫vethxxxxx。你需要在宿主机上找到这张跟容器关联的网卡。方法有很多我最常用的是通过iflink索引来定位# 在宿主机上执行读取容器 eth0 对应的 iflink 索引 iflink$(docker exec 容器名 sh -c cat /sys/class/net/eth0/iflink) # 在宿主机找到 iflink 等于该值的网卡 ip link show | grep -E ^$iflink:|if$iflink执行后会看到类似123: vethf3a2b4cif122: BROADCAST,MULTICAST,UP,LOWER_UP mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default这里的vethf3a2b4c就是这个容器的宿主端虚拟网卡。记住这个名字它会是 iftop、tcpdump、ethtool 这些工具的入口。也有更粗暴的方法在宿主机上执行ip -d link show | grep -B1 link-netnsid然后对照容器eth0的编号但在容器多的机器上容易看花眼。用 iflink 索引做匹配是最稳定的基本不会错。3.2 iftop 查看每条 TCP 连接的实时带宽拿到 veth 网卡名后在宿主机上直接运行 iftopiftop -i vethf3a2b4c -nN -B -s 5解释一下参数-i指定要监听的网卡这里是容器虚拟网卡。-n不解析主机名-N不解析端口名两个同时用输出最干净。-B以 bytes/sec 为单位显示速率。-s 5非交互模式跑 5 秒后自动退出。如果去掉会进入全屏交互界面。输出会按每条 TCP 连接分行显示收发速率再按速率从高到低排序。你一眼就能看到哪条连接在跑大流量、对端是谁、本地端口是什么。如果是在只想临时确认容器整体带宽的场景也可以直接在宿主机上用带宽工具查看 veth 口比如bmon、nload等。但在“排查异常连接”这个目标下iftop 比它们都好用因为它是直接按连接维度输出的。3.3 为什么我对 nethogs 又爱又恨很多人会推荐 nethogs它的特点是按进程维度显示带宽而不是按连接维度。听起来非常理想直接告诉你哪个进程在跑多少流量。但在容器环境下nethogs 的好用程度会打折扣。在宿主机上跑 nethogs默认只能看到宿主机自身进程看不到容器里进程的网络流量。如果你把 nethogs 装进容器里跑又会遇到一个问题它依赖 libpcap精简镜像里没有而且容器内权限模型可能限制它对进程目录的读取。不过当你想确认容器内某个业务进程比如 java、nginx整体在跑多少流量时nethogs 还是很有参考价值的。我的建议是先想清楚自己要解决的问题。要解决“哪条连接占带宽”用 iftop要解决“哪个进程占带宽”就尝试进容器里用 nethogs。二者结合基本能覆盖大多数连接与带宽分析的诉求。还有一种纯内核级替代思路用 tcpdump 抓包按连接统计字节数。例如抓 60 秒 80 端口流量的包再通过 tshark 等工具聚合统计。但这样开销高、操作复杂线上环境不推荐随便抓包。除非问题已经严重到需要看 TCP 层重传、乱序细节否则 iftop 的组合拳已经足够。4. 一次吃满网卡的 MySQL 容器排障从发现流量异常到定位到具体 SQL 的全过程理论讲再多不如完整走一遍排查过程。下面用一个我实际遇过的类似场景作为案例MySQL 容器所在的宿主机带宽告警docker stats明确显示 MySQL 容器网络接收量大但我一开始不知道是谁在连它、在做什么操作。4.1 现场状态docker stats 的 NET I/O 吞吐和接口流量对不上当时的命令结果大致如下$ docker stats --no-stream mysql CONTAINER ID NAME CPU % MEM USAGE / LIMIT NET I/O BLOCK I/O PIDS 7f3d4a2b1c9e mysql 82.01% 1.1GiB / 3.5GiB 812MB / 4.9GB 1.2GB / 3.1GB 47NET I/O 显示接收了 812MB、发送了 4.9GB。注意一个细节发送 4.9GB 说明 MySQL 在大量回传数据而不仅仅是接收查询请求。这种不对称的流量特征通常是某个大查询正在把结果集倒出去。CPU 到 82% 也说明不是简单的心跳而是有实际计算。接着我在宿主机上查看物理网卡的总速率确认是 MySQL 容器把带宽打满了$ sar -n DEV 1 3 ... 平均时间: IFACE rxpck/s txpck/s rxKB/s txKB/s 平均时间: eth0 18246.00 21530.00 218432.00 245891.00换算下来接近 1.9Gbps基本吃满了千兆带宽如果网卡是万兆也相当可观。现在需要定位到底是哪条连接、哪个客户端。4.2 用 nsenter 看容器内的 TCP 连接找到可疑对端 IP按前面的方法第一步拿到 MySQL 容器在宿主机上的主进程 PIDPID$(docker inspect -f {{.State.Pid}} mysql) echo $PID比如拿到 37642然后进入 MySQL 容器的 network namespace只看 3306 端口的已建立连接nsenter -t 37642 -n ss -tnp sport :3306输出里会有一堆连接但很快就能发现某个 IP 的连接特别扎眼。我当时观察到的模式是大部分客户端连接都来自内网 10.10.30.x 网段每个连接的发送队列 Send-Q 都是 0 或个位数因为查询量不大唯独来自10.10.30.188:53210的那条连接Send-Q 一直很大而且ss的 RetrQ重传队列偶尔有数据。ss命令里可以显示连接的一些 TCP 信息nsenter -t 37642 -n ss -tnp -o sport :3306-o会显示 TCP timer 信息比如 rto、backoff、rtt。如果一条连接长时间处于高 rtt、重传频繁可能不只是大流量还可能说明链路质量本身有问题。不过在 MySQL 这个场景里我更在意的是对端是谁、能不能反查业务。4.3 再用 iftop 按连接速率排序确认它确实在跑大流量单纯有连接列表还不够因为连接多的客户端可能只是建立了很多空闲连接。这时需要把“连接”和“带宽”对上账。回到宿主机找到 mysql 容器对应的 veth 网卡iflink$(docker exec mysql sh -c cat /sys/class/net/eth0/iflink) ip link show | grep if$iflink得到vethc1d2e3f4然后跑 iftopiftop -i vethc1d2e3f4 -nN -B -t -s 10输出会列出两条速率靠前的连接$ 10.10.30.188:53210 192.168.1.10:3306 45.2MB 89.5MB 122MB这个速率大概相当于 360-970Mbps几乎把出口打满。到这里已经可以确定造成带宽告警的就是10.10.30.188:53210到 MySQL 容器 3306 端口之间的这条 TCP 连接。接下来去客户端侧查一下是什么程序在发起查询发现是一段数据归档脚本在执行大范围查询结果集巨大持续不断往本地落盘。加了一个分批条件后带宽立刻降下来。4.4 这个案例里最容易踩的坑是什么最容易踩的坑是直接把宿主机的ss当成容器内连接来看。如果我在宿主机上执行ss -tnp | grep 3306看到的结果会包含宿主机上所有连接但 bridge 模式下 MySQL 监听的 3306 在宿主机侧不一定直接对应你想要的 socket。虽然通过端口映射宿主机也会有一个监听 3306 的 docker-proxy 进程但那个进程看到的连接其实经过了 DNAT 和端口转发不是你容器内部真正处理 SQL 的连接。所以排查时一定要先docker inspect拿 PID再用 nsenter 进入容器 namespace否则很容易认为连接不存在或者端口没监听。另一个坑是忘了周期。这类“某个时段流量异常”问题往往不是 7x24 持续发生的。如果你只查一次很可能看到连接列表一切正常。我习惯在ss后面加一条watch -n 1刷新几秒看多个采样点的变化再和 iftop 一起观察才能确认是一条长连接在持续传输还是大量短连接在做高频请求。5. 别把 namespace 和 PID 绕晕不同网络模式下的采集差异与生命周期陷阱很多排查脚本一开始都能跑但过一会儿就报错找不到进程或者查出来的连接不对。这不是脚本写错了而是容器和宿主机的生命周期关系没处理好。5.1 PID 会漂移nsenter 别写死 PID容器一旦重启它在宿主机上对应的主进程 PID 会变network namespace 也会重建。如果你把docker inspect -f {{.State.Pid}}得到的 PID 直接写进脚本里容器重启后 nsenter 会找不到目标或者更糟PID 被宿主机上另一个进程复用导致你进错了 namespace。正确做法是每次执行都对调用docker inspect获取最新 PID或者在循环脚本里重新解析容器对象。下面这个封装思路可以复用container_ss() { local name$1 local pid pid$(docker inspect -f {{.State.Pid}} $name) shift nsenter -t $pid -n $ } # 用法 container_ss mysql ss -tnp state established注意容器不存在或者 PID 为空时docker inspect会返回非零状态需要提前做判断避免 nsenter 拿到一个空值直接报错。这也是我踩过的坑容器名称写错了一个字母脚本还在跑每次输出都是错误信息浪费了不少时间才反应过来。5.2 host 网络模式根本不走 veth别去找虚拟网卡如果你查看容器的网络模式发现是 host那么问题就简单了容器和宿主机共享同一个网络 namespace。你不需要 nsenter都踢开后直接在宿主机上查看到的连接与容器内看到的完全一致。但这会带来另一个问题容易造成“误伤”。host 模式下容器进程监听的端口直接落在宿主机网卡上容器间连接也不经过 veth 和 docker0所以无法用“按网卡名过滤”的思路区分容器流量。这时候我更建议把观察维度改成进程名或者依赖 cgroup 相关的统计工具来划分。比如直接用nethogs、ss -p查看 PID 对应的进程归属。因此在开始排查前先确认容器的网络模式是一项必要检查docker inspect -f {{.HostConfig.NetworkMode}} 容器名如果输出是host那 veth 那套流程可以直接跳过。如果输出是bridge或default再按上面的 veth iftop 链路走。5.3 容器删除后 veth 可能会短暂残留属于正常现象容器停止或删除后对应的 veth 并不会立刻消失。内核销毁网络设备需要一点时间而且 docker 清理是有顺序的先移除容器内的 eth0再移除宿主机侧的 veth。如果你在一个自动化脚本里频繁创建、销毁容器又用 veth 名做缓存可能出现“这个网卡一会儿有一会儿没有”的情况。建议脚本里每次动态去找 veth不要缓存网卡名。虽然 eth0 对应的 iflink 会变但docker exec容器读取的当前值总是正确的。5.4 PID 复用造成的“幽灵 namespace”问题Linux 的 PID 是循环使用的。如果一个容器已经删除但宿主机上某个进程恰好复用了之前的 PID此时执行nsenter -t 旧PID -n是能成功的但进入的 namespace 是那个新进程的跟你要查的容器毫无关系。所以 nsenter 之前最好比对一下进程是否还属于那个容器。最稳妥的办法是配合docker top来看docker top mysql | head如果容器不存在docker top 会直接报错。如果容器存在你看到的 PID 就是主进程可以在执行 nsenter 前先确认 PID 没有被替换。对我来说最简单直觉的判断用 PID 反查进程名ps -p $PID -o comm如果得到的进程名和容器主进程不一致那就说明 PID 被复用了不要再用这个 PID 去做 nsenter。6. 监控不能只靠一次命令短期采样工具组合与长期数据落地的做法排查类任务做完后别忘了一件事网络问题往往是间歇性的。你花 30 分钟定位到了一个大流量的连接第二天可能又出现一个新问题。这时需要一套轻量的监控方式能定时记录容器的 TCP 连接数和接口带宽变化帮助你在问题发生时留下第一手证据。6.1 五分钟内快速观察容器连接数变化的组合命令不需要上 Prometheus一个简单的循环加 awk 就能完成短期观察。比如要观察某个容器过去的 20 个采样点里连接数变化for i in $(seq 1 20); do PID$(docker inspect -f {{.State.Pid}} target 2/dev/null) || { echo 容器不存在; break; } cnt$(nsenter -t $PID -n ss -tn state established | tail -n 2 | wc -l) echo $(date %H:%M:%S) established$cnt sleep 3 done如果想观察容器 eth0 接口的实时收发速率可以用cat /sys/class/net/eth0/statistics/rx_bytes取两次采样做差再除以时间间隔PID$(docker inspect -f {{.State.Pid}} target) nsenter -t $PID -n sh -c R1$(cat /sys/class/net/eth0/statistics/rx_bytes); sleep 5; R2$(cat /sys/class/net/eth0/statistics/rx_bytes); echo rx_rate$(( (R2 - R1) / 5 )) bytes/s不过/sys/class/net/eth0/statistics/统计的是网卡层收发总计包括了 TCP、UDP、ICMP 以及所有广播包。它适合看整体趋势不适合按连接分析。所以我会把这两样东西配合使用定时记录接口总速率一旦发现异常立刻用 iftop 抓连接明细。这种“粗粒度长期记录细粒度临时抓取”的组合是运维中成本最低的排查方式。6.2 按对端 IP 聚合连接数一条 awk 命令筛查“连接风暴”某些场景下问题不是单条连接带宽大而是某个 IP 建立了成千上万条连接耗尽容器端口和文件描述符。此时可以用 ss awk 按对端 IP 做二次聚合。以 Nginx 容器为例在容器 namespace 里执行nsenter -t $PID -n ss -tn state established ( sport :80 ) | tail -n 2 | awk {print $4} | awk -F: {print $1} | sort | uniq -c | sort -rn | head解释一下ss -tn的输出里第 4 列是本地地址:端口第 5 列是远端地址:端口。从 ESTABLISHED 连接中提取本地地址的 IP 部分并计数可以快速看到哪个客户端 IP 建立了最多连接。这里我以sport :80过滤只观察 80 端口的连接你也可以根据业务需求改成 443、3306 等。不过要注意一个现实情况当源端口是转换过的映射端口或者连接经过 NAT 时看到的远端 IP 可能不是真实客户端 IP。如果架构里前置了负载均衡器那么连接来源大概率是 LB 的内网 IP而不是用户公网 IP。这时候需要结合负载均衡的访问日志进一步分析别在容器侧死磕对端 IP 的来源问题。6.3 从手工排查到半自动化巡检一个小脚本的完整形态最后放一个适合日常巡检的半自动化脚本它融合了前面的方法判断容器网络模式、获取连接数 TOP 对端 IP、并输出容器 eth0 的接口速率。这个脚本本身不依赖第三方监控组件适合内网服务器少、又暂时不想搭整套监控系统的场景。#!/bin/bash # usage: ./container_net_check.sh 容器名 [采样秒数] name$1 interval${2:-5} pid$(docker inspect -f {{.State.Pid}} $name 2/dev/null) if [ -z $pid ]; then echo 容器不存在或无法获取 PID: $name exit 1 fi netmode$(docker inspect -f {{.HostConfig.NetworkMode}} $name 2/dev/null) echo 容器 $name (网络模式: $netmode) 主进程 PID: $pid echo --- 已建立的 TCP 连接数 --- nsenter -t $pid -n ss -tn state established 2/dev/null | tail -n 2 | wc -l echo --- ESTABLISHED 状态 TOP10 对端 IP --- nsenter -t $pid -n ss -tn state established 2/dev/null \ | tail -n 2 \ | awk {print $5} \ | sed s/:[0-9]*$// \ | sort | uniq -c | sort -rn | head -10 echo --- 容器 eth0 接口速率(采样 ${interval}s) --- nsenter -t $pid -n sh -c \ r1\$(cat /sys/class/net/eth0/statistics/rx_bytes); \ t1\$(cat /sys/class/net/eth0/statistics/tx_bytes); \ sleep $interval; \ r2\$(cat /sys/class/net/eth0/statistics/rx_bytes); \ t2\$(cat /sys/class/net/eth0/statistics/tx_bytes); \ echo \rx: \$(( (r2 - r1) / $interval )) B/s tx: \$(( (t2 - t1) / $interval )) B/s\这套方案整体非常轻不需要在容器里装任何东西宿主机上只需要 iproute2 和 docker CLI。你在排查完一次问题后把它写成一个固定脚本下次遇到类似告警一条命令就能先确认大方向再决定要不要上 iftop 精细分析。我自己的实践中还遇到过采样间隔设太短导致数据抖动明显的情况。比如 interval1 时 rx_bytes 差值可能受网卡中断合并影响而忽高忽低。建议至少在 5 秒以上更倾向于 10 秒这样拿到的速率曲线才稳定。网络问题排查本来就讲究“先定位再优化”把工具准备好并理解每一步命令的原理比临时翻文档高效得多。