深入剖析Kubernetes-学习笔记-05~08-关于容器的技术基础的一些知识

深入剖析Kubernetes学习笔记

极客时间-张磊-深入剖析kubernetes

05 06

容器的本质

容器是一种沙盒技术。沙盒即是应用的集装箱,把应用封装起来,应用与应用之间有了边界,互不干扰。被装进集装箱的应用,可以方便地搬来搬去。(在不同的机器中迁移)

通常一个程序运行之后,会产生一个进程,这个进程包含了内存中程序需要的数据、寄存器里的值、堆栈中的指令、被打开的文件、各种设备的状态等等信息。容器技术的核心功能,就是约束和修改进程中的信息集合,为程序创造出边界。

对于Docker等大多数Linux容器,Cgroups是制造约束的主要手段,Namesapce是修改进程视图的主要方法。

Cgroups(control groups)是Linux内核提供的用于限制、记录、隔离进程组所使用的cpu、内存、IO等物理资源的机制。

Namespace机制其实只是Linux创建新进程的一个可选参数。

int pid = clone(main_function, stack_size, **CLONE_NEWPID** | SIGCHLD, NULL);

指定CLONE_NEWPID后,新创建的进程会看到全新的进程空间,在这个空间里,它的pid是1,在宿主机的真实进程空间里,它的进程还是正常值,例如100。

容器是一种特殊的进程。Docker容器实际上是在创建容器进程时,指定了一组Namespace参数,这样容器就只能看到当前Namespace限定的资源、文件、设备、状态、配置等。

与虚拟机的区别

虚拟机是将硬件虚拟化,在虚拟硬件上安装新的操作系统Guest OS。运行在虚拟机的进程,能看到的自然就只有Guest OS上的文件,以及OS依赖的虚拟设备。

Docker跟虚拟机不同,它启动的还是原来的进程,但是在创建时,指定了各种各样的Namespace参数,使得进程在各自pid namespace里看到自己pid=1,只能看到各自mount namespace里挂载的文件,访问各自network namespace里的设备等。

因此,docker不需要额外的完整OS,而虚拟机需要,这带来了额外的资源消耗和占用,比如内存,比如系统调用需要经过拦截和处理,尤其对算力资源、网络、磁盘IO损耗非常大。

容器最大的优势,是敏捷和高性能。

容器的缺陷是隔离不彻底,多个容器使用同一个os内核。意味着在win上运行linux容器,在低版本linux上运行高版本linux容器行不通。

其次,linux内核中很多资源不能被namespace化,比如时间。容器内通过系统调用修改时间会影响宿主机。

限制容器使用的资源

容器实际上是特殊的进程,因此,它的资源会被其他进程抢占,它自己也可以占完所有的资源。所以需要通过Cgroups机制限制。

mount -t cgroups

可以看到可以被cgroups限制的资源,比如cpu、memory,称为cpu子系统等。

在对应目录下mkdir,产生一个控制组,目录里自动生成的文件代表这个控制组的限制策略以及被限制的pid。

docker等项目只需要在每个子系统下为每个容器创建一个控制组,在启动容器之后把进程对应的pid填入到对应的控制组tasks文件中。

控制组下的资源文件(限制策略)里填入的值由docker run的参数指定。

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

跟namespace类似,Cgroups对资源的限制也有很多不完善,比如/proc文件系统的问题。

/proc存储的是记录当前内核运行状态的一系列特殊文件,通过访问它们可以获得系统及当前正在运行的进程的信息。

/proc文件系统不了解Cgroups限制的存在,比如在容器中top,显示的是宿主机的cpu和内存数据,而不是当前容器的。

在生产环境中这个问题必须修正,否则很容易带来困惑,分分钟爆资源。这也是容器不如虚拟机的一个地方。

lxcfs是一个小型文件系统,用于让容器正确地看到自己能使用的资源(top等命令)。在容器访问/proc时,会被lxcfs拦截,然后lxcfs访问cgroups,返回正确的资源限制信息。

容器是一个单进程模型

一个正在运行的docker容器,其实是一个启动了多个Linux namespace 的应用进程,它能使用的资源量,受Cgroups配置限制。

用户的应用进程实际上是容器里pid=1的进程,也是后续创建所有进程的父进程。

容器的本质是一个进程。意味着不能在一个容器中同时运行两个不同的应用,除非事先找到一个公共的pid=1的程序来充当两个不同应用的父进程。 这也是很多人用systemd或supervisord代替应用本身作为容器启动进程的原因。(后续会写到更好的办法)

07

容器视角的文件系统

Mount Namespace是对chroot命令不停改进的结果,Mount Namespace更改的是容器对挂载点的认知,需要“挂载”操作实际发生后才有效,否则会继承宿主机的挂载点。出于对用户友好,挂载的操作应该自动完成。

docker项目为待创建的用户进程:

  1. 启用Linux Namespace配置
  2. 设置指定的Cgroups参数
  3. 切换进程的根目录

完整的容器就诞生了。

为了让容器的根目录更真实,一般会在容器根目录下挂载一个完整os的文件系统。这个文件系统用来为容器进程提供隔离后执行环境,称为“容器镜像”,专业名字叫rootfs(根文件系统)。

最后一步会优先使用pivot_root系统调用,不支持才会使用chroot。

