在 Docker 容器中运行应用程序

| 分类 programming  | 标签 Docker  Programming  Utility  《Docker in Action》 

案例说明

运行 3 个容器,实现对网站的监控。

三个容器的说明:

  • 容器 web: 创建自 nginx 映像,使用 80 端口,运行于后台,实现 web 服务。
  • 容器 mailer: 该容器中运行一个 mailer 程序,运行于后台,当接收到事件后会向管理员发送邮件。
  • 容器 agent: 该容器运行一个 watcher 程序,以交互模式运行,用于不断地监测 web 服务的运行情况,一旦出现故障会立即向 mailer 容器发送消息。

创建容器

创建并运行 web 容器

$ docker run --detach --name web nginx:latest

命令执行后,docker 会从 Docker Hub 上下载 nginx:latest 映像文件,根据该映像文件开启一个容器,并在容器中运行 nginx 程序。

运行后,会输出一行字符串,该字符串为该容器的唯一标识符,类似 7cb5d2b9a7eab87f07182b5bf58936c9947890995b1b94f412912fa822a9ecb5。通常我们可以将该标识符保存到一个变量里,以便于在其它命令中使用。

--detach 选项使得该容器在后台运行,也可以用其缩写版本 -d

--name web 将当前容器命名为 web,以便之后引用。

创建并运行 mailer 容器

$ docker run -d --name mailer dockerinaction/ch2_mailer

创建并运行一个交互式的容器 agent

一个交互式的程序可以从用户获取输入或将输出显示到终端中。在 Docker 中运行交互式程序需要将你的终端绑定到容器的输入或输出上。

运行一个交互式容器如下:

$ docker run --interactive --tty \
    --link web:myweb \
    --name web_test \
    busybox:latest /bin/sh

--interactive-i 选项告诉 Docker 为该容器开启标准输入 (stdin)。 --tty-t 选项告诉 Docker 为该容器分配一个虚拟终端,以便于向容器发送信号。通常这两个选项是一起使用的,合记为 -it

--link web:web 选项使得当前容器中能用 myweb 来引用 容器 web。

最后,/bin/sh 是指定在该容器中运行的程序,运行后,可以在 sh 中运行 wget -O - http://myweb:80/ 来检测容器 web 的运行情况。这里的 wget 命令实现向 nginx 服务器发送请求,并将获取的页面内容输出到终端上。

通用 --tty 开启的交互式容器,可以使用 Ctrl-P Q 来使其转入后台运行。

运行 agent 容器

$ docker run -it \
    --name agent \
    --link web:insideweb \
    --link mailer:insidemailer \
    dockerinaction/ch2_agent

该容器会每 1 秒对容器 web 检测一次,并输出类似 System up. 等信息。当看到这些信息后,可以用 Ctrl-P Q 将其转入后台运行。

容器命令

docker ps

docker ps 会列出每个正在运行的容器的下面信息:

  • 容器 ID
  • 使用的映像文件
  • 在容器中运行的命令
  • 自容器创建后的时间
  • 容器已运行的时间
  • 容器使用的端口号
  • 容器的名称

重启容器

$ docker restart web
$ docker restart mailer
$ docker restart agent

查看容器的日志

$ docker logs web

由于容器 agent 对容器 web 进行了多次请求,故上面的命令会输出一长串的 GET / HTTP/1.0" 200

容器运行是的每条输出(或错误输出)都会保存到容器的日志文件中,因此,只要容器一直在运行,它的日志文件会不断的变大。由于没有截断的手段,因而最好用 Volume 来处理日志数据。

$ docker logs mailer

mailer 的日志输出类似: CH2 Example Mailer has started.

docker logs 命令添加 --follow-f 选项时,会一直保持运行,并持续显示最新的日志。可以用 Ctrl C 中断。

关闭容器

$ docker stop web

以上命令将中止容器中的 PID #1 程序的运行。

