京东数科 | Kubernetes实践之contiv支持非docker容器运行时

栏目: 编程工具 · 发布时间: 6年前

京东数科 | Kubernetes实践之contiv支持非 <a href='https://www.codercto.com/topics/20577.html'>docker</a> 容器运行时

导读: 京东数科一直致力于构建基于kubernetes的大规模容器集群,经受618和双11的流量考验。

在我们京东数科的kubernetes容器集群中,网络插件使用的是contiv,容器运行时使用的是默认的docker,一直以来它们配合的非常好。但是当我们把容器运行时更换为containerd或者cri-o时,pod却一直无法创建成功。查看日志发现,一直打印类似这样的错误: Err: invalid nw name space: /var/run/netns/cni-4128c748-11b7-58bf-9a98-6e740c4e7f13 ,于是定位到导致该错误的源码位于nsToPID函数中,如下:

 // mgmtfn/k8splugin/driver.go
 // nsToPID is a utility that extracts the PID from the netns
 func nsToPID(ns string) (int, error) {
     ok := strings.HasPrefix(ns, "/proc/")
     if !ok {
         return -1, fmt.Errorf("invalid nw name space: %v", ns)
     }
     elements := strings.Split(ns, "/")
     return strconv.Atoi(elements[2])
 }

该函数的实现比较简单,首先检查传入的ns字符串是否以/proc/开头,如果不是则直接返回错误。而出错的时候,ns的值类似/var/run/netns/cni-4128c748-11b7-58bf-9a98-6e740c4e7f13这种格式,显然不符合条件。我们也通过日志发现,当容器运行时是默认的docker时,ns的值是类似proc/12345/ns/net这种格式的,它可以通过格式检查,并返回正确的pid。那现在问题基本明确了,当容器运行时是docker时,ns的值是 proc/[pid]/ns/net 这种格式的;当容器运行时是containerd或者cri-o时,ns的值是 /var/run/netns/[xxx] 这种格式的,导致解析失败。所以我们就从最初的源头分析,到底是什么原因导致了两种格式的ns的存在,以及如何解决这个问题。

1. contiv的整体架构

contiv是kubernetes集群中提供容器跨主机通讯的开源网络插件,能够支持二层、三层、overlay、aci等多种模式。它的整体架构如下图:

京东数科 | Kubernetes实践之contiv支持非docker容器运行时

其中,netctl是一个命令行管理工具。netmaster是整个contiv集群的管理控制结点。netplugin需要部署在集群的每个结点上,负责创建ovs流表、通告BGP路由等实际工作。contivk8s也需要部署在集群的每个结点上,它是kubelet与netplugin之间的适配器,负责将kubelet发送过来的cni(container network interface)请求转换为netplugin可以接受的数据格式。

2. kubernetes集群中使用非docker容器运行时

在kubernetes集群中,kubelet里面默认配置的容器运行时是docker,但是可以通过kubelet的参数container-runtime和container-runtime-endpoint来使用其他的容器运行时,例如containerd和cri-o。此时,container-runtime需要设置为remote,同时container-runtime-endpoint需要设置为具体的容器运行时监听的套接字地址,例如 unix:///run/containerd/containerd.sock 。在kubernetes集群中,之所以能够使用各种不同的容器运行时,是因为kubernetes中规定了容器运行时(container runtime interface)的标准,只要按照cri标准开发的容器运行时,都可以集成到kubernete集群中。

我们知道,kubelet在创建pod时,首先创建一个sandbox容器,然后才会创建用户定义的容器。sandbox容器的一个作用是初始化网络资源,例如设置IP地址、路由信息等,也就是说cni网络插件是在此步骤中参与进来的。前面说到,kubernetes中规定了容器运行时cri的标准,其中与sandbox容器相关的api有RunPodSandbox,StopPodSandbox等等,各个具体的容器运行时也都实现了这些api。当kubelet想要创建一个sandbox容器时,它会调用容器运行时提供的RunPodSandbox这个api。所以,去看看各个容器运行时的 RunPodSandbox 这个api是如何实现的,就能知道为什么会出现两种格式的ns。

3. 容器运行时docker

其实,docker的后台服务进程dockerd并没有直接实现cri标准中规定的那些api。因为docker出现的时间比较早,它自己本身就有一套完整的创建、启动、停止、删除容器的api。而cri标准是后来才出现的,并且这两套接口无法实现无缝结合,所以必须有一个中间的适配器adapter存在,这个适配器就是 dockershim ,它的作用就是将cri标准的接口转换为dockerd中可以识别的接口。最后值得注意的是dockershim是集成在kubelet里面的。

