写点什么

Docker:利用 Linux 容器实现可移植的应用部署

  • 2014-02-12
  • 本文字数:7489 字

    阅读完需:约 25 分钟

Docker 是一种在 Linux容器里运行应用的开源工具,一种轻量级的虚拟机。除了运行应用,Docker 还提供了一些工具,借助 Docker Index 或自己托管的 Docker 注册表对进行了集装箱化处理的应用进行分发,从而简化复杂应用的部署过程。

我将在本文介绍如今在部署复杂系统时公司所面临的挑战,Docker 怎样有效地解决这个问题,以及 Docker 的其他用例。

部署的挑战

服务器应用的部署已经越来越复杂了。把几个 Perl 脚本拷贝到正确目录就完成服务器应用的安装,这种时代已经一去不复返了。如今的软件有很多类型的需求:

  • 对已安装软件和库的依赖(“Python 版本高于 2.6.3,使用 Django 1.2”)
  • 依赖于正在运行的服务(“需要一个 MySQL 5.5 数据库和一个 RabbitMQ 队列”)
  • 依赖于特定的操作系统(“在 64 位的 Ubuntu Linux 12.04 上构建、测试”)
  • 资源需求:
    • 最小的可用内存(“需要 1GB 的可用内存”)
    • 能绑定特定的端口(“绑定 80 和 443 端口”)

我们来看一个相对简单的应用的部署: Wordpress 。Wordpress 的安装通常要求:

  • Apache 2
  • PHP 5
  • MySQL
  • Wordpress 源码
  • 一个 Wordpress MySQL 数据库,配置 Wordpress 使用该数据库
  • Apache 的配置:
    • 加载 PHP 模块
    • 支持 URL 重写和.htaccess 文件
    • 指向 WordPress 源码的 DocumentRoot

在服务器上部署、运行这样一个系统,我们可能会遇到下面的问题和挑战:

  1. 隔离性:如果我们已经在这个服务器上部署了不同的网站,已有的网站只能在 nginx 上运行,而 Wordpress 依赖于 Apache,这时我们就会有麻烦:它们都监听 80 端口。同时运行两个网站是可以的,但需要调整配置(修改监听端口),设置反向代理等。库级别也会出现类似的冲突,如果还要运行一个仍然依赖 PHP4 的老应用就会出问题,因为 Wordpress 不再支持 PHP4,同时运行 PHP4 和 PHP5 则非常困难。运行在同一个服务器上的应用没有互相隔离(在文件系统级别和网络级别),所以它们可能会互相冲突。
  2. 安全性:****Wordpress的安全记录并不是非常好。所以还是给它创建个沙箱,至少黑客入侵时不会影响其他运行的应用。
  3. 升级、降级:升级应用一般会覆盖现有文件。升级过程中会发生什么?系统要关闭么?如果升级失败,或者不对该怎么办?我们怎样快速回退到先前的版本?
  4. 快照、备份:一旦所有的内容都设置好,就给系统创建一个“快照”,以便能备份快照,甚至能移到另一个服务器上再次启动,或者拷贝到多个服务器上以备不时之需。
  5. 重复性:系统出新版本之后,比较好的做法是先在测试基础设施上自动部署并测试,然后再发布到生产系统。通常会利用诸如 Chef Puppet 等工具在服务器上自动安装一堆包,等一切内容都就绪后,再在生产系统上运行相同的部署脚本。这在百分之九十九的情况下都没有问题。但有百分之一的例外,在部署到测试环境和生产环境之间的时间跨度里,你依赖的包在包仓库里有了更新,而新版本并不兼容。结果生产环境的设置和测试环境不同,还有可能破坏生产系统。假如没有控制部署的每一个方面(例如托管自己的 APT 或 YUM 仓库),持续在多个阶段(比如测试、预演、生产环境)重复搭建出完全相同的系统就很困难。
  6. 资源限制:如果我们的 Wordpress 耗费 CPU 资源,并占用了所有的 CPU 周期,导致其他应用无法做任何事情怎么办?如果它用尽了全部可用的内存呢?或者疯狂写日志阻塞磁盘呢?要是能限制应用的可用资源,比如 CPU、内存和磁盘空间,就会非常方便。
  7. 易于安装:也许有 Debian 或 CentOS 包,抑或是能自动执行所有复杂步骤并安装 Wordpress 的 Chef 菜谱。但这些菜谱很难稳定下来,因为它们需要考虑目标系统上可能的系统配置。很多情况下,这些菜谱只能在干净的系统上运行。因此,你不太可能更换成自己的包或 Chef 菜谱。这样的话,安装就是个复杂的系统工程,而不是午休期间就能搞定的事情。
  8. 易于移除:软件应该能轻松、干净地移除,不留痕迹。但部署应用通常要调整已有的配置文件、设置状态(MySQL 数据库的数据,日志),完全移除应用也变得不那么容易。

