容器技术

容器技术

容器和虚拟机的区别也是他的特点就是:容器是一种特殊的进程

隔离与限制

限制方案

namespace主要用来限制进程,而容器在运行时与宿主机共享内核,怎样保证容器占用资源不会影响到宿主机的稳定?

用到的技术主要是Linux cgroup

image-20230308152404830

/sys/fs/cgroup/cpu下新建文件夹后会自动生成子系统对应的资源限制文件

此时如果让系统启动一个死循环脚本并不作限制,会导致cpu占用到100

image-20230308152619334

通过cpu.cfs_quota_us的值-1可知未作任何限制

image-20230308153327515

CPU period为默认的100ms

现在修改cpu.cfs_quota_us的值添加限制,比如改为20ms,这表示在每100ms的时间里,这个控制组限制的进程只能使用20ms的CPU时间,也就是只能占用20%的CPU性能

然后把进程与这个控制组绑定

image-20230308153622329

除了cpu子系统之外,Cgroup的每一项子系统都有自己的资源限制能力

  • blkio,为块设备限定I/O限制,一般用于磁盘
  • cpuset,为进程分配单独的CPU核和对应的内存节点
  • memry,为进程设定内存使用限制

像Docker这种容器,只需要在每个子系统下为每个容器创建一个控制组(就是新建一个目录,资源限制文件会自动生成),然后启动容器进程之后再把进程与控制组绑定即可

docker使用run启动容器时可以指定限制的值,比如

docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

大部分资源可以通过Cgroup做限制,但还是有例外,比如/proc,/proc文件系统不了解Cgroup的存在

所以如果在容器里执行top命令,会发现他的信息是宿主机的cpu和内存数据

Docker

核心原理:为带创建的用户进程:

  • 启用Linux Namespace配置
  • 设置指定的Cgroup参数
  • 切换进程的根目录

切换进程的根目录会优先使用pivot_root系统调用,如果系统不支持,才会使用chroot

namespace机制也是在chroot技术上延伸发展而来

一致性

容器与宿主机共用内核,而在linux中万物皆文件,所以如果在宿主机中有另一个系统的完整文件系统,理论上来说就可以依靠这些文件运行宿主机中原本没有环境的程序。

在linux中,rootfs即代表一个操作系统所包含的文件、配置和目录

共用内核,使用配置好环境的rootfs,再结合chroot切换根目录以及namespace和Cgroup限制资源,这样配合下来完成隔离与运行,也就是容器

这样做还是有一些问题,比如:程序或者系统迭代时,需要重复制作rootfs,比如配置好而且未做其他操作的java环境可能会被其他人到,如果我部署了自己的应用之后再封装制作rootfs,那么其他人拿到这个rootfs还要把我的内容清除。

直观一点的解决办法是:每做一次关键操作就封装一次

而在docker中提出的解决办法是,将最初的rootfs作为一个基座,并在镜像的设计中引入了“层(layer)”的概念,也就是用户制作镜像的每一步操作都会生成一个层,也就是一个增量rootfs

我粗浅的理解为虚拟机的快照,在每一个关键节点打一个快照

image-20230308164551988

每一层都是容器系统文件与目录的一部分,而在使用镜像时,Docker会把这些增量联合挂载在一个统一的挂载点上

容器的rootfs结构

  • 可读写层
  • init层(ro+wh)
  • 只读层(ro+wh)

以AuFS为例,如果我们需要操作可读写层的文件,比如删除

AuFS会在可读写层创建一个whiteout文件,如.wh.foo文件,当两个层被联合挂载后,foo文件就被.wh.foo文件遮挡,从而消失,一般称之为“白障”

init层在只读层和可读写层之间,是Docker单独生成的一个内部层,专门用来存储/etc/hosts、/etc/resolv.conf等信息

docker exec的执行原理

Linux namespace创建的隔离空间虽然不可见,但是实际上一个进程的namespace信息在宿主机上是以文件形式存在的

通过docker inspect --format '{{.State.Pid}}' 容器id得到进程ID

image-20230309170030758

通过查看宿主机的proc文件,可以看到该进程下的所有namespace对应的文件

image-20230309170149945

通过/ns看到一个进程的每种Linux namespace都有它对应的一个虚拟文件,并且链接到一个真实的namespace上

容器中的namespace和宿主机中的文件相绑定,那么我们就可以操作这些namespace,比如加入一个已经存在的namespace中

也就是一个进程可以选择加入进程已有的某个namespace中,从而进入这个容器

net为例:

如果我们现在有一个ubuntu容器已经启动,容器执行后PID=25686,这个容器中默认配置了两个网卡,而宿主机有四个网卡

那么如果我们exec进入到容器内,执行ifconfig命令可以看到的就是两个网卡的信息

一个进程选择加入到另一个进程的某个namespace中的操作主要依赖于setns()的Linux系统调用

示例程序

//set_ns
....
int fd;
fd = open(argv[1], O_RDONLY);
if(setns(fd,0) == -1) {
	errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");

这个程序接受两个参数,一个是要进入namespace文件的路径,一个是要在这个namespace中运行的进程,比如/bin/bash

set_ns /proc/25686/ns/net /bin/bash

这样就加入到25686的net namespace中去,那么此时我们用它执行ifconfig就是和在25686容器中进程中执行ifconfig得到一样的结果

这说明这两个进程是共享了Net Namespace

在docker中如果指定--net=host,类似桥接模式,此时和宿主机的网络没有隔离,共享宿主机的网络栈

加入其他Namespace同理,从而实现通过exec进入隔离的容器

容器探针

四种方法

方法 原理
exec 在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功
grpc 使用 gRPC 执行一个远程过程调用。 目标应该实现 gRPC 健康检查。 如果响应的状态是 "SERVING",则认为诊断成功
httpGet 对容器的 IP 地址上指定端口和路径执行 HTTP GET 请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的
tcpSocket 对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。 如果远程系统(容器)在打开连接后立即将其关闭,这算作是健康的
posted @ 2023-07-24 17:21  pr1s0n  阅读(25)  评论(0)    收藏  举报