京东数科 | Kubernetes实践之contiv支持非docker容器运行时

在dockershim中,也就是kubelet中,RunPodSandbox的实现位于源文件 pkg/kubelet/dockershim/docker_sandbox.go

中,具体分为以下几个步骤:

3.1 Pull the image for the sandbox

3.2 Create the sandbox container

8e94b9fffe48793c43b35f906b2b5f5eec23b091c3934e2899d2f2f89b871e04

3.3 Start the sandbox container

3.4 Setup networking for the sandbox

在此步骤中,dockershim会通过cni接口调用网络插件,来设置sandbox容器的网络资源,例如IP地址、路由信息等等。但是容器运行时对网络插件的调用方式有些特殊,它并不是通过网络或者unix域套接字来调用的,而是创建出一个新的进程,然后在新的进程里执行具体的cni网络插件的二进制可执行文件(在contiv里面,这个二进制文件是contivk8s)。并且,传递参数的方式是在新的进程里面设置一些环境变量,当然这种调用方式是cni标准中规定的。这些环境变量主要包括CNI_COMMAND、CNI_ARGS、CNI_NETNS等。其中CNI_COMMAND表示命令类型,当它的值是ADD时表示创建sandbox容器,当它的值是DEL时表示删除sandbox容器。CNI_ARGS里面包含一些pod相关的信息,具体例如 K8S_POD_NAMESPACE=default;K8S_POD_NAME=test-pod;K8S_POD_INFRA_CONTAINER_ID=8e94b9fffe48793c43b35f906b2b5f5eec23b091c3934e2899d2f2f89b871e04

。CNI_NETNS则表示第2步中创建的sandbox容器的网络命名空间的路径,也就是本文开始处的nsToPID函数中的ns参数的值。经过分析源代码,发现环境变量CNI_NETNS的值是通过如下函数获得的:

 // pkg/kubelet/dockershim/docker_service.go
 func (ds *dockerService) GetNetNS(podSandboxID string) (string, error) {
     r, err := ds.client.InspectContainer(podSandboxID)
     if err != nil {
         return "", err
     }
     return fmt.Sprintf("/proc/%v/ns/net", r.State.Pid), nil
 }

在dockershim中调用GetNetNS函数时,需要传入第2步中返回的sandbox容器的容器ID。GetNetNS的实现也比较简单,就是给dockerd发送inspect请求,获得sandbox容器的详细信息,然后从详细信息中取出sandbox容器的pid,最终返回/proc/[pid]/ns/net。至此,dockershim的逻辑已经分析完了,也已经知道了/proc/[pid]/ns/net格式的ns是如何得来的。下面在分析容器运行时containerd之前,需要先了解下两个事情,一是创建sandbox容器时contiv做了些什么事情,二是 linux 内核中namespace的基础知识。

4. 创建sandbox容器时contiv做的具体工作

4.1 kubelet调用contivk8s

4.2 contivk8s将kubelet的cni请求转换为netplugin可以接受的api

4.3 netplugin向netmaster发送创建请求,netmaster分配一个未使用的IP地址

4.4 netplugin在本机上创建虚拟网卡对,例如vport1和vvport11,然后把虚拟网卡vvport1加入到ovs虚拟交换机中。

注意此时这两个虚拟网卡都在主机的网络命名空间内,即在主机上运行ifconfig命令,是可以看到这两个虚拟网卡的。

4.6 netplugin得到sandbox容器的pid之后,会基于pid做如下三个操作:一是将虚拟网卡对的另一端vport1移动到新的网络命名空间  netlink.LinkSetNsPid(link, pid) ,这里的link参数指的是vport1。经过此操作之后,主机的网络命名空间内无法再看到vport1这个虚拟网卡,而这个命令 nsenter -t [pid] -n ifconfig 则可以看到,因为此命令的意思是输出[pid]指定的网络命名空间内的网卡情况。二是执行命令 nsenter -t [pid] -n ip link set dev vport1 name eth0 ,它的意思是将位于[pid]指定的网络命名空间内的虚拟网卡vport1重命名为eth0。三是执行命令 nsenter -t [pid] -n ip address add 192.168.1.1/24 dev eth0

,即设置ip地址。

5. linux内核中namespace的基础知识

