Docker 原理篇(一)Docker 是什么?

前言

此篇博文是笔者所总结的 Docker 系列之一;

本文为作者的原创作品,转载需注明出处;

Docker 是什么?

最初 docker 的设计思想是基于 linux namespace,linux cgroups 和 AUFS 构建一个可以快速复制并且可以快速移植的轻量级的基于操作系统内核之上包含用户应用环境的的虚拟机。后续,为了跨平台,从 docker 1.2 开始,通过 libcontainer 对 linux namespace 和 cgroups 进行了抽象,提供标准化的而接口,从而使得其它不是基于 Linux 内核的操作系统也可以运行 Docker,从此 Docker 开始跨平台。
这里,我暂且将 linux namespace、cgroups、AUFS 以及 libcontainer 视为 Docker 的四大组件来依次进行归纳和总结;

linux namespace

linux namespace 提供了多种隔离机制,比如独立的网络、独立进程(包含独立的 root 进程)、独立的文件系统、独立的用户和组等;通过 linux namespace,事实上,我们可以创建一个独立于当前系统内核的虚拟操作系统。比如当前系统是 centos,但是,通过 linux namespace 的文件隔离机制,单独创建一个 ubuntu 的 rootfs,那么就可以在当前操作系统 centos 之上创建一个 ubuntu 的虚拟机。
Docker 基础技术: Linux Namespace(上)
Docker 基础技术: Linux Namespace(下)
理解Docker(3):Docker 使用 Linux namespace 隔离容器的运行环境

linux cgroups

通过 linux namespace 我们得到了一个独立于当前操作系统的 linux 虚拟机,但是,我们并没有限定该虚拟机所能使用当前操作系统的资源,比如 CPU、内存以及带宽的大小,cgroups 就是帮我们完成这样的事情。
CGroup 介绍、应用实例及原理描述

AUFS

有了 linux namespace 以及 cgroups 其实一个完全独立于当前操作系统的虚拟机运行时已经完成了,但是,还不够,我们需要独立的文件系统,来构建虚拟机文件系统镜像;Docker 使用 AUFS 来构建多层镜像

  • 多层镜像是什么?
    言外之意,就是镜像之上又可以创建另一层镜像,层层堆叠,以构建针对不同用户所需的极度个性化的镜像。举个例子,我们通过linux namespacecgroups构建了虚拟机的运行时( 就是 linux LXC ),然后通过AUFS构建了一个 Debian rootfs, 称作base image ,那么这个时候,一个独立的 Debian 虚拟机就诞生了。
    补充,不同版本的 Linux 操作系统由相同的 Linux 内核,既拥有相同的 bootfs,和不同的 rootfs 构成,正是 rootfs 构成了不同的操作系统。如下图所示,
    那么,随后,用户 A 通过该基础镜像添加了 JVM,既是 JAVA 虚拟机,由此生成了一个新的镜像,这里姑且称作镜像 A;然后,用户 B 在镜像 A 的基础之上,又添加了 Apache Tomcat,得到镜像 B,那么我们得到了这样的的镜像层级结构,
    基础镜像 -> 镜像 A -> 镜像 B,每一个镜像都是基于上一层镜像所创建而得到的;而要能建立这样的层级结构,并且要能够在最顶层的镜像 B之上能够进行编辑,要归功于 AUFS。
    用两张图来理解
    上图表示的就是基础镜像,在 linux 内核的基础之上构建了两个不同的操作系统镜像,Debian 和 BusyBox,既在当前的操作系统之上构建了两个不同的虚拟机,注意,这里是基于 linux LXC 之上建立的,LXC 正是基于 linux namespace 以及 cgroups 构建的,所以,上图中的 基础镜像(虚拟机)只能构建于 linux 内核的操作系统之上。后续,通过介绍 libcontainer 来认识 Docker 是如何跨平台的。
    这张图清晰的描绘了镜像见的层次结构,Debian 镜像层作为基础镜像层,同时也作为虚拟操作系统层,再往上堆加的就是用户自定义的软件环境镜像层;另外最上面一层是 Container,并且是可写的。
  • Container 是什么?
    Container 其实就是基于所有镜像之上的一层可写层,位于层次结构的最上层,在 Container 层所有发生的变动,可以生成新的一层镜像,可以这样来理解,
    1. Container 是当前所有镜像的可写层
    2. Container 生成新的 镜像
      发生在 Container 的所有变化,可以以此变化作为新的一层镜像
      当然,参考完 AUFS 原理以后,会认识得更加的清晰。

