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-olinkshow|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-oaddr show|awk'{print $2, $3, $4}'lo inet127.0.0.1/8 eth0 inet192.168.0.230/24 docker0 inet172.17.0.1/16 $iproute show default via192.168.0.1 dev eth0 proto dhcp src192.168.0.230 metric100192.168.0.0/24 dev eth0 proto kernel scopelinksrc192.168.0.230 metric100172.17.0.0/16 dev docker0 proto kernel scopelinksrc172.17.0.1 linkdown1.2 新建一个 netns(ns1)
$ipnetnsaddns1 $ipnetnsexecns1ip-olinkshow|awk'{print $2, $9}'lo: DOWN $ipnetnsexecns1ip-oaddr show# (空,连回环地址都没有)$ipnetnsexecns1iproute show# (空,没有任何路由)新建的 netns 里一片空白:没有 eth0、没有 IP、没有路由,连lo都是 DOWN。这就是「网络隔离」——容器一启动,看到的也是这样一个干净的、与宿主机完全独立的网络世界。
1.3 给 ns1 接一根 veth,验证「跨命名空间看不见」
$iplinkaddveth-ns1typeveth peer name veth-host $iplinksetveth-ns1 netns ns1 $ipnetnsexecns1ipaddradd10.0.0.2/24 dev veth-ns1 $ipnetnsexecns1iplinksetveth-ns1 up $ipaddradd10.0.0.1/24 dev veth-host $iplinksetveth-host up# 宿主机能看到 veth-host,但看不到 veth-ns1$ip-olinkshow|grepveth-host4: veth-host@if5:<...UP...>link/ether ca:60:4b:78:a7:5c... link-netns ns1# ns1 里能看到 veth-ns1,但看不到 veth-host$ipnetnsexecns1ip-olinkshow|grepveth5: veth-ns1@if4:<...UP...>link/ether aa:d9:b0:b3:11:0e... link-netnsid0# ns1 内能 ping 通宿主端$ipnetnsexecns1ping-c1-W210.0.0.1|tail-1rtt min/avg/max/mdev=0.046/0.046/0.046/0.000 ms# 宿主机的路由表里,根本不会出现 ns1 的 10.0.0.2$iproute show|grep10.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 先记录宿主机原始值
$forpinnet.ipv4.tcp_keepalive_time net.core.somaxconn\net.ipv4.ip_forward net.ipv4.tcp_max_syn_backlog;doecho"$p=$(cat/proc/sys/${p/./\/})"donenet.ipv4.tcp_keepalive_time=7200net.core.somaxconn=4096net.ipv4.ip_forward=0net.ipv4.tcp_max_syn_backlog=10242.2 在宿主机改值,观察 testns 是否跟随
$ipnetnsaddtestns# testns 初始值与宿主机一致(创建时拷贝而来)# 宿主机改掉:$sysctl-wqnet.ipv4.tcp_keepalive_time=12345$sysctl-wqnet.core.somaxconn=1234$sysctl-wqnet.ipv4.ip_forward=1$sysctl-wqnet.ipv4.tcp_max_syn_backlog=123# 对比:net.ipv4.tcp_keepalive_time|host=12345testns=7200net.core.somaxconn|host=1234testns=4096net.ipv4.ip_forward|host=1testns=0net.ipv4.tcp_max_syn_backlog|host=123testns=1024宿主机改了,testns 纹丝不动 —— 说明这几个参数是真·per-netns,每个 netns 各持一份拷贝。
2.3 反向验证:在 testns 里改,宿主机不受影响
$ipnetnsexectestnssysctl-wqnet.ipv4.tcp_keepalive_time=99999$ipnetnsexectestnssysctl-wqnet.core.somaxconn=9999tcp_keepalive_time|host=12345testns=99999somaxconn|host=1234testns=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项缺失)直接读一个全局参数试试:
$ipnetnsexect2cat/proc/sys/net/core/wmem_max cat: /proc/sys/net/core/wmem_max: No suchfileor 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 参数——可读$dockerrun--rmbusyboxcat/proc/sys/net/ipv4/tcp_keepalive_time7200# B) 容器内直接写——失败$dockerrun--rmbusyboxsh-c"echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time"sh: line0: can't create /proc/sys/net/ipv4/tcp_keepalive_time: Read-onlyfilesystem# C) 看挂载属性$dockerrun--rmbusyboxsh-c"mount | grep proc/sys"proc on /proc/systypeproc(ro,nosuid,nodev,noexec,relatime)proc on /proc/sysrq-triggertypeproc(ro,nosuid,nodev,noexec,relatime)注意那个ro——/proc/sys在容器里是 read-only 挂载。这就是为什么echo >报Read-only file system(而不是Permission denied)。
顺带验证全局参数在容器内同样不可见:
$dockerrun--rmbusyboxsh-c"cat /proc/sys/net/core/wmem_max"cat: can't open '/proc/sys/net/core/wmem_max': No suchfileor directory和自建 netns 的行为完全一致——Docker 容器本质就是一个由 runc 创建的、挂载了只读 /proc/sys 的 netns。
4. 解决方案实测:–sysctl 注入
Docker 在创建容器时通过--sysctl把指定参数写进新 netns 的 /proc/sys,此时挂载还没变成 ro,所以能生效。
# E) --sysctl 注入生效验证$dockerrun--sysctlnet.ipv4.tcp_keepalive_time=600\--rmbusyboxsh-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_timevalue:"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.hstructnet{...structctl_table_header*sysctls;// 该 netns 的 /proc/sys 根...structnetns_corecore;// 含 per-netns 的 somaxconn 等structnetns_ipv4ipv4;// 含 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 一段一段定位断点。