那我们应该如何解决这些问题呢?

虚拟机!

我们决定在单独的虚拟机上运行独立的应用,例如 Amazon 的 EC2 ,大部分问题这时会迎刃而解:

  1. 隔离性:在一个 VM 上安装一个应用,应用是完全独立的,除非它们攻入了对方的防火墙。
  2. 重复性:用你喜欢的方式准备系统,然后创建一个 AMI。你可以随意实例化多个 AMI 实例。完全是可重现的。
  3. 安全性:由于我们完全隔离,如果 Wordpress 遭到攻击,其余的基础设施并不会受到影响——除非你没有保管好 SSH 密钥或者在哪里都使用同一个密码,但你应该不会这么做吧?
  4. 资源限制:VM 会分配特定的 CPU 周期、可用内存和磁盘空间,没有加价的话就不能超额。
  5. 易于安装:越来越多的应用能够在 EC2 上运行,只要在 AWS marketplace 上点击一个按钮就能实例化应用。启动只需要几分钟,就是这样。
  6. 易于移除:不需要某个应用了?销毁 VM。干净又方便。
  7. 升级、降级: Netflix 如何部署代码里提到,只需要在新 VM 上部署新版本,然后让负载均衡器指向部署了新版本的 VM。不过应用如果需要在本地保存状态,这种方法就不是很好用了。
  8. 快照、备份:点击一个按钮(或者调用一下 API)就能获得 EBS 磁盘的快照,快照会备份到 S3 中。

完美!

不过……我们有个新问题:虚拟机在两个方面比较昂贵

  • 金钱:你真的有那么多钱为每个应用启动一个 EC2 实例?另外你能预测到需要多少个实例么?如果你以后需要更多的资源,你需要停止 VM 进行升级——否则就要为闲置资源白白付钱,直到真正用起来(除非你用能动态调整大小的 Solaris Zones,比如 Joyent 上的)。
  • 时间:虚拟机相关的操作大多都很慢:启动要几分钟,捕捉快照要几分钟,创建镜像也需要几分钟。世界不停转动,我们可没有这种时间!

我们能做得更好吗?

进入 Docker 的世界吧。

Docker 是由公共 PaaS 提供商 dotCloud 的人发起的开源项目,于去年初发起。从技术角度来说,Docker(主要用 Go 语言编写)试图简化两种已有技术的使用:

  • LXC:Linux 容器,允许独立进程在比普通 Unix 进程更高的隔离级别上运行。使用的技术术语是集装箱化:一个容器里运行一个进程。容器支持的隔离级别有:
    • 文件系统:容器只能访问自己的沙箱文件系统(类似于 chroot ),否则要专门挂载到容器的文件系统中才能访问。
    • 用户名字空间:容器有自己的用户数据库(也就是容器的 root 不等于主机的 root 账户)。
    • 进程名字空间:只有容器里的进程才是可见的(ps aux 的输出会非常简洁)。
    • 网络名字空间:每个容器都有自己的虚拟网络设备和虚拟 IP(因此它可以绑定任意端口,不用占用主机端口)。
  • AUFS:高级多层的统一文件系统,可用来创建联合、写时拷贝的文件系统。

