本文旨在用三篇文章让读者能够清晰的认识与使用docker。本文结合理论与实战,先是从进程隔离、文件隔离、namespace、cgroups、libcontainer展开介绍容器的本质与概念,然后分析docker的技术架构,最后演示docker的常用操作。而今天,我们就先从docker的原理开始。



容器本质之进程隔离


1. 容器本质

容器本质上是一种进程隔离的技术。容器为进程提供了一个隔离的环境,容器内的进程无法访问容器外的进程。


2. 容器及容器中的进程在主机上的呈现

启动一个ubuntu的容器:

docker run -it ubuntu



在主机上可以看到启动了三个进程:


第一个是刚刚执行的命令

第二个是启动的容器,容器在系统上就是一个进程

第三个是在该容器父进程下的一个子进程:/bin/bash


在容器中运行top命令生成一个进程:


在主机上继续查看进程:


可以查看到主机上多了一个top命令的进程,该进程的父进程是上面启动容器时运行的/bin/bash


由此,我们可以得知,容器在主机上是一个进程,容器中的进程在主机上,是容器进程树下的子进程或子子进程。


容器中的进程有什么不同呢,直观上看,就是进程编号不一样了,我们使用docker run -it busybox /bin/sh再启动一个busybox的容器,并查看它的进程号:


/bin/sh的进程号为1

在主机上查看:


/bin/sh的进程号是容器父进程下的37508


小结:容器是一个进程,在容器中启动进程,其实就是在容器这个父进程下启动一个子进程。并且使用“障眼法”对这个子进程的进程编号进行了重新编号,使得用户在容器中查看进程时,如同身处于一个OS环境中。



容器本质之文件隔离


1. 使用chroot来实现文件隔离

容器的本质是进程隔离,那么容器与外部之间也会存在着文件的隔离。文件系统隔离,这也是容器概念的起始。


容器的概念始于 1979 年的 UNIX  chroot,它是一个 UNIX 操作系统上的系统调用,用于将一个进程及其子进程的根目录改变到文件系统中的一个新位置,让这些进程只能访问到该目录。这个功能的想法是为每个进程提供独立的磁盘空间。


接下来我们使用chroot来设置一个隔离的文件系统,也就是将某个目录设置为根目录。


创建根目录 /root/container ,然后运行 chroot container进行设置:

报错,是因为chroot设置该目录时,会启动一个仅在该目录范围内操作的bash,而我们没有将bash命令文件拷贝进行来,拷贝相关的命令文件、动态链接库文件:

然后再运行chroot container设置其为根目录:


在上图中,我们运行了pwd,发现该目录已经是根目录了。我们在此命令行窗口下的所有操作都是在此目录范围内进行了。


基于chroot,我们还可以继续将一些常用命令添加进来、设置该目录的访问权限,让用户登录时就切到此目录下等等。


chroot为进程隔离打开了一扇大门,后面就有更多的进程隔离技术加进来,并逐步建立规范,最终形成了容器技术,诞生了家喻户晓的docker。



容器技术演进历史


1. 容器技术演进历史

1979年,chroot技术的引进开启了进程隔离大门。


2000年,FreeBSD Jails 作为最早的容器技术之一,它由 R&D Associates 公司的 Derrick T. Woolworth 在 2000 年为 FreeBSD 引入。这是一个类似 chroot 的操作系统级的系统调用,但是为文件系统、用户、网络等的隔离增加了进程沙盒功能。因此,它可以为每个 jail 指定 IP 地址、可以对软件的安装和配置进行定制,等等


2006 年,Process Containers 由 Google 实现,用于对一组进程进行限制、记账、隔离资源使用(CPU、内存、磁盘 I/O、网络等)。后来为了避免和 Linux 内核上下文中的“容器”一词混淆而改名为 Control Groups。Cgroups至今仍然作为容器技术的关键组成之一。


2008 年,LXC作为第一个最完善的 Linux 容器管理器的实现方案,是通过 cgroups 和 Linux 名字空间(namespace)实现的。LXC提供了各种编程语言的 API 实现,包括 Python3、Python2、Lua、Go、Ruby 和 Haskell。与其它容器技术不同的是, LXC 可以工作在普通的 Linux 内核上,而不需要增加补丁。


