1. 当带宽限制失效时,咱们在对抗什么?

某个周五下午,运维老张发现他精心配置的Docker Compose网络带宽限制突然失效了:明明在docker-compose.yml里写好了network段的driver_opts配置,但容器间传输速度还是飙到了千兆网卡上限。这种场景常见于:

  • 微服务密集部署:多个容器争夺同一物理网卡资源
  • CI/CD流水线:构建时大量镜像层传输导致网络拥堵
  • 多租户环境:需要保证不同业务组的网络公平性

以老张的案例为例,他用的是以下配置(技术栈:Docker 20.10 + Compose v2.15):

version: '3.8'
services:
  data_processor:
    image: alpine:3.18
    command: tail -f /dev/null
    networks:
      app_net:
        driver_opts:
          com.docker.network.tc.bandwidth: "10mbit"

  web_server:
    image: nginx:1.23
    networks:
      - app_net

networks:
  app_net:
    driver: bridge

然而通过iperf3测试容器间传输速率时,实际测得带宽仍高达940Mbps。接下来咱们一起看看问题出在哪。


2. 排查三板斧:从表象到根源

2.1 第一斧:确认TC规则是否生效

进入容器查看流量控制规则:

# 进入data_processor容器
docker exec -it data_processor sh

# 查看网卡队列规则(需安装tc命令)
apk add iproute2
tc qdisc show dev eth0

预期应该看到类似htb 1: root的层级结构,但实际输出是:

qdisc noqueue 0: dev eth0 root refcnt 2 

这说明流量控制规则根本没生效

2.2 第二斧:检查Docker网络驱动

查看网络详情:

docker network inspect app_net

关键字段输出:

"Options": {
    "com.docker.network.tc.bandwidth": "10mbit"
},
"Driver": "bridge"

问题浮现:bridge驱动本身不支持TC带宽限制,该参数仅适用于macvlan或自定义驱动

2.3 第三斧:物理网卡关联验证

查看宿主机的虚拟网卡:

# 查找与容器关联的veth设备
ip link | grep veth

# 检查宿主侧的TC规则
tc -s qdisc show dev vetha1b2c3d

发现宿主机侧也没有生成任何队列规则,印证了驱动不兼容的结论


3. 修复方案:让流量控制真正落地

3.1 正确使用macvlan驱动

修改docker-compose.yml:

networks:
  app_net:
    driver: macvlan
    driver_opts:
      parent: eth0  # 指定物理网卡
      com.docker.network.tc.bandwidth: "10mbit"

但此时启动会报错:

Error response from daemon: macvlan driver does not support tc bandwidth options

因为官方macvlan驱动也不支持带宽限制,需要改用第三方方案

3.2 使用自定义网络插件

安装带宽控制插件:

docker plugin install --grant-all-permissions gorilla/tc

调整配置:

networks:
  app_net:
    driver: gorilla/tc
    options:
      parent: eth0
      tc.ingress.rate: "10mbit"  # 入方向限制
      tc.egress.rate: "10mbit"   # 出方向限制

此时在容器内执行tc qdisc show,可以看到:

qdisc htb 1: dev eth0 root refcnt 2 r2q 10 default 1 direct_packets_stat 0 direct_qlen 1000

通过iperf3测试,实际带宽稳定在9.8Mbps左右


4. 关键原理:TC如何在Docker中工作

4.1 Linux流量控制体系
  • HTB(Hierarchical Token Bucket):分层令牌桶算法
  • Classful队列:支持多级带宽分配
  • Filter规则:基于IP/端口等条件分流

典型TC命令结构:

tc qdisc add dev eth0 root handle 1: htb default 12
tc class add dev eth0 parent 1: classid 1:1 htb rate 10mbit ceil 10mbit
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dst 172.20.0.3 flowid 1:1
4.2 Docker网络驱动差异
驱动类型 TC支持 适用场景 性能损耗
bridge 单机容器通信
macvlan 直连物理网络 最低
overlay 跨主机容器网络
gorilla/tc 需要QoS控制的场景 较高

5. 避坑指南:那些年我们踩过的雷

5.1 内核模块缺失

确保加载所需模块:

modprobe ifb  # 中间功能块设备
modprobe act_mirred  # 流量重定向
5.2 网络模式选择

避免使用host网络模式:

# 错误配置示例
services:
  bad_service:
    network_mode: host  # 会绕过所有TC规则
5.3 速率单位陷阱

正确使用单位后缀:

# 有效写法
tc.ingress.rate: "10mbit"  # 10兆比特/秒
tc.egress.rate: "1gbit"    # 1千兆比特/秒

# 无效写法
tc.ingress.rate: "10MBps"  # 插件可能无法解析大写单位

6. 扩展应用:当带宽限制遇上K8s

虽然本文聚焦Docker Compose,但Kubernetes的带宽限制方案可做对比:

apiVersion: v1
kind: Pod
metadata:
  name: limited-pod
spec:
  containers:
  - name: app
    image: nginx
    resources:
      limits:
        kubernetes.io/egress-bandwidth: "10M"

但K8s方案存在局限性:

  • 仅支持egress方向控制
  • 依赖CNI插件实现
  • 无法精细到端口级别

7. 总结:从现象到本质的修炼之路

通过这次故障排查,我们掌握了:

  1. Docker网络驱动的特性差异
  2. TC规则的实际生效条件
  3. 第三方插件的选型方法

最终的配置模板:

version: '3.8'
services:
  service_a:
    networks:
      qos_net:
        ipv4_address: 172.22.0.10

networks:
  qos_net:
    driver: gorilla/tc
    options:
      parent: eth0
      tc.ingress.rate: "20mbit"
      tc.egress.rate: "50mbit"
      tc.ingress.burst: "2mbit"  # 突发带宽设置

记住:当配置不生效时,沿着声明配置 → 驱动支持 → 内核实现的路径逐步验证,终能找到突破口。下次遇到类似问题,愿你也能像老张一样从容破解!