Docker基础与实现原理

进程

  • 容器技术兴起源于 PaaS 技术的普及
  • Docker 公司发布的 Docker 项目具有里程碑式意义
  • Docker 项目通过 "容器镜像",解决了应用打包这个根本性难题

容器本身没有价值,有价值的是容器编排
一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运起来后的计算机执行环境的总和,就是我们今天的主角:进程
容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法

Linux Namespace 机制

linux 系统创建进程的一个可选参数

# 系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。
int pid = clone(main_function,stack_size, SIGCHLD, NULL);
# 创建进程 指定参数
# 新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

除了 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作
Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置
实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
容器是一种特殊的进程
在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。
Namespace 的六项隔离
| namespace | 系统调用参数 | 隔离的内容 |
| --- | --- | --- |
| UTS | CLONE_NEWUTS | 主机名域名 |
| IPC | CLONE_NEWIPC | 信号量、消息队列与共享内存 |
| PID | CLONE_NEWPID | 进程编号 |
| Network | CLONE_NEWNET | 网络设备、网络栈、端口等|
| MOUNT | CLONE_NEWNS | 挂载点 |
| USER | CLONE_NEWUSER | 用户与组 |

隔离与限制

敏捷”和“高性能”是容器相较于虚拟机最大的优势,也是它能够在 PaaS 这种更细粒度的资源管理平台上大行其道的重要原因。

  • Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。

namespace 问题(隔离不彻底)

  • 共享宿主机内核
  • 在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。
  • 如果你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。
    而相比之下,拥有硬件虚拟化技术(Hypervisor)和独立 Guest OS 的虚拟机就要方便得多了。最极端的例子是,Microsoft 的云计算平台 Azure,实际上就是运行在 Windows 服务器集群上的,但这并不妨碍你在它上面创建各种 Linux 虚拟机出来。

Linux Cgroups

Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。
查看 Cgroups 挂载信息

[root@instance-1f0rx9em ~]# 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/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/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
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/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。
在子系统下创建的目录中的程序可以限制资源的配额这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。

[root@instance-1f0rx9em cgroup]# ls /sys/fs/cgroup
blkio cpu cpu,cpuacct cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids systemd
[root@instance-1f0rx9em cgroup]# ls /sys/fs/cgroup/cpu
cgroup.clone_children cgroup.procs cpu.cfs_period_us cpu.rt_period_us cpu.shares cpuacct.stat cpuacct.usage_percpu release_agent
cgroup.event_control cgroup.sane_behavior cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat cpuacct.usage notify_on_release tasks

这个目录就称为一个“控制组”。操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。

[root@instance-1f0rx9em cpu]# ls
cgroup.clone_children cgroup.procs container cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat cpuacct.usage notify_on_release tasks
cgroup.event_control cgroup.sane_behavior cpu.cfs_period_us cpu.rt_period_us cpu.shares cpuacct.stat cpuacct.usage_percpu release_agent
[root@instance-1f0rx9em cpu]# ls container/
cgroup.clone_children cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat cpuacct.usage notify_on_release
cgroup.event_control cpu.cfs_period_us cpu.rt_period_us cpu.shares cpuacct.stat cpuacct.usage_percpu tasks

操作死循环,查看 CPU 被打到 100%

top - 19:12:27 up 11 days, 8:54, 1 user, load average: 2.03, 0.68, 0.28
Tasks: 94 total, 3 running, 89 sleeping, 2 stopped, 0 zombie
%Cpu(s):100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

限制每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:

[root@instance-1f0rx9em ~]# echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
[root@instance-1f0rx9em ~]# echo 9482 > /sys/fs/cgroup/cpu/container/tasks

除 CPU 子系统外,Cgroups 的每一项子系统都有其独有的资源限制能力,比如:

  • blkio,为块​​​设​​​备​​​设​​​定​​​I/O 限​​​制,一般用于磁盘等设备;
  • cpuset,为进程分配单独的 CPU 核和对应的内存节点;
  • memory,为进程设定内存使用的限制。
    Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。 而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
    Docker基础与实现原理_第1张图片
    这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了。
    Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息.top 信息来源。
    但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。
lxcfs

top 是从 /prof/stats 目录下获取数据,所以道理上来讲,容器不挂载宿主机的该目录就可以了。lxcfs就是来实现这个功能的,做法是把宿主机的 /var/lib/lxcfs/proc/memoinfo 文件挂载到Docker容器的/proc/meminfo位置后。容器中进程读取相应文件内容时,LXCFS的FUSE实现会从容器对应的Cgroup中读取正确的内存限制。从而使得应用获得正确的资源约束设定。kubernetes环境下,也能用,以ds 方式运行 lxcfs ,自动给容器注入争取的 proc 信息。
把宿主机的 /var/lib/lxcfs/proc/~ 文件挂载到容器的/proc/

在从虚拟机向容器环境迁移应用的过程中,你还遇到哪些容器与虚拟机的不一致问题?
用的是vanilla kubernetes,遇到的主要挑战就是性能损失和多租户隔离问题,性能损失目前没想到好办法,可能的方案是用ipvs 替换iptables ,以及用 RPC 替换 rest。多租户隔离也没有很好的方法,现在是让不同的namespace调度到不同的物理机上。也许 rancher和openshift已经多租户隔离。

你可能感兴趣的:(Docker基础与实现原理)