关于docker那点事儿——docker原理

文章目录

  • 前言
  • 一、namespace
    • 1、认识namespace
    • 2、基于 Linux Namespace 的隔离机制相比于虚拟化技术的不足之处
  • 二、cgroups
    • 1、为什么要限制容器
    • 2、cgroups与容器最亲密的限制能力
    • 3、cgroups不足
  • 三、rootfs

前言

容器本质:
 namespace 空间隔离
 cgroup 资源限制
 rootfs 文件系统

一、namespace

1、认识namespace

  namespace是Linux 容器中用来实现"隔离"的技术手段。namespace 技术实际上修改了应用进程看待整个计算机"视图",即它的"视线"被操作系统做了限制,只能"看到"某些指定的内容。但对于宿主机来说,这些被"隔离"了的进程跟其他进程并没有太大区别。
   docker run -it busybox —name busybox /bin/sh 这条指令翻译成人类的语言就是:请帮我启动一个容器,在容器里执行 /bin/sh,并且给我分配一个命令行终端跟这个容器交互。docker exec -it busybox ps -ef在容器里执行 ps 指令会发现/bin/sh进程,就是这个容器内部的第 1 号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的 /bin/sh,以及我们刚刚执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的世界当中。

PID   USER     TIME  COMMAND
1 root      0:00 /bin/sh
8 root      0:00 ps -ef

  在 Linux 系统中创建线程的系统调用是 clone(),比如:int pid = clone(main_function, stack_size, SIGCHLD, NULL); 这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。 而当用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 这时,新创建的这个进程将会"看到"一个全新的进程空间,在这空间里,它的 PID 是 1。之所以说"看到",是因为这只是一个"障眼法",在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值。 当然可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。

  除了刚用到的 PID Namespace,Linux 操作系统还提供了 Network 、IPC、Mount、UTS和 User 这些 Namespace,用来对各种不同的进程上下文进行"障眼法"操作。

  • pid 命名空间

    不同用户的进程就是通过 pid 名字空间隔离开的,且不同名字空间中可以有相同 pid。所有的 LXC 进程在 Docker中的父进程为Docker进程,每个 LXC 进程具有不同的名字空间。同时由于允许嵌套,因此可以很方便的实现嵌套的Docker 容器。 LXC:Linux Container容器是一种内核虚拟化技术,可以提供轻量级的虚拟化,以便隔离进程和资源。

  • net 命名空间

    有了pid名字空间, 每个名字空间中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net 名字空间实现的,每个 net 名字空间有独立的 网络设备, IP 地址, 路由表, /proc/net 目录。这样每个容器的网络就能隔离开来。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。

  • ipc 命名空间

    容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC), 包括信号量、消息队列和共享内存、socket、管道等。然而同 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 pid 名字空间中的进程间交互,因此需要在 IPC 资源申请时加入名字空间信息,每个 IPC 资源有一个唯一的 32 位 id。

  • mnt命名空间

    类似 chroot,将一个进程放到一个特定的目录执行。mnt 名字空间允许不同名字空间的进程看到的文件结构不同,这样每个名字空间中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个名字空间中的容器在 /proc/mounts 的信息只包含所在名字空间的 mount point。

  • uts 命名空间

    UTS(“UNIX Time-sharing System”) 名字空间允许每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非主机上的一个进程。

  • user 命名空间

    每个容器可以有不同的用户和组 id, 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户。

  所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能"看"到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。所以说,容器,其实是一种特殊的进程而已。所以说人们常把docker与虚拟机相比,其实不恰当。到此下面这张docker与虚拟机的对比图,可以很容易就看懂了。

  容器与虚拟机对比图:
关于docker那点事儿——docker原理_第1张图片

2、基于 Linux Namespace 的隔离机制相比于虚拟化技术的不足之处

1. 隔离得不彻底

  既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。

  尽管可以在容器里通过 Mount Namespace 单独挂载其他不同版本的操作系统文件,比如 CentOS 或者 Ubuntu,但这并不能改变共享宿主机内核的事实。如果你要在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的。

  而相比之下,拥有硬件虚拟化技术和独立 Guest OS 的虚拟机就要方便得多了。最极端的例子是,Microsoft 的云计算平台 Azure,实际上就是运行在 Windows 服务器集群上的,但这并不妨碍你在它上面创建各种 Linux 虚拟机出来。

2. 在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间

  如果你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。相比于在虚拟机里面可以随便折腾的自由度,在容器里部署应用的时候,“什么能做,什么不能做”,就是用户必须考虑的一个问题。

3. 因为共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的

  尽管实践中可以使用 Seccomp 等技术,对容器内部发起的所有系统调用进行过滤和甄别来进行安全加固,但这种方法因为多了一层对系统调用的过滤,一定会拖累容器的性能。何况,默认情况下,谁也不知道到底该开启哪些系统调用,禁止哪些系统调用。

  所以,生产环境中,没有人敢把运行在物理机上的 Linux 容器直接暴露到公网上。当然,基于虚拟化或者独立内核技术的容器实现,则可以比较好地在隔离与性能之间做出平衡。

二、cgroups

  一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。

  Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等。Cgroups提供了对一组进程及将来的子进程的资源的限制,控制和统计的能力,这些资源包括CPU,内存,存储,网络等。通过Cgroups,可以方便的限制某个进程的资源占用,并且可以实时的监控进程的监控和统计信息。

  此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。