容器 web 中止后,容器 agent 将触发对容器 mailer 的请求,进而可以看到容器 mailer 中相关日志 Sending email: To: admin@work Message: The service is down!

已解决的问题及 PID 命名空间

PID 命名空间是可用于标识进程的一个数集。Linux 可创建多个 PID 命名空间,每个命名空间中使用的 PID 相互独立,即每个命名空间都各自可使用 1, 2, 3 等而互不干扰。

Docker 默认为每个容器创建一个 PID 命名空间:

$ docker run -d --name namespaceA \
    busybox:latest /bin/sh -c "sleep 30000"
$ docker run -d --name namespaceB \
    busybox:latest /bin/sh -c "nc -l -p 0.0.0.0:80"

运行这两个容器后,

$ docker exec namespaceA ps

PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c sleep 30000
    6 root       0:00 sleep 30000
    7 root       0:00 ps


$ docker exec namespaceB ps

PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c nc -l -p 0.0.0.0:80
    5 root       0:00 nc -l -p 0.0.0.0:80
    6 root       0:00 ps

可以看到,每个容器中使用的 PID 都是独立的,例如都有 PID #1。

要使容器不创建自己的 PID 命名空间,在运行 docker createdocker run 时要加上 --pid host 选项:

$ docker run --pid host busybox:latest ps

以上命令将列出机器上的所有运行中的进程。

Docker 解决的问题

Docker 基于 Linux namespace, file system roots, virtualized network components 实现的容器隔离性解决了如下的冲突问题:

  • 多个程序想绑定到相同的端口
  • 多个程序想使用相同的临时文件名
  • 各程序想使用全局安装的代码库的不同版本
  • 相同程序的不同进程想使用相同的 PID 文件
  • 多个程序同时修改环境变量

消除 metaconflicts:创建一个网站集群

metaconflicts 即容器间的冲突。

继续上面的例子,这次开启多组 web + agent 容器对,然后只开启一个 mailer 容器,所有的 agent 都将事件发送给容器 mailer。

灵活的容器标识

当执行 docker run -d --name webid nginx 时,生成的容器的名称为 webid,容器的名称不能重复。当没有使用 --name 选项时,Docker 会自动为我们创建一个易读的唯一容器名。

也可以重命名容器:

$ docker rename webid webid-old

每个容器还有一个 1024 位的十六进制编码的唯一 ID,如 7cb5d2b9a7eab87f07182b5bf58936c9947890995b1b94f412912fa822a9ecb5。可以通过这个 ID 对该容器进行引用。如:

docker stop \
    7cb5d2b9a7eab87f07182b5bf58936c9947890995b1b94f412912fa822a9ecb5 

该 ID 值是完全唯一的,即永远不会冲突,若要想在同一台机器上保持唯一性,只需取其前 12 个字符长的字符串即可,因此,上面的命令也可以这样:

