一、背景
K8S集群Calico异常问题,排查下来想查看net namespace相关情况,执行ip netns ls报错: Error: Peer netns reference is invalid:

查找相关AI资料以及处置建议,得到的建议是把kubelet、containerd等服务关闭之后清空/run/netns/目录,之后重新启动kubelet、containerd服务。
但是这个是线上服务,如果没有研究清楚,这个操作心里是没底的。所以基于这个问题做了CNI容器相关的原理分析以及这个/run/netns/目录的相关目录情况,掌握真实情况后才能做出处置决定。
二、原理分析
1、原因
/run/netns/目录是ip netns命令默认创建netns 挂载文件访问内核netns对象的入口, k8s的CNI插件创建的netns 文件挂载入口也是此目录下的文件.
可能原因是Calico CNI插件某种问题/故障,导致本来应该删除的Pod 随之清理对应netns,后面没正确清理,导致残留.并且这个残留文件不符合/run/netns目录下文件的格式等,导致读取错误。
2、测试
手动在这个K8S测试集群的主机的/var/run/netns目录touch 1一个文件,这个是普通文件什么内容都没有。 执行ip netns ls之后,报错信息与线上内容一致。

此目录下/var/run/netns=>/run/netns/创建一个文件,再次执行ip netns ls就会报错. ip netns 默认查找的文件目录就是 /run/netns/*
其实这个内核netns对象文件系统挂载点文件, 可以放在任意目录。只是/run/netns/是约定俗成的目录而已
3、CNI原理分析

Containerd服务主要是为了创建容器,不负责网络network namespace的的IP配置、network namespace创建、veth pair等网络虚拟设备的配置。 这个是由CNI网络插件来完成的。所以这个事情是CNI插件的问题, 锁定就是Calico.
4、Calico CNI插件的职责
1、创建net namespace
为pause容器创建独立的net namespace,并且通过mount --bind 将创建的net namespace 创建一个挂载点文件出来,放到/run/netns/cni-*,方便业务容器,直接引用这个cni-*文件共享这个net namespace
2、创建veth pair 虚拟网卡对---虚拟网线
只创建net namespace,只是给进程一个独立的网络namespace空间而已,这个空间可以配置ip地址,给了一个虚拟网卡. 但是如果需要联网, 那么就需要"网线"
veth pair 虚拟网卡对 是Linux网络里面的一种特殊"虚拟设备",类比物理世界的网线。 网线会有2个头.
CNI插件需要把1头插入宿主机的net namespace, 1插入到容器的net namespace里面。这样宿主机和容器, 就形成了使用一根"虚拟网线"起来了,才能进行通信.
ip link show type veth

宿主机端net namespace:
- 有1个veth的网线头, 名称就是lxc69c37331c351, 这边的网络设备编号131
- @130: 连接的对端是 net namespace里面的130 接口
- 连接哪个net namespace呢:
link-netns cni-83216309-ce91-dae1-cb13-c13e1fd63bda
可以进入这个net namespace查看网卡信息,看是不是130接口,从它的视角看下是不是连接宿主机的131接口呢?
ip netns exec cni-83216309-ce91-dae1-cb13-c13e1fd63bda ip link

ns namespace容器端:
1、130设备ID 正确,和前面分析一样. 名称是eth0 连接的宿主机net namespace的@131设备
2、link-netnsid 0 连接的net namespace id 0代表就是宿主机的net namespace
可以两台电脑直接"网线"连接, 也可以把"网线"插入到例如虚拟交换机、虚拟网桥等设备上,完成网络通信. 这个代表的就是,这个veth pair一端放在了宿主机 net namespace,并且桥接在 这个br-b6xxx的虚拟设备网桥上
3、docker桥接网络分析

存在2个容器,这俩容器都接入到了br-xx docker维护的网桥:
1、因为使用docker-compose部署的2个容器,那么这两个容器,默认会创建一个网桥出来,然后这2个容器连接到这个网桥,能够进行二层通讯



如果没有使用docker-compose部署,那么默认容器的网络模式是桥接模式,则桥接到docker0网桥上,验证如下:
ip link show type bridge

新建2个容器: docker run -d -it busybox /bin/sh
什么参数不指定,默认是bridge桥接网络。 默认桥接到docker0这个网桥,如果是docker-compose一般会创建对应网桥出来.

新增的2个docker容器,都桥接到了docker0网桥: 验证逻辑没问题

ip addr show docker0:

5、网络namespace
1、基本概念和特性
1、Linux 网络命名空间(netns)本身是一种内核对象,没有直接的文件系统路径。
2、用户态访问它的唯一途径是通过 /proc/<pid>/ns/net 文件------这是内核为每个进程暴露的命名空间符号链接,指向该进程所属的网络命名空间。
3、一旦所有引用该命名空间的进程都退出,且没有其他引用(如挂载点)存在,内核就会销毁该命名空间。
内核针对ns的引用计数器,如果引用计数器为0,那么这个ns则会被内核自动清理回收.
2、netns-挂载文件系统
为了让网络命名空间在没有任何进程存在时也能持久化(便于后续配置和管理),可以将某个进程的 /proc/<pid>/ns/net 绑定挂载(bind mount)到文件系统上的一个路径。这样:
-
内核会因为该挂载点的存在而继续保留这个命名空间。进程关了,引用计数器还在,不等于0,不会被内核回收清理,还能继续用
-
用户可以通过这个挂载点路径重新进入命名空间(例如使用 nsenter --net=<mountpoint>)。
-
一般约定俗成的挂载路径: /run/netns IP命令、CNI等都统一规范使用这个路径,将访问内核ns的文件挂载点放在这个目录下面, nsfs (namespace file system) 这个类型文件也可以不放在/run/netns ,没有强制规定.
-
如果希望用 ip netns 命令管理放在其他目录的命名空间,可以使用环境变量 IP_NETNS_DIR 指定目录,例如:
bash
export IP_NETNS_DIR=/tmp/netns
ip netns list
不一定在/run/netns,以下实验可以证明:
1、首先创建一个netns ,这个进程sleep。 查看/tmp/目录不存在mynetns文件

bash
unshare -n sleep 1000 &
PID=$!
echo "进程 PID: $PID"

bash
nsenter -t $PID -n ip addr show

2、findmnt 以及 lsns -n net查看情况
bash
mount --bind /proc/$PID/ns/net /tmp/mynetns


3、赶紧为这个ns内核对象创建file system 一个文件作为内核访问netns的挂载点文件,放到/tmp/mynetns

和之前nsenter 一样的结果:
bash
nsenter --net=/tmp/mynetns ip addr show

4、kill 杀死进程,再次通过nsenter 进入netns 发现还能正常进说明,此时netns还有1个引用计数器(文件在引用这个netns对象),没被内核回收♻️能正常访问
bash
kill $PID
wait $PID 2>/dev/null
nsenter --net=/tmp/mynetns ip addr show

findmnt -t nsfs #查看ns的文件挂载情况
lsns -t net #查看ns当前有哪些进程使用

5、如果删除或者umount 那么netns引用计数器为0,则会被内核回收netns。 先umount卸载,之后再删除文件即可



再删除文件,此时已恢复正常:

3、mount --bind 与硬链接区别

bash
mount --bind /proc/$PID/ns/net /tmp/mynetns:

--bind 临时性,挂载关系在系统重启后消失(除非通过配置持久化)
本质不同 :
- 硬链接是文件系统内部机制,持久、同文件系统、仅文件。
- bind mount 是内核 VFS 挂载机制,临时、可跨文件系统、支持目录。
4、pod找container组、查看container信息容器属性等
bash
1、crictl ps #查找pod
2、crictl inspectp $pod_id #得到这个pod的pause容器的信息, 这里有pause容器的network namespace等相关信息
3、通过pod名称查看对应的容器组:
ctr -n k8s.io containers list \
"labels.io.kubernetes.pod.name==nginx-alpine-5fc8587994-fmhdv" \
"labels.io.kubernetes.pod.namespace==default"
ctr -n k8s.io containers info $container_id #查看实际容器信息
5、原生操作netns: lsns -t net、ip netns attach、findmnt -t nsfs lsns、ip、** **nsenter** **、findmnt** **
bash
lsns -t net #查看内核的ns namespace的列表, 如果文件系统有挂载则列出文件路径,否则可以没有, -t 就是type 类型
ip netns add mynetns #添加ns,且添加文件系统. 新增ns 并且加一个文件mount挂载入口
ip netns ls #查询ns列表, /run/netns/cni-*
unshare --net bash #新增ns 不新增文件系统.
ip netns attach mynetns 12345 #设置某个进程的ns/net到mynetns
#手动等价于 ip netns attach mynetns 12345
touch /var/run/netns/mynetns
mount --bind /proc/12345/ns/net /var/run/netns/mynetns
findmnt -t nsfs #查看挂载类型是nsfs [namespace file system] 根据类型查看挂载点
ip netns list-id #还可以查看ns的id

6、操作netns的命令: ip netns exec / nsenter -t
bash
ip netns exec <netns名称> ping <目标IP或域名> #只能通过ns名称
nsenter -t <进程PID> -n ping <目标IP> # -t是目标pid -n (net) 后面执行命令
nsenter --net=/run/netns/cni-* ping www.baidu.com # namespace enter 进入. --net 指定文件挂载入口到netns内核对象
link #设备 ip link show type veth
address #地址 ip a show
route #路由
netns #网络命令空间
neigh #arp
三、解决方案
一个很简单的判断方式,就是查看/run/netns/目录下面文件的权限,如果是正确的netns的内核挂载点文件,那么权限都是只读。如果已经恢复为普通文件那么肯定不全是只读。
这里可以看到rw这种权限的文件,和报错的个数是一致的。并且如果非普通文件你去mv、cp都会报错。普通文件的话就可以正常mv、cp以及rm删除。把这几个残留文件删除后,执行ip netns ls就恢复正常了。
最后无需重启服务,直接解决问题并且不会影响现有服务的正常运行。
