一. 什么是docker     


GitHub上进行维护。
     Docker 自开源后受到广泛的关注和讨论, 以至于dotCloud公司后来都改名为Docker Inc。 Redhat 已经在其RHEL6.5中集中支持Docker;Google也在其PaaS产品中广泛应用。

     Docker 项目的目标是实现轻量级的操作系统虚拟化解决方案。Docker的基Linux容器(LXC) 等技在LXC的基础上Docker进行了进一步的封装,让用户不需要去关心容器的管理, 使得操作更为简便。 用户操作Docker的容器就像操作一个快速轻量级的虚拟机一样简单。

      Docker 是一个基于 Golang 语言开发的开源的应用容器引擎,可以让开发者打包应用及其依赖到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 服务器。使用容器部署应用程序称为容器化。

     docker有效地将单个操作系统管理的资源划分到独立地组中,以便更好地在各个独立组直接平衡有冲突的资源使用需求。

    下面的图片比较了Docker和传统虚拟化方式的不同之处, 可见容器是在操作系统层面上实现虚拟化, 直接复用本地主机的操作系统, 而传统方式则是在硬件层面实现。

      容器和 VM(虚拟机)的主要区别是,容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。容器使用宿主操作系统的内核,而虚拟机使用独立的内核。

Docker 的局限性之一是,它只能用在 64 位的操作系统上。

    

ENTRYPOINT设置容器挂载 docker 容器挂载_docker

    

ENTRYPOINT设置容器挂载 docker 容器挂载_ENTRYPOINT设置容器挂载_02

二. 为什么使用docker     


    作为一种新兴的虚拟化方式,Docker跟传统的虚拟化方式相比具有众多的优势。

    首先,秒级实现:Docker容器的启动可以在秒级实现,这相比传统的虚拟机方式要快得多。 

    其次,高效使用资源: Docker 对系统资源的利用率很高, 一台主机上可以同时运行数千个Docker容器。容器除了运行其中应用外, 基本不消耗额外的系统资源, 使得应用的性能很高, 同时系统的开销尽量小。
    传统虚拟机方式运行10个不同的应用就要起10个虚拟机, 而Docker只需要启动10 个隔离的应用即可。
 

具体说来, Docker 在如下几个方面具有较大的优势:

2.1 更快速的交付和部署
    对开发和运维(devop) 人员来说, 最希望的就是一次创建或配置, 可以在任意地方正常运行。开发者可以使用一个标准的镜像来构建一套开发容器, 开发完成之后, 运维人员可以直接使用这个容器来部署代码。Docker可以快速创建容器, 快速迭代应用程序, 并让整个过程全程可见, 使团队中的其他成员更容易理解应用程序是如何创建和工作的。Docker容器很轻很快!容器的启动时间是秒级的, 大量地节约开发、测试、 部署的时间。

2.2 更高效的虚拟化

     Docker 容器的运行不需要额外的hypervisor支持, 它是内核级的虚拟化, 因此可以实现更高的性能和效
率。

2.3 轻松的迁移和扩展
    Docker 容器几乎可以在任意的平台上运行, 包括物理机、 虚拟机、 公有云、 私有云、 个人电脑、 服务器
等。 这种兼容性可以让用户把一个应用程序从一个平台直接迁移到另外一个。

2.4 更简单的管理
    使用 Docker, 只需要小小的修改, 就可以替代以往大量的更新工作。 所有的修改都以增量的方式被分发和更新, 从而实现自动化并且高效的管理。

对比传统虚拟机总结:

ENTRYPOINT设置容器挂载 docker 容器挂载_Docker_03

三. docker体系架构    


 3.1 Docker体系架构

依赖于已存在并运行的Linux内核环境。Docker基于客户端-服务器C/S 架构,Docker 系统主要包括 3 大组件:

  • Docker Client:向 Docker Server 进程发起请求,如:build、pull、run 等操作。Docker Client 既可以在访问本地守护(local host)进程,也可以访问远程(remote host)守护进程。 Docker Client 在 Linux 上表现为一个 docker 可执行文件,当 Client 接收到 Daemon 返回的响应并进行简单处理后,Client 一次完整的生命周期就结束了。Client 可以通过 3 种方式和 1)Daemon 建立通信:tcp://host:port
    2)unix:path_to_socket;fd://socketfd
    3)通过设置命令行参数设置 TLS 连接
  • Docker Server:侦听 REST API 请求并管理 Docker 对象,例如:镜像,容器,网络和卷。守护程序还可以与其他守护程序通信以管理 Docker 服务。
  • Docker Registry(注册表,仓库注册服务器):存储 Docker Image 的中央仓库。其中 Docker Hub 是任何人都可以使用的 Public Registry,Docker Server 默认配置在 Docker Hub 上查找 Images。个人也可以运行 Private Registry,如果使用 Docker DataCenter,则其中包括 Docker Trusted registry(DTR)。使用 docker pull 或 docker run 指令时,所需的 Image 将从 Docker Server 配置的 Registry 中提取。