2013年,LMCTFY出现,lmctfy 的意思是“让我为你容器化(Let Me Contain That For You)”。这是一个 Google 容器技术的开源版本,提供 Linux 应用容器。Google 启动这个项目旨在提供能可保证的、高资源利用率的、资源共享的、可超售的、接近零消耗的容器。现在为 Kubernetes 所用的 cAdvisor 工具就是从 lmctfy 项目的成果开始的。


lmctfy 首次发布于 2013 年10月,在 2015 年 Google 决定贡献核心的 lmctfy 概念,并抽象成 libcontainer。libcontainer 项目最初由  Docker 发起,现在已经被移交给了开放容器基金会(Open Container Foundation)。Libcontainer目前仍然是docker的重要组成之一。Docker最初使用LXC,后面替换为libcontainer。


2013年,Docker面世。Docker实际上是一家公司,在2013年这家公司还叫做DotCloud,Docker是他们公司的一个容器管理产品,2013年初,DotCloud决定将Docker开源,Docker在短短几个月间风靡全球,DotCloud公司也更名为Docker。


2. Docker的发展状况

从goole热度上可以获取到docker的热度如下:



容器本质之Namespace


1. Namespace的类别

在后面的容器技术中,实现上都离不开Linux的Namespace技术。Namespace 是 Linux 提供的一种内核级别环境隔离的方法。Namespace的隔离有6大分类:


每个进程都有自己的namespace的id。可以通过ls -l 来查看。

关于6种Namespace的简介如下:

  • Mount namespace 让容器看上去拥有整个文件系统,它与chroot相比,具有更好的安全性和扩展性。

  • IPC 全称 Inter-Process Communication,是 Unix/Linux 下进程间通信的一种方式,IPC 有共享内存、信号量、消息队列等方法。所以,为了隔离,我们也需要把IPC给隔离开来,这样,只有在同一个 Namespace 下的进程才能相互通信。

  • Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。

  • UTS namespace 让容器有自己的 hostname。默认情况下,容器的 hostname 是它的短ID,可以通过 -h 或 --hostname 参数设置

  • 容器拥有自己独立的一套 PID,同一个主机上,不同容器的进程的编号可以是相同的。容器中的进程的编号也可以与主机的进程编号相同。这就是 PID namespace 提供的功能。

  • User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户。


2. 主机中与容器中的进程在Namespace上的呈现

在主机上运行top和ping命令,可以查看到这两个进程的namespace是一致的:

查看top进程的namespace

对比top进程和ping进程的namespace


可以发现,主机上的这两个进程的namespace是一致的。


运行一个容器”centos1”,并在容器中运行top命令,查看容器中的top进程与主机上的top进程的namespace,是不同的:


运行容器,并在容器中运行top命令

在主机上查看容器中的top命令的进程id

查看容器中的top进程的namespace


对比主机上的top进程的namespace


可以发现,主机上的top进程与容器上的top进程的namespace是不同的。

运行第二个centos容器”centos2”,并在容器中运行top进程,与第一个容器”centos1”中的top进程的namespace进行对比:

查看该容器中的top进程的id

查看该容器中top进程的namespace:

与第一个容器”centos1”中的top进程的namespace对比:

可以发现是不一致的。


结论:docker容器,利用namespace实现了文件系统、网络、主机名、进程方面的隔离。



容器核心技术之cgroups


1. cgroups定义及作用

cgroups是Linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。


本质上来说,cgroups是内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。


cgroups主要有以下几个功能:

资源限制:cgroups可以对任务使用的资源总额进行限制,比如内存大小限制;

优先级分配:通过分配的CPU时间片数量及磁盘IO带宽大小,实际上就相当于控制了任务运行的优先级。

资源统计:cgroups可以统计系统的资源使用量,如CPUwetjfta\mwdhetjgtffu,p个功能非常适用计费。

任务控制:cgroups可以对任务执行挂起、恢复等操作。


