Namespace
这一节,我给大家介绍docker的namespace是如何实现资源隔离的
Namespace其实是linux很早就有的一个功能吧,但是因为docker它才被更多的人所熟悉。Linux提供了其中不同的命名空间,分别用于隔离不同的资源
通过这几个命名空间的选项,我们可以在创建新的进程时设置它和宿主机器的其他进程进行哪些资源的隔离。
进程隔离
大家都知道linux的进程是一个树形结构树根是pid为0的上帝进程。还有两个非常特殊的命令一个是pid为1的 /sbin/init进程和pid为2的kthreadd进程。上帝进程idle由系统自动创建,运行在内核态,也是唯一一个没有通过fork或者kernel_thread产生的进程。(这边又跑神了,因为我又跑去看linux的进程创建等,发现全是c的代码,看的好累)总而言之,linux创建后续的进程有三种方法,fork(),vfork(),clone()。这三个函数实际上都是C库的封装,内部分别调用了sys_fork,sys_vfork,sys_clone三个系统调用,而这三个系统调用又统一调用了do_fork函数,只是说携带的参数和标志不同。
- fork:当进程A调用fork创建一个子进程B时,A和B会使用相同的物理页面相当于子进程会使用父进程的资源,但是是只读,当A或者B想要执行写的操作时,会产生特定异常,这时cpu才会执行异常处理函数然后为子进程复制一份新的物理页面,然后继续执行写操作。说白了就是抠门,能省则省(作为一个才开始工作的社畜,要像Linux学习,嘿嘿)
- vfork:这玩意更加粗暴,创建的子进程连虚拟空间都抢自己老爸的,因此当子进程开始的时候,父进程会被挂起,直到子进程结束。这里看到一个很有意思的点,在vfork出来的子进程中不能使用return或者exit来退出,因为子进程和父进程共享堆栈,使用exit会导致标准输入输出的缓冲区被清空,临时文件会被删除,如果只是子进程这样倒也还行,但是轮到父进程退出时再清除一次就会造成异常错误。因此vfork进程退出时只能使用_exit()。
- clone:clone主要是创建轻量级的进程所用,而docker中使用的就是这个clone,这种创建进程的方式设计非常强大,它可以传入参数来选择性的让子进程继承资源。
介绍一下这一节的重头戏 PID namespace。在不同的PID namespace中pid 也是独立的,也就是说不同的命名空间中pid是可以相同的。Linux下的每个进程都有一个对应的/proc/PID目录,该目录包含了大量的有关当前进程的信息。 对一个PID namespace而言,/proc目录只包含当前namespace和它所有子孙后代namespace里的进程的信息。通过这种方式,系统中的PID名空间会形成一个层级体系。父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点名空间中的任何内容,也不可能通过kill或ptrace影响父节点或其他名空间中的进程。
好了 介绍完linux的进程的一些信息之后,我们看一下docker是怎么进行进程管理的。首先,docker鼓励一个容器一个进程的做法(one process per contianer),插一句,我一开始一直以为一个容器就是一个进程,但是其实不然,我觉得可以理解为一个容器是一个PID命名空间,这种一个容器放一个进程的做法很适合以单进程为主的微服务架构应用,但是毕竟有非常多的应用是由紧紧耦合的多个进程构成的,这些进程是难以拆分到不同的容器中,因此在一个容器中放多个进程变成了一种折中的方案,因此如何在一个容器中正确的运行多进程应用给开发者带来很多挑战。
在Docker中,每个Container都是Docker Daemon的子进程(这样说不够具体,因为从docker Daemon 到 container之间有一系列非常复杂的过程,具体可以看这篇博客),每个容器进程在创建的时候都具有不同的pid命名空间,通过这样的技术实现了进程的隔离。当使用docker exec进入容器空间的时候再去用ps -ef命令查看当前命名空间进程,就会发现里面的进程很干净除了一个初始进程之外也就两三个小进程而已,这就是namespace强大。这里我们可以看一个小小的有意思的例子。我生成了两个最基础的flaks的 helloworld容器(同一image不同container)
然后去找这两个容器的进程关系
虽然父进程不一样 但是我们去查一下他们俩的父进程就会发现神奇的一点,这两个容器的初始进程的爷爷进程是一样的 都基于一个叫containerd的进程,而这个containerd进程就有点意思了,它其实是容器的守护进程是被docker的守护进程(dockerd)创建出来,它的作用就是在没有docker daemon的情况下也可以创建并运行容器。这里有一个比较简单易懂答案介绍最新docker各进程之间的关系(旧版本的docker是不太一样的)
docker的关于进程隔离的内容就讲到这里,其实docker的关于进程管理的内容还是非常多的,尤其是想要在docker中去跑多进程任务的话就需要好好思考如何处理僵尸进程和孤儿进程,这一块我还没有看的太多,等我变成名震一方的大佬后再来分享一些这一块的研究吧。
网络隔离
提到隔离,肯定绕不开namespace,Network Namespace 是 Linux 内核提供的功能,是实现网络虚拟化的重要功能,它能创建多个隔离的网络空间,它们有独自网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己都在独立的网络中。而且不同Network Namespace的资源相互不可见,彼此之间无法通信。想要让容器和外界进行通信,docker默认使用的是网桥模式。当 Docker 服务器在主机上启动之后会创建新的虚拟网桥 docker0,随后在该主机上启动的全部服务在默认情况下都与该网桥相连。简单来说,网桥就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。
docker创建容器时会执行下面的操作:
- 创建一堆虚拟的网络接口 veth pair,分别放在本地宿主机和新容器的命名空间中
- 本地主机一端的虚拟的接口连接到默认的docker0网桥上,取名为veth****
- 容器中将网络接口改名为:eth0
- 从网桥(docker0)获取一个空闲的IP地址(和docker0同一网段),分配给容器的eth0网卡
上图的iptables是Linux实现的软件防火墙,用户可以通过iptables设置请求准入和拒绝规则,从而保护系统的安全。我们也可以把iptables理解成一个客户端代理,用户通过iptables这个代理,将用户安全设定执行到对应的安全框架中,这个“安全框架”才是真正的防火墙,这个框架的名字叫netfilter。iptables其实是一个命令行工具,位于用户空间。iptables/netfilter(以下简称iptables)组成了Linux平台下的包过滤防火墙,可以完成封包过滤、封包重定向和网络地址转换(NAT)等功能。
我们使用iptables -t nat -L命令就能发现 NAT配置中docker链中有一些规则
上述规则会将从任意源发送到当前机器某些特定端口(比如8998)的 TCP 包转发到 172.17.0.5:5000 所在的地址上。这个地址其实也是 Docker 为 flask 服务分配的 IP 地址,如果我们在当前机器上直接 ping 这个 IP 地址就会发现它是可以访问到的。
Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。
再加一句,除去网桥模式,docker还有其他实现网络功能的配置参数,比如overlay,主要是通过libnetwork来实现的,感兴趣的可以看一下libnetwork-design。在容器网络模型中,每一个容器内部都包含一个 Sandbox,其中存储着当前容器的网络栈配置,包括容器的接口、路由表和 DNS 设置,Linux 使用网络命名空间实现这个 Sandbox,每一个 Sandbox 中都可能会有一个或多个 Endpoint,在 Linux 上就是一个虚拟的网卡 veth,Sandbox 通过 Endpoint 加入到对应的网络中,这里的网络可能就是我们在上面提到的 Linux 网桥或者 VLAN。