ENTRYPOINT设置容器挂载 docker 容器挂载_spring boot_04

从软件架构的角度来看,Docker 主要包含了以下模块:

  • Docker Client 客户端
  • Docker Daemon 守护进程
  • Docker Registry 镜像仓库
  • Graph本地存储
  • Driver驱动
  • Libcontainer
  • Docker Container

ENTRYPOINT设置容器挂载 docker 容器挂载_docker_05

用户使用 Client 与 Daemon 建立通信,并发送请求给后者 Daemon 作为 Docker 的核心,首先提供 Server 来接受 Client 的请求,而后通过 Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式的存在。

  • 当需要为 Container 提供 Image 时,则从 Registry 中下载镜像,并通过镜像管理驱动 Graphdriver 将下载镜像以 Graph 的形式存储;
  • 当需要为 Container 创建网络环境时,则通过网络管理驱动 Networkdriver 创建并配置 Container 网络环境;
  • 当需要为 Container 限制运行资源或执行用户指令等操作时,则通过 Execdriver 来完成。

而 Libcontainer 则作为一个独立的 Container 管理模块,Networkdriver 以及 Execdriver 都是通过 Libcontainer 来完成对 Container 进行的操作。当执行 docker run 的一系列工作后,一个实际的 Container 就处于运行状态,该 Container 拥有独立的文件系统,独立并且安全的运行环境等。

3.2 Docker 服务端守护进程:

服务端负责构建、运行和分发Docker容器等的工作。

docker daemon作为 server 端接受 client 的请求,并处理(创建、运行、分发容器)。

Docker 客户端和守护进程可以运行在同一个系统上,也可以连接到一个远程 Docker 守护进程。两者通过 UNIX 套接字sockerts或网络接口使用 REST API 进行通信。

Docker Daemon 可以细分为以下模块:

  • API Server:Daemon 会在后台启动一个 Docker Server,是一个 API Server,基于 Golang 的 Gorilla/Mux 包,接受 Client 发送的请求并路由分发到不同的 Handler 进行处理。

值得注意的是:Docker Server 的启动是靠一个名为 serveapi 的 Job 运行来完成的。所以 Server 的本质是众多 Job 中的一个。

  • Engine:是 Docker 的运行引擎,它扮演 Docker Container 存储仓库的角色,并且通过执行 Job 的方式来操纵管理这些容器。Docker Engine 有两个不同的版本:Docker Engine Enterprise(企业版)和 Docker Engine Community(社区版)。
  • Job:一个 Job 可以认为是 Engine 内部最基本的工作执行单元。Docker 做的每一项工作,都可以抽象为一个 Job。例如:在容器内部运行一个进程,这是一个 Job;创建一个新的容器,这是一个 Job,从 Internet上 下载一个文档,这是一个 Job,等等。Job 的设计者,把 Job 设计得与 Unix Processor 相仿。比如说:Job 有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。

ENTRYPOINT设置容器挂载 docker 容器挂载_docker_06

      Docker daemon 一般在宿主主机后台运行,用户使用 client 而直接跟 daemon 交互。Docker client以系统做 bin 命令的形式存在,用户用 docker 命令来跟 docker daemon 交互。

3.3 Docker的三个基本概念组件

docker有三个内部组件
• docker images 镜像
• docker registries 仓库
• docker containers  容器

ENTRYPOINT设置容器挂载 docker 容器挂载_docker_07

3.3.1 镜像images:是一个只读模板,带有创建Docker容器的说明。

       docker images就是一个只读的模板。比如:一个image 可以包含一个ubuntu的操作系统,里面安装了apache或者你需要的应用程序。images可以用来创建docker containers,docker提供了一个很简单的机制来创建images或者更新现有的images,你甚至可以直接从其他人那里下载一个已经做好的images。

镜像工作原理:

操作系统分为内核和用户空间,Linux内核启动后,会挂载root文件系统为其提供用户空间支持。而Docker镜像就相当于是一个root文件系统,这个文件系统里面包含可以运行在 Linux 内核的程序以及相应的数据。可以创建自己的镜像,也可以只使用他人创建并在仓库中发布的镜像。
       每个 docker 都有很多层次构成,docker使用union file systems 将这些不同的层结合到一个image中去。AUFS (AnotherUnionFS)是一种Union FS, 简单来说就是支持将不同目录挂载到同一个虚拟文件
         系统下(unite several directories into a single virtual filesystem)的文件系统,更进一步的理解, AUFS支持为每一个成员目录(类似Git Branch)设定 readonly、readwrite和 whiteout-able 权限,同时 AUFS里有一个类似分层的概念,对readonly 权限的 branch 可以逻辑上进行修改(增量地,不影响readonly部分的)。通常Union FS有两个用途,一方面可以实现不借助LVM、RAID将多个disk 挂到同一个目录下,另一个更常用的就是将一个 readonly 的branch 和一个 writeable 的 branch 联合在一起,LiveCD 正是基于此方法可以允许在 OS image 不变的基础上允许用户在其上进行一些写操作。Docker在AUFS上构建的 container image 也正是如此。

          要构建自己的镜像,可以创建一个Dockerfile定义创建映像和运行映像所需的步骤。Dockerfile中的每个指令都会在映像中创建一个层。当更改Dockerfile并重建图像时,只会重建那些已更改的层。

3.3.2 仓库registries:放镜像文件的地方

    仓库是集中存放镜像文件的场所。 有时候会把仓库和仓库注册服务器(Registry) 混为一谈, 并不严格区分。实际上,仓库注册服务器上往往存放着多个仓库, 每个仓库中又包含了多个镜像, 每个镜像有不同的标签(tag) 。
    仓库分为公开仓库(Public) 和私有仓库(Private)两种形式。
    最大的公开仓库是Docker Hub, 存放了数量庞大的镜像供用户下载。 国内的公开仓库包括Docker Pool等, 可以提供大陆用户更稳定快速的访问。
    当然, 用户也可以在本地网络内创建一个私有仓库。当用户创建了自己的镜像之后就可以使用push命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候, 只需要从仓库上pull下来就可以了。
    *注:Docker仓库的概念跟Git类似, 注册服务器可以理解为GitHub这样的托管服务。

3.3.3 容器containers :镜像运行时的实例


Docker利用容器来运行应用。容器是从镜像创建的运行实例。类似运行起来的虚拟机,里面运行着用户的的应用程序。通过一个镜像可以创建许多个互不影响的Container容器。Docker 镜像类似 Linux 的Root FileSystem,通过镜像启动一个容器。一个镜像就是一个可执行的包,其中包括运行应用程序所需要的代码、运行时、库、环境变量和配置文件等。

       容器可以被启动、 开始、 停止、删除。 每个容器都是相互隔离的、 保证安全
的平台。
       可以把容器看做是一个简易版的Linux环境( 包括root用户权限、进程空间、 用户空间和网络空间等) 和运行在其中的应用程序。
*注:镜像是只读的, 容器在启动的时候创建一层可写层作为最上层。

       容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于独立的命名空间。容器存储层的生存周期和容器一样,皮之不存毛将焉附,容器消亡时,容器存储层也随之消亡。

3.4 Graph本地存储

docker本地存储:每个容器都被自动分配了内部存储,即容器本地存储,采用的是联合文件系统。通过存储驱动进行管理。

存储驱动:控制镜像和容器在主机上的存储和管理方式。

容器本地存储空间:分层结构构成,有一个可写容器层和若干只读的镜像层组成。

联合文件系统uinon FS: docker的一种底层技术,有存储驱动(storage driver)实现,相应的存储驱动:aufs,overlay、overlay2、devicemapper、btrfs、zfs、vfs等。

3.4 驱动Driver

Driver 作为驱动模块,Docker 通过 Driver 来实现对 Container 执行环境的定制。可以分为以下三类驱动:

  1. Graphdriver
  2. Networkdriver
  3. Execdriver

存储驱动Graphdriver

每个 Docker 容器都有一个本地存储空间,用于保存层叠的镜像层(Image Layer)以及挂载的容器文件系统。

默认情况下,容器的所有读写操作都发生在其镜像层上或挂载的文件系统中,所以存储是每个容器的性能和稳定性不可或缺的一个环节。

以往,本地存储是通过存储驱动(Storage Driver)进行管理的,有时候也被称为 Graph Driver 或者 GraphDriver。

Graphdriver 用于完成 Image 的管理,包括存储与获取。当用户需要下载指定的镜像时,Graphdriver 就将镜像存储在本地的指定目录;当用户需要使用指定的镜像来创建容器的 Rootfs 时,Graphdriver 就从本地镜像存储目录中获取指定的容器镜像。

在 Graphdriver 初始化之前,有 4 种文件系统或类文件系统在其内部注册,它们分别是:

  1. Aufs
  2. Btrfs
  3. Vfs
  4. Devmapper

而 Graphdriver 在初始化之时,通过获取系统环境变量 DOCKER_DRIVER 来提取所使用 Driver 的指定类型。而之后所有的 Graph 操作,都使用该 Driver 来执行。