namespace是linux内核提供的一种资源隔离方案,包括network、mount、user、pid、ipc、uts等多种资源类型的隔离。对于系统中的每个进程,都有/proc/[pid]/ns/这样的一个目录存在,里面包含了这个进程所属的各个namespace的信息,例如:

 ls -l /proc/12345/ns/

 lrwxrwxrwx 1 root root 0 Mar 12 16:53 ipc -> ipc:[4026533048]
 lrwxrwxrwx 1 root root 0 Mar 12 16:53 mnt -> mnt:[4026533279]
 lrwxrwxrwx 1 root root 0 Mar 12 16:53 net -> net:[4026533051]
 lrwxrwxrwx 1 root root 0 Mar 12 16:53 pid -> pid:[4026533283]
 lrwxrwxrwx 1 root root 0 Mar 12 16:53 user -> user:[4026531837]
 lrwxrwxrwx 1 root root 0 Mar 12 16:53 uts -> uts:[4026533282]

与namespace相关的api有三个,它们分别是clone、setns、unshare。在linux系统中,我们可以通过fork或者clone来创建新的进程,以fork方式创建出来的新的进程与其父进程在相同的命名空间内,而以clone方式创建新的进程时,可以通过flags参数来指定创建新的命名空间。例如,当flags中包含CLONE_NEWNET时,新的进程则会位于一个新的网络命名空间内,与父进程的网络命名空间是不同的。setns则可以将当前进程加入到一个已经存在的命名空间,它的函数原型是 int setns(int fd, int nstype) 。假设已经存在一个pid为12345的进程,我们想要把当前进程加入到pid为12345的进程所在的网络命名空间,则可以在当前进程中打开/proc/12345/ns/net这个文件,获得它的文件描述符fd,然后调用 setns(fd, CLONE_NEWNET) 来实现。unshare则会使当前进程退出当前的命名空间,然后加入到新创建的命名空间内,它的函数原型是 int unshare(int flags)。 例如我们想要把当前进程加入到一个新的网络命名空间内,则可以在当前进程内调用 unshare(CLONE_NEWNET) 来实现。最后命名空间还有一个特性,就是当一个命名空间中的所有进程都退出时,该命名空间将会被销毁。那么,当所有进程都退出某个命名空间时,我们依然想保留它怎么办?可以通过bind mount的方式。例如 mount --bind /proc/12345/ns/net /file/tmp ,这样就算属于这个网络命名空间的所有进程都退出了,只要/file/tmp这个文件还在,那么/proc/12345/ns/net这个网络命名空间就会一直存在。在其他的进程中,可以打开文件/file/tmp,获得文件描述符fd,最后调用 setns(fd, CLONE_NEWNET) 加入到这个网络命名空间。

6. 容器运行时containerd

在containerd中,RunPodSandbox的实现位于源文件 vendor/github.com/containerd/cri/pkg/server/sandbox_run.go

中,具体分为以下几个步骤:

6.1 Pull the image for the sandbox

6.2 Create a new network namespace

注意注意,此步骤是与容器运行时docker最大的区别

。containerd首先创建出一个新的网络命名空间,而且这个网络命名空间内也没有任何进程存在,自然也就不会得到/proc/[pid]/ns/net这样格式的ns。从我们上面的分析可知,contiv的netplugin却是依赖于这个pid进行后续工作的。我们先看看containerd是如何创建出一个新的网络命名空间的。经过分析源代码发现,containerd会调用NewNS函数去创建一个新的网络命名空间,具体的代码如下,为了便于说明,已经做了修改。

 // vendor/github.com/containernetworking/plugins/pkg/ns/ns_linux.go
 func NewNS() string {
     // b是长度为16的随机字节数组
     nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
     nsPath := path.Join("/var/run/netns", nsName)
     // 此时,nsPath类似这样/var/run/netns/cni-4128c748-11b7-58bf-9a98-6e740c4e7f13

     // 保留当前的网络命名空间
     origNS, _ = GetNS(getCurrentThreadNetNSPath())

     // 创建一个新的网络命名空间,并把当前进程加入进去
     unix.Unshare(unix.CLONE_NEWNET)

     // 将新的网络命名空间挂载到/var/run/netns/目录下的某个文件
     unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")

     // 将当前进程恢复到之前保留的网络命名空间
     origNS.Set()
     
     return nsPath
 }
最终,我们会得到一个文件 /var/run/netns/[xxx]

,而这个文件会指向刚刚创建的新的网络命名空间。

6.3 Setup networking for the sandbox