1、为什么要限制容器

   以 PID Namespace 为例,虽然容器内的 1 号进程在"障眼法"的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等竞争关系。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程占用的。当然,这个进程自己也可能把所有资源吃光。这些情况,显然都不是一个"沙盒"应该表现出来的合理行为。 而Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。

2、cgroups与容器最亲密的限制能力

   Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。
   用 mount 指令把它们展示出来:

mount -t cgroup  
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

   输出结果,是一系列文件系统目录。在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。

   比如,对 CPU 子系统来说,可以看到如下几个配置文件:

ls /sys/fs/cgroup/cpu  

cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release  

cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks  

   比如:cfs_period 和 cfs_quota 这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。

   Linux Cgroups 的设计,简单粗暴地理解,就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。

   至于在这些控制组下面的资源文件里填上什么值,就靠用户执行 docker run 时的参数指定了,比如这样一条命令:

docker run -it --cpu-period=100000 --cpu-quota=20000 daocloud.io/centos /bin/bash

   在centos7里面是下面这个目录:

cat /sys/fs/cgroup/cpu,cpuacct/system.slice/docker-92f76c52e9c0c34e0f8e5bac01f75919ce59838951a86501e7114d218f8aaf3e.scope/cpu.cfs_quota_us
20000

   这就意味着这个 Docker 容器,只能使用到 20% 的 CPU 带宽。

   由于一个容器的本质就是一个进程,用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。这就意味着,在一个容器中,你没办法同时运行两个不同的应用,除非你能事先找到一个公共的 PID=1 的程序来充当两个不同应用的父进程,这也是为什么很多人都会用 systemd 或者 supervisord 这样的软件来代替应用本身作为容器的启动进程。

   这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于"容器是正常运行的,但是里面的应用早已经挂了"的情况,编排系统处理起来就非常麻烦了。

3、cgroups不足

  Cgroups 对资源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 文件系统的问题。

  众所周知,Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。

  但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。

三、rootfs

  实际上,Mount Namespace 是基于对 chroot 的不断改良才被发明出来的,它也是 Linux操作系统里的第一个 Namespace。当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)

  Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是做了一个小小的创新,当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union FileSystem)的能力。Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。现在高版本的docker都使用OverlayFS文件系统,OverlayFS类似AUFS。

  Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。它最关键的目录结构在 /var/lib/docker 路径下的 diff 目录:

ls /var/lib/docker/overlay/diff/

#我们可以使用 docker image inspect 查询Layers
docker image inspect eureka-server:t1

"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:a2ae92ffcd29f7ededa0320f4a4fd709a723beae9a4e681696874932db7aee2c",
        "sha256:0eb22bfb707db44a8e5ba46a21b2ac59c83dfa946228f04be511aba313bdc090",
        "sha256:30339f20ced009fc394410ac3360f387351641ed40d6b2a44b0d39098e2e2c40",
        "sha256:ce6c8756685b2bff514e0b28f78eedb671380084555af2b3833e54bb191b262a",
        "sha256:a3483ce177ce1278dd26f992b7c0cfe8b8175dd45bc28fee2628ff2cf063604c",
        "sha256:6ed1a81ba5b6811a62563b80ea12a405ed442a297574de7440beeafe8512a00a",
        "sha256:c3fe59dd955634c3fa1808b8053353f03f4399d9d071be015fdfb98b3e105709",
        "sha256:35c20f26d18852b74cc90afc4fb1995f1af45537a857eef042a227bd8d0822a3",
        "sha256:70ecef0b4267941f5c4384f7938cbadafd36fdbfadf52c2be58ce2418236f180",
        "sha256:984b7bf3b699879285c3d6dc3f6bba8bbf273ad0576f40c4b89ca055441e6644"
    ]
},

 &emsp可以看到,eureka-server这个共有10层layer,也就是10个rootfs。每一层都是基础OS、java环境、eureka服务所用文件系统的一部分。当运行景象时,这些文件系统会挂载到一个统一的目录—— /var/lib/docker/overlay2/,可以尝试去查看每个目录下文件。

ll 32183dfcce4ea5f0cc2f5dca77937f14aeeb6527ef94014d583dfa7817d23b81/diff/  #读写层
total 32
drwxr-xr-x 10 root root 4096 Nov 14  2019 etc
-rw-r--r--  1 root root  920 Nov 11  2019 ip.txt
dr-xr-x---  2 root root 4096 Nov 14  2019 root
drwxr-xr-x  2 root root 4096 Nov 14  2019 run
drwxr-xr-x  2 root root 4096 Nov 14  2019 test
drwxrwxrwt  2 root root 4096 Nov 14  2019 tmp
drwxr-xr-x  8 root root 4096 Jun  1  2018 usr
drwxr-xr-x  7 root root 4096 Jun  1  2018 var
ll 32183dfcce4ea5f0cc2f5dca77937f14aeeb6527ef94014d583dfa7817d23b81-init/diff/
total 8
drwxr-xr-x 4 root root 4096 Nov 14  2019 dev
drwxr-xr-x 2 root root 4096 Nov 14  2019 etc

你可能感兴趣的:(docker,容器,docker)