在 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
運行後,會輸出一行字符串,該字符串為該容器的唯一標識符,類似 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
在 Docker 容器中運行應用程序