2. Docker中如何使用cgroups

当我们运行:docker run -it --rm --cpu-period=100000 --cpu-quota=200000 centos /bin/bash

--cpu-period和 –cpu-quota 表示在每100毫秒的时间里,运行进程使用的cpu时间最多为200毫秒(也就是要占用两个cpu)


进入容器的cgroups目录,查看启动容器时的cpu配置是否已经生效:

可以看到该配置已经生效,那么接下来容器就在这个cpu限额范围内运行了。



容器核心技术之Libcontainer


1. 其他的进程隔离技术

前面的内容中,详细介绍了进程隔离的两个最主要技术,namespace和cgroups,除了他们之外,比较重要的进程隔离技术还有selinux、apparmor、capability、netlink。


Selinx与apparmor可以增加对容器的访问控制,capability实现了将超级用户root的权限分割为多种不同的capability权限,从而实现对容器的更细颗粒度的权限控制,netlink技术则可实现对docker容器网络环境的配置与创建。


2. LXC、Libcontainer与docker

基于各种进程隔离技术,我们就可以创建一个个满足应用运行要求的容器。但这个过程非常复杂。需要有一个简易的容器管理工具。Docker就是最流行的容器管理工具。但docker并没有直接操作namespace等这些linux内核技术,而是基于lxc或libcontainer之上再进行一次封装。通过lxc\libcontainer来操作这些linux内核技术。


docker1.8之后废弃了LXC(Linux Container,即linux虚拟容器技术),引入了基于Go构建的libcontainer的execution driver. 有了libcontainer这个项目, Docker不再需要依赖于Linux部件( LXC, libvirt, systemd-nspawn... )就可以处理namespaces, control groups, capabilities, apparmor profiles, network interfaces。


LXC是第一个真正意义的容器管理工具,主要是提供了方便的命令行接口供用户操作以调用底层的namespace等进程隔离进行。而libcontainer与lxc相比,实际上是反向定义容器了实现标准,将底层实现都抽象化到Libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要可以与上层的 Libcontainer定义的接口对接,那么libcontainer上层的Docker也就可以运行。这也为Docker实现全面的跨平台带来了可能。


从上面的架构图中可以看到,docker daemon通过docker的execdriver和networkriver完成对容器及容器网络的创建。



3. Libcontainer的工作机制

Open Container Initiative(OCI)组织成立以后,libcontainer进化为runC,因此从技术上说,未来licontainer/runC创建的将是符合Open Container Format(OCF)标准的容器。


当使用docker run命令时,将会在主机上创建一个容器,Docker daemon调用libcontainer创建容器的过程如下:



创建逻辑容器LinuxContainer和逻辑进程Process

  • 在执行docker run的时候,docker中的execdriver调用libcontainer创建一个逻辑容器LinuxContainer和逻辑进程Process,逻辑容器位于/var/lib/docker/containers之下。


  • Execdriver在调用libcontainer时,会将从docker run命令接收到的参数(如cpu-quota等)进行处理之后以libcontainer所能接收的格式传输给libcontainer。而逻辑进程Process,则是容器中所要运行的指令及其参数和环境变量等配置信息。


  • 启动逻辑容器LinuxContainer


  • 使用Container.start(Process)方法来启动逻辑容器和创建物理容器。


  • 启动逻辑容器,实际上是在libcontainer中创建一个initProcess的对象,包含了逻辑容器配置、逻辑进程配置,以及进程与外部的交互管道(pipes)等。这个initProcess对象用于创建真正的物理容器。


  • 创建物理容器Container


  • 使用initProcess.start()方法创建物理容器Container。


  • 这个过程中会配置容器的cgroup,创建容器内部的网络设备,配置容器内部的信息,如hostname、ip地址等


  • 最后启动容器中需要运行的进程Endtrypoint



作者:沈晓龙



好文推荐

使用sqlplus进行Oracle数据库批量自动发布

业务复杂、数据庞大、应用广怎办?了解下分布式事务的解决思路!

SaaS设计:自动化服务启停设计示例 

这里有份选择云服务商的攻略,请查收…