容器底层原理探秘

这篇分享用Namespace和Cgroup的底层原理带着大家直观感受一下容器的玩法,更多相关知识点请自行查阅官方文档。

Namespace

Linux Namespace 有如下种类:

注意,后面的模拟代码会用到相关的系统调用参数

1.1 相关系统调用

与 Linux Namespace 相关的系统调用如下:

1、chroot(),即 change root directory(更改root目录),在Linux系统中,系统默认的目录结构都是以 “/”,即以根(root)开始的,而在使用chroot之后,系统的目录结构将以指定的位置作为 “/” 位置

2、clone(),实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离

3、unshare(),使某进程脱离某个namespace

4、setns(),把某进程加入到某个namespace

5、execv(),这个系统调用会把当前子进程的进程空间全部覆盖掉

注意,后面的模拟代码会用到相关的系统调用!

1.2 模拟Docker镜像

一个最常见的 rootfs,或者说容器镜像,会包括如下所示的目录:

因为我们只是模拟,所以只创建几个目录:

拷贝命令到新创建的bin目录:

注意,还需要把命令依赖的so文件拷贝到lib目录,查找so文件需要用到 ldd 命令:

还有这些文件我们不希望写死在镜像中,如下图所示:

因此,我们在rootfs外面再创建一个conf目录,把上面3个文件放进去,然后把这个conf目录mount进容器,具体操作看后面的代码。

关于mount再多说一嘴,其主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响

简单来说,绑定挂载实际上是一个inode(不清楚inode是啥的自行google吧)替换的过程,如下图所示:

最后一步,执行chroot命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:

这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。至于很多同学都知道的 UnionFS,说白了,就是基于旧的rootfs以增量的方式来维护而已。

有兴趣的同学可以用下面的代码再来模拟一遍Docker:

1.3 模拟Docker网络

容器有自己的 Network Namespace 和与之对应的网络接口eth0,宿主机上也有自己的eth0,而宿主机上的eth0对应着真正的物理网卡,也就是说,宿主机上的eth0才可以和外面通讯。那么容器是怎么跟外面通讯的呢?

如上图所示,要解决容器与外面通讯需要解决两个问题:

首先,启动一个不带网络配置的容器:

找到这个容器进程的pid:

通过 /proc/$pid/ns/net 这个文件得到容器 Network Namespace 的ID,然后在 /var/run/netns/ 目录下建立一个符号链接,指向这个容器的 Network Namespace:

完成这步操作之后,在后面的 ip netns 操作里,就可以用pid的值作为这个容器的 Network Namesapce 的标识了。

接下来,用 ip link 命令来建立一对veth的虚拟设备接口,分别是veth_container和veth_host:

把veth_container这个接口放入到容器的 Network Namespace 中:

把veth_container重新命名为eth0,因为这时候接口已经在容器的 Network Namesapce 里了,所以不会和宿主机上的eth0命名冲突:

接着对容器内的eth0做基本的网络IP和缺省路由配置:

完成这一步,就解决了让数据包从容器的 Network Namespace 发送到宿主机的 Network Namespace 上的问题,如下图所示:

将veth_host这个设备接入到docker0这个bridge上:

至此,容器和docker0组成了一个子网,docker0上的IP就是这个子网的网关IP,如下图所示:

要让子网可以通过宿主机上eth0去访问外网,还需要添加下面的iptables规则:

最终的veth网络配置如下图所示:

最后测试一下是否能ping通外网:

如果发现ping不通,先检查下是否打开IP转发功能:

这里简单提一嘴 macvlan/ipvlan 方案:对于macvlan,每个虚拟网络接口都有自己独立的mac地址;而ipvlan的虚拟网络接口是和物理网络接口共享同一个mac地址。同时它们都有自己的 L2/L3 的配置方式。有兴趣的同学可以按照如下步骤为容器手动配置上ipvlan的网络接口:

docker run --init --name ipvlan-test --network none -d busybox sleep 36000
 