Docker 可以安装在任何支持 AUFS 和内核版本大于等于 3.8 的 Linux 系统上。但从概念上来说它并不依赖于这些技术,以后也可以和类似的技术一起运行,例如 Solaris 的 Zones BSD jails ,并将 ZFS 作为文件系统。不过目前只能选择 Linux 3.8+ 和 AUFS。

那 Docker 为什么有意思呢?

  • Docker 非常轻量。启动 VM 是个大动作,需要占用大量内存;而启动 Docker 容器只耗费很少的 CPU 和内存,并且非常快。几乎和启动一个常规进程没什么区别。不仅运行容器快,构建镜像、捕获文件系统的快照也很快。
  • 它运行在已经虚拟化过的环境中。也就是说,你可以在 EC2 实例、Rackspace VM 或 VirtualBox 里运行 Docker。事实上,在 Mac 和 Windows 上使用 Docker 的首选方式是使用 Vagrant
  • Docker 容器能移植到任何运行 Docker 的操作系统上。无论是 Ubuntu 还是 CentOS,只要 Docker 运行着,你的容器就能运行。

让我们回到前面的部署、操作问题列表,看看 Docker 是怎么解决的:

  1. 隔离性:Docker 在文件系统和网络级别隔离了应用。从这个意义上来讲很像在运行”真正的“虚拟机。
  2. 重复性:用你喜欢的方式准备系统(登录并在所有软件里执行 apt-get 命令,或者使用 Dockerfile ),然后把修改提交到镜像中。你可以随意实例化若干个实例,或者把镜像传输到另一台机器,完全重现同样的设置。
  • 安全性:Docker 容器比普通的进程隔离更为安全。Docker 团队已经确定了一些安全问题,正在着手解决。
  • 资源约束:Docker 现在能限制 CPU 的使用率和内存用量。目前还不能直接限制磁盘的使用情况。
  • 易于安装:Docker 有一个 Docker Index ,这个仓库存储了现成的 Docker 镜像,你用一条命令就可以完成实例化。比如说,要使用 Clojure REPL 镜像,只要运行 docker run -t -i zefhemel/clojure-repl 命令就能自动获取并运行该镜像。
  • 易于移除:不需要应用了?销毁容器就行。
  • 升级、降级:和 EC2 VM 一样:先启动应用的新版本,然后把负载均衡器切换到新的端口。
  • 快照、备份:Docker 能提交镜像并给镜像打标签,和 EC2 上的快照不同,Docker 是立即处理的

怎么使用 Docker

假设你已经安装了 Docker。要在 Ubuntu 容器中运行 bash,只要执行:

复制代码
docker run -t -i ubuntu /bin/bash

根据“ubuntu”镜像的下载情况,Docker 会选择下载或者使用本地可用的拷贝,然后在 Ubuntu 容器里运行 /bin/bash。接着你就能在容器里执行几乎所有典型的 Ubuntu 操作,比如安装新的包。

我们来安装个“hello”:

复制代码
$ docker run -t -i ubuntu /bin/bash
<span>root@78b96377e546:/#</span> apt-get install hello
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
hello
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 26.1 kB of archives.
After this operation, 102 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu/ precise/main hello amd64 2.7-2 [26.1 kB]
Fetched 26.1 kB in 0s (390 kB/s)
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package hello.
(Reading database ... 7545 files and directories currently installed.)
Unpacking hello (from .../archives/hello_2.7-2_amd64.deb) ...
Setting up hello (2.7-2) ...
<span>root@78b96377e546:/#</span> hello
Hello, world!

现在退出,然后再运行一次相同的 Docker 命令:

复制代码
<span>root@78b96377e546:/#</span> exit
exit
$ docker run -t -i ubuntu /bin/bash
<span>root@e5e9cde16021:/#</span> hello
bash: hello: command not found