Graphdriver 的架构如下:

ENTRYPOINT设置容器挂载 docker 容器挂载_ENTRYPOINT设置容器挂载_08

       虽然存储驱动在上层抽象设计中都采用了栈式镜像层存储和写时复制(Copy-on-Write)的设计思想,但是 Docker 在 Linux 底层支持几种不同的存储驱动的具体实现,每一种实现方式都采用不同方法实现了镜像层和写时复制。

       虽然底层实现的差异不影响用户与 Docker 之间的交互,但是对 Docker 的性能和稳定性至关重要。

        在 Linux 上,Docker 可选择的一些存储驱动包括 AUFS(最原始也是最老的)、Overlay2(可能是未来的最佳选择)、Device Mapper、Btrfs 和 ZFS。

         Docker 在 Windows 操作系统上只支持一种存储驱动,即 Windows Filter。

存储驱动的选择是节点级别的。这意味着每个 Docker 主机只能选择一种存储驱动,而不能为每个容器选择不同的存储驱动。

在 Linux 上,读者可以通过修改 /etc/docker/daemon.json 文件来修改存储引擎配置,修改完成之后需要重启 Docker 才能够生效。

下面的代码片段展示了如何将存储驱动设置为 overlay2。

{ "storage-driver": "overlay2" }

通过docker system info命令来检查 Docker 当前的存储驱动类型:

# docker system info
Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
Images: 48
Server Version: 1.13.1
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true

Networkdriver

Networkdriver 用于完成 Container 网络环境的配置,其中包括:

  • Docker deamon 启动时为其创建 Bridge 网桥;
  • Container 创建时为其创建专属虚拟网卡设备,以及为 Container 分配 IP、Port 并与宿主机做端口映射,设置容器防火墙策略等。

Networkdriver 的架构如下:

ENTRYPOINT设置容器挂载 docker 容器挂载_spring boot_09

Execdriver

Execdriver 作为 Container 的执行驱动,负责创建 Container 运行时 namespace,负责容器资源使用的统计与限制,负责容器内部进程的真正运行等。

在 Execdriver 实现的初期使用了 LXC Driver 调用 LXC 的接口,来操纵容器的配置以及生命周期,而现在 Execdriver 默认使用 Native 驱动,不再依赖于 LXC。可以通过启动 Daemon 时指定 ExecDriverflag 参数来进行选择,默认为 native。

Execdriver 架构如下:

ENTRYPOINT设置容器挂载 docker 容器挂载_eureka_10

3.5 底层Libcontainer

Libcontainer 是 Docker 架构中一个使用 Golang 实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问 Kernel 中与容器相关的 API。

正是由于 Libcontainer 的存在,Docker 最终得以操纵 Container 的 Namespace、Cgroups、Apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖 LXC 或者其他库。

Libcontainer 架构如下:

ENTRYPOINT设置容器挂载 docker 容器挂载_ENTRYPOINT设置容器挂载_11

Docker 将底层容器运行时剥离出来,实现更好的平台无关性。LibContainer 是对各种容器的抽象,发展为 RunC,并贡献给 OCP 组织作为定义容器环境的标准。

四、Docker底层引擎技术实现原理


4.1、什么是LXC?

相当于C++中的NameSpace。容器有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。

      LXC容器可以被用于各种用途,包括开发、测试、部署和运行应用程序。容器可以随时创建、启动、停止和删除,容器的配置也可以根据需要进行调整。

       LXC是一个底层的容器技术,它提供了命名空间、cgroups、文件系统隔离等内核级别的隔离特性,而不像Docker那样提供更上层的高级容器管理功能。

      LXC的出现比较早,早于Docker,但是初期一直不温不火,直到Docker的出现加速了它的发展。

4.2、LXC安装和简单使用

       LXC可以独立使用被用来实现容器创建、停止容器等基础功能。基于以下步骤可以简单使用LXC:

4.2.1、安装LXC

首先,需要在Linux系统上安装LXC软件包。在大多数Linux发行版中,可以使用系统自带的包管理器进行安装,例如,在Debian/Ubuntu上,可以使用以下命令进行安装:

sudo apt-get install lxc

4.2.2、创建容器

创建一个新的容器可以通过lxc-create命令完成,该命令可以创建一个基于模板的容器,例如,以下命令将创建一个基于Ubuntu 20.04模板的新容器:

# lxc-create -n mycontainer -t ubuntu -- --release 20.04

其中,-n选项指定容器的名称,-t选项指定使用的模板,--release选项指定模板的版本。执行该命令后,LXC将自动下载并安装Ubuntu 20.04模板,并创建一个名为“mycontainer”的新容器。

4.2.3、启动容器lxc-start

