Docker 底层技术推导

1. 写在最前面

在听《牵丝戏》的时候突然灵光乍现想到了「与其扬汤止沸,不如釜底抽薪」这个观点。

怎么将这个观点应用到这篇文章中呢?

答曰,与其死磕 docker 的实现细节,不如站的高一点。思考下,从操作系统的角度上来看,要实现 docker,有哪些技术需要被使用到。(ps 总结这篇文章的背景是我在 mac 上跑 docker 的时候,发现电脑听起来有种飞机要起飞的赶脚……

2. 什么推动了docker 出现

2.1 出现原因

你以为是时代推动了 docker 的出现嘛?

不不不,推动一类技术热潮的必然是成本,而所谓成本就是钱。比如云计算,更直白点说,就是为资金不足去买服务器和机房的公司提供租赁式服务器,让他们用更少的成本来开公司。

而 docker 这个的使用可以理解为从两方面节省了成本:

  • 使用成本——让一台宿主机器能够部署更多的服务

    注: 这点的比较对象时虚拟机 ,想也知道对比对象如果宿主机的话,这个结果是不成立的,毕竟 docker 自身也要使用一部分资源不是。

  • 人力成本——让开发人员更便捷的开发、部署服务。

    Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications.

    注:一言以蔽之,docker 可以将程序的运行环境纳入版本管理中,排除了因为环境造成的不同运行结果的可能。

2.2 优势总结

  • 资源使用率

    Resource Efficiency: Process level isolation and usage of the container host’s kernel is more efficient when compared to virtualizing an entire hardware server.

  • 可移植性

    Portability: All the dependencies for an application are bundled in the container. This means they can be easily moved between development, test, and production environments.

  • 持续部署和测试

    Continuous Deployment and Testing: The ability to have consistent environments and flexibility with patching has made Docker a great choice for teams that want to move from waterfall to the modern DevOps approach to software delivery.

3. 大胆猜测一下实现

与其思考 docker 使用了什么技术, 不如从操作系统的组成角度来分析一下,实现 docker 的虚拟化需要用什么?

3.1 对比 Virtual Machine

首先来看看 docker 和 Virtual Machine 对比。

在这里插入图片描述

从使用角度的特性对比总结:

特性 容器 Virtual Machine
隔离级别 进程级 操作系统级
系统资源 0 ~ 5% 5 ~ 15%
启动时间 秒级 分钟级
镜像存储 GB-TB KM-MB

总结起来,就是一句话,相比于 Virtual Machine 来说,docker 用起来更香。

3.2 实现 docker 需要什么?

先从操作系统有什么入手分析一下。操作系统可以理解为调度进程系统,而进程又包含着对资源(CPU、内存、磁盘)的使用。

在这里插入图片描述

既然 docker 虚拟的核心点是隔离 ,问题就转化为,让一个主机上可以同时运行多个互不感知的进程,所以怎么实现上述的目标呢?

答曰:从实际生活的角度上来说,要隔离最好办法就是建立一个屏障,比如用篱笆或者墙。墙使用砖 + 水泥垒起来的,那操作系统中是不是存在可类比为砖和水泥的东西呢?

3.3 要隔离什么?

既然知道了需要「砖和水泥」来帮我们做隔离进程,而进程是包括了 CPU、内存、磁盘等各种资源,所以隔离的最终目的是要:

  • 要隔离进程
  • 要隔离资源

问题:运行在同一台机器的容器真的就不会互相影响了嘛?

答案:会,因为笔者之前常常听到同组的大佬抱怨「太坑了,他的容器又又又被同宿主机的其他容器影响到了」。

4. 真实的实现

4.1 隔离进程的工具

Docker 通过使用 Linux 的 namespaces 对不同的容器实现进程隔离。namespaces(命名空间)是 Linux 提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。

Linux 的 namespaces 提供了以下七种不同的命名空间:

Namespace 系统调用参数 内核版本 隔离内容
UTS (Unix Time-sharing System) CLONE_NEWUTS Linux 2.4.19 主机名与域名
IPC (Inter-Process Communication) CLONE_NEWIPC Linux 2.6.19 信号量、消息队列和共享内存
PID (Process ID) CLONE_NEWPID Linux 2.6.19 进程编号
Network CLONE_NEWNET Linux 2.6.24 网络设备、网络栈、端口等等
Mount CLONE_NEWNS Linux 2.6.29 挂载点(文件系统)
User CLONE_NEWUSER Linux 3.8 用户和用户组

以上特性的使用需要用到以下三个系统调用的 API:

  • clone:用来创建新进程,clone 创建进程的时候可以传递 namespces 的系统调用参数,用来控制子进程所共享的内容。
  • setns:让某个进程脱离某个 namespace
  • unshare:让某个进程加入某个 namespace

4.1.1 例子

以下是一个使用了 CLONE_NEWUTS、CLONE_NEWIPC 的例子

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    
    
    fmt.Println(os.Args[1])
    switch os.Args[1] {
    
    
    case "run":
        run()
    case "child":
        child()
    default:
        fmt.Printf("no command, exit")
    }
}