怎么了?我们美丽的 hello 命令哪儿去了?事实上我们刚刚根据干净的 Ubuntu 镜像启动了一个新的容器。要继续先前那个,我们必须把它提交仓库中。我们退出这个容器,看看先前启动容器的 ID 是什么:

复制代码
$ docker ps -a
ID IMAGE COMMAND CREATED STATUS PORTS
e5e9cde16021 ubuntu:12.04 /bin/bash About a minute ago Exit 127
78b96377e546 ubuntu:12.04 /bin/bash 2 minutes ago Exit 0

docker ps 命令能列出当前运行的容器,docker ps -a 还会显示已经退出的容器。每个容器都有一个唯一的 ID,类似于 Git 提交哈希值。命令也列出了容器基于的镜像、运行的命令、创建时间、当前状态,以及容器暴露的端口和与主机端口之间的映射。

上面那个是我们第二次启动的容器,不包含“hello”;下面那个是我们想重用的,所以我们提交一下,再创建一个新的容器:

复制代码
$ docker commit 78b96377e546 zefhemel/ubuntu
356e4d516681
$ docker run -t -i zefhemel/ubuntu /bin/bash
<span>root@0d7898bbf8cd:/#</span> hello
Hello, world!

我用容器 ID 把容器提交到了仓库中。仓库类似于 Git 仓库,包含一或多个打了标签的镜像。如果像我一样没有指定标签名称,标签会被命名为“latest”。运行 docker images 命令可以查看本地安装的所有镜像。

Docker 提供了一些基础镜像(比如 ubuntu 和 centos),你也可以创建自己的镜像。用户仓库的命名模型和 Github 的类似: Docker 用户名后面跟一个斜线,然后再跟仓库名称。

前面创建 Docker 镜像的方式并不是特别正规,你可以试试。更简洁的方式是使用 Dockerfile。

使用 Dockerfile 构建镜像

Dockerfile 是个简单的文本文件,介绍了如何从基础镜像构建镜像。我在 Github 上提供了几个Dockerfile。下面的文件用来运行、安装 SSH 服务器:

复制代码
FROM ubuntu
RUN apt-get update
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo "root:root" | chpasswd
EXPOSE 22

上面的内容一目了然。FROM 命令定义了基础镜像,基础镜像可以是官方的,也可以是我们刚刚创建的 zefhemel/ubuntu。RUN 命令用来配置镜像。在这里,我们更新了 APT 包仓库,安装了 openssh-server,创建了一个目录,然后给我们的 root 账户设置了一个再简单不过的密码。EXPOSE 命令会向外暴露 22 端口(SSH 端口)。接下来看看如何构建并实例化这个 Dockerfile。

第一步是构建一个镜像。在包含 Dockerfile 的目录下运行:

复制代码
$ docker bui ld -t zefhemel/ssh .

这会创建一个 zefhemel/ssh 仓库,包含我们新的 SSH 镜像。如果创建成功,就能进行实例化了:

复制代码
$ docker run -d zefhemel/ssh /usr/sbin/sshd -D

和前面的命令不一样。-d 表示会在后台运行容器,而不是运行 bash,所以我们用前台模式(用 -D 参数指定)运行了 sshd 守护进程。

让我们检查运行中的容器,看看命令做了些什么:

复制代码
$ docker ps
ID IMAGE COMMAND CREATED STATUS PORTS
23ee5acf5c91 zefhemel/ssh:latest /usr/sbin/sshd -D 3 seconds ago Up 2 seconds 49154->22

可以看到我们的容器启动着。PORTS 头下的内容比较有意思。由于我们 EXPOSE 了 22 端口,这个端口现在映射到了主机系统的一个端口(这里是 49154)。让我们看看它能否运行。

复制代码
$ ssh root@localhost -p 49154
The authenticity of host '[localhost]:49154 ([127.0.0.1]:49154)' can't be established.
ECDSA key fingerprint is f3:cc:c1:0b:e9:e4:49:f2:98:9a:af:3b:30:59:77:35.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:49154' (ECDSA) to the list of known hosts.
root@localhost's password: <I typed in 'root' here>
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.8.0-27-generic x86_64)
* Documentation: https://help.ubuntu.com/
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
<span>root@23ee5acf5c91:~#</span>

