Docker涉及的Namespace
01 · Namespace:隔离视野
重要程度:★★★★★ 必学 | 适用角色:全部 | 前置:专题索引
一、为什么必须学
Namespace 是容器隔离的第一层,也是唯一实现"看不见"这层效果的机制。不懂它,下面这些坑会反复踩:
坑 1:容器里 ps 只有几个进程,宿主机上却是一堆
容器内: ps → 1, 7, 22, 29, 30, 31, 32 (7 个)
宿主机: 110 个进程,其中 6 个属于这个容器
很多人以为容器"精简",其实是 PID namespace 重新编号造成的错觉。宿主机上 PID 700000+ 的进程,在容器内可能是 PID 3。
坑 2:docker stop 后退出码 137,数据没落盘
容器里跑的进程是容器内的 PID 1,而 Linux 内核对 PID 1 有特殊保护:默认忽略 SIGTERM。docker stop 等 10 秒后发 SIGKILL 强杀,退出码 137,应用还没来得及写盘。
这是生产环境最常见的"数据 mysteriously 丢了"的原因。 不懂 PID 1 特性,永远查不出来。
坑 3:以为容器有独立内核
容器没有自己的内核,用的是宿主机的。证据是容器内 uname -a 的内核版本和构建时间戳与宿主机完全一致。这决定了容器不能跑不同内核版本的程序,也决定了隔离强度的上限。
坑 4:容器里改了文件,会不会影响宿主机?
会——如果那个路径在宿主机上真实存在。Mount namespace 决定"同一个路径在不同 namespace 下指向不同 inode"。
二、理论:namespace 是什么
Linux 内核为每个进程维护一组资源视图。namespace 就是把这组视图"分叉"出去,让子进程看到一份独立副本。
内核中 namespace 用 inode 号标识,存放于 /proc/<pid>/ns/。
七种 namespace
| 类型 |
隔离对象 |
容器内的效果 |
| PID |
进程号、进程树 |
容器内进程从 1 重新编号 |
| Mount |
挂载点、文件系统树 |
独立的 / 和挂载表 |
| Network |
网卡、IP、路由、端口 |
独立 eth0、独立路由表 |
| UTS |
主机名、domain name |
独立 hostname |
| IPC |
共享内存、信号量、消息队列 |
独立 IPC 命名空间 |
| User |
UID / GID 映射 |
root 被映射(Docker 默认未启用) |
| Cgroup |
cgroup 层级 |
独立资源视图 |
| Time |
虚拟时钟 |
时区/时间偏移隔离(Linux 5.6+) |
关键认知:namespace 只管"看不见"
namespace 不限制资源用量。容器能看到宿主机有多少内存,照样能全部吃掉。资源限制是 cgroup 的事(见 03 Cgroup)。
namespace 也不等于权限控制。容器内是 root 也不代表能 mount(见 04 Capabilities)。
三、实操验证
3.1 宿主机 namespace 基线
lsns --no-headings
4026531835 cgroup 102 1 root /usr/lib/systemd/systemd --switched-root
4026531836 pid 102 1 root /usr/lib/systemd/systemd --switched-root
4026531837 user 102 1 root /usr/lib/systemd/systemd --switched-root
4026531838 uts 99 1 root /usr/lib/systemd/systemd --switched-root
4026531839 ipc 102 1 root /usr/lib/systemd/systemd --switched-root
4026531840 net 102 1 root /usr/lib/systemd/systemd --switched-root
4026531841 mnt 92 1 root /usr/lib/systemd/systemd --switched-root
这是宿主机 init(PID 1)的 namespace。所有宿主进程默认继承这一套。
3.2 核心实验:容器内外 inode 对照
docker run -d --name ns-lab alpine:3.20 sh -c 'while true; do sleep 30; done'
CPID=$(docker inspect ns-lab --format '{{.State.Pid}}')
# 宿主机视角
for ns in mnt pid net uts ipc user cgroup; do
printf "%-9s %-22s %-22s %s\n" "$ns" \
"$(readlink /proc/self/ns/$ns)" \
"$(readlink /proc/$CPID/ns/$ns)" \
"$([ "$(readlink /proc/self/ns/$ns)" = "$(readlink /proc/$CPID/ns/$ns)" ] && echo 共享 || echo 独立)"
done
实测输出:
类型 宿主机 容器 是否独立
mnt mnt:[4026531841] mnt:[4026532323] 独立
pid pid:[4026531836] pid:[4026532326] 独立
net net:[4026531840] net:[4026532328] 独立
uts uts:[4026531838] uts:[4026532324] 独立
ipc ipc:[4026531839] ipc:[4026532325] 独立
user user:[4026531837] user:[4026531837] 共享
cgroup cgroup:[4026531835] cgroup:[4026532327] 独立
这张表是本文档最重要的证据。 六种 namespace 被独立创建,唯独 user namespace 与宿主机共享。
3.3 容器内部视角交叉验证
docker exec ns-lab sh -c 'for ns in mnt pid net uts ipc user cgroup; do
printf " %-9s %s\n" "$ns" "$(readlink /proc/self/ns/$ns)"
done'
mnt mnt:[4026532323]
pid pid:[4026532326]
net net:[4026532328]
uts uts:[4026532324]
ipc ipc:[4026532325]
user user:[4026531837] ← 和宿主机一样
cgroup cgroup:[4026532327]
容器内外读到完全相同的 inode——同一个 namespace,从两侧看是同一个对象。
3.4 PID namespace:重新编号的实证
docker exec ns-lab sh -c 'ls /proc | grep -E "^[0-9]+$" | sort -n | tr "\n" " "; echo'
1 7 22 29 30 31 32
宿主机侧的对应关系:
CPID=$(docker inspect ns-lab --format '{{.State.Pid}}')
ps -o pid,ppid,comm --ppid $CPID
PID PPID COMMAND
779980 779958 sleep
echo "宿主 PID 数量: $(ls /proc | grep -cE '^[0-9]+$')"
echo "容器 PID 数量: $(docker exec ns-lab sh -c 'ls /proc | grep -cE "^[0-9]+$"')"
宿主 PID 数量: 110
容器 PID 数量: 5
宿主机 110 个,容器内只有 5 个。 容器内 PID 1 对应宿主机 PID 779958。
PID 1 的特殊地位
docker exec ns-lab sh -c 'cat /proc/1/comm; echo "cmdline:"; tr "\0" " " < /proc/1/cmdline; echo'
sh
cmdline:
sh -c while true; do sleep 30; done
配置的启动进程就是容器内的 PID 1。PID 1 在 Linux 内核中有两项特权:
- 默认不处理 SIGTERM —— 内核对 PID 1 屏蔽了非 SIGKILL 信号的默认处理
- 是孤儿进程的收养者 —— 容器内所有僵尸进程由 PID 1 回收
坑 2 的实证:SIGTERM 被忽略
docker run -d --name sig-test alpine:3.20 sh -c 'while true; do sleep 1; done'
sleep 2
docker stop -t 3 sig-test
docker inspect sig-test --format '退出码: {{.State.ExitCode}}'
退出码: 137 (137 = 128 + 9 = SIGKILL)
SIGTERM 被忽略,3 秒后被 SIGKILL 强杀。
正确处理信号后:
docker run --rm --name sig-ok alpine:3.20 sh -c \
'trap "echo 收到SIGTERM,优雅退出; exit 0" TERM; while true; do sleep 1; done' &
sleep 3
docker stop -t 5 sig-ok
收到SIGTERM,优雅退出
sig-ok
生产建议:应用必须显式处理 SIGTERM,收到后停止接收新请求 → 完成存量请求 → 关闭数据库连接 → 退出。Python 用 signal.signal(signal.SIGTERM, handler),Node.js 监听 SIGTERM 事件,docker stop -t 30 给足清理时间。
3.5 Mount namespace:同一路径不同视图
docker exec ns-lab sh -c 'ls / | tr "\n" " "'
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
宿主机根目录:
ls / | tr '\n' ' '
afs bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv swap sys tmp usr var vz
差异清晰:
| 目录 |
宿主机 |
容器 |
说明 |
boot |
有 |
无 |
内核模块目录,容器不需要 |
lib64 |
有 |
无 |
宿主兼容链接 |
lost+found |
有 |
无 |
fsck 恢复目录 |
swap |
有 |
无 |
swap 文件 |
vz |
有 |
无 |
VirtIO 挂载点 |
最直观的对比——同一个 /opt 路径:
CPID=$(docker inspect ns-lab --format '{{.State.Pid}}')
echo "宿主机视角 (走容器 /proc/PID/root): [$(ls /proc/$CPID/root/opt 2>&1 | tr '\n' ' ')]"
echo "宿主机自己的 /opt: [$(ls /opt | tr '\n' ' ')]"
echo "容器内 /opt: [$(docker exec ns-lab sh -c 'ls /opt' 2>&1 | tr '\n' ' ')]"
宿主机视角 (走容器 /proc/PID/root): []
宿主机自己的 /opt: [containerd meme-alpha-engine ]
容器内 /opt: []
同一个 /opt 路径,宿主机有两个目录,容器看到的是空的。 这就是 Mount namespace:每个 namespace 有独立的挂载表,同一路径在不同 namespace 下指向不同 inode。
3.6 UTS namespace:独立主机名
echo "宿主 hostname: $(hostname)"
echo "容器 hostname: $(docker exec ns-lab hostname)"
docker run --rm --hostname custom-node alpine:3.20 hostname
宿主 hostname: fine-bytes-2.localdomain
容器 hostname: 54bcc47f3e44
--hostname 定制: custom-node
宿主主机名完全不受影响。
3.7 IPC namespace:共享内存隔离
ipcmk -M 2048
SHMID=$(ipcs -m | awk '$2 ~ /^[0-9]+$/ {print $2}' | head -1)
echo "宿主机可见段数: $(ipcs -m | awk '$2 ~ /^[0-9]+$/' | wc -l)"
echo "容器内可见段数: $(docker exec ns-lab sh -c 'ipcs -m | awk "\$2 ~ /^[0-9]+$/" | wc -l')"
ipcrm -m $SHMID
宿主机可见段数: 1
容器内可见段数: 0
宿主 1 个,容器 0 个。 同一个共享内存段,宿主机进程能看到,容器进程完全看不到。
原理:每个 IPC 对象都记录了它所属的 namespace ID,内核查找时只匹配同 namespace 的对象。
3.8 Network namespace:独立网络栈
ip -br addr show # 宿主机
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 104.244.90.106/22 fe80::a8aa:ff:fe21:7df0/64
docker0 UP 172.17.0.1/16 fe80::d062:1bff:fe14:e231/64
vethaac3e18@if2 UP fe80::d046:3aff:fe75:f49b/64
容器内(busybox 的 ip 不支持 -br,用完整格式):
docker exec ns-lab sh -c 'ip addr show eth0'
2: eth0@if209: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 92:50:9b:36:2f:6f brd ff:ff:ff:ff:ff:ff
inet 172.17.0.6/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
容器看不到宿主机的 eth0(物理网卡,真实 IP 104.244.90.106),只有自己的 eth0。
容器内路由表:
docker exec ns-lab sh -c 'ip route'
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.6
3.9 User namespace:Docker 默认未启用
这是最容易误解的一点,用两个证据确认。
证据一:inode 相同(见 3.2,user 那行是"共享")
证据二:uid_map 相同
docker exec ns-lab sh -c 'cat /proc/self/uid_map'
cat /proc/self/uid_map
容器内: 0 0 4294967295
宿主机: 0 0 4294967295
完全一致 = 没有做任何 UID 映射。
含义:
- 容器内的 root(uid=0)就是宿主机的 root(uid=0)
- 容器内
id 显示 uid=0(root),但真实身份是宿主 root
- 隔离靠的是 capability 裁剪(见 04),不是 UID 映射
如果启用 user namespace(docker run --userns-remap),容器内的 root 会被映射成宿主机上的非特权 UID,即使拿到 CAP_SYS_ADMIN 也无法操作宿主资源——这是更强的隔离。
3.10 Time namespace(Linux 5.6+)
ls -l /proc/$CPID/ns/ | awk '{print " "$9" -> "$10" "$11}'
cgroup -> -> cgroup:[4026532707]
ipc -> -> ipc:[4026532705]
mnt -> -> mnt:[4026532703]
net -> -> net:[4026532708]
pid -> -> pid:[4026532706]
pid_for_children -> -> pid:[4026532706]
time -> -> time:[4026532763]
time_for_children -> -> time:[4026532763]
user -> -> user:[4026531837]
uts -> -> uts:[4026532704]
time 和 time_for_children 也被独立了。Time namespace 用于隔离单调时钟和虚拟时钟偏移,主要服务于容器内的时间敏感逻辑(如计费、重试退避)。
四、常见误解
误解 1:容器里有自己的内核
错。 实测对比:
uname -a # 宿主
# Linux fine-bytes-2.localdomain 5.14.0-611.55.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC ... GNU/Linux
docker run --rm alpine:3.20 uname -a # 容器
# Linux c4a6280b1ad6 5.14.0-611.55.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC ... Linux
内核版本和构建时间戳完全一致(Tue May 19 15:19:29 EDT 2026),因为用的就是宿主机那个内核。差别只在 hostname(变成容器 ID,说明 UTS namespace 生效)。
再看用户空间:
docker run --rm alpine:3.20 cat /etc/os-release | grep NAME
# NAME="Alpine Linux"
grep NAME= /etc/os-release
# NAME="AlmaLinux"
同一个内核,两个不同的用户空间。 容器换的是「用户空间」,虚拟机换的是「整个内核 + 硬件模拟」。
误解 2:容器启动快是因为镜像小
不是。 启动快是因为不需要启动内核。VM 启动慢在几秒的固件 + bootloader + 内核初始化;容器启动只是 fork + execve 一个进程。
1GB 的镜像和 12MB 的镜像,启动速度几乎一样。
误解 3:容器有独立的文件系统
不准确。 容器共享镜像的只读层,只有一个独立的可写层(upperdir)。详见 02 OverlayFS。
误解 4:namespace 能限制资源用量
不能。 namespace 管"看不见",cgroup 管"管得住"。容器能看见宿主机全部内存,照样能吃光。详见 03 Cgroup。
误解 5:docker stop 会立刻停
不会。 实测退出码 137 = SIGKILL,走的是强杀路径。
docker stop 是两阶段:先 SIGTERM,等 10 秒(--time 可调),还没退出就 SIGKILL。优雅退出的退出码应是 0 或 143(128+15)。看到 137 就说明应用没处理好 SIGTERM。
误解 6:restart=always 任何停止都会重启
错。 手动 docker stop / docker kill 会抑制重启策略。
docker run -d --name restart-demo --restart=always alpine:3.20 sh -c 'sleep 100'
docker kill restart-demo
sleep 5
docker inspect restart-demo --format '{{.RestartCount}}'
# 0 ← 手动 kill 的容器不会自动重启
只有进程自行退出(崩溃/OOM)才触发重启。验证重启策略必须让进程自己退出。
误解 7:容器里的 root 就是宿主 root
字面上是的(因为 user namespace 未启用),但权限上不是。
容器内 id 显示 uid=0(root),可它连 mount 都做不到——因为 CAP_SYS_ADMIN 被裁掉了。"容器内 root" 和 "宿主 root" 是两个概念,详见 04 Capabilities。
五、本篇速查
# 宿主机 namespace 全集
lsns --no-headings
# 某进程的 namespace inode
readlink /proc/<pid>/ns/{mnt,pid,net,uts,ipc,user,cgroup}
# 容器内可见 PID
docker exec <c> sh -c 'ls /proc | grep -E "^[0-9]+$" | sort -n'
# 容器 PID 1 是谁
docker exec <c> sh -c 'cat /proc/1/comm'
# 同一路径不同视图
ls /proc/<pid>/root/opt # 容器视角
ls /opt # 宿主视角
# 挂载表对比
docker exec <c> sh -c 'mount' | head -10
mount | head -10
# 独立主机名
docker run --rm --hostname custom alpine:3.20 hostname
# 共享内存隔离
ipcmk -M 2048 && ipcs -m
docker exec <c> sh -c 'ipcs -m' # 看不到宿主创建的段
下一篇:02 · OverlayFS:镜像与容器文件层
相关:专题索引 | Docker 基础与容器生命周期