此步骤与3.4章节中的流程相同,不再重复,唯一的区别就是环境变量CNI\_NETNS变为这样的格式/var/run/netns/[xxx]。这个值最终被传递到netplugin中nsToPID函数中,但是nsToPID函数却期望/proc/12345/ns/net这样的格式,所以解析失败,返回invalid nw name space的错误。

6.4 Create the sandbox container in the new network namespace

7. 问题修复

原因已经找到了,netplugin期望从传入的ns参数中获得代表新的网络命名空间的pid,但是在容器运行时containerd中构造出的ns是/var/run/netns/[xxx]这样的格式,无法获得pid。那我们的解决方式就是在这两种格式的ns之间做一个转换,将格式为 /var/run/netns/[xxx] 的ns转换为 /proc/[pid]/ns/net 这样的格式。要想转换,就需要一个pid的存在,那这个pid是哪个进程的pid呢?而且还要确保这个pid指向那个刚刚创建的网络命名空间。我们先把创建sandbox容器时涉及到的组件整理一下,如下图:

京东数科 | Kubernetes实践之contiv支持非docker容器运行时

(1)kubelet调用cri标准的RunPodSandbox接口

(2)容器运行时containerd创建新的网络命名空间,将环境变量CNI_NETNS设置为这样的格式/var/run/netns/[xxx],然后调用cni网络插件接口创建新的进程并执行contivk8s

(3)contivk8s将cni请求转换为netplugin可以接受的api,其中一步是读取这样格式/var/run/netns/[xxx]的环境变量CNI_NETNS,然后将其赋值给ns参数

(4)netplugin设置ip地址、ovs流表等等

经过这些分析之后,我们可以在contivk8s这里搞点小事情。contivk8s进程启动后,读取环境变量CNI_NETNS的值,如果是类似/var/run/netns/[xxx]这种格式的,则打开该文件并获得文件描述符fd,然后调用 setns(fd, CLONE_NEWNET) 将自己加入到/var/run/netns/[xxx]指定的网络命名空间。接着获得自己进程的进程号pid,并格式化为/proc/[pid]/ns/net这样的格式,最后将其发送到netplugin。具体的代码片段如下:

 if !strings.HasPrefix(ppInfo.NwNameSpace, "/proc/") {
     nsHandle, err := netns.GetFromPath(ppInfo.NwNameSpace)
     if err != nil {
         return fmt.Errorf("error getting ns %s: %s", ppInfo.NwNameSpace, err)
     }
     err = netns.Set(nsHandle)
     if err != nil {
         return fmt.Errorf("error switching to ns %s: %s", ppInfo.NwNameSpace, err)
     }
     ppInfo.NwNameSpace = fmt.Sprintf("/proc/%d/ns/net", os.Getpid())
 }

8. 总结

contivk8s经过修改之后,不管容器运行时是dockerd还是containerd,最终netplugin里面接收到的ns参数始终是/proc/[pid]/ns/net这样的格式,从而可以进行正确的处理。我们已经将该功能合并到github的contiv项目中了,欢迎大家使用,如果有任何问题,可以通过邮件zhouzijiang@jd.com进行交流。

最后,我们京东数科一直致力于构建基于kubernetes的大规模容器集群,经受618和双11的流量考验,欢迎有意向的同学加入我们一起努力,可以发送简历到邮箱hejun3@jd.com,谢谢!

K8S培训推荐

Kubernetes线下实战培训, 采用3+1+1新的培训模式(3天线下实战培训,1年内可免费再次参加,每期前10名报名,可免费参加价值3600元的线上直播班 ),资深一线讲师,实操环境实践,现场答疑互动,培训内容覆盖:Docker方面:Docker架构、镜像、数据存储、网络、以及最佳实践。Kubernetes实战内容,Kubernetes设计、Pod、常用对象操作,Kuberentes调度系统、QoS、Helm、网络、存储、CI/CD、日志监控等。

北京:5月10-12日

报名:https://www.bagevent.com/event/2376547

上海:5月17-19日

报名:https://www.bagevent.com/event/2409655

深圳:5月24-26日

报名:https://www.bagevent.com/event/2409699

京东数科 | Kubernetes实践之contiv支持非docker容器运行时


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacking

Hacking

Jon Erickson / No Starch Press / 2008-2-4 / USD 49.95

While other books merely show how to run existing exploits, Hacking: The Art of Exploitation broke ground as the first book to explain how hacking and software exploits work and how readers could deve......一起来看看 《Hacking》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试