使用lxc-start命令启动:#lxc-start -n mycontainer

使用-d 以后台模式启动容器,使用-l选项指定日志文件的路径,例如:

sudo lxc-start -n mycontainer-l /var/log/lxc/mycontainer.log

4.2.4、进入运行的容器

# lxc-attach -n mycontainer

该命令将在当前终端中打开一个新的shell,允许在容器中运行命令和操作文件系统。当完成操作并退出容器时,只需键入“exit”即可返回主机系统。

4.2.5、停止容器lxc-stop

# lxc-stop -n mycontainer

该命令将发送一个SIGPWR信号给容器进程,以请求它优雅地关闭并停止。

4.3、Docker和LXC的关系

在Docker的早期版本中,它使用LXC作为底层容器引擎来实现容器隔离和虚拟化,把LXC复杂的容器创建与使用方式简化为Docker自己的一套命令体系。Docker确实也让LXC发展得更快。

但是随着时间的推移,Docker逐渐摆脱了对LXC的依赖,并自己实现了一个名为libcontainer的容器引擎,以取代LXC。libcontainer是一个纯Go语言编写的库,它实现了Linux内核的命名空间、cgroups等特性,并提供了更多的容器管理功能。从Docker 1.11版本开始,Docker默认使用libcontainer作为容器引擎,而非LXC。自 1.20 版本开始 docker 已经抛开 lxc。

随着Docke将底层实现都抽象化到Libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要实现了Libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。

此外,Docker 提供了更加完整的工具链和生态系统,使得容器的创建、管理和部署变得更加简单和可靠。

因此,Docker 可以看作是 LXC 技术的一种封装和扩展,提供了更多的功能和工具,让容器技术更容易被使用和推广。

ENTRYPOINT设置容器挂载 docker 容器挂载_ENTRYPOINT设置容器挂载_12

LXC不需要创建虚拟机,而是通过自身的进程和网络空间来实现虚拟化的,通过命名空间来强制执行进程隔离。LXC使用名称空间来实现进程隔离,同时使用自己的cgroup来解决并限制一个或多个进程中的CPU、内存、磁盘I/O和网络使用情况。

从使用范围来讲:LXC仅可以在Linux环境中运行;而Docker既可以在Linux上运行,也可以在Windows、MacOS上运行,因此Docker并不依赖于Linux。

从人气方面来讲:LXC已经很老了,由于一些限制,在开发人员中并没有被太多的普及;而Docker使容器超越了操作系统级别,可以说Docker是LXC的扩展,受到了大众的欢迎及喜爱。

目前虽然Docker默认使用了libcontainer,但是仍然可以配置Docker底层使用LXC作为容器引擎。

4.4、配置Docker使用LXC

配置Docker使用LXC

Docker默认使用的是自己的容器引擎,即Docker引擎,而不是LXC容器引擎。如果您想在Docker中使用LXC容器引擎,可以按照以下步骤进行配置:

4.4.1、安装LXC和LXD

首先,需要安装LXC和LXD。LXC是Linux容器的用户空间接口,而LXD是一个容器管理器,提供了更高级别的容器管理功能。您可以使用以下命令在Ubuntu系统上安装它们:

    sudo apt-get update
    sudo apt-get install lxc lxd

4.4.2、创建一个LXD容器

在LXD中,可以通过创建一个新的容器来运行应用程序。使用以下命令创建一个新的LXD容器:

sudo lxc launch ubuntu:18.04 mycontainer

这将使用Ubuntu 18.04镜像创建一个名为“mycontainer”的新容器。
 

4.4.3 配置Docker使用LXC

在Docker配置文件中增加以下配置,以告诉Docker使用LXD容器来运行应用程序。

 

{
       "runtimes": {
         "lxd": {
           "path": "/usr/bin/runc"
         }
       },
       "default-runtime": "lxd"
     }

默认情况下,Docker配置文件位于/etc/docker/daemon.json。请确保在修改配置文件之前备份它。

重启Docker服务以使更改生效。使用以下命令重启Docker服务:

sudo service docker restart

完成后,Docker就能够使用LXD容器引擎来运行应用程序,底层使用LXC。
 

4.5 docker底层Libcontainer技术实现原理

在Docker中,对容器管理的模块为execdriver,目前Docker支持的容器管理方式有两种,一种就是最初支持的LXC方式,另一种称为native,即使用Libcontainer进行容器管理。

Docker Deamon启动过程中就会对execdriver进行初始化,会根据驱动的名称选择使用的容器管理方式。虽然在execdriver中只有LXC和native两种选择,但是native(即Libcontainer)通过接口的方式定义了一系列容器管理的操作,包括处理容器的创建(Factory)、容器生命周期管理(Container)、进程生命周期管理(Process)等一系列接口

