需求
我想进入容器中执行 curl 命令探测某个地址的连通性,但是容器镜像里默认没有 curl 命令。我这里是一个内网环境不太方便使用 yum 或者 apt 安装,怎么办?
这个需求比较典型,这里教大家一个简单的方法,使用 nsenter 进入容器的 net namespace,即可使用宿主机的 curl、ip、ifconfig 等命令,其效果,就跟进入容器中执行是一样的。
核心要点
nsenter可以让宿主机上的命令进入目标进程所在的 namespace 执行。- 排查容器网络问题时,常用的是
nsenter -t <pid> -n bash,其中-n表示进入 net namespace。 - 这种方式适合容器镜像缺少
curl、telnet、ip、ifconfig等排障工具,且临时安装工具不方便的场景。 - 进入 net namespace 后,命令看到的是容器网络视角;退出 bash session 后回到宿主机视角。
- 如果要使用
ip netns exec,需要先把目标进程的 net namespace 挂到/var/run/netns/下。
原理
容器像是一个轻量级虚拟机,有自己的 IP,宿主机如果已经监听了 8080 端口,容器里的进程仍然可以重复监听 8080 端口。核心就是因为容器有自己的 namespace,和宿主机的 net namespace 互不影响。关于容器的 namespace 相关知识,可以 Google 一下关键字:「容器 namespace 原理」。
nsenter,是个关键工具。其用法如下:
nsenter --help
用法:
nsenter [选项] [<程序> [<参数>...]]
以其他程序的名字空间运行某个程序。
选项:
-a, --all enter all namespaces
-t, --target <pid> 要获取名字空间的目标进程
-m, --mount[=<文件>] 进入 mount 名字空间
-u, --uts[=<文件>] 进入 UTS 名字空间(主机名等)
-i, --ipc[=<文件>] 进入 System V IPC 名字空间
-n, --net[=<文件>] 进入网络名字空间
-p, --pid[=<文件>] 进入 pid 名字空间
-C, --cgroup[=<文件>] 进入 cgroup 名字空间
-U, --user[=<文件>] 进入用户名字空间
-S, --setuid <uid> 设置进入空间中的 uid
-G, --setgid <gid> 设置进入名字空间中的 gid
--preserve-credentials 不干涉 uid 或 gid
-r, --root[=<目录>] 设置根目录
-w, --wd[=<dir>] 设置工作目录
-F, --no-fork 执行 <程序> 前不 fork
-Z, --follow-context 根据 --target PID 设置 SELinux 环境
-h, --help display this help
-V, --version display version
更多信息请参阅 nsenter(1)。
通过 nsenter -t
这里的关键点是:nsenter 并不是“进入容器文件系统”,而是让命令进入目标进程的 namespace。本文只进入网络 namespace,所以它解决的是网络视角问题,不负责补齐容器里的软件包。
实战
在 Linux 上,通过夜莺的 docker-compose 拉起一套夜莺,使用 bridge 模式,做个演示。
- 拉起夜莺
[root@aliyun-2c2g40g3m compose-bridge]# docker compose up -d
[+] Running 7/7
✔ Network compose-bridge_nightingale Created 0.1s
✔ Container redis Started 0.1s
✔ Container victoriametrics Started 0.1s
✔ Container mysql Started 0.1s
✔ Container ibex Started 0.1s
✔ Container nightingale Started 0.1s
✔ Container categraf Started
然后,inspect 一下 nightingale 容器,拿到其 Pid:
[root@aliyun-2c2g40g3m compose-bridge]# docker ps |grep nigh
0500a886538e flashcatcloud/nightingale:latest "sh -c /app/n9e" 31 minutes ago Up 31 minutes 0.0.0.0:17000->17000/tcp, :::17000->17000/tcp nightingale
[root@aliyun-2c2g40g3m compose-bridge]# docker inspect 0500a886538e |grep Pid
"Pid": 1619207,
"PidMode": "",
"PidsLimit": null,
- 根据 Pid 进入 net namespace 1619207 就是 Pid,根据此 Pid 进入容器的 net namespace:
[root@aliyun-2c2g40g3m compose-bridge]# nsenter -t 1619207 -n bash
[root@aliyun-2c2g40g3m compose-bridge]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.22.0.6 netmask 255.255.0.0 broadcast 172.22.255.255
ether 02:42:ac:16:00:06 txqueuelen 0 (Ethernet)
RX packets 104463 bytes 33707078 (32.1 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 89615 bytes 10817199 (10.3 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 112 bytes 7096 (6.9 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 112 bytes 7096 (6.9 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
如上,使用 bash 命令进入 net namespace,然后执行 ifconfig,看到 IP:172.22.0.6,显然这就是容器的 IP,说明 nsenter 达成所愿,之后在这个 bash session 内执行 curl、telnet 之类的,就相当于在容器里执行一样的效果。完事执行 exit 命令可以退出这个 net namespace。
排查时常见动作包括:
- 在宿主机上通过
docker inspect找到容器的一号进程 PID。 - 使用
nsenter -t <pid> -n bash进入该进程的网络命名空间。 - 执行
ip addr、ifconfig、curl、telnet等宿主机上已有的命令。 - 验证容器 IP、DNS、目标端口连通性或路由是否符合预期。
- 使用
exit退出当前 namespace。
补充知识
上面的 1619207 这个 Pid 其实就是容器的一号进程的 Pid,在宿主机上执行下面的命令可以看出:
[root@aliyun-2c2g40g3m compose-bridge]# ps aux|grep n9e
root 1619207 0.0 0.0 2576 888 ? Ss 15:22 0:00 sh -c /app/n9e
root 1619327 0.4 4.0 741264 78720 ? Sl 15:22 0:09 /app/n9e
root 1620612 0.0 0.0 221528 864 pts/0 S+ 16:01 0:00 grep --color=auto n9e
夜莺这个容器,核心执行的命令是 /app/n9e,不过在 docker-compose.yaml 中 command 写的是:
sh -c "/app/n9e"
所以这个容器的一号进程就是 sh 了。1619207 和 1619327 这俩进程是在一个容器里的,也就是在一个 net namespace 中的,这俩 Pid 都可以用于 nsenter,效果是一样的。
补充知识2
除了 nsenter,使用 ip netns exec 也可以达到类似的效果。通过 ip netns help 可以看到相关帮助信息。
[root@aliyun-2c2g40g3m compose-bridge]# ip netns help
Usage: ip netns list
ip netns add NAME
ip netns attach NAME PID
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id [target-nsid POSITIVE-INT] [nsid POSITIVE-INT]
NETNSID := auto | POSITIVE-INT
从上面的 help 信息可以看出,要执行 ip netns exec,需要知道 net namespace 的 NAME,如何找到容器的 net namespace name 呢?
实际上,在 Linux 中,每个进程只要知道其 Pid 了,根据 Pid 就可以找到 net namespace 了,比如上面的例子,我们知道进程的 Pid 是:1619207,其 net namespace 就是:/proc/1619207/ns/net。
但是,执行 ip netns list 却看不到,这是因为,ip netns list 是罗列的 /var/run/netns/ 下面的内容,而容器的 net namespace 并未挂到这里,此时,我们只需要做个软链挂过去即可:
ln -s /proc/1619207/ns/net /var/run/netns/1619207
如果发现 /var/run/netns 目录不存在,使用 root 账号 mkdir 一下即可。之后,我们就可以使用 ip netns exec 了:
ip netns exec 1619207 ifconfig
看到的输出和 nsenter 方式看到的输出是一样的。
如上,希望这个小知识可以帮到大家。
FAQ
Q1:nsenter -t <pid> -n bash 中的 -n 是什么意思?
A:-n 表示进入目标进程的网络 namespace。进入后执行网络相关命令,看到的是容器里的网络视角。
Q2:为什么不用直接 docker exec?
A:如果容器镜像里没有 curl、telnet、ip、ifconfig 等工具,docker exec 进去也没法执行这些命令。nsenter 可以复用宿主机已有工具进入容器的网络 namespace 排障。
Q3:ip netns exec 和 nsenter 有什么区别?
A:两者都能进入网络 namespace。nsenter 可以直接基于 PID 进入;ip netns exec 通常需要 namespace 名称,而容器的 net namespace 默认不一定出现在 /var/run/netns/ 下,需要手动建立软链。
enjoy :-)
