使用 Docker Compose 構建一鍵啓動的運行環境
如今,不管你是否喜歡,不管你是否承認,微服務架構模式的流行就擺在那裏。作爲架構師的你,如果再將系統設計成個大單體結構,那麼即便不懂技術的領導,都會給你送上幾次白眼。好吧,妥協了!開拆!“沒喫過豬肉,還沒見過豬跑嗎!”。拆不出 40-50 個服務,我就不信還拆不出 4-5 個服務 ^_^。
終於拆出了幾個服務,但又犯難了:以前單體程序,搭建一個運行環境十分 easy,程序往一個主機上一扔,配置配置,啓動就 ok 了;但自從拆成服務後,開發人員的調試環境、集成環境、測試環境等搭建就變得異常困難。
有人會說,現在都雲原生了?你不知道雲原生操作系統 k8s[1] 的存在麼?讓運維幫你在 k8s 上整環境啊。一般小廠,運維人員不多且很忙,開發人員只能 “自力更生,豐衣足食”。開發人員自己整 k8s?別扯了!沒看到這兩年 k8s 變得越來越複雜了嗎!如果有一年不緊跟 k8s 的演進,新版本中的概念你就可能很陌生,不知源自何方。一般開發人員根本搞不定 (如果你想搞定,可以看看我的 k8s 實戰課程 [2] 哦,包教包會 ^_^)。
那怎麼辦呢?角落裏曾經的沒落雲原生貴族 docker 發話了:要不讓我兄弟試試!
1. docker compose
docker[3] 雖然成了 “過氣網紅”,但 docker 依然是容器界的主流。至少對於非 docker 界的開發人員來說,一提到容器,大家首先想到的還是 docker。
docker 公司的產品推出不少,開發人員對多數都不買賬也是現實,但我們也不能一棒子打死,畢竟 docker 是可用的,還有一個可用的,那就是 docker 的兄弟:docker compose[4]。
Compose 是一個用於定義和運行多容器 Docker 應用程序的工具。使用 Compose,我們可以使用一個 YAML 文件 [5] 來配置應用程序的所有服務組件。然後,只需一條命令,我們就可以創建並啓動配置中的所有服務。
這不正是我們想要的工具麼! Compose 與 k8s 很像,都算是容器編排工具,最大的不同:Compose 更適合在單節點上的調試或集成環境中(雖然也支持跨主機,基於被淘汰的 docker swarm)。Compose 可以大幅提升開發人員以及測試人員搭建應用運行環境的效率。
2. 選版本
使用 docker compose 搭建運行環境,我們僅需一個 yml 文件。但 docker compose 工具也經歷了多年演化,這個文件的語法規範也有多個版本,截至目前,docker compose 的配置文件的語法版本就有 2、2.x 和 3.x 三種。並且不同規範版本支持的 docker 引擎版本還不同,這個對應關係如下圖。圖來自 docker compose 文件規範頁面 [6]:
選版本是最鬧心的。選哪個呢?設定兩個條件:
-
docker 引擎版本怎麼也得是 17.xx
-
規範版本怎麼也得是 3.x 吧
這樣一來,版本 3.2 是最低要求的了。我們就選 3.2:
// docker-compose.yml
version: "3.2"
3. 選網絡
docker compose 默認會爲 docker-compose.yml 中的各個 service 創建一個 bridge 網絡,所有 service 在這個網絡裏可以相互訪問。以下面 docker-compose.yml 爲例:
// demo1/docker-compose.yml
version: "3.2"
services:
srv1:
image: nginx:latest
container_name: srv1
srv2:
image: nginx:latest
container_name: srv2
啓動這個 yml 中的服務:
# docker-compose -f docker-compose.yml up -d
Creating network "demo1_default" with the default driver
... ...
docker compose 會爲這組容器創建一個名爲 demo1_default 的橋接網絡:
# docker network ls
NETWORK ID NAME DRIVER SCOPE
f9a6ac1af020 bridge bridge local
7099c68b39ec demo1_default bridge local
... ...
關於 demo1_default 網絡的細節,可以通過 docker network inspect 7099c68b39ec 獲得。
對於這樣的網絡中的服務,我們在外部是無法訪問的。如果要訪問其中服務,我們需要對其中的服務做端口映射,比如如果我們要將 srv1 暴露到外部,我們可以將 srv1 監聽的服務端口 80 映射到主機上的某個端口,這裏用 8080,修改後的 docker-compose.yml 如下:
version: "3.2"
services:
srv1:
image: nginx:latest
container_name: srv1
ports:
- "8080:80"
srv2:
image: nginx:latest
container_name: srv2
這樣啓動該組容器後,我們通過 curl localhost:8080 就可以訪問到容器中的 srv1 服務。不過這種情況下,服務間的相互發現比較麻煩,要麼藉助於外部的發現服務,要麼通過容器間的 link 來做。
開發人員大多隻有一個環境,不同服務的服務端口亦不相同,讓容器使用 host 網絡要比單獨創建一個 bridge 網絡來的更加方便。通過 network_mode 我們可以指定服務使用 host 網絡,就像下面這樣:
version: "3.2"
services:
srv1:
image: bigwhite/srv1:1.0.0
container_name: srv1
network_mode: "host"
在 host 網絡下,容器監聽的端口就是主機上的端口,各個服務間通過端口區別各個服務實例 (前提是端口各不相同),ip 使用 localhost 即可。
使用 host 網絡還有一個好處,那就是我們在該環境之外的主機上訪問環境中的服務也十分方便,比如查看 prometheus 的面板等。
4. 依賴的中間件先啓動,預置配置次之
如今的微服務架構系統,除了自身實現的服務外,外圍還有大量其依賴的中間件,比如:redis、kafka(mq)、nacos/etcd(服務發現與註冊)、prometheus(時序度量數據服務)、mysql(關係型數據庫)、jaeger server(trace 服務器)、elastic(日誌中心)、pyroscope-server(持續 profiling 服務) 等。
這些中間件若沒有啓動成功,我們自己的服務多半啓動都要失敗,因此我們要保證這些中間件服務都啓動成功後,再來啓動我們自己的服務。
如何做呢?compose 規範中有一個迷惑人的 “depends_on”[7],比如下面配置文件中 srv1 依賴 redis 和 nacos 兩個 service:
version: "3.2"
services:
srv1:
image: bigwhite/srv1:1.0.0
container_name: srv1
network_mode: "host"
depends_on:
- "redis"
- "nacos"
environment:
- NACOS_SERVICE_ADDR=127.0.0.1:8848
- REDIS_SERVICE_ADDR=127.0.0.1:6379
restart: on-failure
不深入瞭解,很多人會認爲 depends_on 可以保證先啓動依賴項 redis 和 nacos,並等依賴項 ready 後再啓動我們自己的服務 srv1。但實際上,depends_on 僅能保證先啓動依賴項,後啓動我們的服務。但它不會探測依賴項 redis 或 nacos 是否 ready,也不會等依賴項 ready 後,才啓動我們的服務。於是你會看到 srv1 啓動後依舊出現各種的報錯,包括無法與 redis、nacos 建立連接等。
要想真正實現依賴項 ready 後才啓動我們自己的服務,我們需要藉助外部工具了,docker compose 文檔對此有說明 [8]。其中一個方法是使用 wait-for-it 腳本 [9]。
我們可以改變一下自由服務的容器鏡像,將其 entrypoint 從執行服務的可執行文件變爲執行一個 start.sh 的腳本:
// Dockerfile
... ...
ENTRYPOINT ["/bin/bash", "./start.sh"]
這樣我們就可以在 start.sh 腳本中 “定製” 我們的啓動邏輯了。下面是一個 start.sh 腳本的示例:
#! /bin/sh
./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo "nacos is up" && \
./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo "redis is up" && \
exec ./srv1
我們看到,在 start.sh 腳本中,我們使用 wait_for_it.sh 腳本 [10] 等待 nacos 和 redis 啓動,如果在限定時間內等待失敗,根據 restart 策略,我們的服務還會被 docker compose 重新拉起,直到 nacos 與 redis 都 ready,我們的服務纔會真正開始執行啓動過程。
在 exec ./srv1 之前,很多時候我們還需要進行一些配置初始化操作,比如向 nacos 中寫入預置的 srv1 服務的配置文件內容以保證 srv1 啓動後能從 nacos 中讀取到自己的配置文件,下面是加了配置初始化的 start.sh:
#! /bin/sh
./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo "nacos is up" && \
./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo "redis is up" && \
curl -X POST --header 'Content-Type: application/x-www-form-urlencoded' -d dataId=srv1.yml --data-urlencode content@./conf/srv1.yml "http://127.0.0.1:8848/nacos/v1/cs/configs?group=MY_GROUP" && \
exec ./srv1
我們通過 curl 將打入鏡像的./conf/srv1.yml 配置寫入已經啓動了的 nacos 中供後續 srv1 啓動時讀取。
5. 全家桶,一應俱全
就像前面提到的,如今的系統對外部的中間件 “依存度” 很高,好在主流中間件都提供了基於 docker 啓動的官方支持。這樣我們的開發環境也可以是一個一應俱全的“全家桶”。不過要有一個很容易滿足的前提:你的機器配置足夠高,才能把這些中間件全部運行起來。
有了這些全家桶,我們無論是診斷問題 (看 log、看 trace、看度量數據),還是作性能優化(看持續 profiling 的數據),都方便的不要不要的。
6. 結合 Makefile,簡化命令行輸入
docker-compose 這個工具有一個 “嚴重缺陷”,那就是名字太長 ^_^。這導致我們每次操作都要敲入很多命令字符,當你使用的 compose 配置文件名字不爲 docker-compose.yml 時,更是如此,我們還需要通過 - f 選項指定配置文件路徑。
爲了簡化命令行輸入,減少鍵盤敲擊次數,我們可以將複雜的 docker-compose 命令與 Makefile 相結合,通過定製命令行命令並將其賦予簡單的 make target 名字來實現這一簡化目標,比如:
// Makefile
pull:
docker-compose -f my-docker-compose.yml pull
pull-my-system:
docker-compose -f my-docker-compose.yml pull srv1 srv2 srv3
up: pull-my-system
docker-compose -f my-docker-compose.yml up
upd: pull-my-system
docker-compose -f my-docker-compose.yml up -d
up2log: pull-my-system
docker-compose -f my-docker-compose.yml up > up.log 2>&1
down:
docker-compose -f my-docker-compose.yml down
ps:
docker-compose -f my-docker-compose.yml ps -a
log:
docker-compose -f my-docker-compose.yml logs -f
# usage example: make upsrv service=srv1
service=
upsrv:
docker-compose -f my-docker-compose.yml up -d ${service}
config:
docker-compose -f my-docker-compose.yml config
另外服務依賴的中間件一般都時啓動與運行開銷較大的系統,每次和我們的服務一起啓停十分浪費時間,我們可以將這些依賴與我們的服務分別放在不同的 compose 配置文件中管理,這樣我們每次重啓自己的服務時,沒有必要重新啓動這些依賴,這樣可以節省大量 “等待” 時間。
7. .env 文件
有些時候,我們需要在 compose 的配置文件中放置一些 “變量”,我們通常使用環境變量來實現“變量” 的功能,比如:我們將 srv1 的鏡像版本改爲一個環境變量:
version: "3.2"
services:
srv1:
image: bigwhite/srv1:${SRV1_VER}
container_name: srv1
network_mode: "host"
... ...
docker compose 支持通過同路徑下的. env 文件的方式 docker-compose.yml 中環境變量的值,比如:
// .env
SRV1_VER=dev
這樣 docker compose 在啓動 srv1 時會將. env 中 SRV1_VER 的值讀取出來並替換掉 compose 配置文件中的相應環境變量。通過這種方式,我們可以靈活的修改我們使用的鏡像版本。
8. 優點與不足
使用 docker compose 工具,我們可以輕鬆擁有並快速啓動一個 all-in-one 的運行環境,大幅度加速了部署、調試與測試的效率,在特定的工程環節,它可以給予開發與測試人員很大幫助。
不過這樣的運行環境也有一些不足,比如:
-
對部署的機器 / 虛擬機配置要求較高;
-
這樣的運行環境有侷限,用在功能測試、持續集成、驗收測試的場景下可以,但不能用來執行壓測或者說即便壓測也只是摸底,數據不算數的,因爲所有服務放在一起,相互干擾;
-
服務或中間件多了以後,完全啓動一次也要耐心等待一段時間。
Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily
我的聯繫方式:
-
微博:https://weibo.com/bigwhite20xx
-
微信公衆號:iamtonybai
-
博客:tonybai.com
-
github: https://github.com/bigwhite
-
“Gopher 部落” 知識星球:https://public.zsxq.com/groups/51284458844544
參考資料
[1]
雲原生操作系統 k8s: https://kubernetes.io
[2]
我的 k8s 實戰課程: https://coding.imooc.com/class/284.html
[3]
docker: https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building
[4]
docker compose: https://docs.docker.com/compose/
[5]
YAML 文件: https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/
[6]
docker compose 文件規範頁面: https://docs.docker.com/compose/compose-file/
[7]
迷惑人的 “depends_on”: https://docs.docker.com/compose/compose-file/compose-file-v3/#depends_on
[8]
docker compose 文檔對此有說明: https://docs.docker.com/compose/startup-order/
[9]
wait-for-it 腳本: https://github.com/vishnubob/wait-for-it
[10]
wait_for_it.sh 腳本: https://github.com/vishnubob/wait-for-it/blob/master/wait-for-it.sh
[11]
鏈接地址: https://m.do.co/c/bff6eed92687
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KVaAQW3LbWeY_ElpRhwDIg