pid1=$(docker inspect ipvlan-test | grep -i Pid | head -n 1 | awk '{print $2}' | awk -F "," '{print $1}')
echo $pid1
ln -s /proc/$pid1/ns/net /var/run/netns/$pid1
 
ip link add link eth0 ipvt1 type ipvlan mode l2
ip link set dev ipvt1 netns $pid1
 
ip netns exec $pid1 ip link set ipvt1 name eth0
ip netns exec $pid1 ip addr add 172.17.3.2/16 dev eth0
ip netns exec $pid1 ip link set eth0 up

完成上面的操作后,ipvlan网络二层的连接如下图所示:

有兴趣的同学可以再深入思考下Kubernetes相关的网络问题:

Cgroup

使用Cgroup可以根据具体情况来控制系统资源的分配、优先顺序、拒绝、管理和监控,提高总体效率。

2.1 Cgroup子系统

下面列举的是 Cgroup V1 相关子系统:

Linux Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下,可以通过 mount 命令查看:

如果没有看到上述目录,可以自行做mount,如下所示:

mkdir cgroup
mount -t tmpfs cgroup_root ./cgroup
mkdir cgroup/cpuset
mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/
mkdir cgroup/cpu
mount -t cgroup -ocpu cpu ./cgroup/cpu/
mkdir cgroup/memory
mount -t cgroup -omemory memory ./cgroup/memory/

一旦mount成功,那些mount的目录下面就会有很多相关文件,如下图所示:

2.2 限制CPU的栗子

在使用 CPU Cgroup 前,先介绍三个重要的参数:

1、cpu.cfs_period_us,用来配置时间周期长度,一般它的值是 100000,单位是微秒(us)

2、cpu.cfs_quota_us,用来配置当前Cgroup在设置的周期长度内所能使用的CPU时间数,单位也是微秒(us),通常不会修改

3、cpu.shares,用来设置CPU的相对值,并且是针对所有的CPU(内核),默认值是1024。假设系统中有两个Cgroup,分别是A和B,A的shares值是1024,B的shares值是512,那么A将获得1024/(1204+512)=66%的CPU资源,而B将获得33%的CPU资源,注意相对值这个概念:

了解了上面的参数,那么应该能看懂下面这条docker命令:

接下来,我们使用原生的Cgroup来实现上面的命令。

首先,创建一个 CPU Cgroup:

设置 cpu.cfs_quota_us 为 20000:

查看当前Shell进程的PID:

然后,在bash中启动一个死循环来消耗cpu,正常情况下应该使用100%的CPU,即消耗一个内核:

将这个测试Shell进程的PID加入 CPU Cgroup:

加入之后可以看到CPU使用率降到了20%:

关于Kubernetes的CPU限制出门右转看 [Assign CPU Resources to Containers and Pods](https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/)。

2.3 Cgroup V2

Cgroup有V1和V2两个版本,V1版本中的 blkio Cgroup 只能限制 Direct IO,不能限制 Buffered IO。

这是因为 Buffered IO 会把数据先写入到内存 Page Cache 中,然后由内核线程把数据写入磁盘,而Cgroup V1 的子系统都是独立的,即 Cgroup V1 blkio 的子系统是独立于 memory 子系,所以无法统计到由 Page Cache 刷入到磁盘的数据量。

这个 Buffered IO 无法被限速的问题,在 Cgroup V2 里被解决了。Cgroup V2 从架构上允许一个控制组里有多个子系统协同运行,这样在一个控制组里只要同时有 io 和 memory 子系统,就可以对 Buffered IO 作磁盘读写的限速,如下图所示:

虽然 Cgroup V2 能解决 Buffered IO 磁盘读写限速的问题,但是目前 runC、containerd以及Kubernetes都是刚刚开始支持 Cgroup V2,所以还需要等待一段时间。

展开阅读全文

页面更新:2024-05-07

标签:容器   宿主   子系统   底层   进程   接口   命令   原理   文件   目录   系统   网络

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top