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,还是容器“偷”了你的配置?本文用一组真实实验讲清楚三件事:
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 还是全局:
下一篇:《容器网络不通怎么调试?veth 对与网桥的完整排查路径》—— 我们会把 nginx 容器跑起来,再人为制造 3 种故障,用 tcpdump 一段一段定位断点。



