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

发布时间:2026/7/24 13:42:02
Network Namespace:容器里改 /proc/sys/net 参数为什么不生效? Network Namespace容器里改 /proc/sys/net 参数为什么不生效实验环境Ubuntu 24.04内核 6.8.0-106-genericDocker 29.1.3Cgroup v2。所有命令均在实验机ecs-a8bb-0004上以 root 执行只操作 docker0 / veth / 自建 netns未触碰 eth0 与主路由。0. 一个让人困惑的现象很多同学遇到过这样的场景宿主机上sysctl -w net.ipv4.tcp_keepalive_time600明明成功了进到容器里一看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 Namespacenetns是 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 新建一个 netnsns1$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-hostif5:...UP...link/ether ca:60:4b:78:a7:5c... link-netns ns1# ns1 里能看到 veth-ns1但看不到 veth-host$ipnetnsexecns1ip-olinkshow|grepveth5: veth-ns1if4:...UP...link/ether aa:d9:b0:b3:11:0e... link-netnsid0# ns1 内能 ping 通宿主端$ipnetnsexecns1ping-c1-W210.0.0.1|tail-1rtt min/avg/max/mdev0.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绝大部分是全局的、只在初始 netnsinit_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_time7200net.core.somaxconn4096net.ipv4.ip_forward0net.ipv4.tcp_max_syn_backlog10242.2 在宿主机改值观察 testns 是否跟随$ipnetnsaddtestns# testns 初始值与宿主机一致创建时拷贝而来# 宿主机改掉$sysctl-wqnet.ipv4.tcp_keepalive_time12345$sysctl-wqnet.core.somaxconn1234$sysctl-wqnet.ipv4.ip_forward1$sysctl-wqnet.ipv4.tcp_max_syn_backlog123# 对比net.ipv4.tcp_keepalive_time|host12345testns7200net.core.somaxconn|host1234testns4096net.ipv4.ip_forward|host1testns0net.ipv4.tcp_max_syn_backlog|host123testns1024宿主机改了testns 纹丝不动 —— 说明这几个参数是真·per-netns每个 netns 各持一份拷贝。2.3 反向验证在 testns 里改宿主机不受影响$ipnetnsexectestnssysctl-wqnet.ipv4.tcp_keepalive_time99999$ipnetnsexectestnssysctl-wqnet.core.somaxconn9999tcp_keepalive_time|host12345testns99999somaxconn|host1234testns9999双向独立确认无误。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_maxxxx去设置它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-cecho 600 /proc/sys/net/ipv4/tcp_keepalive_timesh: line0: cant create /proc/sys/net/ipv4/tcp_keepalive_time: Read-onlyfilesystem# C) 看挂载属性$dockerrun--rmbusyboxsh-cmount | grep proc/sysproc 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-ccat /proc/sys/net/core/wmem_maxcat: cant 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_time600\--rmbusyboxsh-ccat /proc/sys/net/ipv4/tcp_keepalive_time600从 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 --sysctlK8s 用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 一段一段定位断点。