【Docker】原理杂谈

194 阅读6分钟

更新历史

  • 2022-08-16 初稿

Docker是什么

Docker封装了整个软件运行时环境,是可以做资源隔离和限制的进程。既然可以做隔离和限制,那么也可以不做隔离和限制,这样就和在宿主机上启动一个进程差不多了。同宿主机直接启动进程相比,Docker容器由Docker Engine进行进程管理,裸机进程由Systemd做进程管理。容器内的文件系统可以是其他的Linux发行版。

Docker Engine根据需求将隔离和限制后的容器,以指定的容器镜像做文件系统挂载,启动容器内的指定可执行文件作为子进程,提供服务,将服务端口转发映射到宿主机上。可以看到Docker Engine做了大量底层工作,着重可以关注隔离用的Namespace概念,限制用的Cgroup概念,容器镜像概念。

Namespace

“Namespace做隔离,Cgroup做限制。”学过Docker原理的同学都知道这么一句话。那么,有哪些类型的Namespace呢?

类型用途
UTSUTS(UNIX Time-sharing System) namespace提供了主机名和域名的隔离,这样,每个docker容器就可以拥有独立的主机名和域名了,在网络上可以被视为一个独立的节点,而非宿主机上的一个进程。Docker中,每个镜像基本都以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响,其原理就是利用了UTS namespace。
IPC进程间通信(Inter-Process Communication, IPC) 涉及的IPC资源包括常见的信号量、消息队列和共享内存。申请IPC资源就申请了一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同的IPC namespace下的进程互不可见。
PIDPID namespace隔离非常实用,它对进程PID重新标号,即两个不同的PID namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace。它所创建的新PID namespace被称为child namespace,而原先的PID namespace就是新创建的PID namespace的parent namespace。通过这种方式,不同的PID namespace就会形成一个层级体系。所属的父节点可以看到字节点中的进程,并可以通过信号等方式对字节点中的进程产生影响。反过来,子节点却不能看到父节点PID namespace中的任何内容,由此产生如下结论。1,每个PID namespace 中的第一个进程PID 1,都会像传统Linux中的init进程一样拥有特权,起特殊作用。2,一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义。3,如果你在新的PID namespace中重新挂载/proc文件系统,就会发现其下只显示同属于一个PID namespace中的其他进程。4,在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。
MNTmount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个Linux namspace,所以标识位比较特使,为CLONE_NEWNS。隔离后,不同mount namespace中的文件结构发生变化也互不影响。可以通过/proc/[pid]mounts查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等。
NETnetwork namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字等。一个物理的网络设备最多存在于一个network namespace中,可以通过创建veth pair(虚拟网络设备对:有两端,类似管道,如果数据从一端传入另一端也能收到,反之亦然)在不同的network namespace间创建通道,以达到通信目的。
USERuser namespace主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID,用户组ID,root目录,key(密钥)以及特殊权限。说的通俗一点,一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。
CgroupLinux 内核4.6之后又添加了Cgroup这namespace, 用于隔离Cgroup的根目录
TimeLinux 内核5.6 之后又添加了时钟namespace,用于隔离容器和宿主机的时间

Cgroup

Cgroup到底可以限制哪些资源呢?

  • CPU
  • 内存
  • 磁盘IO

容器镜像

容器隔离文件系统之后,只能看到挂载点下的文件,并共享宿主机的内核。

容器、Docker Engine和宿主机的关系

容器实际上是宿主机上的一个进程,只是其很多namespace和宿主机不同或相同。Docker Engine是容器进程的父进程。

容器和镜像的关系

可以用可执行文件和进程的关系,来类比镜像和容器的关系。即镜像是容器的静态表现,容器是镜像启动后的动态表现。

容器和虚拟机的区别

  • 容器的性能比虚拟机好,几乎接近宿主机的性能
  • 容器的隔离性不如虚拟机好,一个容器的崩溃,有可能影响到宿主机上其他的容器运行
  • 虚拟机上可以启动容器,而几乎不会有人在容器中启动虚拟机

如何使用容器

  • 将容器看成一个函数来使用,所有变量由外部传入,结果输出到容器外
  • 避免使用容器内的变化配置,比如IP地址,避免变量写死在容器内,由外部传入管理更方便。
  • 避免在容器内使用多进程,特别是业务不相关的不同进程
  • 容器的进程要前台启动,否则启动后就停止了,避免后台启动主进程,然后使用无关命令挂前台的情况,比如"tail -f /var/log/message",这事关容器生命周期管理的正确性
  • 尽量使用Dockerfile来编排容器镜像的构建,声明式API的优势明显

什么情况下不推荐用容器

  • IO密集场景,例如数据库,日志管理等