欢迎光临
我们一直在努力

Network Namespace:容器里改 /proc/sys/net 参数为什么不生效?

Network Namespace:容器里改 /proc/sys/net 参数为什么不生效?

实验环境:Ubuntu 24.04,内核 6.8.0-106-generic,Docker 29.1.3,Cgroup v2。
所有命令均在实验机(ecs-a8bb-0004)上以 root 执行;只操作 docker0 / veth / 自建 netns,未触碰 eth0 与主路由。

0. 一个让人困惑的现象

很多同学遇到过这样的场景:

  • 宿主机上 sysctl -w net.ipv4.tcp_keepalive_time=600 明明成功了;
  • 进到容器里一看,cat /proc/sys/net/ipv4/tcp_keepalive_time 还是 7200;
  • 在容器里直接 echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time,报 Read-only file system;
  • 更诡异的是,想在容器里调大 net.core.wmem_max,结果 cat 直接提示 No such file or directory。

这到底是 Docker 的 bug,还是容器“偷”了你的配置?本文用一组真实实验讲清楚三件事:

  • Network Namespace 到底隔离了哪些网络资源;
  • /proc/sys/net 下的参数,哪些是真的「每命名空间一份」,哪些是「全局唯一、容器内根本看不到」;
  • 容器内改不了的根本原因,以及 –sysctl 为什么能解决。

  • 1. Network Namespace 把网络资源隔成了两份

    Network Namespace(netns)是 Linux 七种 namespace 之一,它把「网络设备、IP 地址、路由表、端口、以及 /proc/sys/net 下的部分内核参数」打包成一套独立的视图。每个 netns 之间互不干扰。

    我们用 ip netns 手工建一个命名空间,直接和宿主机对比。

    1.1 宿主机的网络视图

    # 宿主机
    $ ip -o link show | awk '{print $2, $9, $17}'
    lo: UNKNOWN 00:00:00:00:00:00
    eth0: UP fa:16:3e:2c:84:6a
    docker0: DOWN brd

    $ ip -o addr show | awk '{print $2, $3, $4}'
    lo inet 127.0.0.1/8
    eth0 inet 192.168.0.230/24
    docker0 inet 172.17.0.1/16

    $ ip route show
    default via 192.168.0.1 dev eth0 proto dhcp src 192.168.0.230 metric 100
    192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.230 metric 100
    172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown

    1.2 新建一个 netns(ns1)

    $ ip netns add ns1

    $ ip netns exec ns1 ip -o link show | awk '{print $2, $9}'
    lo: DOWN

    $ ip netns exec ns1 ip -o addr show
    # (空,连回环地址都没有)

    $ ip netns exec ns1 ip route show
    # (空,没有任何路由)

    新建的 netns 里一片空白:没有 eth0、没有 IP、没有路由,连 lo 都是 DOWN。这就是「网络隔离」——容器一启动,看到的也是这样一个干净的、与宿主机完全独立的网络世界。

    1.3 给 ns1 接一根 veth,验证「跨命名空间看不见」

    $ ip link add veth-ns1 type veth peer name veth-host
    $ ip link set veth-ns1 netns ns1
    $ ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth-ns1
    $ ip netns exec ns1 ip link set veth-ns1 up
    $ ip addr add 10.0.0.1/24 dev veth-host
    $ ip link set veth-host up

    # 宿主机能看到 veth-host,但看不到 veth-ns1
    $ ip -o link show | grep veth-host
    4: veth-host@if5: <...UP...> link/ether ca:60:4b:78:a7:5c ... link-netns ns1

    # ns1 里能看到 veth-ns1,但看不到 veth-host
    $ ip netns exec ns1 ip -o link show | grep veth
    5: veth-ns1@if4: <...UP...> link/ether aa:d9:b0:b3:11:0e ... link-netnsid 0

    # ns1 内能 ping 通宿主端
    $ ip netns exec ns1 ping -c1 -W2 10.0.0.1 | tail -1
    rtt min/avg/max/mdev = 0.046/0.046/0.046/0.000 ms

    # 宿主机的路由表里,根本不会出现 ns1 的 10.0.0.2
    $ ip route show | grep 10.0.0.2 || echo "(未出现在宿主路由表)"
    (未出现在宿主路由表)

    结论:网络资源是跟着 netns 走的。一根 veth 的两个端分别属于不同 netns,各自只看见自己那一半;路由表也是每命名空间独立。这正是 Docker 给每个容器分配独立 IP、彼此网络不通的底层原因。


    2. /proc/sys/net 下的参数,哪些是「每 netns 一份」?

    这是全文最关键、也最容易被误解的一点。普遍认知是「net.* 都是 per-netns 的」,但真相是:只有一小部分参数是 per-netns,绝大部分是全局的、只在初始 netns(init_net)里存在。

    我们建一个测试 netns,把一批参数在宿主机改掉,再看 netns 里变不变。

    2.1 先记录宿主机原始值

    $ for p in net.ipv4.tcp_keepalive_time net.core.somaxconn \\
    net.ipv4.ip_forward net.ipv4.tcp_max_syn_backlog; do
    echo "$p = $(cat /proc/sys/${p/./\\/})"
    done
    net.ipv4.tcp_keepalive_time = 7200
    net.core.somaxconn = 4096
    net.ipv4.ip_forward = 0
    net.ipv4.tcp_max_syn_backlog = 1024

    2.2 在宿主机改值,观察 testns 是否跟随

    $ ip netns add testns
    # testns 初始值与宿主机一致(创建时拷贝而来)
    # 宿主机改掉:
    $ sysctl -wq net.ipv4.tcp_keepalive_time=12345
    $ sysctl -wq net.core.somaxconn=1234
    $ sysctl -wq net.ipv4.ip_forward=1
    $ sysctl -wq net.ipv4.tcp_max_syn_backlog=123

    # 对比:
    net.ipv4.tcp_keepalive_time | host=12345 testns=7200
    net.core.somaxconn | host=1234 testns=4096
    net.ipv4.ip_forward | host=1 testns=0
    net.ipv4.tcp_max_syn_backlog| host=123 testns=1024

    宿主机改了,testns 纹丝不动 —— 说明这几个参数是真·per-netns,每个 netns 各持一份拷贝。

    2.3 反向验证:在 testns 里改,宿主机不受影响

    $ ip netns exec testns sysctl -wq net.ipv4.tcp_keepalive_time=99999
    $ ip netns exec testns sysctl -wq net.core.somaxconn=9999

    tcp_keepalive_time | host=12345 testns=99999
    somaxconn | host=1234 testns=9999

    双向独立,确认无误。per-netns 参数清单(本文实测):net.ipv4.tcp_keepalive_time、net.core.somaxconn、net.ipv4.ip_forward、net.ipv4.tcp_max_syn_backlog 等。

    2.4 更大的真相:绝大多数 net.* 在容器里「根本不存在」

    上面的实验里只试了几个「能 per-netns」的参数。如果我们对比宿主机和 netns 里 /proc/sys/net/core/ 与 /proc/sys/net/ipv4/ 整个目录,会发现大量参数在 netns 里直接缺失:

    # /proc/sys/net/core 在 host 有 41 项,在 testns 只有 7 项:
    [host core]: bpf_jit_enable bpf_jit_harden bpf_jit_kallsyms bpf_jit_limit busy_poll
    busy_read default_qdisc dev_weight ... rmem_default rmem_max somaxconn
    wmem_default wmem_max ...(共 41 项)
    [t2 core]: optmem_max rps_default_mask somaxconn txrehash xfrm_acq_expires
    xfrm_aevent_etime xfrm_aevent_rseqth (仅 7 项)

    # testns 里缺失的 core 参数(只在 init_net 存在,属全局):
    bpf_jit_enable bpf_jit_harden bpf_jit_kallsyms bpf_jit_limit busy_poll busy_read
    default_qdisc dev_weight ... rmem_max wmem_max ...(共 34 项缺失)

    # /proc/sys/net/ipv4 在 testns 缺失的(全局/只读):
    tcp_mem udp_mem tcp_max_orphans tcp_low_latency
    icmp_msgs_per_sec icmp_msgs_burst inet_peer_threshold ...(共 14 项缺失)

    直接读一个全局参数试试:

    $ ip netns exec t2 cat /proc/sys/net/core/wmem_max
    cat: /proc/sys/net/core/wmem_max: No such file or directory

    所以「容器里改不了 net.core.wmem_max」的真正原因,不是权限问题,而是这个文件在容器的 netns 里压根没被创建——它是全局参数,只注册在 init_net 上。docker 里你甚至无法用 –sysctl net.core.wmem_max=xxx 去设置它(Docker 会拒绝,因为该 key 不在 per-netns 白名单中)。

    经验法则:per-netns 的通常是「协议栈行为类」参数(keepalive、somaxconn、forward、syn_backlog、tcp_rmem/wmem 等);全局的通常是「资源/设备类」参数(bpf_jit、netdev_*、tcp_mem/udp_mem 内存上限、wmem_max/rmem_max 等)。前者能在容器里各自调,后者必须在宿主机(init_net)层面统一调。


    3. 容器内改不了 /proc/sys 的「直接原因」:只读挂载

    即便参数是 per-netns 的(比如 tcp_keepalive_time,容器里看得到、也确实是独立一份),普通容器里照样写不进去。直接原因是:Docker 把 /proc/sys 以只读方式挂载进了容器。

    # A) 容器内读 per-netns 参数——可读
    $ docker run –rm busybox cat /proc/sys/net/ipv4/tcp_keepalive_time
    7200

    # B) 容器内直接写——失败
    $ docker run –rm busybox sh -c "echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time"
    sh: line 0: can't create /proc/sys/net/ipv4/tcp_keepalive_time: Read-only file system

    # C) 看挂载属性
    $ docker run –rm busybox sh -c "mount | grep proc/sys"
    proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)
    proc on /proc/sysrq-trigger type proc (ro,nosuid,nodev,noexec,relatime)

    注意那个 ro —— /proc/sys 在容器里是 read-only 挂载。这就是为什么 echo > 报 Read-only file system(而不是 Permission denied)。

    顺带验证全局参数在容器内同样不可见:

    $ docker run –rm busybox sh -c "cat /proc/sys/net/core/wmem_max"
    cat: can't open '/proc/sys/net/core/wmem_max': No such file or directory

    和自建 netns 的行为完全一致——Docker 容器本质就是一个由 runc 创建的、挂载了只读 /proc/sys 的 netns。


    4. 解决方案实测:–sysctl 注入

    Docker 在创建容器时通过 –sysctl 把指定参数写进新 netns 的 /proc/sys,此时挂载还没变成 ro,所以能生效。

    # E) –sysctl 注入生效验证
    $ docker run –sysctl net.ipv4.tcp_keepalive_time=600 \\
    –rm busybox sh -c "cat /proc/sys/net/ipv4/tcp_keepalive_time"
    600

    从 7200 变成 600,注入成功,且只影响这一个容器自己的 netns,宿主机和其他容器不受影响(呼应第 2 节的 per-netns 特性)。

    Kubernetes 对应做法:在 Pod 的 securityContext 里用 sysctls 字段:

    securityContext:
    sysctls:
    – name: net.ipv4.tcp_keepalive_time
    value: "600"

    注意 K8s 只允许「安全的 per-netns sysctl」(如 net.ipv4.*、net.core.somaxconn 等);全局 sysctl(如 net.core.wmem_max)需要节点级 kubelet 配置 –allowed-unsafe-sysctls 才能下发,且影响整台节点。


    5. 原理:netns 里的 sysctl 表从哪来?

    为什么「有的参数 per-netns,有的全局」?答案在内核数据结构里。

    每个网络命名空间是一个 struct net:

    // include/net/net_namespace.h
    struct net {
    ...
    struct ctl_table_header *sysctls; // 该 netns 的 /proc/sys 根
    ...
    struct netns_core core; // 含 per-netns 的 somaxconn 等
    struct netns_ipv4 ipv4; // 含 per-netns 的 tcp_keepalive_time 等
    ...
    };

    内核启动时,各个子系统通过 register_pernet_subsys() / register_pernet_device() 注册「每 netns 初始化回调」。例如 TCP 协议栈的 tcp_init_net() 会为每一个新建的 netns 调用 register_net_sysctl(),把 net.ipv4.tcp_* 的一套 ctl_table 挂到该 netns 自己的 proc 目录下——这就形成了「每命名空间一份」的拷贝,所以你在容器里改 tcp_keepalive_time 不会动到宿主机。

    而像 net.core.wmem_max、net.ipv4.tcp_mem 这类,它们的 ctl_table 只在 init_net(初始命名空间) 上注册一次,没有对应的 per-net 初始化回调。因此非 init netns 的 proc 目录里压根不生成这些文件——这就是第 2.4 节 No such file or directory 的来由。

    为什么 Docker 不给 /proc/sys 写权限? 表面上是安全(避免容器篡改内核网络行为影响宿主机),但根本约束是「容器与宿主机共享同一个内核」。允许任意写 /proc/sys,意味着容器能改掉全局参数(如 net.ipv4.ip_forward、conntrack 表项上限),那就会越界影响宿主机和其他容器。所以 Docker 的设计是:

    • 默认把 /proc/sys 挂成 ro,杜绝运行时篡改;
    • 只开放一个创建期的白名单通道 –sysctl,且只放行确实 per-netns 的 key;
    • 全局参数一律不让容器碰,必须回到宿主机(init_net)调。

    6. 小结与排障清单

    • netns 隔离了:网络设备、IP、路由、端口、以及一部分 per-netns 的内核参数。
    • per-netns 参数(容器内独立、可 –sysctl 设置):net.ipv4.tcp_keepalive_time、net.core.somaxconn、net.ipv4.ip_forward、net.ipv4.tcp_max_syn_backlog 等。
    • 全局参数(容器内看不到、不能改,只在宿主机 init_net 生效):net.core.wmem_max、net.core.rmem_max、net.ipv4.tcp_mem、net.ipv4.udp_mem、net.ipv4.tcp_max_orphans、bpf_jit_*、netdev_* 等。
    • 容器内写不进 /proc/sys 的直接原因:/proc/sys 被以 ro 挂载;echo > 报 Read-only file system。
    • 正确的改法:单容器用 docker run –sysctl;K8s 用 securityContext.sysctls;全局参数只能上宿主机改。

    排障时请先分清你要改的参数是 per-netns 还是全局:

  • 进容器 ls /proc/sys/net/<sub> 看文件在不在 → 不在就是全局参数,容器里无解,回宿主机改;
  • 文件在但不能写 → 确认是 ro 挂载,用 –sysctl;
  • –sysctl 报 “invalid sysctl” / “not whitelisted” → 该 key 是全局的,Docker 拒绝,必须宿主机层面调;
  • 改完用 docker exec <c> cat /proc/sys/… 验证确实生效、且只在本容器内。

  • 下一篇:《容器网络不通怎么调试?veth 对与网桥的完整排查路径》—— 我们会把 nginx 容器跑起来,再人为制造 3 种故障,用 tcpdump 一段一段定位断点。

    赞(0)
    未经允许不得转载:171主机测评 » Network Namespace:容器里改 /proc/sys/net 参数为什么不生效?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址