docker将底层实现都抽象化到Libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要实现了Libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。

前版本的Libcontainer,功能实现上涵盖了包括namespaces使用、cgroups管理、Rootfs的配置启动、默认的Linux capability权限集、以及进程运行的环境变量配置。内核版本最低要求为2.6,最好是3.8,这与内核对namespace的支持有关。

       Docker容器的实现原理就是通过Namespace命名空间实现进程隔离、UnionFilesystem联合文件系统实现文件系统隔离、ControlGroups控制组实现资源隔离。

        Docker利用Linux中的核心分离机制,例如Cgroups,以及Linux的核心Namespace(名字空间)来创建独立的容器。

           一句话概括起来Docker就是利用Namespace做资源隔离,用Cgroup做资源限制,利用Union FS做容器文件系统的轻量级虚拟化技术。

     NamespaceCgroups这两个技术都是Linux内核本身支持的功能,Docker如果只使用这两大技术也不可能造就出道即巅峰的火热程度,Docker创新点恰恰是引入镜像概念,并使用联合文件系统(UnionFS)技术很好的实现了镜像分层,这样就可以将应用部署介质、依赖环境配置文件以及操作系统二进制文件进行分层叠加构建出应用运行时文件系统环境。

ENTRYPOINT设置容器挂载 docker 容器挂载_spring boot_13

镜像包含一个基础镜像(Base Image),这个一般包含操作系统介质,比如centosdebian,但是它只包括使用的操作系统二进制文件,并没有包括内核相关,所以,它的体积远远小于部署整个操作系统占用的空间,比如一个centos基础镜像大概只有70-80MB。另外,镜像分层设计进一步减少存储占用,比如现在100+应用组件都是基于centos基础镜像部署,实际部署时只需要拉取一份centos基础镜像,就像搭积木一样,将每一层使用的文件进行组合叠加,最终构建出程序运行时完整的目录结构。

ENTRYPOINT设置容器挂载 docker 容器挂载_eureka_14

Namespaces命名空间:

Linux Namespace,即Linux 命名空间,是 Linux 内核Kernel提供的功能,它可以隔离一系列的系统资源,如 进程 ID、User ID、Network、文件系统等。

Docker 利用 Linux Namespace 功能,实现多个 Docker 容器相互隔离,实现进程隔离,保证A容器看不到B容器.

6个命名空间:User,Mnt,Network,UTS,IPC,Pid

1)pid namespace
    不同用户的进程就是通过 pid namespace 隔离开的,且不同 namespace 中可以有相同 pid。所有的LXC进程在docker 中的父进程为 docker 进程,每个 lxc 进程具有不同的 namespace。同时由于允许嵌套,因此可以很方便的实现Docker in Docker。

2) net namespace
   有了 pid namespace, 每个 namespace中的pid 能够相互隔离,但是网络端口还是共享 host 的端口。
   网络隔离是通过 net namespace实现的, 每个net namespace 有独立的 network devices, IPaddresses, IP routing tables, /proc/net目录。这样每个container 的网络就能隔离开来。docker默认采用veth的方式将 container 中的虚拟网卡同 host 上的一个 docker bridge: docker0 连接在一起。

3) ipc namespace

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

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

5) uts namespace

   UTS("UNIX Time-sharing System") namespace 允许每个 container 拥有独立的 hostname和domainname,使其在网络上可以被视作一个独立的节点而非Host上的一个进程。

6) user namespace
    每个 container 可以有不同的 user 和 group id, 也就是说可以在 container 内部用 container内部的
用户执行程序而非 Host 上的用户。Control groups 主要用来隔离各个容器和宿主主机的资源利用。

控制组cgroups

Cgroups 是 Control Group 的缩写,控制组。cgroups 容器资源统计和隔离:可以对一组进程及这些进程的子进程进行资源限制。

主要用到的cgroups子系统: CPU、内存、存储、网络、设备访问权限,通过 Cgroups 可以很轻松的限制某个进程的资源占用并且统计该进程的实时使用情况。

实际上 Docker 是使用了很多 Linux 的隔离功能,让容器看起来像一个轻量级虚拟机在独立运行,容器的本质是被限制了的 Namespaces,cgroup,具有逻辑上独立文件系统,网络的一个进程。

Docker容器就是:通过Cgroups控制组实现资源隔离的。

Cgroups 由 3 个组件构成:分别是 cgroup(控制组)、subsystem(子系统)、以及 hierarchy(层级树),3 者相互协同作用。

  • cgroup 是对进程分组管理的一种机制,一个 cgroup 通常包含一组(多个)进程,Cgroups 中的资源控制都以 cgroup 为单位实现。
  • subsystem 是一组(多个)资源控制的模块,每个 subsystem 会管理到某个 cgroup 上,对该 cgroup 中的进程做出相应的限制和控制。
  • hierarchy 会将一组(多个)cgroup 构建成一个树状结构,Cgropus 可以利用该结构实现继承等功能