```bash
docker stop 7cb5d2b9a7ea

容器 ID 值不适合人读,但可在脚本处理或自动化程序中使用。

如何获取容器 ID

当开启一个在后台运行的容器时,容器 ID 会自动输出到终端,因此可以获取。但如果开启的是交互式的容器,就不能获取 ID。这种情况下可以先用 docker create 命令创建一个容器(不立即运行),该命令和 docker run 的格式完成一样,同样也会输出容器的 ID。

将 ID 值保存到一个 Shell 变量中:

CID=$(docker create nginx:latest)
echo $CID

这种方式获取的 ID 只能在一个脚本或程序中使用,不能在多个程序间共享。如果要在多个程序间共享该容器 ID 值,可以将值保存在 container ID(CID) 文件中。docker rundocker create 命令都可以用 --cidfile 选项指定 CID 文件的位置,如:

$ docker create --cidfile /tmp/web.cid ngix

然后用 cat /tmp/web.cid 来获取该值。用这种方式时, CID 文件可能会冲突。幸运的是,当指定的 CID 冲突时(即该文件已经存在),Docker 会报错,不会创建该容器。 CID 文件可以在多个容器间共享,并且可以通过 Volume 功能进行重命名。

另一种获取 ID 的方式是使用 docker ps:

CID=$(docker ps --latest --quiet) # or CID=$(docker ps -l -q)
echo $CID

这种方式获取的是截取的 12 字节长的 ID,要想获取整个 ID,要加 --no-trunc 选项。

容器 ID 不适合人使用,因此 Docker 还会为容器自动创建一个可读的唯一名字,名字的结构是: 一个形容词_某个名人的名字,如 hungry_swartz, distracted_turing 等。

容器的状态及其依赖

用脚本加载容器:

MAILER_CID=$(docker run -d dockerinaction/ch2_mailer)
WEB_CID=$(docker create nginx)

AGENT_CID=$(docker create --link $WEB_CID:insideweb \
    --link $MAILER_CID:insidemailer \
    dockerinaction/ch2_agent)

以上针对 web, agent 容器的命令只是创建容器,还没有运行,因此 docker ps 默认不会列出 web, agent 这两个容器,要想查看所有状态的容器,使用 docker ps -a

容器的所有状态为: running, paused, restarting, exited 等。各状态相互转化如下:

Docker 容器状态转换图

容器创建后,再开启:

docker start $AGENT_CID
docker start $WEB_ID

但运行以上的命令会出错:

Error response from daemon: Cannot start container
03e65e3c6ee34e714665a8dc4e33fb19257d11402b151380ed4c0a5e38779d0a: Cannot
link to a non running container: /clever_wright AS /modest_hopper/
insideweb
FATA[0000] Error: failed to start one or more containers

这是因为 agent 容器依赖于 web 容器,故要先启动 web 容器,如下:

docker start $WEB_ID
docker start $AGENT_CID

创建环境无关的系统

安装软件和维护的工作量主要在于对计算环境的定制。这种定制工作有:

  • 全局依赖(如主机上的文件系统位置)
  • 硬编码的部署架构(如在代码或配置中检测变量值)
  • 数据的存储位置(如数据保存在一个特定的机器上)

Docker 可以利用以下 3 个特性来帮助实现环境无关的系统,从而减少维护量:

  • 只读文件系统
  • 环境变量注入
  • Volume

本次实现的案例是使用 Docker 运行多个 WordPress 博客。每个博客共享 WordPress 程序,只是博客内容不同。

只读文件系统

使用 --read-only 选项开启一个只读的 WordPress 容器:

$ docker run -d --name wp --read-only wordpress:4

--read-only 确保该容器的内容不可修改。

执行后,再使用 docker inspect --format "" wp 来查看容器是否已经开启了,输出 true 和 false。

这里会输出 false,用 docker logs wp 查看日志:

error: missing required WORDPRESS_DB_PASSWORD environment variable
  Did you forget to -e WORDPRESS_DB_PASSWORD=... ?

  (Also of interest might be WORDPRESS_DB_USER and WORDPRESS_DB_NAME.)

可见,WordPress 依赖 MySQL。

使用 docker 运行一个 Mysql 容器:

$ docker run -d --name wpdb \
    -e MYSQL_ROOT_PASSWORD=ch2demo \
    mysql:5

上面命令中的 -e 选项向容器注入了一个环境变量值,以便容器使用。

现在再开启一个新的 WordPress 容器,并与 MySQL 数据库连接起来:

$ docker run -d --name wp2 \
    --link wpdb:mysql \
    -p 80 --read-only \
    wordpress:4

再查看该容器是否已正常运行:

$ docker inspect --format "" wp2

发现还是没有启动,用 docker logs wp2 再次检查,可看到类似以下的日志:

Fatal Error Unable to create lock file: Bad file descriptor (9)

可以看到因为 WordPress 容器是只读的,从而无法生成一个 lock 文件,而导致该容器启动失败。

因此,需要通过挂载 Volume 使该只读容器中的某些目录可写:

# start the container with specific volumes for read only exceptions
$ docker run -d --name wp3 --link wpdb:mysql -p 80 \
    -v /run/lock/apach2/ \
    -v /run/apache2/ \
    --read-only wordpress:4

上面 -v /datadir 选项使得主机上的某个临时目录挂载到容器中的 /datadir 目录。

至此,一个可用于开启 WordPress 及监控程序的脚本如下:

SQL_CID=$(docker create -e MYSQL_ROOT_PASSWORD=ch2demo mysql:5)

docker start $SQL_CID

MAILER_CID=$(docker create dockerinaction/ch2_mailer)
docker start $MAILER_CID

WP_CID=$(docker create --link $SQL_CID:mysql -p 80\
    -v /run/lock/apache2/ -v /run/apache2/ \
    --read-only wordpress:4)

docker start $WP_CID

AGENT_CID=$(docker create --link $WP_CID:insideweb \
    --link $MAILER_CID:insidemailer \
    dockerinaction/ch2_agent)

docker start $AGENT_CID

环境变量注入

很多程序可根据环境变量进行配置。而 Docker 也会利用环境变量来共享主机名、容器等信息,同时还有可向容器注入环境变量的机制。

env 命令可列出当前会话上下文里的所有环境变量值。向容器注入环境变量并显示:

$ docker run --env MY_ENVIRONMENT_VAR="this is a test" \
    busybox:latest env

上面的 --env-e 选项可用来向容器注入环境变量值,如果映像里已经设置了该变量,那么本次设置会覆盖原来的设置值。

WordPress 用到下面这些环境变量:

  • WORDPRESS_DB_HOST
  • WORDPRESS_DB_USER
  • WORDPRESS_DB_PASSWORD
  • WORDPRESS_DB_NAME
  • WORDPRESS_AUTH_KEY
  • WORDPRESS_SECURE_AUTH_KEY
  • WORDPRESS_LOGGED_IN_KEY
  • WORDPRESS_NONCE_KEY
  • WORDPRESS_AUTH_SALT
  • WORDPRESS_SECURE_AUTH_SALT
  • WORDPRESS_LOGGED_IN_SALT
  • WORDPRESS_NONCE_SALT

创建 WordPress 容器时这样注入环境变量:

$ docker create 
    --env WORDPRESS_DB_HOST=<my_database_hostname> 、
    --env WORDPRESS_DB_USER=site_admin \
    --env WORDPRESS_DB_PASSWORD=MeowMix42 \
    wordpress:4

要能开启多个 WordPress 容器,还需要为每个容器指定使用的数据库名:

docker create --link wpdb:mysql \
    -e WORDPRESS_DB_NAME=client_a_wp wordpress:4

docker create --link wpdb:mysql \
    -e WORDPRESS_DB_NAME=client_b_wp wordpress:4

至此,可以更新启动脚本了:

# 先启动 mysql 和 mailer 容器:
DB_CLD=$(docker run -d -e MYSQL_ROOT_PASSWORD=ch2demo mysql:5)
MAILER_CID=$(docker run -d dockerinaction/ch2_mailer)

# 假设 $CLIENT_ID 变量会传入脚本
if [ ! -n "$CLIENT_ID" ]; then
    echo "Client ID not set"
    exit 1
fi

WP_CID=$(docker create \
    --link $DB_CID:mysql \
    --name wp_$CLIENT_ID \
    -p 80 \
    -v /run/lock/apach2/ -v /run/apache2/ \
    -e WORDPRESS_DB_NAME=$CLIENT_ID \
    --read-only wordpress:4)

docker start $WP_CID

AGENT_CID=$(docker create \
    --name agent_$CLIENT_ID \
    --link $WP_CID:insideweb \
    --link $MAILER_CID:insidemailer \
    dockerinaction/ch2_agent)

docker start $AGENT_CID

创建可持续运行的容器

Docker 的选项可用于监测并自动重启容器。

自动重启容器

在创建容器时,可用 --restart 选项指定以下的重启策略:

  • 不重启(默认)
  • 当检测到某种条件后才重启
  • 不管什么情况问题重启

重启的等待时间采用 exponential backoff strategy。

采用这种方式重启会有空白时间,期间容器没有启动。

使用监管程序来保持容器运行

监管进程,或者 init 进程,可用来加载和维护其它进程状态。在 Linux 上,PID #1 是一个 init 进程,它用于开启所有其它系统进程,并且当出现异常时重启这些系统进程。

在容器中也可以采用类似的模式,主要可用的监管程序有 init, systemd, runit, upstart 和 supervisord 等。

Tutum 公司有一个 Docker 映像,包含 LAMP 和 supervisord,通过以下方式运行容器后可确保该容器一直运行:

$ docker run -d -p 80:80 --name lamp-test tutum/lamp

可以用 docker exec lamp-test ps 来查看容器中当前运行的进程,可以看到运行有 supervisord, mysqld_safe 和 apache2 等进程。

PID TTY          TIME CMD
    1 ?        00:00:00 supervisord
  439 ?        00:00:00 mysqld_safe
  440 ?        00:00:00 apache2
  827 ?        00:00:00 ps

现可以测试 supervisord 的监控重启功能,先 kill 掉 apache2 进程:

$ docker exec lamp-test kill 440 # 440 是 apache2 的 PID

当 apache2 结束后,supervisord 会记录日志,并重启该进程,可用 docker logs lamp-test 查看:

2016-10-10 01:23:39,784 INFO exited: apache2 (exit status 0; expected)
2016-10-10 01:23:40,787 INFO spawned: 'apache2' with pid 841
2016-10-10 01:23:41,821 INFO success: apache2 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

使用 entrypoint 脚本

相对于使用 init 或监管程序,另一种方法是使用启动脚本(通常的脚本名为 entrypoint.sh) 来至少检测容器能正常开启的一些先决条件。这个启动脚本有时也会用作容器的默认开启程序。

例如,之前开启的 WordPress 容器中就有一个启动脚本,它会在开启 WordPress 进程前先验证和配置相关的环境变量。可以通过覆盖默认命令为 cat 来查看该启动脚本:

$ docker run --entrypoint="cat" wordpress:4 /entrypoint.sh

以上命令覆盖设置了默认命令为 cat, 同时最后的 /entrypoint.sh 为该默认命令的参数。

启动脚本 + Docker 的重启策略是确保容器持续运行的重要方式。

清理

所有的容器都会使用硬盘空间来存储日志、容器元数据和写入容器内的文件。所有容器也会消耗全局命名空间的资源(如容器名,主机端口绑定等)。因此,不再使用的容器应该要删除。

状态为 exited 的容器可以用 docker rm container_name 来删除,而其它状态(如 running, paused, restarting) 的容器必须用 docker stop container_name 先关闭后才能删除,或者用 docker rm -f container_name 来强制删除。

docker stop 会向容器发送 SIG_HUG 信号,从而容器有时间来进行一些清理工作,而强制删除会向容器发送 SIG_KILL 信号,从而直接退出。 docker kill 命令也可用于向容器发送 SIG_KILL 信号 。

docker run 命令加 --rm 选项时,该容器在运行退出后,即状态为 existed 时,会自动删除,如:

$ docker run --rm --name auto-exit-test busybox:latest echo Hello World

下面的命令能删除所有的容器(没有退出的会强制删除):

$ docker rm -vf $(docker ps -a -q)
  • -v 选项表示一并删除容器的 Volumes
  • -q 选项表示只列出容器的数字 ID

参考文献:


上一篇     下一篇