Docker foundation in linux

作者:杨润炜
日期:2022/3/22 23:04

Why we need container?

工程化往往需要保持各个功能间的环境相对独立/互不干扰,来达到扩展性/稳定性的要求。
在Pass中,“应用托管”将应用/资源抽象出来,将部署/分发/调度工作通过平台统一管理和实现,这也逐渐成为云的核心竞争力,而这个功能的基础就是容器。
起初主要是通过虚拟机来实现容器,但存在应用打包困难的问题,因为应用运行需要一整套第三方依赖,包括操作系统的文件,如果是部署在虚拟机,这些依赖将需要通过部署脚本重现,但又因为同一虚拟机可能有不同的应用需要同样依赖的不同版本,或错误更改了别的应用的依赖,就很可能导致应用部署或运行异常。
这时候docker打包机制的优势就体现出来了,它把整个操作系统文件/相关依赖和应用都打成一个包,当容器被运行起来,应用看到的文件数据就跟开发时是一样的,保证了开发和部署环境的一致性。docker也保证了不同容器间的文件是独立的。

接下来,咱就来瞧瞧docker的实现原理吧。

Docker is a process

Docker就是操作系统中的一个进程。
docker给我的第一印象是把应用包在一个独立的环境,达到与宿主机互不干扰的效果。
我们知道进程需要各种资源,如:内存/CPU/网络/文件,这些是进程赖以生存的环境,一般多个进程在操作系统中都是共享这些资源的(除非有权限限制)。
那么,docker又是怎样构建起不同的容器或者说不同的进程间的“边界”的呢?

Make a border

overview

docker-foundations

Namespace

对容器(进程)资源视图的调整,诸如网络/文件,让容器看到我们想让它看到的资源范围。

mount namespace

创建独立的文件系统空间,如将宿主机的某一目录映射到容器内,容器将其挂载为系统根目录。

network namespace

创建独立的网络空间,即在容器内创建虚拟的网卡。但在容器这个子网络空间里,是看不到宿主机这个父级网络空间的。
与宿主机的通信需要veth pair这个虚拟网络设备。
实际应用中,如果需要打破网络隔离,可以用-net=host,这样就单独将network namespace去掉,该容器就能直接看到宿主机的网络资源了。

Cgroups

namespace只是解决了资源能否看到的问题,但距离要实现资源隔离还差一步,就是资源限制。
Cgroups能够对容器(进程)资源进行限制管理。包括CPU/内存/网络带宽/磁盘等。
有个问题:当使用top在容器内查看容器资源时,看到的却是整台宿主机的,这是为啥?
因为Cgroups是通过限制进程的运行来达到限制的效果,具体是进程启动前配置好/sys/fs/cgroup下的相关进程信息,但top查看的是/proc下的进程信息,这些信息是没有关联到Cgroup配置的,所以产生了上述问题。
有个解决方法是将cgroup的信息关联到/proc,如lxcfs

Rootfs

为docker容器提供独立的文件系统。
除此之外,docker还为镜像打包/共享/部署做了优化。
用实际的场景举例。想象在应用版本迭代时,我们是不是要修改应用的文件,这时候已经有一个包含应用与操作系统打包后的镜像,如果这个镜像是一个不可分割的文件,那么修改一点点应用的数据就需要重新生成镜像,这会带来不少影响,如:打包性能,镜像共享效率,部署空间浪费。
docker通过union fs+layer方案解决上述问题。

union fs

联合文件系统,将不同的文件系统合并为一个。
用一个简单的例子描述下。
fs A

  1. .
  2. ├── A
  3. ├── a
  4. └── x

fs B

  1. |── B
  2. ├── b
  3. └── x

union A & B = C

  1. |── C
  2. ├── a
  3. ├── b
  4. └── x

layer

layer是指docker镜像的分层管理。
docker将镜像分为以下几层,分别对不同的数据进行管理:

  • readonly layer(只读层)
    • 此层的数据只读,不可更改;
    • docker commit时容器里的操作系统/应用文件等数据;
    • 包含了多个共享层,每个共享层都是一次dockerfile原语执行的结果,减小了共享的粒度,也就提高了共享率。
  • init层
    • 为容器启动时的配置预留,如hosts文件/dns配置文件;
    • 只为当前容器使用,不会被docker commit提交;
  • rw层(可读写层):
    • 在容器里进行增/删/改数据,都是在此层进行操作
    • 如果是对只读层数据进行操作,会在此层创建whiteout“遮挡”只读层,这就是所谓的增量修改
    • 可以通过docker commit 将此层的修改持久化到只读层。

容器在打包时,分将不同的layer合并,最终形成一个完整镜像。在后续修改发布时,只需要增量地提交和拉取,提高了共享的效率。部署时也可以复用相同共享层的数据,减少镜像和容器和空间占用,提高了空间利用率。

performance

layer读写优化: copy on write
当要修改只读层数据时,是在可写层写一份修改后的数据,“遮挡”掉只读层相应的数据,达到修改的目的。在可写层修改前,会从只读层复制待修改的数据到可写层,即所谓的写时复制。

volume

  1. docker run -v /tmp:/container_tmp

linux将docker里的/container_tmp目录inode指针,重定向到宿主机的/tmp inode

future

CRI—容器运行时规范。
因为社区出现了很多容器,如docker, cri-o, rkt等,然后容器主要的使用价值和场景在编排管理系统上,如k8s,为了让上层更容易接入使用,所以制定了CRI,各个容器都需要实现CRI定义的接口。编排系统k8s只需要使用CRI 接口就能切换不同的容器。最终,docker容器的价值将逐渐由CRI和容器编排替代。

reference

《深入剖析 Kubernetes》

感谢您的阅读!
如果看完后有任何疑问,欢迎拍砖。
欢迎转载,转载请注明出处:http://www.yangrunwei.com/a/128.html
邮箱:glowrypauky@gmail.com
QQ: 892413924