UnionFS 联合文件系统

Docker容器是通过UnionFilesystem(Union FS)联合文件系统实现文件系统隔离。

我们都知道 Docker 镜像是一种分层结构,每一层构建在其他层之上,从而实现增量增加内容的功能,这是如何实现的?

要理解这个问题,首先需要理解 Union File System(简称,UnionFS),它是为 Linux 系统设计的将其他文件系统联合到一个联合挂载点的文件系统服务。UnionFS 使用 branch(分支)将不同文件系统的文件和目录透明地叠加覆盖,形成一个单一一致的文件系统,此外 UnionFS 使用写时复制(Copy on Write,简称,CoW)技术来提高合并后文件系统的资源利用。

Docker 使用的第一种存储驱动为 AUFS(Advanced Multi-layered unification filesytem),AUFS 完全重写了早期的 UnionFS,目的是提高其性能与可靠性,此外还引入了如 branch 负载均衡等新功能。

与 UnionFS 类似,AUFS 可以在基础的文件系统上增量的增加新的文件系统,通过叠加覆盖的形式最终形成一个文件系统。通常 AUFS 最上层是可读可写层,而其他层只是只读层,每一层都只是一个普通的文件系统。

AUFS (AnotherUnionFS) 是一种 Union FS,


真实的例子来帮助我们理解AUFS:

安装aufs文件系统# 进入repo目录 cd /etc/yum.repo.d # 下载文件 wget https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo # 安装 yum install kernel-ml-aufs

我们建立company 和 home两个目录,并且分别为他们创造两个文件:

ENTRYPOINT设置容器挂载 docker 容器挂载_spring boot_15

然后我们将通过 mount 命令把hgs1 和 hg2 两个目录「联合」起来,建立一个 AUFS 的文件系统,并挂载到当前目录下的 mnt 目录下:

