Docker

Abstract

Docker是一个开源的容器引擎,可以将应用软件和应用所需的依赖打包在一起,形成一个标准化单元,类似于Java基于jvm的write once, run anywhere, docker可以实现build once, run anywhere。最近几年,微服务架构火热,而在大型企业中,微服务的运维由于服务的数量比较多而进展困难,容器化服务势在必行。服务容器化使应用程序能够从组件快速组装到发布,消除了开发和QS和IT的摩擦。容器是微服务的基石,没有容器化的微服务是没有灵魂的。docker让容器化操作变得简单,docker的出现让大型的微服务架构得以流行,而微服务的火热又促进了docker发展。

Why

一致的运行环境

开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 「这段代码在我机器上没问题啊」 这类问题。

持续交付和部署

对开发和运维(DevOps)人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 Dockerfile 来进行镜像构建,并结合 持续集成(Continuous Integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 持续部署(Continuous Delivery/Deployment) 系统进行自动部署。
而且使用 Dockerfile 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。

更轻松的迁移

由于 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。

更轻松的维护和扩展

Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker 团队同各个开源项目团队一起维护了一大批高质量的 官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。

VM, Vagrant, Docker

最开始,大家就想着直接把整个系统打包再部署,即虚拟机技术,保证了系统和环境的稳定,减少了大量手工操作。但是,缺点也是有的,那就是无法自动化打包过程,于是Vargrant被做了出来,Vargrant使用了vargrantfile文件给开发者提供了定义自动化构建VM镜像的功能。但是,还是有缺点,首先,打包出来的镜像太大,包含了过多不需要使用的依赖;其次,服务启动必须等得VM的系统启动完成后才能启动服务。
为了解决VM的缺点,Docker被开发了出来,它提供了系统上的进程级别的虚拟化技术。与VM需要虚拟化整个硬件和操作系统不同,Docker是直接基于宿主机的操作系统进行虚拟化,性能几乎没有损耗。具体结构差异如下:

虚拟机:
    +------------+-------------+
    | App A      |  App B      |
    +------------+-------------+
    | Libs       |  Libs       |
    +------------+-------------+
    |Guest OS    |  Guest OS   |
    +------------+-------------+
    |       Hypervisor         |
    +--------------------------+
    |        HOST OS           |
    +--------------------------+
    |         SERVER           |
    +--------------------------+

Docker:
    +------------+-------------+
    | App A      |  App B      |
    +------------+-------------+
    | Libs       |  Libs       |
    +------------+-------------+
    |       Docker Engine      |
    +--------------------------+
    |        HOST OS           |
    +--------------------------+
    |         SERVER           |
    +--------------------------+

Docker的组成

docker主要由以下组件组成:

  • docker client
  • docker daemon
  • docker image
  • docker container
  • docker registry

Docker本身是C/S架构,C端是docker client,S端是docker daemon,docker daemon管理者docker image和docker container,docker index提供镜像索引以及用户认证的功能,docker registry在远程存放镜像,大致架构如下:



                           +-----------------+
                           | Docker Daemon   |
        ,---.              |  -------------- |                )------------------(
       /     \             |  Container1     |                |                  |
      / Docker\            |                 |--------------- |  Docker Registry |
     (  Client )---------- |  Container2     |                |                  |
      \       /            |                 |                )------------------(
       \     /             |  Container3     |                          |
        `---'              |                 |                          |
     docker pull           |  .......        |                          |
     docker run            |                 |                          |
     docker build          +-----------------+                          |
     docker ...                     |                                   |
                                    |                                   |
                            +----------------+                          |
                            |  Docker Index  |--------------------------+
                            +----------------+

Docker Image

docker image是docker容器的基石,保存了docker启动的各种条件,docker container的启动必须基于一个docker image。Docker image位于docker生命周期的构建和打包阶段。
docker使用了union mount的方式构建镜像,采用了ufs的模型。就是docker一次性加载多个文件系统形成一个文件系统层叠的形式,但是在外部看来容器只有一个文件系统可用,docker将这样的文件系统称为docker image。
docker image是一个层层叠加的只读文件系统。最底部是bootfs,即引导文件系统,当容器启动后,引导文件系统将会被卸载。bootfs之上是rootfs,rootfs可以是ubuntu/centos/debian/fedora,与虚拟机启动后由只读状态变为读写状态不同,docker的rootfs永远是只读。

Docker的UFS示例:
 +---------------+
 |  add django   |
 +---------------+
 |  add python3  |
 +---------------+
 |rootfs(ubuntu) |
 +---------------+
 |     bootfs    |
 +---------------+

Docker Container

Docker Container是Docker的执行单元,通过镜像来启动。Docker container位于docker生命周期的启动和执行阶段。
Docker Container基于镜像启动时,会在镜像构建出的ufs上再添加一个writable layer,一个可写层。当容器刚启动时,该读写层是空的,当容器状态发生改变时,读写层才会有变化。比如当需要修改一个文件时,docker把改文件从下层的只读文件系统中复制到读写层,再进行修改,该文件的只读版本仍然存在,但是已经被读写层上的副本隐藏起来了。这就是Docker中重要的技术Copy on Write(中文翻译为写时复制),每个只读镜像层永远都是只读的,不会变化,当创建出一个新容器时,docker会构建出一个镜像栈,在栈的顶层增加读写层,并与底下的只读层和一些数据就构成了一个容器。容器的分层框架能够使我们快速的构建镜像并发布应用程序。

Docker container创建的镜像栈

 +---------------+
 | add my app    |  读写层(copy on write)
 +---------------+
 |  add django   |  Read Only
 +---------------+
 |  add python3  |  Read Only
 +---------------+
 |rootfs(ubuntu) |  Read Only
 +---------------+
 |     bootfs    |  Read Only
 +---------------+

Docker Registry

Docker仓库分为公有和私有,放着一些已有的镜像,官方的docker registry是Docker Hub

Namespaces

C++中有namespace这个概念,它实现了代码块的封装,更重要的是代码的隔离,使得不同namespace下可以有相同的变量名但意义不同。在linux里,namespace是一个重要的特性,提供了系统资源的隔离,包括进程/网络/文件系统等。linux实现namespace的重要目的就是为了实现轻量级的虚拟化,也就是容器(linux里的lxc了解一下)。在同一namespace下的进程可以感知彼此的变化,而不能发现其他进程,从而使得一个namespace下的进程认为自身处于一个独立的系统中,便达到了独立和隔离的目的。
Docker目前用到了以下几种namespace进行资源隔离。

  • PID, Process ID, 进程隔离;
  • NET, Network, 网络隔离;
  • IPC, InterProcess Communication, 跨进程通信访问隔离;
  • MNT, Mount, 挂载点隔离;
  • UTS, Unix Timesharing System, 内核和版本标识隔离;
  • USER, 用户和组隔离;

而隔离资源的管理就需要用到Control Groups

Control Groups

Control groups是linux提供的限制/记录/隔离主机物理资源的机制,简称cgroups,为了更好的管理容器,cgroups在2007年并入到linux kernel中。没有cgroups,就没有容器。
cgroups主要负责容器相关的以下内容:

  • 资源限制,比如为进程组设置一个内存使用的上限;
  • 优先级设定,比如为某些进程组设定更大的cpu和内存资源。
  • 资源计量,比如计算进程组使用了多少系统资源。
  • 资源控制,比如将进程组挂起和恢复。

正是因为namespaces和cgroups,Docker的容器才拥有了以下能力:

  • 文件系统隔离:每个容器都有自己的rootfs;
  • 进程隔离: 每个容器都运行在自己的进程环境中;
  • 网络隔离: 容器间的虚拟网络接口和IP地址都是分离的;
  • 资源隔离和分组: cgroups将CPU和内存等资源独立分配给每个不同的Docker容器。

容器的状态

  • 运行
  • 已暂停
  • 重新启动
  • 已退出

Command

$ docker help

$ docker help cp
$ docker run --detach --interactive --tty --name web busybox:latest /bin/bash

$ docker run -d -name wp3 --link wpdb:mysql -p 80 -v /run/lock/apache2 -v /run/apache2 --read-only wordpress:4

$ docker run -d --rm --name wp --env MY_ENVIRONMENT_VAR="test" --read-only wordpress:4

$ docker inspect --format "{{.State.Running}}" wp

$ docker ps

$ docker top web

$ docker run --it

$ docker restart web

$ docker logs web

$ docker exec web ps

$ docker stop xxxx

$ docker search postgres

$ docker save -o myfile.tar busybox:latest

$ docker load -i myfile.tar

$ docker rmi busybox

Dockerfile指令

类似于Makefile, Docker可以根据Dockerfile来构建镜像。Dockerfile是一个文本文件,每一行都是一个Dockerfile指令。接着使用docker build命令就可以构建出一个docker镜像了。Dockerfile是自动化运维的关键。

指令格式

注释

# Comment 这是注释

指令

INSTRUCTION arguments

样例

FROM ubuntu:16:04
LABEL author=fwz version=1.0.1
WORKDIR /root/myapp
COPY myapp .
RUN apt update \
    && apt install -y python3 python3-pip
    && pip3 install -r requirement.txt
EXPOSE 8000
ENTRYPOINT ["/usr/bin/python3", "/root/app/manage.py", "runserver"]

FROM

FROM指令指定了一个基础镜像,接下来的指令都是基于这个基础镜像进行构建。每个Dockerfile都必须包含FROM指令。如果FROM指定的基础镜像不在本地,则会从Dockerhub上拉取。

FROM ubuntu:16:04
FROM python:3.7

RUN

RUN指令是构建时执行的指令,。格式是RUN <command>或者RUN ['executable', 'param1', 'param2', ...],RUN指令默认使用shell执行命令,即/bin/sh -c执行命令,如果想使用不同的shell,比如bash,那么可以像这么写:

RUN ['/bin/bash', '-c' 'python3 bootstrap.py']

而且最好RUN指令的内容都写在同一行,防止ufs的层数过多。

CMD

CMD指令是容器启动时执行的指令,可以包含参数,比如:

CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]

EXPOSE

用于指定容器监听的内部端口号,不过可以在docker run的时候更改端口映射;

EXPOSE 27017

COPY

把文件从本地复制到容器中

COPY application.yml /app/

ENTRYPOINT

和CMD类似,也是容器启动时执行的命令,不过可以在容器启动即docker run时提供参数

ENTRYPOINT ["java", "app.jar"]

LABEL

记录容器相关的变量,docker inspect时可以看到

LABEL author="菜徐坤" version="7.7.7"