聊城汽车网站建设,北京手机网站,住房城乡建设部网站,莱芜雪野湖风景区前言
Linux容器的本质#xff0c;是一个被限制和隔离的进程。它通过Linux内核提供的命名空间#xff08;Namespaces#xff09;实现资源隔离#xff0c;通过控制组#xff08;Cgroups#xff09;实现资源限制#xff0c;通过联合文件系统#xff08;UnionFS#xff0…前言
Linux容器的本质是一个被限制和隔离的进程。它通过Linux内核提供的命名空间Namespaces实现资源隔离通过控制组Cgroups实现资源限制通过联合文件系统UnionFS实现轻量化的镜像分层。与传统虚拟机不同容器共享宿主机内核仅通过内核特性隔离进程视图如PID、网络、用户等命名空间因此启动更快、开销更小。典型实现如 Docker实质是通过 runC 等运行时工具调用内核 API将应用及其依赖打包为可移植的标准化单元。
本文基于 Linux 内核 API 实现一个简易版容器通过 shell 脚本的开发方式。
容器三板斧
命名空间Namespaces—— 实现资源隔离
Namespaces 是 Linux 内核提供的一种资源隔离机制它使得进程拥有独立的视图让进程“看起来”自己拥有独立的 网络栈、文件系统挂载、主机名域名、用户和 PID 等资源。
这种隔离有什么用呢
举个例子你通过容器部署多个后端服务服务均占用 8080 端口如果容器没有独立的网络栈容器服务的端口之间就会冲突这显然不是容器应该出现的问题。
再举个例子在宿主机上你只有普通用户的权限但你想在容器里以 root 用户折腾一下这完全是可以的。在容器里你甚至可以有单独的文件系统随意折腾都不会影响到宿主机。
控制组Cgroups—— 实现资源限制
Namespaces 更像是逻辑上的隔离容器本质上和宿主机上其它进程没有任何区别。这就会带来一个问题如果某个容器失控耗尽了所有的内存、CPU、IO 资源就会导致宿主机上的其它进程或容器“饿死”甚至宿主机本身崩溃这肯定不是我们想看到的。
所以 Linux 内核提供了 Cgroups 来限制进程对资源的占用限制。
举个例子在一台 16c 64g 的机器上通过容器部署服务限制每个容器最多占用 2c 4g这样即使某个容器失控它最多也只会占用 2c 4g 的资源影响不会很大。 根文件系统Rootfs—— 独立的文件系统
要想让容器隔离的更彻底容器应该具备一个独立的根文件系统。在这个根文件系统里具备容器服务运行的所有依赖和三方库这就避免因为运行环境不同而带来各种各样的问题。
举个例子我开发的一个服务应该运行在 centos 上同时要求其已经安装了 Tomcat 和 JDK。那么在启动这个容器时宿主机就应该为容器准备好这么一个根文件系统在这个根文件系统里已经装好了 Tomcat 和 JDK同时具备 centos 的基础依赖和库。最后把容器进程的根文件系统切到这个下面即可。
实现简易容器
有了上述前置知识实现一个简易容器还是很容易的。为了方便这里采用 shell 脚本的方式。
要实现一个简易版容器首先你要有一台 Linux 机器然后它必须支持 Namespaces 和 Cgroups 同时你还要准备一个镜像文件也就是容器的根文件系统。
如下所示在/pocker目录下有2个文件 image.tar 镜像文件解压后是一个基础的Ubuntu文件系统同时它里面安装了 nginxpocker 实现容器的脚本文件参考 docker 命名方式
pocker 脚本代码如下所示
定义了容器运行的基础工作目录、和 cgroup 的目录image_file 是容器镜像文件通过命令行参数指定container_name 容器的名字随机8位字符container_path 容器的路径在工作目录下以容器名创建的目录container_rootfs_path 容器根文件系统路径镜像文件会解压到这里cgroup_path 容器关联的 cgroup 路径容器启动会创建这个目录然后配置 CPU、内存的限制netns_name 容器网络命名空间的名称netns_加上容器名变量赋值后解压镜像文件到容器根文件系统创建容器关联的 cgroup 目录配置 CPU、内存的限制创建 veth peer可以把它看作是一根网线一端 veth0 插在容器上一端 veth1 插在宿主机上宿主机通过 veth1 访问容器内的 nginx 服务调用 unshare 让容器进程拥有独立的命名空间chroot 切换容器进程的根目录到容器的根文件系统最后挂载 proc修改 hostname启动nginx再启动一个 bash 方便和容器交互最后容器进程推出释放相关资源 #!/bin/bash
base_path/var/pocker
cgroup_base_path/sys/fs/cgroup
image_file
container_namerun(){image_file$1container_name$(tr -dc A-Za-z0-9 /dev/urandom | head -c 8)container_path$base_path/$container_namecontainer_rootfs_path$base_path/$container_name/rootfscgroup_path$cgroup_base_path/$container_namenetns_namenetns_$container_nameprint_initmkdir -p $container_rootfs_pathtar -xf $image_file -C $container_rootfs_pathprintf The image file has been successfully decompressed.echo $cmd $container_path/init.cmdmkdir -p $cgroup_pathecho 20000 100000 $cgroup_path/cpu.maxecho 256M $cgroup_path/memory.maxecho $$ $cgroup_path/cgroup.procsprintf cgroup configuration successful.\nveth0veth0-$container_nameveth1veth1-$container_namesudo ip netns add $netns_namesudo ip link add $veth0 type veth peer name $veth1sudo ip link set $veth0 netns $netns_namesudo ip netns exec $netns_name ip link set $veth0 name eth0sudo ip netns exec $netns_name ip addr add 10.1.1.1/24 dev eth0sudo ip netns exec $netns_name ip link set eth0 upsudo ip addr add 10.1.1.2/24 dev $veth1sudo ip link set $veth1 upprintf Network configuration successful.\nsudo ip netns exec $netns_name \unshare -fmuip \chroot $container_rootfs_path bash -c mount -t proc proc /proc hostname $container_name mknod -m 666 /dev/null c 1 3 /sbin/nginx bashsudo ip netns delete $netns_namerm -rf $container_pathprintf quit...\n
}print_init(){printf prepare to start the container: $container_name \nprintf container image: $image_file \nprintf container path: $container_path \n
}case $1 inrun)shiftrun $;;*)echo Unknown command: $1;;
esac启动容器
在启动容器前先看下宿主机的网络设备发现是没有 veth 设备的。 接着在/pocker目录下执行下述命令即可启动一个部署了nginx的容器
$ ./pocker run image.tar如上所示没有报错就启动成功了。
现在当前bash终端是运行在容器内的同时可以看到hostname 已经是容器名1h0MuJlK了。 当前bash是容器内的1号进程容器进程PID是和宿主机隔离的。 容器的网络栈也是和宿主机隔离的只有一个eth0它其实是我配置的veth0只不过被我改名成 eth0了。 容器内 nginx 也已经部署成功了 先别着急访问服务先看下 cgroup 是否生效。在脚本里我们对 CPU 的配置是20000 100000也就是在 100000 微秒内允许容器占用 20000 微秒的 CPU即允许容器最多使用 1/5 颗CPU核心。
在容器内执行while : ; do : ; done死循环耗光CPU另开一个bashtop 命令查看发现容器进程占用了单颗CPU核心的20%cgroup 确实生效了再也不担心单个容器把资源耗光了。 接下来在宿主机上开一个bash。为了体现“容器的本质就是进程”这句话可以看到容器内运行的nginx在宿主机上就是一个普通的进程和其它进程并无二致。
事实上nginx 的实际进程PID51256但是在容器看来它的进程PID6这就是 Namespace 视图的“障眼法”在起作用。 最后我们访问容器服务。因为容器服务绑定的端口是在自己独立的网络栈里面的也就是 veth0要访问它必须通过 veth1因为它俩是相互连接的。
在宿主机上也能看到 veth 设备了它的IP是 10.1.1.2对端 veth0 的 IP 是 10.1.1.1。 因为nginx默认绑定的是80端口要想访问容器nginx服务直接通过10.1.1.1:80访问即可。 容器退出后相关的资源都会被释放掉。
至此容器启动演示完毕。
尾巴
容器的本质是进程只不过它是被 Namespaces 视图隔离同时被 cgroups 资源限制的进程它拥有自己的根文件系统 Rootfs根文件系统包含容器运行的除内核外所有依赖的文件这些文件打包以后就是容器镜像。
通过 shell 脚本利用 Linux 内核特性可以很轻松实现一个简易版的容器本文重点是帮助大家理解容器是怎么一回事儿这种方式实现的容器远不到可用的程度。
不过话说回来容器本身没有什么价值有价值的是容器编排。