# mount -t aufs -o dirs=./home:./company none ./mnt
#
tree ./mnt/
./mnt/
|-- code
|-- eat
|-- meeting
`-- sleep

通过 ./mnt 目录结构的输出结果,可以看到原来两个目录下的内容都被合并到了一个 mnt 的目录下。

默认情况下,如果我们不对「联合」的目录指定权限,内核将根据从左至右的顺序将第一个目录指定为可读可写的,其余的都为只读。那么,当我们向只读的目录做一些写入操作的话,会发生什么呢?

# echo apple > ./mnt/code
# cat company/code
# cat home/code
apple

通过对上面代码段的观察,我们可以看出,当写入操作发生在 company/code 文件时, 对应的修改并没有反映到原始的目录中。

而是在 home 目录下又创建了一个名为 code 的文件,并将 apple 写入了进去。

看起来很奇怪的现象,其实这正是 Union File System 的厉害之处:

    Union File System 联合了多个不同的目录,并且把他们挂载到一个统一的目录上。
 

Docker 镜像分层、增量增加等功能正是通过利用 AUFS 的分层文件系统结构、增量增加等功能实现,这也导致了运行 Docker 容器如果没有指定 volume(数据卷)或 bind mount,则 Docker 容器结束后,运行时产生的数据便丢失了。

五、容器工作原理


 当我们运行 docker run -i -t ubuntu /bin/bash 命令时,docker 在后台运行的操作如下:

  1.  拉取镜像:如果本地有 ubuntu 这个 image 就从它创建容器,否则从公有仓库下载
  2. 创建容器:从image 创建容器
  3. 挂载文件:分配一个文件系统,并在只读的 image 层外面挂载一层可读写的层
  4. 网络桥接:从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  5. 分配 IP:从地址池配置一个ip 地址给容器
  6. 执行命令:执行你指定的程序,在这里启动一个/bin/bash进程
  7. 输入输出: -i -t 指定标准输入和输出

六. docker安装并运行spring boot


4.1 安装

对于CentOS6,可以使用 EPEL库安装Docker,命令如下

$   sudo    yum install http://mirrors.yun-idc.com/epel/6/i386/epel-release-6-8.noarch.rpm  

$   sudo    yum install docker-io  

CentOS7系统CentOS-Extras 库中已带Docker,可以直接安装:

  1. $   sudo    yum install docker  

4.2 启动 Docker 服务

安装之后启动Docker 服务,并让它随系统启动自动加载。

$   sudo    service docker  start 

$   sudo    chkconfig   docker  on  

4.3 下载官方的 CentOS 镜像到本地

必须先启动docker才能下载:

docker pull centos

4.4 运行一个 Docker 容器:

# docker run -i -t centos /bin/bash

[root@dbf66395436d /]#

我们可以看到,CentOS 容器已经被启动,并且我们得到了 bash 提示符。在 docker 命令中我们使用了 “-i 捕获标准输入输出”和 “-t 分配一个终端或控制台”选项。若要断开与容器的连接,输入 exit。

[root@cd05639b3f5c /]# cat /etc/redhat-release

CentOSLinux release 7.3.1611(Core)
[root@cd05639b3f5c /]#exit
exit
[root@localhost ~]#

4.5 利用Dockerfile来创建镜像

我们安装java8, 基础镜像是centos。新建一个目录和一个Dockerfile

$ mkdir java8
$ cd java8
$ touch Dockerfile

Dockerfile 中每一条指令都创建镜像的一层:

# Pull base image.  
FROM centos  
  
# 设置jdk 的环境变量。
ENV JAVA_HOME /usr/java8
  
# 复制/usr/java文件到镜像中(jdk1.8.0_66 目录和Dockerfile在同一目录)
#ADD jdk1.8.0_66 /usr/java8
COPY jdk1.8.0_66 /usr/java8
# Define default command.  
CMD ["bash"]

Dockerfile 基本的语法是
使用 # 来注释
FROM 指令告诉Docker使用哪个镜像作为基础
接着是维护者的信息
RUN 开头的指令会在创建中运行。

Build构建:

docker build -t='java8'  ./

语法:docker build [OPTIONS] PATH| URL| -
常见选项:
          -t 设置镜像的名称和TAG,格式为name:tag
          -f Dockerfile的名称,默认为PATH/Dockerfile
例子:docker build -f ~/php.Dockerfile . 
注意:PATH是编译镜像使用的工作目录,Docker Daemon在编译开始时,会扫描PATH中的所有文件,可以在编译目录中加入.dockerignore过滤不需要的文件

Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如 

$ docker build -f /path/to/a/Dockerfile .

然后查看镜像:

docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
java8               latest              2abc9f66aba0        4 minutes ago       484.9 MB
docker.io/centos    latest              36540f359ca3        9 days ago          192.5 MB

 运行已经构件好的镜像:

docker run -it java8 /bin/bash  

然后echo $JAVA_HOME 看看。

4.6 构建sping-boot

这个java jar 包就是turing-api-1.0-SNAPSHOT.jar 简单http服务:

直接下载jar包:https://github.com/huangguisu/k8s.git

https://github.com/huangguisu/k8s/blob/master/docker/springboot/turing-api-1.0-SNAPSHOT.jar

Dockerfile:

#基础镜像:仓库是java,标签用java8 
FROM java8  
#当前镜像的维护者和联系方式  
MAINTAINER guisu guisu@example.com  
#将打包好的spring程序拷贝到容器中的指定位置  
ADD turing-api-1.0-SNAPSHOT.jar /opt/turing-api-1.0-SNAPSHOT.jar
#容器对外暴露8080端口  
EXPOSE 8080  
#容器启动后需要执行的命令  
CMD $JAVA_HOME/bin/java  -jar /opt/turing-api-1.0-SNAPSHOT.jar

构建:

docker build-t="spingboot"

启动:

本地80端口映射到容器的8081端口

sudo docker run-d -p 80:8080 spingboot

然后就可以访问主机的80端口了。

五. 问题


1、具体错误:Error response from daemon: conflict: unable to delete b227a9dfe196 (must be forced) - image is being used by stopped container 7b080f1e1f17

错误解析:这是由于要删除的目标镜像中有容器存在,故无法删除镜像

解决办法:先删除镜像中的容器,再删除该镜像。

ENTRYPOINT设置容器挂载 docker 容器挂载_eureka_16

2、docker无法启动:

   通过systemctl status docker.service 查看错误信息:

    docker.service failed to run 'start' task: No such file or directory

 解决:

这个一般是/usr/lib/systemd/system/docker.service的

EnvironmentFile项目某个文件不存在:比如/run/docker_opts.env文件不存在,就启动报这个错误。

ENTRYPOINT设置容器挂载 docker 容器挂载_ENTRYPOINT设置容器挂载_17

3、docker启动报错:docker Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network

这是由于启动Docker的时候,默认的网络模式是桥接模式,这就需要向操作系统发送信号,让它帮我们建立一个bridge网络命名为docker0, 并且分配172.17.0.1/16。但是出于某种原因,该网络没有建立起来,我们只要手动执行这一系列操作就可以。

解决:添加一个桥接网络,执行以下命令

 ip link add name docker0 type bridge

 ip addr add dev docker0 172.17.0.1/16