pivot_root把当前mount namespace的整个文件系统切换到一个新的root目录,移除到之前root文件系统的依赖。chroot针对某个进程,系统的其他部分依旧运行在老的root目录。

rootfs只是一个os包含的文件、配置、目录,不包括内核。在Linux中,只有在开机时os才会加载指定版本的内核镜像。因此同一宿主机上所有容器,共享宿主机os的内核。

对一个应用来说,os才是它运行所需最完整的依赖。由于rootfs打包整个os的文件和目录,应用及它需要的依赖,都被封装在一起。

rootfs的维护方式

为了使rootfs可重用,docker在镜像设计中,引入layer概念,用户制作镜像的每一步操作,都生成一个层,即增量rootfs。

UnionFS可以把两个目录A、B联合(取并集)挂载到一个目录C上,如果在C上修改,也会在A、B中生效。(如何判断A、B的文件是同一个?md5之类?)

docker使用的是AuFS,是对Linux原生UnionFS的重写和改进。

docker镜像的层都放在/var/lib/docker/aufs/diff/{layer_id}目录下,被联合挂载到/var/lib/docker/aufs/mnt/{公共目录id}上。通过查看AuFS的挂载信息(cat /proc/mounts | grep aufs),找到当前公共目录对应的AuFS的内部ID(si),使用si可以在/sys/fs/aufs下查看被联合挂载的各个层信息(layer_id,只读,可读写权限等)

以Ubuntu镜像为例,从下到上

第一部分,只读层以增量形式包含os的各个目录

第二部分,docker单独生成的init层,存放/etc/hosts/etc/resolv.conf等配置信息。它们本来属于只读的一部分,但用户往往需要根据个人对当前容器修改,在启动时写入指定值,这些修改只对当前容器有效,不会在docker commit时一起提交。在修改了这些文件后,docker以一个单独的层(init层)挂载出来,commit时只提交可读写层,不包含这些。

第三部分,可读写层为空,在容器内做了写操作,修改内容以增量形式出现在这一层,如果操作是删除只读层的文件,AuFS会在可读写层创建一个whiteout文件,比如.wh.foo,使得在可读写层与只读层被联合挂载后,只读层的foo文件不可见。

08

docker exec如何进入容器里

一个进程的Namespace信息在宿主机上以一个文件的方式存在

docker inspect --format '{{ .State.Pid }}' 容器id

通过上述指令,查看正在运行的容器的进程号pid

ls -l /proc/pid/ns

查看对应进程的所有Namespace对应的文件

一个进程的每种Linux Namespace,在/proc/pid/ns下有一个对应的虚拟文件,并链接到真实的Namespace文件

一个进程可以选择加入到某个进程已有的Namespace中,达到进入那个进程所在容器的目的,这就是docker exec的原理,依赖setns()的Linux系统调用实现。

打开/proc/pid/ns下的虚拟文件(如/proc/pid/ns/net),得到文件描述符fd,调用setns(fd,其他参数),即可让当前进程进入相同的namespace。

在宿主机中,查看/proc/进入者pid/ns/net下,可以看到进入者与容器进程共享了同一个namespace。

docker run -it --net container:容器id 镜像名 进程

可以用这种方式让新启动的容器加入到指定容器的network namespace中。如果指定-net=host,意味着这个容器不会为进程启用network namespace,与宿主机其他进程直接共享宿主机的网络。

docker如何与宿主机进行文件交互

Volume机制允许将宿主机上指定的目录或文件挂载到容器里进行读取和修改。

docker run -v /test ...

docker run -v {指定目录}:/test ...

挂载操作出现在容器的可读写层。

第一种方式会在宿主机创建临时目录/var/lib/docker/volumes/{volume_id}/_data然后把它挂载到容器的/test目录上。volume_id可以用docker volume ls命令查看。

在07中提到过,容器进程创建后,尽管开启mount namespace,在执行chroot或pivot_root之前,容器可以看到宿主机的文件系统,即可以看到/var/lib/docker/aufs/mnt/下容器各个层的文件。

在容器的rootfs准备好之后,执行chroot前,将指定目录(如/home)挂载到容器内目录在宿主机上的对应目录(/var/lib/docker/aufs/mnt/{可读写层id}/test)上,volume的挂载工作就完成了。

在执行这个挂载操作时,“容器进程”(不是应用进程,而是容器初始化进程dockerinit,它会负责根目录准备、挂载设备和目录、配置hostname等初始化操作,最后通过execv系统调用,让应用进程取代自己成为容器里pid=1的进程)已经创建,此时mount namespace开启,这个挂载操作只对容器可见,因此,在容器外部访问/var/lib/docker/aufs/mnt/{可读写层id}/test,只会看到空目录,被挂载的内容不会被docker commit提交(但是仍然会提交一个空的test目录,毕竟执行了mkdir操作)。

容器对挂载目录的修改,会反映到指定目录,或者/var/lib/docker/volumes/{volume_id}/_data中。

volume使用的技术是Linux的绑定挂载(bind mount)机制。在容器内部,/test目录的指针,指向指定目录如/home,所以修改/test实际上就是修改/home,而在容器外部,/test对应的宿主机目录/var/lib/docker/aufs/mnt/{可读写层id}/test仍然指向自身,所以看不到挂载。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值