在 Docker 容器中运行应用程序
案例说明 运行 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 create或docker 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 run和docker 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 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 参考文献: 《Docker in Action》by Jeff Nickoloff: Running software in containers http://www.atjiang.com/running-software-in-docker-containers/