我的碎碎念:Docker 入门指南

原文请参考A Not Very Short Introduction to Docker , 国内打不开的话请移步[A Not Very Short Introduction to Docker (backup)](https://rillhudev.coding.net/p/blogres/d/blogres/git/blob/master/A Not Very Short Introduction to Docker.md)
之前曾经翻译过很多 Docker 入门介绍的文章,之所以再翻译这篇,是因为 Anders 的角度很独特,思路也很调理。你也可以看下作者的演讲稿《Docker, DevOps 的未来》。本文介绍了 Docker 的一些基本概念、诱人的特性、Docker 的工作原理、日常管理基本操作,以及一些 Docker 的问题的解决方案。
什么是 Docker,你应该知道些什么?
相比很多人的解释,我相信说 Docker 是一个轻量级的虚拟机更容易理解。另外一种解释是:Docker 就是操作系统中的 chroot。如果你不知道 chroot 是什么的话,后一种解释可能无法帮助你理解什么是 Docker。
chroot 是一种操作,能改变当前运行的进程和子进程的根目录。 程序运行在这样的一个被修改的环境中,它不能访问这个环境目录树之外的文件和命令,这个被修改的环境就是“chroot 牢笼”。
-- Arch Linux 的 wiki 中对 chroot 的解释
虚拟机 vs. Docker
下面这张图描述了虚拟机和 Docker 之间的差异。 在 VM 中,宿主 OS 上是 hypervisor(虚拟机监视器), 最上层是客户机操作系统,而 Docker 则使用 Docker 引擎和容器。 这样解释你能理解吗? Docker 引擎和 hypervisor 之间的区别又是什么呢?你可以列出运行在宿主 OS 上的进程来理解它们的区别。

下面这个简单的进程树可以看出它们的差异。虽然虚拟机中运行了很多进程,但是运行虚拟机的宿主机上却只有一个进程。
而运行 Docker 引擎的主机上则可以看到所有的进程。 容器进程是运行在宿主 OS 上的!,他们可以通过普通的 ps,kill 等命令进行检查和维护。
所有的东西都是透明的, 意味着什么呢?意味着 Docker 容器比虚拟机更小,更快,更容易与其它东西集成。如下图所示。

安装 CoreOS 的小型虚拟机居然有 1.2GB, 而装上 busybox 的小型容器只有 2.5MB。最快的虚拟机启动时间也是分钟级的,而容器的启动时间通常不到一秒。在同一宿主机上安装虚拟机需要正确的设置网络, 而安装 Docker 非常简单。
这么来看,容器是轻量、快速并且易集成,但这并不是它的全部!
Docker 是一份合约
Docker 还是开发者和运维之间的“合约”。 开发和运维在选择工具和环境时的姿态通常差别很大。开发者想要使用一些闪亮的新东西,比如 Node.js、Rust、Go、微服务、Cassandra、Hadoop、blablabla.........而运维则倾向于使用以往用过的工具,因为事实证明那些旧的工具很有效。
但这恰恰是 Docker 的亮点, 运维喜欢它,因为 Docker 让他们只要关心一件事: 部署容器, 而开发者也一样很开心,只要写好代码,然后往容器里一扔,剩下的交给运维就完事了。

不过别急,这还没完。运维还能帮助开发者构建优化好的容器以便用于本地开发。
更好的资源利用
很多年前,那时候还没有虚拟化,当我们需要创建一个新服务时,我们必须申请实际的物理机硬件。 这可能要花上数月,依赖于公司的流程。一旦服务器到位,我们创建好服务,很多时候它并没有像我们希望的那样成功,因为服务器的 CPU 使用率只有 5%。 太奢侈了。
接着,虚拟化来了。它可以在几分钟之内把一台机器运转起来,还可以在同一硬件上运行多个虚拟机,资源使用率就不只 5%了。但是,我们还需要给每个服务分配一个虚拟机,因此我们还是不能如愿的使用这台机器。
容器化是演化进程的下一步。容器可以在几秒之内创建起来,而且还能以比虚拟机更小的粒度进行部署。
依赖

Docker 启动速度真的很酷。 但是,我们为什么不把所有的都服务部署到同一台机器上呢? 原因很简单:依赖的问题。在同一台机器上安装多个独立的服务,不管是真是机器还是虚拟机都是一场灾难。用 Docker 公司的说法是:地狱一样的矩阵依赖。
而 Docker 通过在容器中保留依赖关系解决了矩阵依赖的问题。
速度

快当然不错,但是能快 100 倍就太不可思议了。速度让很多事情成为可能,增加了更多新的可能性。比如,现在可以快速创建新的环境,如果需要从 Clojure 开发环境完整的切换到 Go 语言吗?启动一个容器吧。需要为集成和性能测试提供生产环境 DB ?启动一个容器吧! 需要从 Apache 切换整个生产环境到 Nginx?启动容器吧!
Docker 是怎么工作的?
Docker 是一个 Client-Server 结构的系统,Docker 守护进程运行在主机上, 然后通过 Socket 连接从客户端访问, 客户端和守护进程也可以运行再同一主机上,但这不是必须的。Docker 命令行客户端也是类似的工作方式,但它通常通过 Unix 域套接字而不是 TCP 套接字连接。
守护进程从客户端接受命令并管理运行在主机上的容器。

Docker 概念及相互作用
主机, 运行容器的机器。
镜像,文件的层次结构,以及包含如何运行容器的元数据
容器,一个从镜像中启动,包含正在运行的程序的进程
Registry, 镜像仓库
卷,容器外的存储
Dockerfile, 用于创建镜像的脚本

我们可以通过 Dockerfile 来构建镜像, 还可以通过 commit 一个运行的容器来创建一个镜像,这个镜像可以会被标记,可以推到 Registry 或者从 Registry 上拉下来,可以通过创建或者运行镜像的方式来启动容器,可以被 stop,也可以通过 rm 来移除它。
镜像
镜像是一种文件结构,包含如何运行容器的元数据。Dockerfile 中的每条命令都会在文件系统中创建一个新的层次结构,文件系统在这些层次上构建起来,镜像就构建于这些联合的文件系统之上。

当容器启动后,所有镜像都会统一合并到一个进程中。 联合文件系统中的文件被删除时, 它们只是被标记为已删除,但实际上仍然存在。
镜像大小
这是一些经常使用的镜像相关的数据:
scratch - 基础镜像, 0 个文件,大小为 0
busybox - 最小 Unix 系统,2.5MB,10000 个文件
debian:jessie - Debian 最新版, 122MB, 18000 个文件
ubuntu:14.04 - 188MB,23000 个文件
创建镜像
可以通过 docker commit container-id、docker import url-to-tar 或者 docker build -f Dockerfile .来创建镜像。先看 commit 的方式:
从上面可以看出,我们可以通过 docker commit 来创建镜像,但是这种方式有点凌乱而且很难复制, 更好的方式是通过 Dockerfile 来构建镜像,因为它步骤清晰并且容易复制:
然后用下面的命令来构建:
Dockerfile 中的每一个命令都创建了新版的 layer,通常把类似的命令放在一起,通过 &&和续行符号把命令组合起来:
这些行中命令的顺序很重要,因为 Docker 为了加速镜像的构建,会缓存中间的镜像。 组织 Dockerfile 的顺序时,注意把经常变化的行放在文件的底部,当缓存中相关的文件改变时,镜像会重新运行,即使 Dockerfile 中的行没有发生变化也是如此。
Dockerfile 中的命令
Dockerfile 支持 13 个命令, 其中一些命令用于构建镜像,另外一些用于从镜像中运行容器,这是一个关于命令什么时候被用到的表格:

BUILD 命令:
FROM - 新镜像是基于哪个镜像的
MAINTAINER - 镜像维护者的姓名和邮箱地址
COPY - 拷贝文件和目录到镜像中
ADD - 同 COPY 一样,但会自动处理 URL 和解压 tarball 压缩包
RUN - 在容器中运行一个命令, 比如:apt-get install
ONBUILD - 当构建一个被继承的 Dockerfile 时运行命令
.dockerignore - 不是一个命令, 但它能控制什么文件被加入到构建的上下文中,构建镜像时应该包含.git 以及其它的不需要的文件。
RUN 命令:
CMD - 运行容器时的默认命令,可以被命令行参数覆盖
ENV - 设置容器内的环境变量
EXPOSE - 从容器中暴露出端口, 必须显式的通过在主机上的 RUN 命令带上-p 或者-P 来暴露端口
VOLUME - 指定一个在文件系统之后的存储目录。如果不是通过 docker run -v 设置的, 那么将被创建为/var/lib/docker/volumes
ENTRYPOINT - 指定一个命令不会被 docker run image cmd 命令覆盖。常用于提供一个默认的可执行程序并使用命令作为参数。
BUILD, RUN 命令都有的命令:
USER - 为 RUN、CMD、ENTRYPOINT 命令设置用户
WORKDIR - 为 RUN、CMD、ENTRYPOINT、ADD、COPY 命令设置工作目录

运行的容器
容器启动后,进程在它可以运行的联合文件系统中获得了新的可写层。
从 1.5 版本起,它还可以让最顶层的 layer 设置为只读,强制我们为所有文件输出(如日志、临时文件)使用卷。
docker run
如上所述, docker run 是用户启动新容器的命令, 这里是一些通用的运行容器的方法:

这是一个可以让你像普通的终端程序一样交互式的运行容器的方法, 如果你想把管道输出到容器中,可以使用-t 选项。
--interactive (-i) - 将标准输入发送给进程
-tty (-t) - 告诉进程有终端连接。 这个功能会影响程序的输出和它如何处理 Ctrx-C 等信号。
--rm - 退出时删除镜像。
docker run -env
--name - 给容器命名, 否则它是一个随机容器
--env (-e)- 设置容器中的环境变量
--env-file - 从 env-file 中引入所有环境变量(同 Linux 下的 source env-file 功能)
mysql - 指定镜像名为 mysql:lastest
docker run -publish
nginx 镜像,比如暴露出 80 和 443 端口。
docker run --link
连接容器需要设置容器到被连接的容器之间的网络,有两件事要做:
通过容器的连接名,更新 /etc/hosts 。 在上面的例子中,连接名是 db, 可以方便的通过名字 db 来访问容器。
为暴露的端口设置环境变量。这个好像没啥实际用处,你也可以通过 主机名:端口的形式访问对应的端口。
docker run limits
还可以通过 run limits 来限制容器可以使用的主机资源
设置 CPU 份数为 1024 中的 512 份并不意味着可以使用一半的 CPU 资源,这意味着在一个无任何限制的容器中,它最多可以使用一半的份数。比如我们有两个有 1024 份的容器,和一个 512 份的容器(1024:1024:512) ,那么 512 份的那个容器,就只能得到 1/5 的总 CPU 份数
docker exec container
docker exec 允许我们在已经运行的容器内部执行命令,这点在 debug 的时候很有用。
卷

卷提供容器外的持久存储。 这意味着如果你提交了新的镜像,数据将不会被保存。
如果目录不存在,则会被自动创建为:/var/lib/docker/valumes/ec3c543bc..535
实际的目录名可以通过命令:docker inspect container-id 找到。
还可以使用--valumes-from 选项从别的容器中挂载卷。
Docker Registry
Docker Hub 是 Docker 的官方镜像仓库,支持私有库和共有库,仓库可以被标记为官方仓库,意味着它由该项目的维护者(或跟它有关的人)策划。
Docker Hub 还支持自动化构建来自 Github 和 Bitbucket 的项目,如果启用自动构建功能,那么每次你提交代码到代码库都会自动构建镜像。
即使你不想用自动构建,你还是可以直接 docker push 到 Docker Hub,Docker pull 则会拉取镜像下来。docker run 一个本地不存在的镜像,则会自动开始 docker pull 操作。
你也可以在任意地方托管镜像,官方有 Registry 的开源项目,但是,还有很多 Bug。
此外,Quay、Tutum 和 Google 还提供私有镜像托管服务。
检查容器
检查容器的命令有一大把:
下面详细讲一下 docker ps 和 docker inspect,这两个命令最常用了。
技巧花招
获取容器 id。写脚本时很有用。
docker inspect 可以带格式化的字符串----Go 语言模板作为参数,详细描述所需的数据。写脚本时同时有用。
使用 docker exec 来跟运行中的容器进行交互。
通过卷来避免每次运行时都重建镜像, 下面是一个 Dockerfile,每次构建时,会拷贝当前目录到容器中。
构建并运行镜像:
为避免重建,创建一次性镜像并在运行时挂载本地目录。
安全

大家可能听说过使用 Docker 不那么安全。这不是假话,但这不成问题。
目前 Docker 存在以下安全问题:
镜像签名未被正确的核准。
如果你在容器中拥有 root 权限,那你潜在的拥有对真个主机的 root 权限。
安全解决办法:
从你的私有仓库中使用受信任的镜像
尽量不要以 root 运行容器
把容器中的 root 当作是主机上的 root? 还是把容器的根目录设置为容器内的根目录 ?
如果服务器上所有的容器都是你的,那你不需要担心他们之间会有危险的交互。
“选择”容器
我给选择两字加了引号, 因为目前根本没有任何别的选择, 但是很多容器爱好者想玩玩,比如 Ubuntu 的 LXD、微软的 Drawbridge,还有 Rocket。
Rocket 由 CoreOS 开发,CoreOS 是一个很大的容器平台。 他们开发 Rocket 的理由是觉得 Docker 公司让 Docker 变得臃肿,并且还和 CoreOS 有业务冲突。
他们在这个新的容器中,尝试移除那些因为历史原因而留下来的 Docker 瑕疵。并通过 socket activation 提供简单的容器和彻底的安全构建。

编排
当我们把应用程序拆开到多个不同的容器中时,会产生一些新的问题。怎么让不同的部分进行通信呢? 这些容器在单个主机上怎么办? 多个主机上又是怎么处理?
单个主机上,Docker 通过连接来解决编排的问题。
为简化容器的链接操作,Docker 提供了一个叫 docker-compose 的工具。(以前它叫 fig, 由另一家公司开发,然后最近 Docker 收购了他们)

docker-compose
在单个 docker-compose.yml 文件中声明多个容器的信息。来看一个例子,管理 web 和 redis 两个容器的配置文件:
启动上述容器,可以使用 docker-compose up 命令
也可以通过 detached 模式(detached mode)启动: docker-compose up -d,然后可以通过 docker-compose ps 查看容器中跑了啥东西:
还可以同时让命令在一个容器或者多个容器中同时工作。
从以上命令可以看出,扩展很容易,不过应用程序必须写成支持处理多个容器的方式。在容器外,不支持负载均衡。
Docker 托管
很多公司想做在云中托管 Docker 的生意,如下图。

这些提供商尝试解决不同的问题, 从简单的托管到做"云操作系统"。其中有两家比较有前景:
CoreOS
如上图所示,CoreOS 是可以在 CoreOS 集群中托管多个容器的一系列服务的集合:

CoreOS Linux 发行版是裁剪的 Linux,在初次启动时使用 114MB 的 RAM,没有包管理器, 使用 Docker 或者它自己的 Rocket 运行一切程序。
CoreOS 使用 Docker(或 Rocket)在主机上安装应用。
使用 systemd 作为 init 服务,它的性能超级好,还能很好的处理启动依赖关系, 强大的日志系统,还支持 socket-activation。
etcd 是分布式的,一致性 K-V 存储用于配置共享和服务发现。
fleet,集群管理器,是 systemd 的扩展,能与多台机器工作,采用 etcd 来管理配置并运行在每一个台 CoreOS 服务器上。
AWS
Docker 容器托管在 Amazon 有两种途径:Elastic Beanstalk 部署 Docker 容器,它工作的很好,但就是太慢了, 一次全新的部署需要好几分钟,感觉跟一般的容器秒级启动不大对劲。ECS、Elastic Container Server 是 Amazon 上游容器集群解决方案, 目前还在预览版 3,看起来很有前途,跟 Amazon 其它服务一样,通过简单的 web service 调用与它交互。
总结
Docker is here to stay
解决了依赖问题
容器各方面都很快
有集群解决方案,但不能无缝对接
版权声明: 本文为 InfoQ 作者【hongfei】的原创文章。
原文链接:【http://xie.infoq.cn/article/487329ac9837bba58cce998b6】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论 (1 条评论)