再次成功了!现在有了一个运行的 SSH 服务器,我们能登录它。在有人猜出密码并攻击容器之前,让我们先从 SSH 退出,杀掉容器。

复制代码
$ docker kill 23ee5acf5c91

如你所见,容器的 22 端口映射到了 49154 端口,但这是完全随机的。要把它映射到特定端口,运行命令时传入 -p 参数:

复制代码
docker run -p 2222:22 -d zefhemel/ssh /usr/sbin/sshd -D

现在,如果 2222 端口可用,我们的端口就会映射到 2222 上。我们在 Dockerfile 的结尾再添加一行内容,以便我们的镜像对用户更加友好:

复制代码
CMD /usr/sbin/sshd -D

CMD 表示构建镜像时并不会运行命令,实例化时才运行。所以不传递其它参数时就会执行 /usr/sbin/sshd -D。然后我们可以直接运行:

复制代码
docker run -p 2222:22 -d zefhemel/ssh

得到的结果和前面一样。要发布新创建的镜像,只要运行 docker push 就可以了:

复制代码
docker push zefhemel/ssh

登录之后,镜像就可用了,用先前的 docker run 命令就能执行命令。

让我们回到 Wordpress 的例子。怎样在容器里用 Docker 运行 Wordpress 呢?要构建一个 Wordpress 镜像,我们要创建一个 Dockerfile:

  1. 安装 Apache、PHP5 和 MySQL
  2. 下载 Wordpress,解压到文件系统的某个地方
  3. 创建一个 MySQL 数据库
  4. 更新 WordPress 的配置文件,指向 MySQL 数据库
  5. 把 WordPress 设置为 Apache 的 DocumentRoot
  6. 启动 MySQL 和 Apache(比如用 supervisord

幸运的是,很多人已经成功了,比如 John Fink 的 GitHub 库就包括创建这样一个 Wordpress 镜像需要的所有内容。

Docker 用例

除了用可靠、可重复的方式简化复杂应用的部署,Docker 还有很多用途。下面是一些有趣的 Docker 用法和项目:

  • 持续集成和部署:在 Docker 容器里构建软件,确保构建之间的隔离性。构建好的软件镜像可以自动推到私有的 Docker 仓库中,并部署到测试环境或生产环境。
  • Dokku :一个简单的 PaaS,用不到一百行的 Bash 构建而成。
  • Flynn Deis ,两个使用 Docker 的开源 PaaS 项目。
  • 在容器里运行桌面环境
  • CoreOS 验证了 Docker 的合理性,CoreOS 是个非常轻量级的 Linux 发行版,其中的应用都用 Docker 安装、运行,由 systemd 管理。

Docker 不是什么

尽管 Docker 有助于系统的可靠部署,但它本身并不是个完全成熟的部署系统。它操作的是容器里运行的应用。哪个容器安装在哪个服务器上,以及如何启动它们,则超出了 Docker 的范围。

同样的,Docker 也不处理跨多个容器(可能在多个物理服务器上,也可能在多个 VM 上)运行的应用。要让容器互相通信,需要某些发现机制,来找出哪些 IP 和端口上的其他应用可用。这和跨常规虚拟机的服务发现非常相似。 etcd 等工具,或者其他的服务发现机制都能用来解决这个问题。

结论

虽然本文描述的所有内容用原始的 LXC、cgroups 和 AUFS 也可能实现,但实现起来绝对没有那么容易或简单。Docker 提供了一种简单的方式将复杂应用打包到容器中,而且能轻松版本化、可靠分发。进而让轻量级的Linux 容器和真正的虚拟机一样灵活、强大,但成本更低、方式更为便捷。即便 Vagrant VirtualBox VM 在 Macbook Pro 上,使用运行在其中的 Docker 创建的 Docker 镜像也能很好地运行在 EC2 Rackspace Cloud 或物理硬件上,反之亦然。

Docker 可以从它的网站上获取,并免费使用。交互式的入门指南很不错。项目的路线图指出,第一个生产就绪的版本是2013 年10 月发布的0.8 版本,不过此前大家已经在生产环境里使用Docker 了。

作者简介

Zef Hemel LogicBlox 公司的开发者布道师和产品管理团队成员,LogicBlox 基于逻辑编程(尤其是 Datalog )开发应用服务器和数据库引擎。此前他是 Cloud9 IDE 的工程副总裁,开发基于浏览器的 IDE。Zef 工作之初就涉足 Web 领域,从二十世纪九十年代起就在开发 Web 应用。他是声明式编程环境的拥趸。

查看英文原文: Docker: Using Linux Containers to Support Portable Application Deployment

2014-02-12 09:2126884
用户头像

发布了 151 篇内容, 共 61.6 次阅读, 收获喜欢 18 次。

关注

评论

发布
暂无评论
发现更多内容

盘一盘Java中的abstract和interface

李子捌

28天写作 21天挑战 12月日更

分布式数据缓存中的一致性哈希算法

程序员历小冰

缓存 一致性哈希 28天写作 12月日更

PassJava 开源 (三):快速生成前后端代码

悟空聊架构

SpringCloud 28天写作 passjava 悟空聊架构 12月日更

Prometeus 2.32.0 新特性

耳东@Erdong

Prometheus 28天写作 12月日更

彩色图像色彩空间原理(理论篇—6)

不脱发的程序猿

机器视觉 图像处理 色彩空间原理

管理文化

圣迪

管理 文化 强势文化 弱势文化

【架构实战营】模块七作业

liu🍊

22《重学JAVA》--字节流

杨鹏Geek

Java25周年 28天写作 12月日更

优秀程序员的30种思维--理解认知篇

hackstoic

程序员 架构思维

如何设计异地多活架构

天天向上

架构实战营

Dubbo 框架学习笔记十

风翱

dubbo 12月日更

.NET6新东西--模式匹配中的Extended Property Patterns

喵叔

28天写作 12月日更

蓝绿发布、滚动发布、灰度发布

xcbeyond

灰度发布 28天写作 12月日更 蓝绿发布 滚动发布

单步调试理解webpack里通过require加载nodejs原生模块实现原理

汪子熙

前端 前端开发 webpack 28天写作 12月日更

如何给产品做拉新?

石云升

AARRR 产品思维 28天写作 产品增长 12月日更

Python 的元类设计起源自哪里?

Python猫

Python ruby

040022-week7-design

InfoQ_70156470130f

直播预告|百万人观看无卡顿!京东云CDN支撑直播丝般顺滑

京东科技开发者

CDN 直播

在线问诊初体验

mtfelix

28天写作

视频知识点(6)- MPEG vs JPEG

liuzhen007

28天写作 12月日更

王者商城异地多活设计

天天向上

架构实战营

架构营模块七作业

GTiger

架构实战营

DDD领域驱动实战(二)-限界上下文(bounded context)

JavaEdge

12月日更

今年读了多少书?(22/28)

赵新龙

28天写作

2021的科技卦象·兑·一场“双碳”催化雨

脑极体

绩效沟通准备

搬砖的周狮傅

绩效管理

《谈谈人生选择的思考逻辑》读后感

Changing Lin

12月日更

盘点2021|「避坑宝典」为大家分享一下笔者在2021年所遇到“匪夷所思”的Bug趣事(上)

洛神灬殇

2021年展望 2021年度技术盘点与展望 盘点2021 避坑宝典

前端开发:Mac OS环境下的通过对Chrome浏览器设置SameSite解决跨域请求方法

三掌柜

28天写作 12月日更

性能分析之TPS从300到750的过程

zuozewei

性能测试 性能分析 12月日更

盘点 2021|学习、分享、努力中成长

小隐乐乐

盘点2021

Docker:利用Linux容器实现可移植的应用部署_DevOps & 平台工程_Zef Hemel_InfoQ精选文章