参考,Docker基础技术:AUFS

libcontainer

一句话,就是将linux namespacecgroups的 linux 实现抽象成接口,不再依赖特定的操作系统内核,这样 windows、mac 等操作系统可以根据该接口,实现自定义的namespace以及cgroups,这样,Docker便达到了跨平台的特性。

Docker 的架构剖析

总共七大模块构成,

Docker Client

Docker 客户端,连接 Docker Server

Docker Daemon

Docker 的后台守护进程

Docker Server

用来接收并转发 Docker Client 的连接,类似 Spring MVC / Structs 这样的功能。

Engine

Docker 的运行引擎,用来处理各种各样的用户请求,在 Engine 内部,将用户的请求封装成一个一个的 Job 来执行。

Job

Docker Container内部运行的任何一个进程,这是一个 job;创建一个新的容器,这是一个 job,从 Internet 上下载一个文档,这是一个 job;Job的设计者,把Job设计得与Unix进程相仿。比如说:Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。

Docker Register

Docker 的镜像的镜像仓库,比如 Docker Hub

Graph

已下载容器的保管者;

Driver

Docker 容器的驱动模块,通过 Driver 驱动,Docker可以实现对Docker容器执行环境的定制

graph driver

在graphdriver的初始化过程之前,有4种文件系统或类文件系统在其内部注册,它们分别是aufs、btrfs、vfs和devmapper。而Docker在初始化之时,通过获取系统环境变量”DOCKER_DRIVER”来提取所使用driver的指定类型。而之后所有的graph操作,都使用该driver来执行。

network driver

完成Docker容器网络环境的配置,其中包括Docker启动时为Docker环境创建网桥;Docker容器创建时为其创建专属虚拟网卡设备;以及为Docker容器分配IP、端口并与宿主机做端口映射,设置容器防火墙策略等。

exec driver

Docker容器的执行驱动,负责创建容器运行命名空间 namespaces,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。在原本的 execdriver 的实现过程中,原先可以使用 LXC 驱动调用 LXC 的接口,来操纵容器的配置以及生命周期,而现在 execdriver 默认使用 native 驱动,通过调用 libcontainer 来实现跨平台的特性。

libcontainer

Docker 是由跨平台 Go 语言所编写,正是由于 libcontainer 的存在,Docker可以直接调用 libcontainer,而最终操纵容器的 namespace、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖 LXC 或者其他包, 从而实现 Docker 的跨平台特性;

Docker Container

Docker container(Docker容器)是Docker架构中服务交付的最终体现形式。
Docker按照用户的需求与指令,订制相应的Docker容器:

  • 用户通过指定容器镜像,使得Docker容器可以自定义rootfs等文件系统;
  • 用户通过指定计算资源的配额,使得Docker容器使用指定的计算资源;
  • 用户通过配置网络及其安全策略,使得Docker容器拥有独立且安全的网络环境;
  • 用户通过指定运行的命令,使得Docker容器执行指定的工作。

执行流程

docker pull

从 Docker Register 中获取镜像,然后存储在 Graph 存储引擎中。

docker run

Docker在执行这条命令的时候,所做工作可以分为两部分:第一,创建 Docker 容器所需的 rootfs;第二,创建容器的网络等运行环境,并真正运行用户指令;因此,在整个执行流程中,Docker Client 给 Docker Server 发送了两次请求,第二次请求的发起取决于第一次请求的返回状态。

  • 第一步,创建 Docker 容器所需的 rootfs
    这里需要注意的是,如果当前的 image 在 Graph 容器中不存在,会直接从 Docker Register 中 pull
  • 第二步,创建容器的网络等运行环境,并真正运行用户指令
    核心目的是,初始化 Docker 容器内部的运行环境,如命名空间,资源控制与隔离,以及用户命令的执行,相应的操作在 libcontainer 中完成。

参考

http://www.infoq.com/cn/articles/docker-source-code-analysis-part1/