func run() {
    
    
    fmt.Printf("running %v\n", os.Args[2:])
    cmd := exec.Command("/proc/self/exe", append([]string{
    
    "child"}, os.Args[2:]...)...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{
    
    
      // 隔离主机名与域名
      // 进程编号
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
    }

    err := cmd.Run()
    if err != nil {
    
    
        panic(err)
    }
}

func child() {
    
    
    fmt.Printf("running %v as pid: %d\n", os.Args[2:], os.Getpid())

    cmd := exec.Command(os.Args[2], os.Args[3:]...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    err := syscall.Sethostname([]byte("InNamespace"))
    if err != nil {
    
    
        panic(err)
    }

    err = cmd.Run()
    if err != nil {
    
    
        panic(err)
    }

}

  • syscall.CLONE_NEWUTS 用法体现如下图所示:

    • 当前的主机名是 docker
    • 运行 go 程序 clone 子进程后
    • 进入子进程后主机名为 InNamespce

    注:syscall.Sethostname([]byte(“InNamespce”)) 用于给子进程设置了独立的主机名

在这里插入图片描述

  • syscall.CLONE_NEWPID 用法体现如下图所示:

    隔离进程编号后 PID 打印的时候存在一个明显的矛盾结果,running [/bin/bash] as pid: 1echo $$ 打印的结果不同,同时 ps 命令显示的 PID 也是不对的。

在这里插入图片描述

尝试挂载一个虚拟文件夹 proc 到本地文件夹查看校验一下。(ps 没有 proc 目录的需要提前mkdir proc

注:此刻父子进程共享的 /proc 目录还是正常的,可以通过 ls proc 查看

在这里插入图片描述

上述的用法在子进程中执行的挂载操作仍会影响父进程,可以用 CLONE_NEWNS 隔离挂载点(文件系统 ),子进程影响父进程的体现如下图所示:

注:子进程挂载影响到父进程

在这里插入图片描述

4.2 隔离资源的工具

Docker 使用 Linux 的 Control Groups 隔离资源,例如 CPU、内存、磁盘等。每一个 CGroup 都是一组被相同的标准和参数限制的进程,不同的 CGroup 之间存在层级关系。

4.2.1 子系统介绍

典型的子系统介绍如下:

  1. cpu 子系统,主要限制进程的 cpu 使用率。
  2. cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
  3. cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
  4. memory 子系统,可以限制进程的 memory 使用量。
  5. blkio 子系统,可以限制进程的块设备 io。
  6. devices 子系统,可以控制进程能够访问某些设备。
  7. net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
  8. freezer 子系统,可以挂起或者恢复 cgroups 中的进程。
  9. ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace。

在 CGroup 中,所有的任务都是系统的一个进程,而 CGroup 就是一组按照某种标准划分的进程,在 CGroup 这种机制中,所有的资源控制都以 CGroup 作为单位实现,每一个进程都可以随时加入或者退出一个 CGroup。

4.2.2 使用介绍

在这里插入图片描述

  • CGroupA:包括了 cpu_cgrop 其中包括 cpu 和 cpuacct 子系统,用于对 cpu 的资源进行限制已经使用情况进行统计。改组下可以对资源的不同权重进行限制,比如 /cgrp1(60%) 和 /cgrp2(20%)

  • GGroupB:包括 memory_cgrop 其中包括了 memory 子系统,用于对 memery 资源进行限制。

  • cgroups subsystem set(css):指进程被加入到当前的 css 中,一个进程只能属于一个 css,但是一个 css 可以包含多个进程

  • M * N:cgroups subsystem set 和 cgroup 是一种多对多的关系

    注:一个 cgroups subsystem set 可以被加入到不同的 cgroups 的节点中。表名对当前的 cgroups subsystem set 下的所有进程需要多种资源限制。

4.3 附加工具

解决了 docker 的隔离进程和资源的问题,但是 docker 的重要概念——镜像问题还没有解决。

注:容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。

没有解决貌似我也是写不动了,那就等下一篇在来研究吧。

5. 碎碎念

啦啦啦,终于写完了。虽然今年除夕不能回家,但是也要开心哦。

  • 人生是一场旅程。我们经历了几次轮回,才换来这个旅程。而这个旅程很短,因此不妨大胆一些,不妨大胆一些去爱一个人,去攀一座山,去追一个梦……有很多事我都不明白。但我相信一件事。上天让我们来到这个世上,就是为了让我们创造奇迹。 ——取自《大鱼海棠》

  • talk is cheap, show me the code。

6. 参考资料

猜你喜欢

转载自blog.csdn.net/phantom_111/article/details/113761341