現在是時候開始思考如何在生產環境中實際執行Docker了。在撰寫本文時,雖然很多人都在談論Docker,許多人也在試驗Docker,但相對較少的人在生產環境中執行Docker。雖然批評者有時將此視為Docker的失敗,但他們似乎忽略了幾個關鍵點。考慮到Docker相對年輕,有這麼多人在生產環境中使用它(包括Spotify、Yelp和百度)是非常鼓舞人心的,而只在開發和測試中使用它的人仍然獲得了許多優勢。

話雖如此,今天在生產環境中使用容器是完全可行與合理的。較大的專案和組織可能希望從小規模開始逐步擴大,但對於大多數專案來說,這已經是一個可行與直接的解決方案。

目前,佈署容器最常見的方式是首先設定虛擬機器,然後在虛擬機器上啟動容器。這不是一個理想的解決方案—它產生大量開銷,減慢擴充套件速度,並迫使用者以多容器粒度進行設定。在虛擬機器內執行容器的主要原因只是安全性。客戶無法存取其他客戶的資料或網路流量是至關重要的,而容器本身目前只提供弱隔離保證。此外,如果一個容器壟斷了核心資源或導致當機,它將影響在同一主機上執行的所有容器。即使大多數專業解決方案—Google Container Engine(GKE)和Amazon EC2 Container Service(ECS)—內部仍然使用虛擬機器。目前有兩個例外:Giant Swarm和Joyent的Triton,我們稍後會討論這兩者。

在本章中,玄貓將展示如何在各種雲端環境和專業Docker託管服務上佈署我們的簡單網頁應用程式。我們還將探討在生產環境中執行容器的一些問題和技術,包括雲端和使用本地資源的情況。

本章的程式碼可在本章的GitHub上取得。我們不會再根據之前的Python程式碼進行開發,但會繼續使用我們已建立的映像。你可以選擇使用自己版本的identidock映像,或直接使用amouat/identidock儲存函式庫 你可以使用v0標籤簽出章節開始的程式碼:

$ git clone -b v0 \
https://github.com/using-docker/deploying-containers/
...

後續標籤代表整章程式碼的進展。

或者,你可以從GitHub專案的Releases頁面下載任何標籤的程式碼。

使用Docker Machine設定資源

設定新資源並在其上執行容器的最快、最簡單的方法是透過Docker Machine。Machine可以建立伺服器、在其上安裝Docker,並設定本地Docker客戶端存取它們。Machine附帶了大多數主要雲端提供商(包括AWS、Google Compute Engine、Microsoft Azure和Digital Ocean)以及VMWare和VirtualBox的驅動程式。

Beta軟體警告!

在撰寫本文時,Docker Machine處於beta階段(玄貓測試的是Docker Machine 0.4.1版本)。這意味著你可能會遇到錯誤和缺失功能,但它仍應可用與相當穩定。不幸的是,這也意味著命令和語法可能與你在這裡看到的略有不同。因此,我不建議在生產環境中使用Machine,儘管它對測試和實驗非常有用。

(是的,這個警告對本章中的幾乎所有內容都適用。只是覺得是時候再次指出這一點了….)

讓我們看如何使用Machine在雲端上啟動和執行identidock。首先,你需要在本地電腦上安裝Machine。如果你透過Docker Toolbox安裝了Docker,它應該已經可用。如果沒有,你可以從GitHub下載二進位檔案,然後將其放在你的路徑中(例如/usr/bin/)。

Docker Machine 應用:從本地到雲端的容器佈署

初探 Docker Machine 的功能與優勢

Docker Machine 是一個強大的工具,它讓我們能夠在各種環境中輕鬆建立和管理 Docker 主機。無論是本地虛擬機器、私有雲還是公有雲平台,Docker Machine 都能提供一致的操作體驗。在玄貓多年的容器實踐中發現,這種一致性大幅降低了開發人員在不同環境間切換的認知負擔。

首先,確認 Docker Machine 已正確安裝後,我們可以檢視現有的主機:

$ docker-machine ls
NAME     ACTIVE   DRIVER       STATE     URL                         SWARM
default  -        virtualbox   Running   tcp://192.168.99.100:2376   

這個指令會列出所有 Docker Machine 管理的主機。如果你剛開始使用,可能不會看到任何輸出,或者只會看到如上所示的本地 boot2docker 虛擬機器。

在雲端建立 Docker 主機

接下來,我們將在雲端平台上建立一個 Docker 主機。以下以 Digital Ocean 為例,但相似的步驟也適用於 AWS 和其他雲端服務商。

在開始之前,你需要:

  1. 在 Digital Ocean 註冊帳號
  2. 生成個人存取權杖(在「Applications & API」頁面)

準備就緒後,執行以下命令:

$ docker-machine create --driver digitalocean \
  --digitalocean-access-token 4820... \
  identihost-do
Creating SSH key...
Creating Digital Ocean droplet...
To see how to connect Docker to this machine, run: docker-machine env identi...

這個命令會在 Digital Ocean 上建立一個新的虛擬機器,並安裝 Docker 引擎。請注意,使用雲端資源會產生費用,完成後記得移除機器。

連線到雲端 Docker 主機

建立雲端主機後,我們需要將本地 Docker 客戶端指向它:

$ docker-machine env identihost-do
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://104.236.32.178:2376"
export DOCKER_CERT_PATH="/Users/amouat/.docker/machine/machines/identihost-do"
export DOCKER_MACHINE_NAME="identihost-do"
# Run this command to configure your shell:
# eval "$(docker-machine env identihost-do)"
$ eval "$(docker-machine env identihost-do)"

執行最後一行命令後,所有 Docker 指令都將在雲端主機上執行,而非本地環境。我們可以透過 docker info 來確認連線情況:

$ docker info
Containers: 0
Images: 0
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 0
Dirperm1 Supported: false
Execution Driver: native-0.2
Logging Driver: json-file
Kernel Version: 3.13.0-57-generic
Operating System: Ubuntu 14.04.3 LTS
CPUs: 1
Total Memory: 490 MiB
Name: identihost-do
ID: PLDY:REFM:PU5B:PRJK:L4QD:TRKG:RWL6:5T6W:AVA3:2FXF:ESRC:6DCT
Username: amouat
Registry: https://index.docker.io/v1/
WARNING: No swap limit support
Labels:
  provider=digitalocean

這表明我們已成功連線到執行在 Digital Ocean 上的 Ubuntu 主機。現在,如果執行 docker run hello-world,容器將在雲端伺服器上執行,而非本地機器。

在雲端佈署應用程式

現在我們可以在雲端主機上佈署 identidock 應用。以下是一個簡單的 docker-compose.yml 檔案,使用 Docker Hub 上的映像:

identidock:
  image: amouat/identidock:1.0
  ports:
    - "5000:5000"
    - "9000:9000"
  environment:
    ENV: DEV
  links:
    - dnmonster
    - redis
dnmonster:
  image: amouat/dnmonster:1.0
redis:
  image: redis:3

執行 Compose 命令佈署服務:

$ docker-compose up -d
...
Creating identidock_identidock_1...
$ curl $(docker-machine ip identihost-do):5000
<html><head><title>Hello...

我們可以使用 docker-machine ip identihost-do 命令取得主機的 IP 地址,然後透過瀏覽器存取應用。

使用反向代理改進佈署

雖然我們已經成功佈署了應用,但它仍有一些不足:

  1. 應用執行在開發版 Python web 伺服器上
  2. 直接暴露了應用連線埠,沒有任何防護層

為了改進佈署,我們可以使用 Nginx 作為反向代理。這樣做有幾個好處:

  • 提供更好的安全性
  • 允許在不變更外部 IP 的情況下修改後端架構
  • 可以實作負載平衡

建立反向代理

首先,建立一個名為 identiproxy 的新資料夾,並在其中建立以下 Dockerfile:

FROM nginx:1.7
COPY default.conf /etc/nginx/conf.d/default.conf

接著建立 default.conf 檔案:

server {
  listen 80;
  server_name 45.55.251.164;
  
  location / {
    proxy_pass http://identidock:9090;
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_redirect off;
    proxy_buffering off;
    proxy_set_header Host 45.55.251.164;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

請將 server_name 和 Host 替換為你的 Docker 主機 IP 或網域名稱。

在雲端伺服器上構建映像:

$ docker build --no-cache -t identiproxy:0.1 .

更新佈署設定

回傳 identidock 資料夾,建立一個名為 prod.yml 的新 Compose 設定檔案:

proxy:
  image: identiproxy:0.1
  links:
    - identidock
  ports:
    - "80:80"
identidock:
  image: amouat/identidock:1.0
  links:
    - dnmonster
    - redis
  environment:
    ENV: PROD
dnmonster:
  image: amouat/dnmonster:1.0
redis:
  image: redis:3

注意以下變更:

  1. 不再直接暴露 identidock 容器的連線埠
  2. 環境變數設為 PROD,啟動生產版 web 伺服器
  3. 所有映像都使用了特定標籤,避免使用 latest

停止舊版本並啟動新版本:

$ docker-compose stop
$ docker-compose -f prod.yml up -d

測試新佈署:

$ curl $(docker-machine ip identihost-do)
<html><head><title>Hello...

現在,我們的應用已經透過代理在標準 HTTP 連線埠 (80) 上執行,而不是直接暴露應用連線埠。

改進代理設定:使用環境變數

目前的代理設定中,主機 IP 和容器名稱是硬編碼的,這不夠靈活。我們可以使用環境變數來改進:

  1. 更新 default.conf,使用佔位符替代硬編碼值:
server {
  listen 80;
  server_name {{NGINX_HOST}};
  
  location / {
    proxy_pass {{NGINX_PROXY}};
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_redirect off;
    proxy_buffering off;
    proxy_set_header Host {{NGINX_HOST}};
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
  1. 建立 entrypoint.sh 指令碼處理佔位符替換:
#!/bin/bash
set -e
sed -i "s|{{NGINX_HOST}}|$NGINX_HOST|;s|{{NGINX_PROXY}}|$NGINX_PROXY|" \
  /etc/nginx/conf.d/default.conf
cat /etc/nginx/conf.d/default.conf
exec "$@"
  1. 更新 Dockerfile:
FROM nginx:1.7
COPY default.conf /etc/nginx/conf.d/default.conf
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
  1. 設定執行許可權並構建新映像:
$ chmod +x entrypoint.sh
$ docker build -t proxy:1.0 .
  1. 更新 prod.yml 使用新映像:
proxy:
  image: proxy:1.0
  links:
    - identidock
  ports:
    - "80:80"
  environment:
    - NGINX_HOST=45.55.251.164
    - NGINX_PROXY=http://identidock:9090
identidock:
  # 保持不變

這種方法使代理設定更加靈活,我們可以透過環境變數控制代理行為,而不必重建映像。

深入測試應用

在容器佈署中,確保有效的測試是至關重要的。雖然通常我們用 curl 檢查前端頁面來確認服務運作,但這只能證明 identidock 容器正在執行。更全面的測試應該包含驗證容器間的通訊。

例如,可以測試 identicon 影像的生成:

$ curl localhost:5000/monster/gordon | head -c 4
PNG

這個測試不僅確認 identidock 容器執行,還驗證了 identidock 和 dnmonster 容器之間的通訊。

使用 Compose 的 extends 功能

對於更複雜的佈署,可以使用 Compose 的 extends 功能來分享設定。建立 common.yml:

identidock:
  image: amouat/identidock:1.0
  environment:
    ENV: DEV
dnmonster:
  image: amouat/dnmonster:1.0
redis:
  image: redis:3

然後修改 prod.yml:

proxy:
  image: proxy:1.0
  links:
    - identidock
  ports:
    - "80:80"
  environment:
    - NGINX_HOST=45.55.251.164
    - NGINX_PROXY=http://identidock:9090
identidock:
  extends:
    file: common.yml
    service: identidock
  environment:
    ENV: PROD
dnmonster:
  extends:
    file: common.yml
    service: dnmonster
redis:
  extends:
    file: common.yml
    service: redis

這種方法的好處是自動繼承基礎檔案的變更,但需注意 links 和 volumes-from 不會被繼承,以避免意外的連線問題。

在容器化應用從開發到生產的過程中,Docker Machine 提供了一種簡單與一致的方式來管理不同環境中的 Docker 主機。結合反向代理和適當的設定管理,我們能夠構建出更加健壯、安全和可維護的容器佈署。

容器佈署與自動化:從本地到雲端的實踐

在現代軟體開發流程中,將應用程式從開發環境佈署到生產環境是一個至關重要與常充滿挑戰的過程。透過Docker容器技術,我們可以大幅簡化這個流程,確保應用程式在不同環境中的一致性執行。在本文中,玄貓將分享如何利用Docker Compose、Docker Machine以及其他工具,建立一個可靠與易於管理的容器佈署架構。

生產環境設定檔案的設計與使用

在將應用移至生產環境時,我們需要對Docker Compose設定進行適當調整。以一個簡單的微服務架構為例,生產環境的prod.yml設定看起來如下:

identidock:
  image: amouat/identidock:1.0
  links:
    - dnmonster
    - redis
  environment:
    ENV: PROD
dnmonster:
  image: amouat/dnmonster:1.0
redis:
  image: redis:3

這個設定檔案定義了三個服務:主應用identidock、頭像生成服務dnmonster以及快取服務redis。與開發環境的設定不同,生產環境設定通常使用預先建立好的映像檔,而不是在佈署時建立。

值得注意的是,在這個設定中我們使用了環境變數ENV: PROD來指示應用程式執行在生產模式。這種方法讓我們可以根據環境變數調整應用行為,是容器化應用的常見實踐。

使用Docker Machine進行雲端佈署

Docker Machine是一個強大的工具,可以讓我們在各種雲平台上快速建立並管理Docker主機。以Digital Ocean為例,我們可以這樣建立一個Docker主機:

$ docker-machine create --driver digitalocean \
  --digitalocean-access-token=YOUR_ACCESS_TOKEN \
  --digitalocean-region=nyc3 \
  --digitalocean-size=1gb \
  identihost-do

建立完成後,我們可以透過以下命令將本地Docker客戶端連線到遠端主機:

$ eval $(docker-machine env identihost-do)

這個命令設定了必要的環境變數,讓後續的Docker命令都指向遠端主機。這是Docker Machine的核心功能之一,讓我們可以無縫地在本地和遠端環境間切換。

使用Compose佈署到生產環境

連線到遠端主機後,我們可以使用Docker Compose來佈署應用:

$ docker-compose -f prod.yml up -d

這個命令會根據prod.yml設定檔案啟動所有服務。-d引數讓容器在背景執行,這對於生產環境是必要的。

當然,在實際佈署前,我們需要確保應用能夠正確處理生產環境的特殊需求,如設定正確的主機IP:

proxy:
  image: amouat/proxy:1.0
  links:
    - identidock
  ports:
    - "80:80"
  environment:
    NGINX_HOST: 45.55.251.164  # 設定為你的主機IP或網域名稱
    NGINX_PROXY: http://identidock:9090

這裡我們使用了一個代理容器來處理外部請求,並將它們轉發到內部的identidock服務。

簡化設定檔案管理

如果你不想每次都指定設定檔案,可以設定COMPOSE_FILE環境變數:

$ COMPOSE_FILE=prod.yml
$ docker-compose up -d

這樣Docker Compose就會使用prod.yml而不是預設的docker-compose.yml

超級設定檔案生成

在實際應用中,我們經常需要根據環境變數動態生成設定檔案。雖然可以使用簡單的字串替換,但更複雜的場景可能需要使用專業的範本處理工具,如Jinja2或Go範本。

Jason Wilder開發的dockerizedocker-gen工具在這方面非常有用:

  • dockerize可以從範本檔案和環境變數生成設定檔案,然後呼叫正常的應用程式。
  • docker-gen更進一步,可以使用容器元資料(如IP位址)以及環境變數,甚至可以持續執行,回應Docker事件如新容器建立,自動更新設定檔案。

這些工具對於建立動態設定的系統,如自動化負載平衡,非常有價值。

佈署選項比較

雖然Docker Compose和Machine是快速佈署的好工具,但它們相對較新,可能不適合所有生產環境,特別是關鍵業務系統。因此,讓我們看其他可能的佈署選項。

1. Shell指令碼

最簡單的方法是編寫shell指令碼來執行Docker命令。以下是一個佈署identidock服務的範例指令碼:

#!/bin/bash
set -e
echo "Starting identidock system"
docker run -d --restart=always --name redis redis:3
docker run -d --restart=always --name dnmonster amouat/dnmonster:1.0
docker run -d --restart=always \
  --link dnmonster:dnmonster \
  --link redis:redis \
  -e ENV=PROD \
  --name identidock amouat/identidock:1.0
docker run -d --restart=always \
  --name proxy \
  --link identidock:identidock \
  -p 80:80 \
  -e NGINX_HOST=45.55.251.164 \
  -e NGINX_PROXY=http://identidock:9090 \
  amouat/proxy:1.0
echo "Started"

這個指令碼基本上是將Docker Compose設定轉換為等效的shell命令。我們使用了--restart=always引數確保容器在結束時自動重啟。

2. 使用系統管理工具(systemd)

另一種方法是使用系統管理工具如systemd或upstart來管理容器生命週期。這在你有依賴於容器的主機服務時特別有用。

以下是使用systemd管理Redis容器的服務檔案範例:

[Unit]
Description=Redis Container for Identidock
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop redis
ExecStartPre=-/usr/bin/docker rm redis
ExecStartPre=/usr/bin/docker pull redis
ExecStart=/usr/bin/docker run --rm --name redis redis

[Install]
WantedBy=multi-user.target

對於依賴其他服務的identidock容器,設定會更複雜一些:

[Unit]
Description=identidock Container for Identidock
After=docker.service
Requires=docker.service
After=identidock.redis.service
Requires=identidock.redis.service
After=identidock.dnmonster.service
Requires=identidock.dnmonster.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop identidock
ExecStartPre=-/usr/bin/docker rm identidock
ExecStartPre=/usr/bin/docker pull amouat/identidock
ExecStart=/usr/bin/docker run --name identidock \
  --link dnmonster:dnmonster \
  --link redis:redis \
  -e ENV=PROD \
  amouat/identidock

使用systemd管理容器有幾個需要注意的地方:

  1. 不要在docker run命令中使用--restart=always,因為這會與systemd的重啟機制衝突。
  2. systemd通常監控docker客戶端程式而非容器內的程式,這在某些情況下可能導致問題。
  3. 使用systemd-docker專案可以解決這些問題,它透過控制容器的cgroup來更好地管理容器。

零停機更新策略

在生產環境中,我們常需要在不中斷服務的情況下更新容器。典型的零停機更新流程如下:

  1. 使用更新的映像檔啟動新容器
  2. 將負載平衡器指向新容器(部分或全部流量)
  3. 測試新容器是否正常工作
  4. 關閉舊容器

這種藍綠佈署策略可以大幅降低佈署風險,是容器化應用的一大優勢。

生產環境佈署的注意事項

在將容器佈署到生產環境時,有幾個重要考量:

  1. 安全性:在對外開放應用之前,必須考慮如何保護應用程式的安全。
  2. 監控與日誌:建立適當的監控和日誌收集機制,以便及時發現和解決問題。
  3. 資料持久化:確保重要資料儲存在持久化卷中,而不是容器內部。
  4. 網路設定:在多主機佈署中,需要考慮更高階的網路和服務發現功能。

容器連結的注意事項

在較舊版本的Docker中,容器重啟時連結可能會斷開。如果遇到類別似問題,確保使用最新版本的Docker。在Docker 1.8及以後的版本中,容器IP位址的變更會自動傳播到連結的容器。

需要注意的是,只有/etc/hosts會被更新,環境變數不會在連結容器變更時更新。這是一個重要的設計考量,在構建依賴於連結的應用時需要牢記。

在實際生產環境中,我們可能需要超越Docker Compose和基本連結機制,使用更高階的網路和服務發現解決方案,如Docker Swarm、Kubernetes或其他容器協調平台。這些工具提供了更強大的功能來管理分散式應用。

透過這些佈署策略和工具,我們可以建立一個堅固、可擴充套件與易於管理的容器化應用架構,從而充分發揮Docker的優勢。無論是簡單的單主機佈署還是複雜的多主機叢集,良好的佈署實踐都是確保應用穩定執行的關鍵。

在容器化的世界裡,佈署不再是一個痛點,而是一個機會——讓我們能夠以更高效、更一致的方式將應用從開發環境無縫遷移到生產環境。

Docker 佈署:從 systemd 到設定管理工具的實戰

systemd 與 Docker 的深度整合

systemd 作為現代 Linux 系統的初始化系統,可以提供強大的容器管理功能。在實務上,使用 systemd 來管理 Docker 容器有多重優勢,特別是可以處理容器間的依賴關係與啟動順序。

我們來分析一下 identidock 應用中的 proxy 服務設定檔案:

[Unit]
Description=Proxy Container for Identidock
After=docker.service
Requires=docker.service
Requires=identidock.identidock.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop proxy
ExecStartPre=-/usr/bin/docker rm proxy
ExecStartPre=/usr/bin/docker pull amouat/proxy
ExecStart=/usr/bin/docker run --name proxy \
--link identidock:identidock \
-p 80:80 \
-e NGINX_HOST=0.0.0.0 \
-e NGINX_PROXY=http://identidock:9090 \
amouat/proxy

[Install]
WantedBy=multi-user.target

這個設定檔案中有幾個關鍵點值得注意:

  1. 相依性管理:透過 AfterRequires 指令,確保 Docker 服務和 identidock 服務已經啟動
  2. 容器生命週期管理:在啟動前會先嘗試停止並移除舊有容器,然後提取最新映像檔
  3. 自動重啟:設定 Restart=always 確保服務當機時會自動重啟

同樣地,dnmonster 服務的設定如下:

[Unit]
Description=dnmonster Container for Identidock
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop dnmonster
ExecStartPre=-/usr/bin/docker rm dnmonster
ExecStartPre=/usr/bin/docker pull amouat/dnmonster
ExecStart=/usr/bin/docker run --name dnmonster amouat/dnmonster

[Install]
WantedBy=multi-user.target

使用 systemd 管理容器的一個重要特性是:當一個依賴容器 (如 Redis) 停止時,systemd 會觸發整個相依容器鏈的重啟。這與 Docker 原生行為不同,Docker 只會更新連結而不會重啟容器。

值得注意的是,CoreOS 和 Giant Swarm PaaS 都採用 systemd 來控制容器,這顯示了 systemd 在容器管理領域的重要性。不過,Docker 與 systemd 之間確實存在一些尚未解決的衝突,因為兩者都希望掌控主機上服務的生命週期。

設定管理工具與容器整合

當你負責管理多台主機時,使用設定管理 (CM) 工具變得至關重要。這類別工具能確保 Docker 主機的作業系統保持更新,特別是安全補丁方面,同時也能確保所執行的 Docker 映像檔是最新版本。

使用設定管理工具與容器整合有兩種主要方法:

  1. 將容器視為虛擬機器:使用 CM 軟體來管理和更新容器內的軟體
  2. 將容器視為黑盒子:使用 CM 軟體管理 Docker 主機並確保容器執行正確版本的映像檔,但容器本身視為不可修改的黑盒

從 Docker 的哲學和微服務架構的角度來看,第二種方法更為合適。在這種方法中,容器類別似於虛擬機器中的黃金映像檔,一旦執行就不應被修改。當需要更新時,應該用執行新映像檔的容器來替換整個舊容器,而不是嘗試修改容器內的內容。

Ansible 與 Docker 的實戰整合

玄貓在多個專案中使用 Ansible 來管理 Docker 佈署,因為它易於上手與不需要在被管理主機上安裝代理程式。Ansible 主要依靠 SSH 來設定主機,並提供了功能強大的 Docker 模組。

以下是使用 Ansible 佈署 identidock 應用的例項:

首先,建立一個 hosts 檔案,列出所有要管理的伺服器:

[identidock]
46.101.162.242

然後,建立 identidock 的「劇本」(playbook):

---
- hosts: identidock
  sudo: yes
  tasks:
  - name: easy_install
    apt: pkg=python-setuptools
  - name: pip
    easy_install: name=pip
  - name: docker-py
    pip: name=docker-py
  - name: redis container
    docker:
      name: redis
      image: redis:3
      pull: always
      state: reloaded
      restart_policy: always
  - name: dnmonster container
    docker:
      name: dnmonster
      image: amouat/dnmonster:1.0
      pull: always
      state: reloaded
      restart_policy: always
  - name: identidock container
    docker:
      name: identidock
      image: amouat/identidock:1.0
      pull: always
      state: reloaded
      links:
        - "dnmonster:dnmonster"
        - "redis:redis"
      env:
        ENV: PROD
      restart_policy: always
  - name: proxy container
    docker:
      name: proxy
      image: amouat/proxy:1.0
      pull: always
      state: reloaded
      links:
        - "identidock:identidock"
      ports:
        - "80:80"
      env:
        NGINX_HOST: www.identidock.com
        NGINX_PROXY: http://identidock:9090
      restart_policy: always

這個設定與 Docker Compose 類別似,但有幾個重要區別:

  1. 需要在主機上安裝 docker-py 才能使用 Ansible Docker 模組
  2. pull: always 確保每次任務執行時都會檢查映像檔更新
  3. state: reloaded 確保設定變更時容器會重新啟動

執行 playbook:

$ docker run -it \
  -v ${HOME}/.ssh:/root/.ssh:ro \
  -v $(pwd)/identidock.yml:/ansible/identidock.yml \
  -v $(pwd)/hosts:/etc/ansible/hosts \
  --rm=true generik/ansible ansible-playbook identidock.yml

這個簡單的範例只是展示了 Ansible 與 Docker 整合的基礎。在實際生產環境中,你可以利用 Ansible 的更多功能,特別是定義能夠執行容器滾動更新的流程,在不破壞依賴關係或造成重大停機的情況下更新服務。

Docker 主機設定考量

選擇適合的主機作業系統和儲存驅動程式對 Docker 的生產佈署至關重要。

作業系統選擇

對於小型到中型應用,最簡單的方法是堅持使用你熟悉的系統,如 Ubuntu 或 Fedora。但如果你計劃執行大型應用或叢集(跨多台主機的數百或數千個容器),則應考慮更專業的選項,如 CoreOS、Project Atomic 或 RancherOS,並搭配適當的協調解決方案。

在雲端環境中,大多數提供商都有專門為其基礎設施最佳化的 Docker 映像檔。

儲存驅動程式選擇

Docker 支援多種儲存驅動程式,選擇合適的驅動程式對確保生產環境的可靠性和效率至關重要:

  1. AUFS:Docker 的第一個儲存驅動程式,至今仍是最受測試和最常用的驅動程式之一。與 Overlay 一樣,它支援容器間分享記憶體頁面,這意味著如果兩個容器從相同的底層載入程式函式庫料,作業系統能夠為兩個容器使用相同的記憶體頁面。AUFS 的主要問題是它不在主流核心中,並且在檔案級別操作,這意味著對大檔案的小改動會導致整個檔案被複製到容器的讀/寫層。

在我多年的 Docker 佈署經驗中,發現對於生產環境,綜合考量穩定性與效能後,儲存驅動程式的選擇往往會根據工作負載特性而有所不同。例如,對於 I/O 密集型應用,我常推薦使用 overlay2 驅動程式;而對於舊系統或需要最大相容性的環境,AUFS 仍是一個可靠選擇。

無論使用哪種佈署方式,持續的監控和定期的健康檢查都是確保容器化應用穩定執行的關鍵。在我的實戰經驗中,結合自動化佈署工具與完善的監控系統,能夠大幅降低維護成本並提高系統可靠性。

Docker 佈署不僅關乎技術選擇,更是一門平衡藝術——需要在應用需求、團隊技能、基礎設施限制間找到最佳平衡點。透過本文討論的方法,你應該能夠為你的特定環境選擇最合適的 Docker 佈署策略。

Docker儲存驅動的關鍵選擇:效能與相容性的平衡

在容器技術的世界中,儲存驅動是影響容器效能、穩定性與資源使用的重要因素。經過多年與各種規模公司合作的經驗,玄貓發現許多開發團隊往往低估了儲存驅動選擇的重要性,直到遇到效能瓶頸或穩定性問題才開始關注。本文將深入解析Docker各種儲存驅動的特點,提供選擇,並分享實戰中的最佳實踐。

理解薄資源設定與厚資源設定

在討論儲存驅動前,需要先理解一個重要概念:薄資源設定(thin provisioning)與厚資源設定(thick provisioning)的區別:

  • 薄資源設定:資源僅在客戶端實際需要時才進行分配,而非預先分配全部請求的資源。
  • 厚資源設定:當客戶端請求資源時,系統立即為其保留所有請求的資源,即使客戶端可能只使用其中一小部分。

在容器環境中,薄資源設定允許更高效地使用磁碟空間,但可能在高負載情況下引起效能波動。Docker的大多數儲存驅動都採用薄資源設定策略來提高空間利用率。

Docker主要儲存驅動解析

AUFS (Advanced Multi-Layered Unification Filesystem)

AUFS是Docker早期的預設選擇,在Ubuntu和Debian系統上仍然被廣泛使用。

核心特點

  • 實作了檔案級別的寫時複製(CoW)機制
  • 穩定性高,經過長時間的實戰驗證
  • 對小檔案處理較為高效

使用建議: 如果你使用Ubuntu或Debian主機,很可能預設就是AUFS驅動。當應用需要處理大量小檔案時,AUFS表現良好。在我為一家媒體公司構建內容管理系統時,AUFS在處理大量小型影像和文字檔案方面表現出色。

Overlay/Overlay2

Overlay與AUFS非常相似,但已被合併到Linux核心3.18版本中。

核心特點

  • 效能略優於AUFS
  • 作為未來主流儲存驅動的有力候選
  • 需要較新版本的核心支援

使用建議: 如果你的系統支援最新的核心版本,Overlay很可能是最佳選擇。從長遠來看,它很可能成為Docker的主要儲存驅動。在我最近的幾個專案中,已經逐步將客戶的環境從AUFS遷移到Overlay2,在大多數情況下都獲得了約5-15%的效能提升。

BTRFS (B-Tree File System)

BTRFS是一個專注於容錯和大檔案支援的寫時複製檔案系統。

核心特點

  • 區塊級別的寫時複製,對大檔案處理效率高
  • 支援非常大的檔案尺寸和卷
  • 具有一些獨特功能,如內建快照和校驗和

使用建議: 除非你的組織已經有BTRFS的使用經驗,或者需要特定的BTRFS功能,否則不建議選擇。在我參與的一個科學資料處理專案中,由於需要處理TB級別的大檔案,BTRFS成為了理想選擇,但在日常開發環境中,它的各種特性和陷阱可能會導致額外的管理負擔。

ZFS (Z File System)

ZFS最初由Sun Microsystems開發,是一個備受喜愛的檔案系統。

核心特點

  • 在許多方面與BTRFS相似
  • 效能和可靠性可能優於BTRFS
  • 由於許可證問題,無法直接包含在Linux核心中

使用建議: 在Linux上執行ZFS並不簡單。只有在組織內部已有大量ZFS經驗的情況下,才建議使用。我曾在一個儲存密集型應用中使用ZFS,其資料完整性檢查功能確實令人印象深刻,但設定和維護的複雜性不容忽視。

Device Mapper

Device Mapper是Red Hat系統的預設驅動,是多種技術的基礎元件。

核心特點

  • 使用區塊級別而非檔案級別的寫時複製
  • 預設從100GB的稀疏檔案中分配"薄池"
  • 建立的容器預設可擴充套件至100GB (自Docker 1.8起)

使用建議: Device Mapper可能是Docker儲存驅動中最複雜的一個,也是問題和支援請求的常見來源。如果可能,我建議使用其他替代方案。但如果必須使用,請注意有許多選項可以調整以提供更好的效能,特別是將儲存從預設的"環回"裝置移至真實裝置是個好主意。

在我為一家金融科技公司設計高用性系統時,不得不使用Device Mapper,因為系統根據RHEL。透過調整引數並將儲存移至專用SSD,我們確實獲得了不錯的效能,但調優過程耗時與需要專業知識。

VFS (Virtual File System)

VFS是預設的Linux虛擬檔案系統。

核心特點

  • 不實作寫時複製機制
  • 啟動容器時需要完整複製映像
  • 大增加啟動時間和磁碟空間需求

使用建議: 只有在你遇到其他驅動問題與不介意效能損失的情況下,VFS才是合理的選擇。例如,在擁有少量長壽命容器的環境中,可能是可接受的。在開發環境或特殊測試場景中,VFS的簡單性有時也是一個優勢。

儲存驅動切換

切換儲存驅動相對簡單,假設你已安裝所需依賴。只需重啟Docker守護程式,並傳遞適當的--storage-driver值(或簡寫為-s)。例如,使用以下命令啟動使用overlay儲存驅動的守護程式(如果你的核心支援):

docker daemon -s overlay

同樣重要的是注意--graph-g引數,它設定Docker執行時的根目錄——你可能需要將其移至執行適當檔案系統的分割槽上。例如,對於BTRFS驅動:

docker daemon -s btrfs -g /mnt/btrfs_partition

要永久更改,需要編輯Docker服務的啟動指令碼或設定檔案。在Ubuntu 14.04上,這意味著編輯/etc/default/docker檔案中的DOCKER_OPTS變數。

在儲存驅動間遷移映像

切換儲存驅動後,你將無法存取所有舊容器和映像。切回舊儲存驅動將還原存取。要將映像移至新儲存驅動,只需將映像儲存為TAR檔案,然後在新檔案系統中載入。例如:

# 儲存映像到TAR檔案
docker save -o /tmp/debian.tar debian:wheezy

# 停止Docker服務
sudo stop docker

# 使用新的儲存驅動啟動Docker
docker daemon -s vfs

# 在新終端中確認映像已清空
docker images
# 應顯示空列表

# 載入之前儲存的映像
docker load -i /tmp/debian.tar

# 確認映像已載入
docker images
# 應顯示debian映像資訊

專業容器託管選項

目前市場上已有一些專業容器託管選項,無需直接管理主機。

Triton

Joyent的Triton可能是最有趣的選項,因為它內部不使用虛擬機器。這給Triton帶來了顯著的效能優勢,並允許按容器為基礎進行資源設定。

Triton不使用Docker引擎,而是在SmartOS管理程式(起源於Solaris)上使用自己的容器引擎,並採用Linux虛擬化。透過實作Docker遠端API,Triton與普通Docker客戶端完全相容,後者被用作Triton的標準介面。Docker Hub上的映像可以正常工作。

Triton是開放原始碼的,有Joyent雲執行的託管版本和本地佈署版本。我們可以使用Joyent公共雲快速執行容器應用。設定Triton帳戶並將Docker客戶端指向Triton後,嘗試執行docker info

$ docker info
Containers: 0
Images: 0
Storage Driver: sdc
SDCAccount: amouat
Execution Driver: sdc-0.3.0
Logging Driver: json-file
Kernel Version: 3.12.0-1-amd64
Operating System: SmartDataCenter
CPUs: 0
Total Memory: 0 B
Name: us-east-1
ID: 92b0cf3a-82c8-4bf2-8b74-836d1dd61003
Username: amouat
Registry: https://index.docker.io/v1/

注意OS和執行驅動程式的值,它們表明我們不是在普通的Docker引擎上執行。我們可以使用Compose和以下triton.yml檔案來啟動應用,因為Triton支援大部分Docker引擎API:

proxy:
  image: amouat/proxy:1.0
  links:
    - identidock
  ports:
    - "80:80"
  environment:
    - NGINX_HOST=www.identidock.com
    - NGINX_PROXY=http://identidock:9090
  mem_limit: "128M"

identidock:
  image: amouat/identidock:1.0
  links:
    - dnmonster
    - redis
  environment:
    ENV: PROD
  mem_limit: "128M"

dnmonster:
  image: amouat/dnmonster:1.0
  mem_limit: "128M"

redis:
  image: redis
  mem_limit: "128M"

這與之前的prod.yml幾乎相同,增加了記憶體設定以告訴Triton要啟動的容器大小。我們還使用公共映像而不是構建自己的(Triton目前不支援docker build)。

啟動應用:

$ docker-compose -f triton.yml up -d
...
Creating triton_proxy_1...
$ docker inspect -f {{.NetworkSettings.IPAddress}} triton_proxy_1
165.225.128.41
$ curl 165.225.128.41
<html><head><title>Hello...

當Triton看到已發布的連線埠時,會自動使用可公開存取的IP。

在Triton上執行容器後,請確保停止並移除它們;Triton會對已停止但未移除的容器收費。

使用原生Docker工具與Triton互動是一種很好的體驗,但也有一些粗糙的邊緣;並非所有API呼叫都受支援,Compose處理卷的方式也存在一些問題,但這些應該會隨時間解決。

在主流雲提供商確信Linux內核的隔離保證足夠強大,可以在沒有安全顧慮的情況下執行容器之前,Triton是執行容器化系統最具吸引力的解決方案之一。

Google Container Engine

Google Container Engine (GKE)採用更具定見的方法來執行容器,建立在Kubernetes協調系統之上。

Kubernetes是由Google設計的開放原始碼專案,借鑑了他們使用Borg叢集管理器在內部執行容器的經驗。將應用程式佈署到GKE需要對Kubernetes有基本瞭解,並建立一些特定於Kubernetes的設定檔案。

作為設定應用程式的額外工作的回報,你可以獲得自動複製和負載平衡等服務。這些聽起來可能像是隻有大型服務(具有高流量和許多分散式部分)才需要的服務,但對於任何想要保證執行時間的服務來說,它們很快就變得重要。

我強烈推薦Kubernetes,特別是GKE,用於佈署容器系統,但要注意這會將你繫結到Kubernetes模型,使系統在提供商之間的遷移變得更加困難。

Amazon EC2 Container Service

Amazon的EC2 Container Service (ECS)幫助你在Amazon的EC2基礎設施上執行容器。ECS提供了一個Web介面和API,用於啟動容器和管理底層EC2叢集。

在叢集的每個節點上,ECS將啟動一個容器代理,該代理與ECS服務通訊,負責

使用 Amazon ECS 與 Giant Swarm 佈署容器:比較與實踐

在開發完容器應用後,選擇合適的佈署平台成為關鍵決策。本文將探討兩個專業容器代管平台:Amazon ECS 與 Giant Swarm,並分析容器在生產環境中的資料持久化與機密管理問題。

Amazon ECS:AWS 的容器協調服務

Amazon Elastic Container Service (ECS) 是 AWS 提供的容器管理服務,讓開發者能在 AWS 基礎設施上輕鬆佈署、管理和擴充套件 Docker 容器。

ECS 核心概念與架構

ECS 的主要組成部分包括:

  1. 叢集 (Cluster) - ECS 容器的執行環境,由多個 EC2 執行個體組成
  2. 任務定義 (Task Definition) - 定義應用程式的 Docker 容器集合
  3. 任務 (Task) - 任務定義的例項,代表正在執行的容器集合
  4. 服務 (Service) - 確保指定數量的任務持續執行

實際佈署 Identidock 應用

以下是在 ECS 上佈署多容器應用的流程:

  1. 建立 ECS 叢集:可透過 AWS 控制檯快速設定

  2. 定義任務:建立包含所有容器的任務定義,例如 identidock 應用:

{
  "family": "identidock",
  "containerDefinitions": [
    {
      "name": "proxy",
      "image": "amouat/proxy:1.0",
      "cpu": 100,
      "memory": 100,
      "essential": true,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "links": ["identidock"]
    },
    {
      "name": "identidock",
      "image": "amouat/identidock:1.0",
      "cpu": 100,
      "memory": 100,
      "links": ["dnmonster", "redis"],
      "essential": true
    },
    {
      "name": "dnmonster",
      "image": "amouat/dnmonster:1.0",
      "cpu": 100,
      "memory": 100,
      "essential": true
    },
    {
      "name": "redis",
      "image": "redis:3",
      "cpu": 100,
      "memory": 100,
      "essential": false
    }
  ]
}

在任務定義中,每個容器需指定記憶體和 CPU 單位。essential 屬性決定該容器失敗時是否應停止整個任務。在這個例子中,Redis 容器被標記為非必要,因為即使它失敗,應用仍能運作。

  1. 建立服務:將任務作為服務執行,ECS 會監控容器確保可用性,並提供與 Elastic Load Balancer 連線的選項,實作負載平衡

  2. 清理資源:停止服務時需要多個步驟:

    • 更新服務,將任務數量設為 0
    • 刪除服務
    • 取消註冊容器例項
    • 停止關聯資源(如負載平衡器或 EBS 儲存)

ECS 優勢

  • 能輕鬆佈署數百或數千個容器,提供強大擴充套件能力
  • 高度可設定的容器排程,可根據需求最佳化效率或可靠性
  • 與現有 AWS 服務無縫整合,如 Elastic Load Balancer 和 Elastic Block Store
  • 支援自定義或第三方排程器

Giant Swarm:微服務架構的專屬解決方案

Giant Swarm 是一個專注於微服務架構的容器平台,提供快速簡便的方式來啟動根據 Docker 的系統。它有三種服務模式:

  1. 託管版本(分享叢集)
  2. 專用服務(Giant Swarm 為使用者提供和維護裸機主機)
  3. 內部佈署解決方案

Giant Swarm 的獨特之處在於它幾乎不使用虛擬機器。對於安全要求嚴格的使用者,會提供獨立的裸機主機,而分享叢集則允許不同使用者的容器並排執行。

在 Giant Swarm 上佈署 Identidock

假設已獲得 Giant Swarm 存取許可權並安裝了 swarm CLI,首先建立設定檔案 swarm.json

{
  "name": "identidock_svc",
  "components": {
    "proxy": {
      "image": "amouat/proxy:1.0",
      "ports": [80],
      "env": {
        "NGINX_HOST": "$domain",
        "NGINX_PROXY": "http://identidock:9090"
      },
      "links": [{
        "component": "identidock",
        "target_port": 9090
      }],
      "domains": { "80": "$domain" }
    },
    "identidock": {
      "image": "amouat/identidock:1.0",
      "ports": [9090],
      "links": [
        {
          "component": "dnmonster",
          "target_port": 8080
        },
        {
          "component": "redis",
          "target_port": 6379
        }
      ]
    },
    "redis": {
      "image": "redis:3",
      "ports": [6379]
    },
    "dnmonster": {
      "image": "amouat/dnmonster:1.0",
      "ports": [8080]
    }
  }
}

接著啟動服務:

$ swarm up --var=domain=identidock-$(swarm user).gigantic.io
Starting service identidock_svc...
Service identidock_svc is up.

檢查服務狀態並測試:

$ swarm status identidock_svc
Service identidock_svc is up
component    image                  instanceid    created             status
dnmonster    amouat/dnmonster:1.0   m6eyoilfiei1  2015-09-04 09:50:40 up
identidock   amouat/identidock:1.0  r22ut7h0vx39  2015-09-04 09:50:40 up
proxy        amouat/proxy:1.0       6dr38cmrg3nx  2015-09-04 09:50:40 up
redis        redis:3                jvcf15d6lpz4  2015-09-04 09:50:40 up

$ curl identidock-amouat.gigantic.io
<html><head><title>Hello...

Giant Swarm 特色

  • 支援範本變數,如上例中的 $domain
  • 能定義 pod(一組一起排程的容器)
  • 可指定容器應執行的例項數量
  • 提供 Web UI 監控服務和檢視日誌
  • 提供 REST API 實作與 Giant Swarm 的自動化互動

容器的持久化資料管理

在生產環境中,容器的資料持久化是一個重要考量。對於大規模資料儲存,Docker 容器並未顯著改變現有方案:

  • **大型資料函式庫:無論使用容器、VM 或裸機,由於資料量大,容器實際上會被繫結到主機上,限制了容器的可移植性優勢。

    • 若擔心效能,可使用 --net=host--privileged 確保容器效率接近主機
    • 也可考慮使用如 Amazon RDS 等託管服務
  • 小型服務的資料儲存:對於設定檔案和中等量資料,標準卷(volume)可能受限於主機繫結,使容器擴充套件和遷移變得困難

    • 考慮將資料移至獨立的鍵值儲存或資料函式庫 - 使用 Flocker 管理資料卷,利用 ZFS 檔案系統功能支援資料隨容器遷移
    • 在實施微服務時,盡可能保持容器無狀態將大簡化系統架構

容器中的機密管理

敏感資料如密碼和 API 金鑰需要安全地與容器分享。以下是幾種主要方法及其優缺點:

1. 將機密儲存在映像中(不推薦)

絕對不要這樣做。雖然最簡單,但這意味著任何能存取映像的人都能取得機密。即使刪除,機密仍存在於之前的層中,與一旦映像被意外分享,機密就暴露了。

2. 透過環境變數傳遞機密

方法簡單:

$ docker run -d -e API_TOKEN=my_secret_token myimage

這是「十二因素應用程式」方法論推薦的方式,但有嚴重缺點:

  • 環境變數對所有子程式、docker inspect 和連結的容器可見
  • 環境常被記錄下來用於除錯,機密可能出現在日誌中
  • 無法刪除,理想情況下我們希望使用後覆寫或擦除機密

3. 透過卷傳遞機密

稍好的方案:

$ docker run -d -v $(pwd):/secret-file:/secret-file:ro myimage

可以建立一個設定環境變數的指令碼:

$ cat /secret/env.sh
export DB_PASSWORD=s3cr3t
$ source /secret/env.sh >> run_my_app.sh

這樣可避免將變數暴露給 docker inspect 或連結的容器,但缺點是:

  • 需將機密儲存在檔案中,容易被誤加入版本控制
  • 通常需要編寫指令碼,較為繁瑣

4. 使用鍵值儲存(推薦)

最佳解決方案是使用鍵值儲存存機密,在容器執行時檢索。這提供了對機密的更好控制,但需更多設定。

主要解決方案有:

  • KeyWhiz:由支付處理公司 Square 開發,將機密加密儲存在記憶體中,透過 REST API 和命令列介面提供存取
  • Vault:由 Hashicorp 開發,可在多種後端(包括檔案和 Consul)中加密儲存機密,具有 CLI 和 API
  • Crypt:在 etcd 或 Consul 鍵值儲存中加密儲存值

透過容器協調平台和適當的機密管理策略,可以構建既安全又高效的容器化應用系統。在選擇佈署方案時,應根據團隊技術能力、應用規模和安全需求綜合考量。

無論選擇哪種方案,持續關注最佳實踐並根據應用需求調整策略,才能充分發揮容器技術的優勢。

容器佈署與管理的進階策略

在容器技術的實戰應用中,從開發環境順利過渡到生產環境需要考慮諸多因素。在玄貓多年來協助企業建置容器基礎設施的經驗中,我注意到許多團隊常在佈署階段遇到瓶頸。本文將分享一套完整的生產環境容器佈署與日誌監控策略,從秘密管理到日誌集中化,幫助你建立一個穩固可靠的容器化架構。

管理敏感資訊的最佳實踐

在容器化環境中管理秘密(如API金鑰、密碼等敏感資訊)是一個常被忽視但極其重要的議題。處理不當不僅會帶來安全隱憂,還可能導致資料外洩。

常見的秘密管理方式及其限制

在容器世界中,主要有幾種方式來處理敏感資訊:

  1. 環境變數 - 雖然便捷,但存在嚴重安全隱憂:

    • 環境變數會被記錄在歷史命令中
    • 透過 docker inspect 可以輕易檢視
    • 若容器當機,錯誤報告可能包含這些變數
  2. 掛載卷(Volumes) - 相對安全但仍有挑戰:

    • 需要確保主機上的檔案安全
    • 必須防止檔案被意外提交到版本控制系統
    • 在多主機環境中分發這些檔案較為複雜
  3. 專用秘密管理系統 - 更加安全的進階方案:

    • HashiCorp的Vault
    • Square的KeyWhiz
    • Docker Swarm內建的秘密管理

在實際專案中,我曾協助一家金融科技公司從環境變數遷移到專用的秘密管理系統。初期雖然增加了設定的複雜度,但長期來看大幅提升了系統安全性,並簡化了秘密輪換的流程。

實用的秘密管理策略

根據我的經驗,以下策略可以有效提升容器環境中的秘密管理:

  • 為秘密設定租約(leases),讓它們在特定時間後自動過期
  • 在安全警示時能夠立即鎖定對秘密的存取
  • 使用一次性權杖,在使用後立即復原
  • 考慮使用專用的卷外掛程式,將秘密從儲存系統直接掛載為容器內的檔案

雖然這類別解決方案代表容器安全的未來,但實作複雜度較高。在工具成熟之前,我建議謹慎使用卷來分享秘密,並確保不將它們提交到版本控制系統中。

容器網路的效能最佳化

在生產環境中,預設的Docker網路設定可能導致顯著的效能損失。這是因為Docker橋接(bridge)和虛擬乙太網路(veth)的設定意味著大量網路由在使用者空間進行,而非由路由硬體或核心處理,這會慢得多。

在一個高流量的電商平台專案中,我們透過調整網路設定,將API回應時間減少了約30%。這種最佳化對於高流量服務尤為重要。詳細的網路最佳化技術將在後續章節中探討。

建立生產級容器登入檔

雖然在開發階段使用Docker Hub很方便,但大多數生產環境會需要自己的登入檔,以提供更快的映像存取並避免依賴第三方基礎設施。這對於那些對將程式碼儲存在第三方服務(無論是私有還是公開)感到不安的組織尤為重要。

登入檔管理的關鍵考量

確保登入檔中的映像保持最新與正確至關重要—你不希望主機能夠提取舊的或潛在有漏洞的映像。因此,定期對登入檔進行稽核是個好主意。不過,請記住,每個Docker主機也會維護自己的映像快取,這也需要檢查。

Docker分發專案目前正在開發映象和類別似使用案例,以實作可擴充套件的拓撲和高用性。這將進一步增強生產環境中的映像管理能力。

持續佈署與交付策略

持續交付(Continuous Delivery)是將持續整合延伸到生產環境的做法;工程師能夠在開發中進行修改,讓這些修改透過測試,然後只需按一下按鈕就可以佈署。持續佈署(Continuous Deployment)更進一步,自動將透過測試的修改推播到佈署環境。

無停機佈署技術

將持續整合擴充套件到持續佈署可以透過將映像推播到生產登入檔並將執行中的容器遷移到新映像來實作。在不停機的情況下遷移映像需要啟動新容器並在停止舊容器之前重新路由流量。

有幾種可能的方法可以安全地實作這一點:

  • 藍綠佈署(Blue/Green Deployments):準備一個完全相同的環境(綠色),在確認無誤後將流量從原環境(藍色)切換過去
  • 階梯式佈署(Ramped Deployments):逐步將流量從舊版本轉移到新版本

這些技術通常使用內部工具實作,不過像Kubernetes這樣的框架提供了內建解決方案。我預期市場上會出現更多專門的工具。

容器日誌與監控實戰

有效的監控和日誌記錄對於維護任何非簡單系統的執行和有效除錯至關重要。在微服務架構中,由於機器數量增加,日誌和監控變得更加重要。考慮到容器的短暫性質,在除錯問題時,特定容器可能已不存在,這使得集中式日誌成為不可或缺的工具。

近年來,可用於日誌和監控的解決方案數量激增。現有的監控和日誌供應商已開始提供專門的容器解決方案和整合。接下來我們將概述各種可用的選項和技術,重點關注免費和開放原始碼產品。

Docker原生日誌功能

讓我們先了解Docker提供的基本日誌功能。如果不指定任何引數或安裝任何日誌軟體,Docker會記錄所有傳送到STDOUT和STDERR的內容。然後可以使用docker logs命令檢索這些日誌:

$ docker run --name logtest debian sh -c 'echo "stdout"; echo "stderr" >>2'
stderr
stdout
$ docker logs logtest
stderr
stdout

我們還可以使用-t引數取得時間戳:

$ docker logs -t logtest
2015-04-27T10:30:54.002057314Z stderr
2015-04-27T10:30:54.005335068Z stdout

對於執行中的容器,我們可以使用-f即時串流日誌:

$ docker run -d --name streamtest debian \
sh -c 'while true; do echo "tick"; sleep 1; done;'
13aa6ee6406a998350781f994b23ce69ed6c38daa69c2c83263c863337a38ef9
$ docker logs -f streamtest
tick
tick
tick
...

我們也可以透過Docker遠端API來實作這一點,這為以程式方式路由和處理日誌開啟了可能性:

$ curl -i --cacert ~/.docker/machine/certs/ca.pem \
--cert ~/.docker/machine/certs/ca.pem \
--key ~/.docker/machine/certs/key.pem \
"https://$(docker-machine ip default):2376/containers/\
$(docker ps -lq)/logs?stderr=1&stdout=1"
tick
tick
tick
...

原生日誌的限制

儘管Docker提供了基本的日誌功能,但存在一些明顯的限制:

  1. 只能處理STDOUT和STDERR - 如果應用程式只記錄到檔案,這就成了問題
  2. 沒有日誌輪換 - 這意味著如果你使用像yes這樣的應用程式(它只是重複向STDOUT寫入"yes")來保持容器執行,你會發現容器很快消耗掉磁碟上的所有可用空間

Docker提供了多種日誌驅動選項,可以透過--log-driver引數來指定:

  • json-file: 預設日誌方式
  • syslog: syslog驅動程式
  • journald: systemd日誌驅動程式
  • gelf: Graylog擴充套件日誌格式(GELF)驅動程式
  • fluentd: 將日誌訊息轉發到fluentd
  • none: 關閉日誌功能

在某些情況下,關閉日誌功能可能是有用的,例如前面提到的yes範例。

日誌聚合策略

無論使用哪種日誌驅動程式,它只能提供部分解決方案,特別是在多主機系統中。我們真正需要的是將所有日誌(可能跨主機)聚合到單一位置,以便執行分析和監控工具。

實作這一目標有兩種基本方法:

  1. 在所有容器內執行充當代理的次要程式,將日誌轉發到聚合服務
  2. 在主機上或在單獨的容器中收集日誌,然後轉發到聚合服務

第一種技術雖然可行與有時被使用,但會增加映像體積並不必要地增加執行程式數量,所以我們將只考慮第二種技術。

從主機存取容器日誌的方法有幾種:

  1. 使用Docker API程式化存取日誌 - 這具有官方支援的優勢,但使用HTTP連線會帶來一些開銷
  2. 使用syslog驅動程式自動轉發日誌
  3. 直接從Docker目錄存取日誌檔案

ELK堆積積疊實戰:整合式日誌解決方案

要為我們的應用程式新增日誌功能,我們將使用所謂的ELK堆積積疊,即Elasticsearch、Logstash和Kibana的組合:

  • Elasticsearch: 一個具有近實時搜尋功能的文字搜尋引擎。它設計為可以輕鬆地跨節點擴充套件以處理大量資料,非常適合搜尋大量日誌資料。

  • Logstash: 一個用於收集、解析和轉換日誌的工具。它可以從多個來源取得資料,轉換它,然後傳送到指定的目的地,如Elasticsearch。

  • Kibana: 一個用於視覺化和探索Elasticsearch中資料的工具。它提供了豐富的圖表和儀錶板功能。

這種組合非常強大,可以處理從小型應用到大型分散式系統的各種日誌需求。在我協助建置的一個電信業客戶的系統中,ELK堆積積疊每天處理超過500GB的日誌資料,並支援跨20多個微服務的即時監控和警示。

實際設定ELK堆積積疊

設定ELK堆積積疊的基本步驟包括:

  1. 設定Elasticsearch作為後端儲存
  2. 設定Logstash來收集、解析和轉換日誌
  3. 設定Kibana提供視覺化介面
  4. 使用適當的收集器從容器取得日誌並傳送到Logstash

對於容器環境,我們可以使用像Logspout這樣的工具,它可以收集所有容器的標準輸出和標準錯誤,並將其轉發到指定目的地,例如Logstash。

處理特殊日誌需求

某些應用程式堅持將日誌寫入檔案而非STDOUT或STDERR,處理這類別情況有幾種方法:

  1. 修改應用程式 - 理想情況下,修改應用程式使其將日誌輸出到STDOUT/STDERR
  2. 使用日誌轉發工具 - 例如可以在容器中執行tail命令,將檔案日誌傳送到標準輸出
  3. 使用專用的日誌卷 - 將日誌目錄對映為卷,然後在主機上或透過另一個容器處理這些檔案

在一個遺留系統容器化專案中,我們使用了第二種方法,透過一個

建立容器化環境的日誌管理系統

在容器化環境中,日誌管理是一個常被忽視但極其重要的環節。當你的系統從單一服務擴充套件到多個微服務,日誌追蹤變得既複雜又關鍵。在我幾年前負責重構一個金融科技平台時,缺乏統一的日誌系統導致問題診斷時間延長了3倍以上,這讓玄貓深刻體會到良好日誌系統的價值。

在本文中,我們將探討如何使用ELK Stack(Elasticsearch, Logstash, Kibana)結合Logspout,為容器化應用建立一個全面的日誌管理解決方案。

ELK Stack的核心元件

ELK Stack由三個主要元件組成:

  • Elasticsearch:一個分散式搜尋與分析引擎,用於儲存日誌並提供強大的搜尋功能
  • Logstash:一個資料處理管道工具,負責收集、解析和轉換日誌,然後傳送到Elasticsearch
  • Kibana:一個根據JavaScript的視覺化介面,用於查詢和展示Elasticsearch中的資料

此外,我們還需要一個名為Logspout的工具,它專門用於從Docker容器收集日誌並將其轉發到指定的端點。

日誌系統架構設計

我們的目標是建立一個如下圖所示的日誌系統架構:

  1. 使用Logspout從各個容器收集日誌
  2. 將收集到的日誌轉發給Logstash進行處理
  3. Logstash解析和過濾日誌,然後傳送到Elasticsearch
  4. 使用Kibana視覺化和查詢Elasticsearch中的資料

雖然在生產環境中,我們通常會將ELK容器佈署在獨立的主機上以實作關注點分離,但為了簡化示範,我們將所有元件保留在同一主機上。

使用Logspout收集Docker容器日誌

首先,我們需要設定Logspout來收集Docker容器的日誌。Logspout是一個輕量級工具,使用Docker API來串流容器日誌到指定端點。由於我們希望將日誌傳送到Logstash,我們還需要安裝logspout-logstash轉接器。

建立基本的容器協調檔案

讓我們建立一個名為prod-with-logging.yml的Docker Compose檔案:

proxy:
  image: amouat/proxy:1.0
  links:
    - identidock
  ports:
    - "80:80"
  environment:
    - NGINX_HOST=45.55.251.164
    - NGINX_PROXY=http://identidock:9090

identidock:
  image: amouat/identidock:1.0
  links:
    - dnmonster
    - redis
  environment:
    ENV: PROD

dnmonster:
  image: amouat/dnmonster

redis:
  image: redis

logspout:
  image: amouat/logspout-logstash
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock
  ports:
    - "8000:80"

這個設定檔案包含了我們的主要應用程式(identidock)及其相依服務,以及一個Logspout容器。讓我解釋一些關鍵設定:

  • 請將IP位址替換為你主機的IP
  • 掛載Docker socket (/var/run/docker.sock),使Logspout能夠連線到Docker API
  • 公開Logspout的HTTP介面(連線埠8000)用於檢視日誌(注意:在生產系統中不要將此連線埠暴露)

測試Logspout日誌收集

現在,讓我們啟動應用程式並測試Logspout的日誌收集功能:

$ docker-compose -f prod-with-logging.yml up -d
...
$ curl localhost:8000/logs

在瀏覽器中開啟identidock應用,你應該能在終端中看到一些日誌:

logging_proxy_1|192.168.99.1 - - [24/Sep/2015:11:36:53 +0000] "GET / HTTP/1....
logging_identidock_1|[pid: 6|app: 0|req: 1/1] 172.17.0.14 () {40 vars in 660...
logging_identidock_1|Cache miss
logging_proxy_1|192.168.99.1 - - [24/Sep/2015:11:36:53 +0000] "GET /mon...

這表示Logspout正在成功收集容器日誌。但我們的目標是將這些日誌傳送到一個中央位置進行處理和儲存,這就是Logstash的用武之地。

設定Logstash處理容器日誌

接下來,我們需要將Logspout連線到Logstash,以便進行日誌處理。在多主機系統中,你需要在每個主機上執行一個Logspout容器,所有容器都路由到一個中央化的Logstash例項。

更新Docker Compose檔案

讓我們更新Compose檔案,新增Logstash服務:

...
logspout:
  image: amouat/logspout-logstash
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock
  ports:
    - "8000:80"
  links:
    - logstash
  command: logstash://logstash:5000

logstash:
  image: logstash
  volumes:
    - ./logstash.conf:/etc/logstash.conf
  environment:
    LOGSPOUT: ignore
  command: -f /etc/logstash.conf

關鍵變更包括:

  • 新增一個指向Logstash容器的連結
  • 使用"logstash"字首,告訴Logspout使用Logstash模組輸出
  • 對映Logstash設定檔案
  • 設定LOGSPOUT: ignore環境變數,防止Logspout收集Logstash自己的日誌(避免迴圈日誌)

建立Logstash設定檔案

現在,我們需要建立一個名為logstash.conf的設定檔案,內容如下:

input {
  tcp {
    port => 5000
    codec => json
  }
  udp {
    port => 5000
    codec => json
  }
}

output {
  stdout { codec => rubydebug }
}

這個設定檔案設定Logstash監聽TCP和UDP連線埠5000,使用JSON編解碼器處理輸入,並將結果輸出到標準輸出(用於測試)。

測試Logstash日誌處理

讓我們重啟應用程式並測試Logstash的處理能力:

$ docker-compose -f prod-with-logging.yml up -d
...
$ curl -s localhost > /dev/null
$ docker-compose -f prod-with-logging.yml logs logstash

你應該能看到類別似以下的日誌條目,包含了由Logspout新增的額外資訊,如容器名稱和ID:

logstash_1 | {
logstash_1 | "message" => "2015/09/24 12:50:25 logstash: write u...",
logstash_1 | "docker.name" => "/logging_logspout_1",
logstash_1 | "docker.id" => "d8f69d05123c43c9da7470951547b23ab32d4...",
logstash_1 | "docker.image" => "amouat/logspout-logstash",
logstash_1 | "docker.hostname" => "d8f69d05123c",
logstash_1 | "@version" => "1",
logstash_1 | "@timestamp" => "2015-09-24T12:50:25.708Z",
logstash_1 | "host" => "172.17.0.11"
logstash_1 | }

增強Logstash過濾功能

Logstash的真正價值在於其強大的過濾能力。例如,我們可以解析Nginx日誌格式,提取更多有用資訊。讓我們更新Logstash設定檔案,新增過濾部分:

input {
  tcp {
    port => 5000
    codec => json
  }
  udp {
    port => 5000
    codec => json
  }
}

filter {
  if [docker.image] =~ /^amouat\/proxy.*/ {
    mutate { replace => { type => "nginx" } }
    grok {
      match => { "message" => "%{COMBINEDAPACHELOG}" }
    }
  }
}

output {
  stdout { codec => rubydebug }
}

這個過濾器檢查訊息是否來自amouat/proxy映像。如果是,它使用預定義的COMBINEDAPACHELOG過濾器解析訊息,從而提取額外欄位。

重啟應用後,你應該能看到更豐富的日誌條目,包含HTTP狀態碼、請求類別和URL等資訊:

logstash_1 | {
logstash_1 | "message" => "87.246.78.46 - - [24/Sep/2015:13:02:...",
logstash_1 | "docker.name" => "/logging_proxy_1",
logstash_1 | "docker.id" => "5bffa4f4a9106e7381b22673569094be20e8...",
logstash_1 | "docker.image" => "amouat/proxy:1.0",
logstash_1 | "docker.hostname" => "5bffa4f4a910",
logstash_1 | "@version" => "1",
logstash_1 | "@timestamp" => "2015-09-24T13:02:59.751Z",
logstash_1 | "host" => "172.17.0.23",
logstash_1 | "type" => "nginx",
logstash_1 | "clientip" => "87.246.78.46",
logstash_1 | "ident" => "-",
logstash_1 | "auth" => "-",
logstash_1 | "timestamp" => "24/Sep/2015:13:02:59 +0000",
logstash_1 | "verb" => "GET",
logstash_1 | "request" => "/",
logstash_1 | "httpversion" => "1.1",
logstash_1 | "response" => "200",
logstash_1 | "bytes" => "266",
logstash_1 | "referrer" => "\"-\"",
logstash_1 | "agent" => "\"curl/7.37.1\""
logstash_1 | }

使用這種技術,你可以為各種容器映像設定自定義過濾器,從而獲得更有價值的日誌資訊。

整合Elasticsearch和Kibana完成ELK堆積積疊

最後,我們需要將Logstash連線到Elasticsearch,並新增Kibana作為視覺化介面。

更新Docker Compose檔案

讓我們再次更新Compose檔案,新增Elasticsearch和Kibana服務:

...
logspout:
  image: amouat/logspout-logstash
  volumes:
    - /var/run/docker.sock:/tmp/docker.sock
  ports:
    - "8000:80"
  links:
    - logstash
  command: logstash://logstash:5000

logstash:
  image: logstash:1.5
  volumes:
    - ./logstash.conf:/etc/logstash.conf
  environment:
    LOGSPOUT: ignore
  links:
    - elasticsearch
  command: -f /etc/logstash.conf

elasticsearch:
  image: elasticsearch:1.7
  environment:
    LOGSPOUT: ignore

kibana:
  image: kibana:4
  environment:
    LOGSPOUT: ignore
    ELASTICSEARCH_URL: http://elasticsearch:9200
  links:
    - elasticsearch
  ports:
    - "5601:5601"

主要變更包括:

  • 新增了一個指向Elasticsearch容器的連結
  • 建立了根據官方映像的Elasticsearch容器
  • 建立了Kibana 4容器,連線到Elasticsearch並公開5601連線埠

更新Logstash設定

我們還需要更新logstash.conf檔案的輸出部分,指向Elasticsearch容器:

...
output {
  elasticsearch { host => "elasticsearch" }
  stdout { codec => rubydebug }
}

這個設定將以Elasticsearch可讀格式輸出資料到名為"elasticsearch"的主機。你可以移除標準輸出行,因為它只用於除錯,目前會導致日誌重複。

啟動完整的ELK堆積積疊

現在讓我們重新啟動應用程式:

$ docker-compose -f prod-with-logging.yml up -d

在瀏覽器中開啟應用(localhost),並且identidock應用互動一段時間,以便我們的日誌堆積積疊有一些資料可分析。準備就緒後,在瀏覽器中開啟Kibana(localhost:5601)。

Kibana日誌視覺化與分析

當你首

Kibana 日誌視覺化與分析

在 Docker 容器環境中建立有效的日誌管理系統,Kibana 提供了強大的視覺化分析能力。在設定 Kibana 時,選擇 @timestamp 作為時間欄位名稱並點選「Create」後,系統會顯示 Elasticsearch 識別的所有欄位,包括 nginx 和 Docker 相關欄位。

點選「Discover」後,你會看到一個包含日誌數量直方圖和最近日誌列表的頁面。這個視覺化介面讓我們能夠快速掌握系統的執行狀態和潛在問題。

Kibana 的強大搜尋功能

Kibana 提供了靈活的時間範圍調整功能,只需點選右上角的時鐘圖示即可變更查詢時間段。更實用的是其強大的篩選能力:

  1. 可針對特定欄位搜尋特定內容
  2. 例如搜尋「message」欄位中的「Cache miss」,即可獲得快取未命中的時間分佈直方圖

在我為一個大型電商平台建立監控系統時,透過 Kibana 的搜尋功能,我們迅速識別出了一個間歇性的快取問題,這在傳統日誌檢視方式下可能需要數小時才能發現。

Kibana 進階視覺化

除了基本的日誌檢視功能外,Kibana 的「Visualize」標籤提供了更進階的圖表和視覺化功能:

  • 折線圖和圓餅圖
  • 資料表格
  • 自訂指標

這些視覺化工具讓團隊能夠從日誌資料中提取有價值的業務和技術洞見,例如識別使用者行為模式或系統效能瓶頸。

Kibana 版本差異

值得注意的是,Kibana 3 和 4 版本在架構上有所不同:

  • Kibana 3:根據 JavaScript 的客戶端應用,需確保瀏覽器可以直接存取 Elasticsearch 容器
  • Kibana 4 以後:連線透過 Kibana 代理,移除了上述要求

這個變化大幅簡化了佈署流程,我在多個專案中從 Kibana 3 升級到較新版本後,不再需要處理複雜的網路存取問題。

日誌儲存與旋轉策略

無論你使用什麼日誌驅動程式和分析解決方案,日誌的儲存和保留策略都是關鍵考量。若未妥善規劃,容器使用預設日誌設定可能會逐漸耗盡主機硬碟空間,最終導致系統當機。

logrotate 實用設定

Linux 的 logrotate 工具可有效管理日誌檔案的增長。典型設定是使用多代日誌檔案,定期在不同代之間移動檔案。例如,除了目前的日誌外,還可以有父代、祖父代和曾祖父代日誌,後兩者通常會壓縮以節省空間。

以下是一個實用的 logrotate 設定,可儲存為 /etc/logrotate.d/docker 或加入 /etc/logrotate.conf

/var/lib/docker/containers/*/*.log {
    daily
    rotate 3
    compress
    delaycompress
    missingok
    copytruncate
}

這個設定的核心功能包括:

  • daily:每天旋轉日誌
  • rotate 3:保留三代日誌檔案
  • compress:使用壓縮節省空間
  • delaycompress:延遲一代再壓縮
  • missingok:檔案缺失時不報錯
  • copytruncate:複製後截斷檔案而非移動,避免 Docker 因檔案消失而出問題

長期日誌儲存策略

對於需要長期儲存的關鍵日誌,我建議將日誌轉發到更穩健的資料函式庫,如 PostgreSQL。這可以透過 Logstash 或類別似工具作為第二輸出輕鬆實作。

在我的經驗中,不要僅依賴 Elasticsearch 等索引解決方案作為永久儲存,因為它們沒有 PostgreSQL 等成熟資料函式庫錯保證。特別是在處理金融或醫療行業的應用時,這點尤為重要。

值得一提的是,你可以使用 Logstash 過濾器過濾掉敏感資訊,如個人識別資訊,這在處理需要遵守隱私法規的應用時非常有用。

處理寫入檔案的應用程式日誌

許多傳統應用程式並不遵循容器最佳實踐,依然將日誌寫入檔案而非 STDOUT/STDERR。對於這種情況,我們有幾種解決方案:

使用輔助容器方案

如果已經使用 Docker API 進行日誌收集(如使用 Logspout 容器),最簡單的解決方案是執行一個程式(通常是 tail -F)將檔案輸出到 STDOUT。一個優雅的實作方式是使用第二個容器掛載日誌檔案,這樣可以維持每個容器單一程式的理念。

例如,如果有一個在 /var/log 宣告卷的容器 “tolog”,可以使用以下命令:

$ docker run -d --name tolog-logger \
  --volumes-from tolog \
  debian tail -F /dev/log/*

這種方法在我處理遺留系統容器化時特別有用,它讓我們無需修改原始應用程式即可整合進現代日誌系統。

主機掛載方案

另一種方法是將日誌掛載到主機的已知目錄,然後在其上執行收集器如 fluentd。這種方法適用於需要在主機層面進行額外日誌處理的情況。

Docker 與 syslog 整合

假設 Docker 主機支援 syslog,可以使用 syslog 驅動程式將容器日誌傳送到主機的 syslog。以下例子說明瞭操作方式:

$ ID=$(docker run -d --log-driver=syslog debian \
  sh -c 'i=0; while true; do i=$((i+1)); echo "docker $i"; sleep 1; done;')
$ docker logs $ID
"logs" command is supported only for "json-file" logging driver (got: syslog)
$ tail /var/log/syslog
Sep 24 10:17:45 reginald docker/181b6d654000[3594]: docker 48
Sep 24 10:17:46 reginald docker/181b6d654000[3594]: docker 49
Sep 24 10:17:47 reginald docker/181b6d654000[3594]: docker 50

需要注意的是,使用 syslog 驅動程式時,docker logs 命令無法使用,因為它只支援 json-file 日誌驅動程式。不過,我們可以直接從 syslog 檔案檢視日誌。

在 Ubuntu 主機上,Docker 日誌會傳送到 /var/log/syslog,而在其他 Linux 發行版上可能有所不同。

篩選容器日誌

由於 syslog 檔案包含各種服務和其他容器的訊息,我們可以使用 grep 工具根據容器 ID 篩選特定容器的訊息:

$ grep ${ID:0:12} /var/log/syslog
Sep 24 10:16:58 reginald docker/181b6d654000[3594]: docker 1
Sep 24 10:16:59 reginald docker/181b6d654000[3594]: docker 2
Sep 24 10:17:00 reginald docker/181b6d654000[3594]: docker 3

這裡使用容器 ID 的縮短形式(前12個字元)進行過濾。

最佳化 syslog 設定

為了更好地管理 Docker 日誌,我們可以設定 syslog 將 Docker 日誌存放到單獨的檔案中。Docker 日誌使用 syslog 中的 “daemon” 設施,所以可以輕鬆設定 syslog 將所有守護程式的訊息傳送到指設定檔案。

如果使用 rsyslog 7 或更高版本(很可能是這樣),可以使用以下規則:

:syslogtag,startswith,"docker/" /var/log/containers.log

這將把所有 Docker 容器訊息放入 /var/log/containers.log。將此規則儲存到 rsyslog 設定檔案中,例如在 Ubuntu 上可以新建 /etc/rsyslog.d/30-docker.conf 並儲存此規則。然後重啟 syslog:

$ sudo service rsyslog restart
rsyslog stop/waiting
rsyslog start/running, process 15863

現在 Docker 容器的日誌會出現在新檔案中:

$ cat /var/log/containers.log
Sep 24 10:30:46 reginald docker/1a1a57b885f3[3594]: docker 1
Sep 24 10:30:47 reginald docker/1a1a57b885f3[3594]: docker 2

如果不希望日誌同時寫入其他檔案(如 /var/log/syslog),可以在規則後立即新增一行 &stop

:syslogtag,startswith,"docker/" /var/log/containers.log
&stop

這種方法在我管理過的多節點容器叢集中特別有用,它使系統管理員能夠輕鬆區分容器日誌和主機系統日誌。

Docker Events API 的強大功能

除了容器日誌和 Docker 守護程式日誌外,還有一組可能需要監控和回應的資料—Docker 事件。Docker 容器生命週期的大多數階段都會記錄事件,包括:

  • create(建立)
  • destroy(銷毀)
  • die(結束)
  • export(匯出)
  • kill(殺死)
  • pause(暫停)
  • attach(附加)
  • restart(重啟)
  • start(啟動)
  • stop(停止)
  • unpause(還原)

對於映像檔,還有 untag 和 delete 事件。untag 發生在標籤被刪除時,這會在成功呼叫 docker rmi 時發生。delete 事件則在底層映像檔被刪除時發生。

使用 events 命令

我們可以使用 docker events 命令取得事件:

$ docker events
2015-09-24T15:23:28.000000000+01:00 44fe57bab...: (from debian) create
2015-09-24T15:23:28.000000000+01:00 44fe57bab...: (from debian) attach
2015-09-24T15:23:28.000000000+01:00 44fe57bab...: (from debian) start
2015-09-24T15:23:28.000000000+01:00 44fe57bab...: (from debian) die

由於 docker events 命令回傳的是資料流,需要在另一個終端執行一些 Docker 命令才能看到結果。此命令還支援篩選結果和控制回傳結果的時間段。

Events API 的實際應用

Docker events API 在需要自動回應容器事件時非常有用。例如:

  1. Logspout 工具使用 API 檢測容器何時啟動,並開始從中流式輸出日誌
  2. Jason Wilder 的 nginx-proxy 使用 events API 在容器啟動時自動進行負載平衡

在玄貓負責的一個大規模微服務架構中,我們利用 Events API 構建了一套完整的自動化維運系統,能夠在容器異常終止時自動分析原因、記錄問題並在必要時重啟服務,大幅降低了人工介入的需求。

此外,記錄這些資料還可以用於分析容器的生命週期,幫助最佳化資源使用和佈署策略。

Docker 容器的生命週期如同一個有機體,從建立、執行到停止、銷毀,每個階段都有其特定的事件標記。透過監聽和回應這些事件,我們可以構建更人工智慧、更自動化的容器管理系統,讓開發和維運團隊專注於更有價值的工作。

Docker 的日誌管理系統雖然看似複雜,但一旦建立起合適的基礎設施,它能提供前所未有的洞察力

系統日誌與Docker Machine虛擬機器

在撰寫本文時,系統日誌(syslog)在Docker Machine所建立的boot2docker虛擬機器中預設並未執行。你可以透過登入虛擬機器並執行syslogd來啟動它進行測試。例如:

$ docker-machine ssh default
...
docker@default:~$ syslogd

你可以透過在boot2docker虛擬機器內的/var/lib/boot2docker/bootsync.sh檔案中呼叫syslogd來讓這個變更永久生效,該檔案會在Docker啟動前被虛擬機器執行。例如:

$ docker-machine ssh default
...
docker@default:~$ cat /var/lib/boot2docker/bootsync.sh
#!/bin/sh
syslogd

需要注意的是,boot2docker虛擬機器使用busybox的預設syslog實作,它不如rsyslogd那麼靈活。

你可以透過在Docker守護程式初始化時新增--log-driver=syslog引數來將syslog設定為Docker容器的預設日誌選項。通常是透過編輯Docker服務的組態檔案來實作(例如在Ubuntu中,在/etc/default/docker檔案中的DOCKER_OPTS中新增)。

使用rsyslog轉發日誌

我們也可以設定rsyslog將日誌轉發到另一台伺服器,而不是儲存在本地。這可以用於將日誌提供給中央服務,如Logstash或其他syslog伺服器,而無需使用Logspout等工具的額外開銷。

要在我們的identidock範例中用rsyslog替換Logspout,我們需要更改Logstash設定以接收syslog輸入、在主機上轉發連線埠到Logstash供rsyslog通訊,並告訴rsyslog將日誌傳送到網路而不是檔案。

首先重新設定Logstash。更新設定檔為:

input {
  syslog {
    type => syslog
    port => 5544
  }
}

filter {
  if [type] == "syslog" {
    syslog_pri { }
    date {
      match => [ "syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ]
    }
  }
}

output {
  elasticsearch { host => "elasticsearch" }
  stdout { codec => rubydebug }
}

現在設定rsyslog。設定與上述幾乎相同,只是我們使用@@localhost:5544語法而非指定/var/log/containers.log檔案:

:syslogtag,startswith,"docker/" @@localhost:5544
&stop

這將告訴rsyslog使用TCP將日誌傳送到localhost的5544連線埠。若要使用UDP,只需使用單個@

最後一步是重寫我們的Compose檔案。在此之前,最好停止任何正在執行的identidock例項:

$ docker-compose -f prod-with-logging.yml stop
...

現在我們可以安全地從Compose檔案中移除Logspout,並在主機上發布一個連線埠供rsyslog與Logstash通訊:

...
logstash:
  image: logstash:1.5
  volumes:
    - ./logstash.conf:/etc/logstash.conf
  environment:
    LOGSPOUT: ignore
  links:
    - elasticsearch
  ports:
    - "127.0.0.1:5544:5544"
  command: -f /etc/logstash.conf

elasticsearch:
  image: elasticsearch:1.7
  environment:
    LOGSPOUT: ignore

kibana:
  image: kibana:4.1
  environment:
    LOGSPOUT: ignore
    ELASTICSEARCH_URL: http://elasticsearch:9200
  links:
    - elasticsearch
  ports:
    - "5601:5601"

我們發布了5544連線埠,但只繫結到127.0.0.1介面,這樣主機可以連線到該連線埠,但網路上的其他機器不能。

最後,重新啟動rsyslog和identidock。現在我們可以看到日誌透過rsyslog傳送到Logstash,而不是使用較慢的Logspout方法。雖然還需要進行一些過濾器設定工作,以將Logspout中的所有資訊都匯入Logstash,但使用rsyslog轉發日誌是一個非常高效與穩健的解決方案。

保證日誌記錄

設計日誌基礎架構時,不論你是否意識到,你都在為效率而犧牲完整準確性和可靠性。如果你只需要日誌用於除錯和監控目的,你可能只需使用你認為最簡單的解決方案。然而,如果某些日誌訊息必須立即觸發警示,或者你的日誌必須可驗證完整以符合政策,則必須考慮日誌基礎架構各環節的特性和保證。

需要考慮的關鍵點包括:

  • 用於傳送日誌的傳輸協定是什麼?UDP較快但提供的可靠性保證比TCP少(但TCP仍不能保證完全可靠)。
  • 網路中斷時會發生什麼?注意許多工具,包括rsyslog,可以設定為緩衝訊息直到可以連線遠端伺服器。
  • 訊息如何儲存和備份?資料函式庫案系統提供更高的可靠性和容錯保證。

另一個相關問題是日誌的安全性;你的日誌可能包含敏感資訊,控制誰可以存取它們很重要。你需要確保透過公共網路傳輸的日誌被加密,與只有適當的人員可以存取儲存的日誌。

從檔案取得日誌

另一種高效的轉發日誌方式是直接從檔案系統中存取原始日誌。

如果你使用預設的日誌記錄方式,Docker目前將容器日誌檔案儲存在/var/lib/docker/containers/<container id>/<container id>-json.log

直接從檔案取得日誌很高效,但依賴於Docker內部實作細節而非公開API。因此,根據此方法的日誌解決方案可能會因Docker引擎更新而失效。

監控與警示

在微服務系統中,你可能有數十個,可能數百或數千個執行中的容器。你需要盡可能多的協助來監控執行容器和系統的狀態。好的監控解決方案應該能一眼看出系統的健康狀況,並在資源不足時(如磁碟空間、CPU、記憶體)提前發出警告。我們還希望在事情開始出錯時收到警示(如請求開始需要幾秒鐘或更長時間處理)。

使用Docker工具進行監控

Docker自帶一個基本的命令列工具docker stats,它能回傳資源使用的即時流。這個命令接受一個或多個容器名稱並為它們列印各種統計資料,類別似Unix應用程式top。例如:

$ docker stats logging_logspout_1
CONTAINER           CPU %  MEM USAGE/LIMIT     MEM %  NET I/O
logging_logspout_1  0.13%  1.696 MB/2.099 GB  0.08%  4.06 kB/9.479 kB

統計資料涵蓋CPU和記憶體使用情況以及網路利用率。注意,除非你為容器設定了記憶體限制,否則你看到的記憶體限制將代表主機上的總記憶體量,而不是容器可用的記憶體量。

取得所有執行容器的統計資料

大多數時候,你會想要取得主機上所有執行容器的統計資料(在玄貓看來這應該是預設行為)。你可以使用一些shell指令碼技巧來實作:

$ docker stats \
$(docker inspect -f {{.Name}} $(docker ps -q))
CONTAINER                 CPU %  MEM USAGE/LIMIT     ...
/logging_dnmonster_1      0.00%  57.51 MB/2.099 GB
/logging_elasticsearch_1  0.60%  337.8 MB/2.099 GB
/logging_identidock_1     0.01%  29.03 MB/2.099 GB
/logging_kibana_1         0.00%  61.61 MB/2.099 GB
/logging_logspout_1       0.14%  1.7 MB/2.099 GB
/logging_logstash_1       0.57%  263.8 MB/2.099 GB
/logging_proxy_1          0.00%  1.438 MB/2.099 GB
/logging_redis_1          0.14%  7.283 MB/2.099 GB

docker ps -q取得所有執行容器的ID,作為docker inspect -f {{.Name}}的輸入,後者將ID轉換為名稱並傳遞給docker stats

這在它所能提供的範圍內很有用,同時也暗示了Docker API的存在,可以程式化地取得此類別資料。這個API確實存在,你可以呼叫/containers/<id>/stats端點來取得容器的各種統計資料流,比命令列提供的詳細。這個API有些不靈活;你可以每秒串流所有值的更新,或者只取得一次所有統計資料,但沒有控制頻率或過濾的選項。這意味著你可能會發現stats API對於連續監控來說開銷太大,但對於臨時查詢和調查仍然很有用。

Docker公開的大多數各種指標也可以直接從Linux核心取得,透過CGroups和名稱空間功能,可以由各種函式庫具存取,包括Docker的runc函式庫果你有特定想要監控的指標,可以使用runc或直接進行核心呼叫來編寫高效的解決方案。你需要使用允許進行低階核心呼叫的語言,如Go或C。還有一些需要注意的陷阱,例如何避免為更新指標而建立新程式。Docker文章《Runtime Metrics》解釋瞭如何做到這一點,並探討了核心提供的各種指標。一旦你公開了所需的值,你可能想要檢視諸如statsd之類別的工具來聚合和計算指標,InfluxDB和OpenTSDB用於儲存,以及Graphite和Grafana用於顯示結果。

使用Logstash進行監控和警示

雖然Logstash非常明顯是一種日誌工具,但值得指出的是,你已經可以使用Logstash實作一定水平的監控,而日誌本身是一個重要的監控指標。例如,你可以檢查nginx狀態碼,並在接收到大量500錯誤時自動傳送電子郵件或訊息警示。Logstash還為許多常見的監控解決方案提供輸出模組,包括Nagios、Ganglia和Graphite。

在大多數情況下,你會想使用現有工具來收集和聚合指標並產生視覺化。有許多商業解決方案可用,但我們將看領先的開放原始碼和容器特定解決方案。

cAdvisor

cAdvisor(Container Advisor的縮寫)來自Google,是最常用的Docker監控工具。它提供了主機上執行的容器資源使用情況和效能指標的圖形概覽。

由於cAdvisor本身可作為容器使用,我們可以迅速啟動它。只需使用以下引數啟動cAdvisor容器:

$ docker run -d \
  --name cadvisor \
  -v /:/rootfs:ro \
  -v /var/run:/var/run:rw \
  -v /sys:/sys:ro \
  -v /var/lib/docker/:/var/lib/docker:ro \
  -p 8080:8080 \
  google/cadvisor:latest

如果你使用Red Hat(或CentOS)主機,你還需要掛載cgroups資料夾,使用--volume=/cgroup:/cgroup

容器執行後,將瀏覽器指向http://localhost:8080。你應該會看到一個包含大量圖表的頁面。你可以點選「Docker Containers」連結,然後點選你感興趣的容器名稱來深入檢視特定容器。

在現代容器化環境中,有效的日誌管理和監控策略對於保持系統健康和快速解決問題至關重要。透過結合使用系統日誌、rsyslog轉發和專用監控工具如cAdvisor,可以建立一個強大的可觀察性架構,讓團隊能夠主動識別和解決潛在問題。

容器網路與服務發現:進階管理技術

容器監控工具使用實戰

cAdvisor的圖形化CPU監控分析

cAdvisor不僅能收集和處理各種統計資料,還透過REST API提供這些資料以供進一步處理和儲存。這些資料也可以匯出到InfluxDB——一種專為儲存和查詢時間序列資料(包括指標和分析)而設計的資料函式庫Advisor的未來發展藍圖包括提供如何改進和調整容器效能的建議,以及向叢集協調和排程工具提供使用預測資訊等功能。

叢集監控解決方案

雖然cAdvisor功能強大,但它本質上是一個單主機解決方案。對於大型系統,你需要取得跨所有主機的容器統計資訊以及主機本身的資料。你可能希望瞭解容器群組的表現,這些群組可能代表子系統或跨例項的功能切片。例如,你可能想檢視所有nginx容器的記憶體使用情況,或執行資料分析任務的一組容器的CPU使用情況。

由於所需的指標往往是特定於應用和問題的,一個好的解決方案應提供查詢語言,使你能夠構建新的指標和視覺化。

Google根據cAdvisor開發了一個名為Heapster的叢集監控解決方案,但在撰寫本文時,它僅支援Kubernetes和CoreOS,因此玄貓在此不做探討。

相反,讓我們看Prometheus——來自SoundCloud的開放原始碼叢集監控解決方案,它可以從多種來源(包括cAdvisor)取得輸入。它專為支援大型微服務架構而設計,並被SoundCloud和Docker Inc.所採用。

Prometheus深度解析

Prometheus的獨特之處在於它採用根據提取的模型。應用程式需要自行暴露指標,然後由Prometheus伺服器提取這些指標,而非直接將指標傳送給Prometheus。Prometheus UI可用於互動式查詢和圖形化資料,而單獨的PromDash可用於將圖表和儀錶板儲存起來。Prometheus還具有Alertmanager元件,可以彙總和抑制警示,並轉發到電子郵件等通知服務,以及PagerDuty和Pushover等專業服務。

將Prometheus與容器整合

讓我們看如何將Prometheus與identidock應用整合。我們不會新增任何專門的指標,但我們可以使用Python客戶端函式庫地在Python程式碼中裝飾呼叫來實作這一點。

相反,我們會將Prometheus連線到cAdvisor容器。如果你已經啟動了cAdvisor容器,可以在/metrics端點檢視它向Prometheus暴露的指標:

$ curl localhost:8080/metrics
# HELP container_cpu_system_seconds_total Cumulative system cpu time consume...
# TYPE container_cpu_system_seconds_total counter
container_cpu_system_seconds_total{id="/",name="/"} 97.89
container_cpu_system_seconds_total{id="/docker",name="/docker"} 40.66
container_cpu_system_seconds_total{id="/docker/071c24557187c14fb7a2504612d4c...
container_cpu_system_seconds_total{id="/docker/1a1a57b885f33d2e16e85cee7f138...
...

設定與啟動Prometheus

啟動Prometheus容器很簡單,但需要建立一個設定檔案。將以下內容儲存為prometheus.conf

global:
  scrape_interval: 1m
  scrape_timeout: 10s
  evaluation_interval: 1m
scrape_configs:
  - job_name: prometheus
    scheme: http
    target_groups:
      - targets:
        - 'cadvisor:8080'
        - 'localhost:9090'

這個設定告訴Prometheus每五秒檢索一次統計資料(這是相對較高的值,在生產環境中,你需要權衡抓取成本與過時指標的成本)、指定抓取cAdvisor的URL(我們將使用連結來設定主機名),並且也抓取Prometheus自己在連線埠9090上的指標端點。

使用以下命令啟動Prometheus容器:

$ docker run -d --name prometheus -p 9090:9090 \
  -v $(pwd)/prometheus.conf:/prometheus.conf \
  --link cadvisor:cadvisor \
  prom/prometheus -config.file=/prometheus.conf

現在你應該能夠在http://localhost:9090開啟Prometheus應用程式。首頁會提供關於Prometheus設定方式和抓取端點狀態的資訊。在"Graph"標籤頁,你可以開始調查Prometheus內部的資料。

Prometheus查詢語言例項

Prometheus有自己的查詢語言,支援過濾器、正規表示式和各種運算元。作為一個簡單的例子,嘗試輸入以下表達式:

sum(container_cpu_usage_seconds_total {name=~"logging*"}) by (name)

這應該提供每個identidock容器隨時間的CPU使用情況。{name=~"logging*"}表示式過濾掉不屬於Compose應用的容器(例如cAdvisor和Prometheus本身)。你需要將"logging"替換為你的Compose專案或資料夾名稱。由於CPU使用率是按CPU報告的,所以需要sum函式。

使用PromDash建立儀錶板

我們可以進一步設定帶有PromDash容器的儀錶板。這相對簡單,留給讀者作為練習。PromDash還支援顯示來自多個分散式Prometheus例項的圖表,這在按地理位置或跨部門顯示圖表時很有用。

當然,這只是Prometheus的基本用法。現實世界的安裝將涉及抓取分散式主機上的更多端點、設定儀錶板和使用PromDash進行深入視覺化,以及使用Alertmanager進行警示。

商業監控和日誌解決方案

本章中,玄貓主要深入研究了開放原始碼和本地解決方案,但市場上存在許多發展良好與有技術支援的商業解決方案。這個領域競爭激烈與不斷發展。在這個快速演變的領域,與其提及特定解決方案,不如建議你值得探索這些選項,特別是當你尋求成熟或託管解決方案時。

網路與服務發現基礎

在容器環境中,服務發現和網路之間的界限可能變得模糊。服務發現是自動為服務的客戶端提供適當服務例項的連線資訊(通常是IP位址和連線埠)的過程。這個問題在靜態、單主機系統中很簡單,因為每個服務只有一個例項,但在分散式系統中,服務例項會隨時間增減,問題就變得複雜得多。

一種服務發現方法是讓客戶端簡單地透過名稱(如db或api)請求服務,然後在後端進行解析以找到適當的位置。這種「魔法」可以採取多種形式,如簡單的大使容器、Consul這樣的服務發現解決方案、包括服務發現功能的Weave等網路解決方案,或者這些方案的組合。

對於我們的目的,網路可以被視為連線容器的過程。它不涉及插入實體乙太網線,但通常涉及軟體等效物(如veth)。容器網路從主機之間有可用路由的假設開始,無論該路由是穿越公共網際網路還是僅是快速本地交換機。

服務發現允許客戶端發現例項,而網路則負責建立連線。網路和服務發現解決方案在功能上往往重疊,因為服務發現解決方案可以指向跨網路,而網路解決方案通常包含服務發現功能。

有效的日誌記錄和監控對執行根據微服務的應用程式至關重要。透過ELK堆積積疊、cAdvisor和Prometheus等工具,我們可以快速建立有效的解決方案,即使這些解決方案本身可能比我們的應用程式更重量級。

未來,我們將看到Docker本身提供更多的日誌支援和選項。商業產品在日誌記錄、監控和警示方面非常強大,因此可以預期所有供應商都將推出針對Docker和微服務的專業產品。

服務探索與容器網路:解構分散式系統通訊策略

服務探索與網路解決方案的平衡

單純使用服務探索機制仍然需要某種網路設定,無論是使用預設的Docker橋接網路(bridge network)配合主機連線埠對映,還是主機網路(host networking)模式,都需要對連線埠進行管理。純粹的服務探索解決方案,如Consul,通常在健康檢查、容錯轉移和負載平衡方面提供更豐富的功能。而網路解決方案則提供不同的容器連線和路由選擇,同時還具備流量加密和容器群組隔離等特性。

在實際應用中,服務探索和網路解決方案通常需要同時使用(與總是需要某種形式的網路)。具體需求取決於你的使用場景,最佳實踐也在不斷演進。網路設定在開發、測試和生產環境間可能會有所不同,但服務探索通常涉及應用層級的決策,會在不同環境中保持一致。

本章將按照複雜性遞增的順序探討網路和服務探索方案,從最簡單的跨主機解決方案—大使容器(ambassador containers)開始,然後探討服務探索解決方案—包括etcd和Consul,最後深入Docker網路細節以及Weave、Flannel和Project Calico等解決方案。

大使容器:簡化跨主機容器通訊

大使容器(Ambassadors)是連線跨主機容器的一種方式。這些代理容器替代真實的容器(或服務),並將流量轉發給實際服務。大使容器提供了關注點分離,使其在許多場景中都很有用,不僅是用於跨主機連線服務。

大使容器的優勢與劣勢

大使容器的主要優勢在於它們允許生產環境的網路架構與開發架構不同,而無需更改程式碼。開發者可以使用本地版本的資料函式庫他資源,同時維運人員可以重新設定應用程式,使其使用自己的叢集服務或遠端資源,而無需修改程式碼。大使容器還可以動態重新設定以使用不同的後端服務,而直接透過連結連線到服務則需要重啟客戶端容器。

大使容器的劣勢是需要額外設定,會產生額外開銷,並可能成為潛在的故障點。當需要多個連線時,它們很快會變得過於複雜,成為管理負擔。

實際應用場景

在典型的開發設定中,開發者直接使用Docker連結將應用程式連線到本地筆電上執行的資料函式庫。這對於快速變更和在開發時能夠丟棄並重新開始是很好的。

在生產環境中,維運人員可能會使用大使容器將應用程式連線到在單獨伺服器上執行的生產服務。維運人員只需設定大使容器將流量傳遞給服務,並使用連結將應用程式連線到大使容器。程式碼繼續使用與之前相同的主機名和連線埠,新設定在大使容器內處理。

另一種設定是應用程式與遠端主機上的容器通訊,而該容器本身也在大使容器後面。這種設定允許遠端主機透過更新大使容器將流量轉移到新地址上的新容器。同樣,無需更改程式碼即可使用新設定。

amouat/ambassador 映像

大使容器本身可以是一個非常簡單的容器,它只需要在應用程式和服務之間設定連線。Docker Hub上沒有官方的大使映像,所以你需要自己建立或從Hub上選擇使用者映像。

本章使用名為amouat/ambassador的映像。這個映像是Sven Dowideit的大使容器的簡單移植,修改為使用alpine基礎映像,並作為自動構建在Docker Hub上執行。

該映像使用socat工具在大使容器和目標之間設定中繼。與Docker連結建立的相同形式的環境變數(例如REDIS_PORT_6379_TCP=tcp://172.17.0.1:6379)定義了中繼連線的位置。這意味著可以透過很少的設定來設定與本地連結容器的中繼。

由於該映像建立在最小的Alpine Linux發行版上,它的大小隻有7MB多一點,因此小到可以快速下載,並且對系統的開銷很小。

實際操作:使用大使容器連線跨主機應用

讓我們看如何使用大使容器將identidock應用程式與在單獨主機上執行的Redis容器連線起來。我們將使用Docker machine設定兩個VirtualBox虛擬機器,但你也可以使用雲端的Docker主機輕鬆執行此範例。

首先,準備好主機:

$ docker-machine create -d virtualbox redis-host
...
$ docker-machine create -d virtualbox identidock-host
...

接下來,在redis-host上設定一個Redis容器(名為real-redis)和一個大使容器(名為real-redis-ambassador):

$ eval $(docker-machine env redis-host)
$ docker run -d --name real-redis redis:3
Unable to find image 'redis:3' locally
3: Pulling from redis
...
60bb8d255b950b1b34443c04b6a9e5feec5047709e4e44e58a43285123e5c26b
$ docker run -d --name real-redis-ambassador \
-p 6379:6379 \
--link real-redis:real-redis \
amouat/ambassador
be613f5d1b49173b6b78b889290fd1d39dbb0fda4fbd74ee0ed26ab95ed7832c

我們需要在主機上發布6379連線埠以允許遠端連線。大使容器使用來自連結的real-redis容器的環境變數來設定中繼,該中繼將在6379連線埠上載入的請求流式傳輸到real-redis容器。

然後在identidock-host上設定大使容器:

$ eval $(docker-machine env identidock-host)
$ docker run -d --name redis_ambassador --expose 6379 \
-e REDIS_PORT_6379_TCP=tcp://$(docker-machine ip redis-host):6379 \
amouat/ambassador
Unable to find image 'amouat/ambassador:latest' locally
latest: Pulling from amouat/ambassador
31f630c65071: Pull complete
cb9fe39636e8: Pull complete
3931d220729b: Pull complete
154bc6b29ef7: Already exists
Digest: sha256:647c29203b9c9aba8e304fabfd194429a4138cfd3d306d2becc1a10e646fcc23
Status: Downloaded newer image for amouat/ambassador:latest
26d74433d44f5b63c173ea7d1cfebd6428b7227272bd52252f2820cdd513f164

我們需要手動設定環境變數來告訴大使容器連線到遠端主機。使用docker-machine ip命令檢索遠端主機的IP位址。

最後,啟動identidock和dnmonster,將identidock連結到我們的大使容器:

$ docker run -d --name dnmonster amouat/dnmonster:1.0
Unable to find image 'amouat/dnmonster:1.0' locally
1.0: Pulling from amouat/dnmonster
...
c7619143087f6d80b103a0b26e4034bc173c64b5fd0448ab704206b4ccd63fa
$ docker run -d --link dnmonster:dnmonster \
--link redis_ambassador:redis \
-p 80:9090 \
amouat/identidock:1.0
Unable to find image 'amouat/identidock:1.0' locally
1.0: Pulling from amouat/identidock
...
5e53476ee3c0c982754f9e2c42c82681aa567cdfb0b55b48ebc7eea2d586eeac

測試一下:

$ curl $(docker-machine ip identidock-host)
<html><head>...

太好了!我們剛透過使用兩個小型大使容器,在不更改任何程式碼的情況下實作了跨主機執行identidock。這種方法可能有點繁瑣,需要使用額外的容器,但它也非常簡單和靈活。很容易想像涉及更複雜大使容器的場景,例如:

  • 對不受信任的連結上的流量進行加密
  • 透過監控Docker事件流在容器啟動時自動連線容器
  • 將讀取請求代理到只讀伺服器,將寫入請求代理到不同的伺服器

在所有情況下,客戶端都不需要知道大使容器中的額外人工智慧。

然而,雖然大使容器可能很有用,但在大多數情況下,使用網路和/或服務探索解決方案來查詢和連線遠端服務和容器更容易與更具可擴充套件性。

服務探索深入分析

在本章開始時,我們將服務探索定義為自動為服務客戶端提供該服務適當例項的連線資訊的過程。

對於客戶端應用程式,這意味著他們需要以某種方式請求或被提供服務的地址。我們將看到需要客戶端明確呼叫API來取得服務地址的解決方案,以及根據DNS的解決方案,這些解決方案可以輕鬆與現有應用程式整合。

本文將介紹當今與Docker一起使用的主要服務探索解決方案。我們將深入瞭解etcd、Consul和SkyDNS,然後快速概述一些其他值得注意的解決方案。這些解決方案都不是專門為容器編寫的,但都是為在大型分散式系統中使用而設計的。

etcd:分散式鍵值儲存

etcd是一個分散式鍵值儲存。它是Raft共識演算法在Go中的實作,旨在既高效又容錯。共識是多個成員就值達成一致的過程,在面對故障和錯誤時這個過程很快變得複雜。Raft演算法確保值是一致的,並且在大多數成員可用時可以新增新值。

etcd叢集中的每個成員執行一個etcd二進位例項,該例項將與其他成員通訊。客戶端透過在所有成員上執行的REST介面存取etcd。

etcd叢集的建議最小規模是3,以在出現故障時提供容錯能力。然而,對於以下範例,我們將使用僅兩個成員來說明etcd的工作方式。

最佳叢集規模

對於etcd和Consul,建議的叢集規模為3、5或7,在故障容忍和效能之間取得平衡。

如果只有一個成員,在發生故障時資料將丟失。如果有兩個成員並且一個失敗,剩餘的成員將無法達到法定人數(quorum),進一步的寫入將失敗,直到第二個成員回傳。

表:叢集規模含義

伺服器數量達成多數所需數量容錯能力
110
220
321
431
532
642
743

如表所示,新增更多成員可以提高容錯能力。然而,更多的成員也意味著寫入需要更多節點之間的協定和通訊,從而減慢系統。一旦叢集規模超過7,足夠多的節點同時發生故障而導致系統當機的可能性已經足夠低,以至於新增更多節點不值得效能上的犧牲。此外,偶數規模通常最好避免,因為它們增加了叢集大小(因此降低效能)但不會提高容錯能力。

當然,許多分散式系統執行的主機遠不止7個。在這些情況下,通常使用5或7個主機形成叢集,其餘節點執行可以連線到叢集的客戶端。

建立跨主機容器服務發現基礎設施

在微服務架構中,服務發現是一個關鍵挑戰。當應用被拆分為多個獨立服務,跨越不同主機執行時,這些服務需要一種可靠的方式來相互定位。在本文中,我將分享如何使用 etcd 與 SkyDNS 建立一個強大的服務發現解決方案。

服務發現的核心挑戰

在傳統環境中,我們通常會在設定檔中硬編碼服務位址,但在動態的容器環境中,這種方法變得不可行。容器可能在任何時候被建立或銷毀,IP 位址經常變化。這就需要一個中央序號產生器制,讓服務能夠發布自己的位置,並讓消費者能夠找到所需的服務。

etcd 與 Consul:分散式鍵值儲存

etcd 和 Consul 是兩種流行的分散式鍵值儲存系統,它們都使用一致性演算法(如 Raft)來確保資料一致性。在生產環境中,這些系統通常佈署為 3-5 個節點的叢集,以提供高用性。然而,對於測試和開發環境,我們可以使用較小的叢集。

在本文中,我們將使用 etcd 作為我們的分散式儲存後端,並透過 Docker Machine 在虛擬機器上佈署它。

使用 Docker Machine 佈署 etcd 叢集

首先,我們需要建立兩台虛擬機器來託管我們的 etcd 叢集:

docker-machine create -d virtualbox etcd-1
docker-machine create -d virtualbox etcd-2

接下來,我們將在這些虛擬機器上啟動 etcd 容器。因為我們預先知道叢整合員,所以可以在啟動時明確列出它們。對於不預先知道地址的叢集,etcd 也支援根據 URL 或 DNS 的發現機制。

為了簡化命令,我先將虛擬機器 IP 地址儲存在環境變數中:

HOSTA=$(docker-machine ip etcd-1)
HOSTB=$(docker-machine ip etcd-2)
eval $(docker-machine env etcd-1)
docker run -d -p 2379:2379 -p 2380:2380 -p 4001:4001 \
--name etcd quay.io/coreos/etcd \
-name etcd-1 -initial-advertise-peer-urls http://${HOSTA}:2380 \
-listen-peer-urls http://0.0.0.0:2380 \
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
-advertise-client-urls http://${HOSTA}:2379 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster \
etcd-1=http://${HOSTA}:2380,etcd-2=http://${HOSTB}:2380 \
-initial-cluster-state new

理解 etcd 的啟動引數

這個啟動命令看起來複雜,但讓我們分解一下關鍵部分:

  1. 我們從 quay.io 註冊中心取得官方的 etcd 映像

  2. 設定各種 URL 用於存取 etcd:

    • 讓 etcd 在 0.0.0.0 上監聽,以接受遠端和本地連線
    • 告訴其他客戶端和對等節點透過主機的 IP 地址連線
    • 在實際環境中,etcd 節點應該在不對外開放的內部網路上通訊
  3. 明確列出叢集中的所有節點,包括正在啟動的節點(這可以替換為其他發現方法)

現在我們需要在第二台虛擬機器上啟動另一個 etcd 節點,設定幾乎相同,只是需要使用 etcd-2 的 IP 地址:

eval $(docker-machine env etcd-2)
docker run -d -p 2379:2379 -p 2380:2380 -p 4001:4001 \
--name etcd quay.io/coreos/etcd \
-name etcd-2 -initial-advertise-peer-urls http://${HOSTB}:2380 \
-listen-peer-urls http://0.0.0.0:2380 \
-listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
-advertise-client-urls http://${HOSTB}:2379 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster \
etcd-1=http://${HOSTA}:2380,etcd-2=http://${HOSTB}:2380 \
-initial-cluster-state new

驗證 etcd 叢集運作

現在我們的 etcd 叢集已經啟動並執行。我們可以透過 HTTP API 向 etcd 查詢成員列表:

curl -s http://$HOSTA:2379/v2/members | jq '.'

輸出將顯示叢集中的兩個成員:

{
  "members": [
    {
      "clientURLs": [
        "http://192.168.99.100:2379"
      ],
      "peerURLs": [
        "http://192.168.99.100:2380"
      ],
      "name": "etcd-1",
      "id": "30650851266557bc"
    },
    {
      "clientURLs": [
        "http://192.168.99.101:2379"
      ],
      "peerURLs": [
        "http://192.168.99.101:2380"
      ],
      "name": "etcd-2",
      "id": "9636be876f777946"
    }
  ]
}

我使用 jq 工具來美化輸出。值得注意的是,叢整合員可以透過向同一端點傳送 POST 和 DELETE HTTP 請求來動態新增和刪除。

在 etcd 中儲存和檢索資料

接下來,讓我們新增一些資料並確保可以從兩個主機讀取。etcd 中的資料以目錄形式儲存,並以 JSON 格式回傳。以下範例使用 HTTP PUT 請求在 service_name 目錄中儲存值 “service_address”:

curl -s http://$HOSTA:2379/v2/keys/service_name \
-XPUT -d value="service_address" | jq '.'

輸出顯示:

{
  "node": {
    "createdIndex": 17,
    "modifiedIndex": 17,
    "value": "service_address",
    "key": "/service_name"
  },
  "action": "set"
}

要檢索這個值,我們只需對該目錄進行 GET 請求:

curl -s http://$HOSTA:2379/v2/keys/service_name | jq '.'

輸出:

{
  "node": {
    "createdIndex": 17,
    "modifiedIndex": 17,
    "value": "service_address",
    "key": "/service_name"
  },
  "action": "get"
}

預設情況下,etcd 除了回傳值外,還會回傳關於鍵的一些元資料。值得注意的是,我們在 etcd-1 上設定資料,但從 etcd-2 讀取。由於它們屬於同一個叢集,使用哪個主機進行操作並不重要;它們都會給出相同的答案。

使用 etcdctl 命令列工具

除了 HTTP API,還有一個名為 etcdctl 的命令列客戶端可以用來與 etcd 叢集通訊。我們可以使用容器來執行它:

docker run binocarlos/etcdctl -C ${HOSTB}:2379 get service_name

輸出將直接顯示值:

service_address

檢視 etcd 容器的日誌可以更好地瞭解 etcd 的工作原理,它會顯示成員如何合作選舉長官者等細節。Raft 演算法的完整描述和規範可以在 https://raftconsensus.github.io/ 找到。

使用 SkyDNS 實作 DNS 服務發現

儘管我們可以直接在應用程式中使用 etcd 進行服務發現,但這需要修改應用程式碼。為了避免這種修改,我們可以使用 SkyDNS,它提供根據 DNS 的服務發現,建立在 etcd 之上。

SkyDNS 是 Google Container Engine 在其 Kubernetes 產品中用於服務發現的工具。我們將使用 SkyDNS 來完成 etcd 解決方案,並在兩個主機上執行 identidock 應用,而不需要修改任何程式碼。

設定 SkyDNS

首先,我們需要向 etcd 新增一些 SkyDNS 設定,告訴它啟動後該做什麼:

curl -XPUT http://${HOSTA}:2379/v2/keys/skydns/config \
-d value='{"dns_addr":"0.0.0.0:53", "domain":"identidock.local."}' | jq .

輸出:

{
  "action": "set",
  "node": {
    "key": "/skydns/config",
    "value": "{\"dns_addr\":\"0.0.0.0:53\", \"domain\":\"identidock.local.\"}",
    "modifiedIndex": 6,
    "createdIndex": 6
  }
}

這告訴 SkyDNS 在所有介面的 53 連線埠上監聽,並將其設為 identidock.local 域的權威伺服器。

現在我們可以在 etcd-1 上啟動 SkyDNS 容器:

eval $(docker-machine env etcd-1)
docker run -d -e ETCD_MACHINES="http://${HOSTA}:2379,http://${HOSTB}:2379" \
--name dns skynetservices/skydns:2.5.2a

我們需要提供額外的設定來告訴 SkyDNS 在哪裡找到它的 etcd 後端。雖然 DNS 伺服器已經執行,但我們還沒有告訴它任何服務資訊。

註冊服務到 SkyDNS

讓我們在 etcd-2 上啟動 Redis 伺服器,並將其新增到 SkyDNS:

eval $(docker-machine env etcd-2)
docker run -d -p 6379:6379 --name redis redis:3
curl -XPUT http://${HOSTA}:2379/v2/keys/skydns/local/identidock/redis \
-d value='{"host":"'$HOSTB'","port":6379}' | jq .

我們使用以 /local/identidock/redis 結尾的路徑進行 curl 請求,這對映到網域名稱 redis.identidock.local。JSON 資料指定我們希望名稱解析到的 IP 地址和連線埠。我們使用主機的 IP 地址而不是 Redis 容器的地址,因為容器 IP 只在 etcd-2 本地有效。

測試 DNS 解析

現在,讓我們啟動一個新容器,並使用 –dns 標誌將其指向我們的 DNS 容器進行查詢:

eval $(docker-machine env etcd-1)
docker run --dns $(docker inspect -f {{.NetworkSettings.IPAddress}} dns) \
-it redis:3 bash

在容器內部,我們可以嘗試 ping 我們的服務:

root@3baff51314d6:/data# ping redis.identidock.local
PING redis.identidock.local (192.168.99.101): 48 data bytes
56 bytes from 192.168.99.101: icmp_seq=0 ttl=64 time=0.102 ms
56 bytes from 192.168.99.101: icmp_seq=1 ttl=64 time=0.090 ms
56 bytes from 192.168.99.101: icmp_seq=2 ttl=64 time=0.096 ms
^C--- redis.identidock.local ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.090/0.096/0.102/0.000 ms

我們甚至可以使用 Redis CLI 連線到服務:

root@3baff51314d6:/data# redis-cli -h redis.identidock.local ping
PONG

簡化服務名稱

雖然 redis.identidock.local 可以工作,但這個名稱有點冗長。如果我們只能使用 redis,那會更好,但直接使用這個名稱不會解析:

root@3baff51314d6:/data# ping redis
ping: unknown host

我們可以透過新增搜尋域來解決這個問題。如果我們啟動一個新容器並

容器服務探索與網路設定:SkyDNS與Consul實戰

容器化應用程式的快速擴充套件帶來了新的挑戰,特別是在服務發現和網路通訊方面。本文將探討兩種強大的服務發現解決方案:根據etcd的SkyDNS和HashiCorp的Consul,並提供實際操作。

使用SkyDNS完成容器服務發現

在前面的實作中,我們已經設定了etcd和SkyDNS,現在讓我們繼續完成服務註冊與測試流程。

首先,我們需要註冊dnmonster服務:

docker run -d --name dnmonster amouat/dnmonster:1.0
DNM_IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} dnmonster)
curl -XPUT http://$HOSTA:2379/v2/keys/skydns/local/identidock/dnmonster \
-d value='{"host": "'$DNM_IP'","port":8080}'

這裡我們使用了容器的內部IP位址,這意味著dnmonster只能從etcd-1主機存取。在多SkyDNS伺服器的環境中,最好將此記錄標記為host local,避免混淆其他伺服器。這可以透過在啟動SkyDNS時定義host local域來實作。

現在,讓我們啟動identidock服務,並確認它能夠正常工作:

docker run -d -p 80:9090 amouat/identidock:1.0
curl $HOSTA

如此一來,我們就建立了一個不需要修改應用程式實作的服務發現介面,而與它執行在分散式與容錯的etcd儲存基礎上。

深入瞭解SkyDNS

如果你想了解SkyDNS的工作原理,可以使用SkyDNS映像中包含的dig工具:

docker exec -it dns sh
/ # dig @localhost SRV redis.identidock.local

執行後,DNS伺服器會回傳redis.identidock.local的SRV記錄,包含IP位址、連線埠號碼以及優先順序、權重和TTL等資訊。

SkyDNS除了使用傳統的A記錄(用於IPv4解析)外,還使用SRV(服務)記錄。SRV記錄包含服務的連線埠、存活時間(TTL)、優先順序和權重等欄位。設定TTL允許記錄在客戶端或代理未定期更新值時自動清除,這可用於實作容錯轉移和更優雅的錯誤處理機制。

其他功能包括將多個主機分組為地址池(可用作負載平衡的一種形式),以及支援將指標和統計資料發布到Prometheus和Graphite等服務。

Consul:HashiCorp的服務發現解決方案

Consul是HashiCorp提供的服務發現解決方案。除了作為分散式、高用性的鍵值儲存之外,它還提供高階健康檢查功能並預設包含DNS伺服器。

CAP理論在服務發現中的應用

在研究鍵值儲存和服務發現時,你會遇到CAP理論,該理論大致指出分散式系統不能同時滿足一致性(Consistency)、可用性(Availability)和分割槽容錯性(Partition tolerance)。

AP系統會優先考慮可用性而非一致性,所以讀寫操作幾乎總是可行(通常很快),但可能不是最新的資料。CP系統則優先考慮一致性,某些情況下寫入可能會失敗,但只要回傳資料,就會是正確與最新的。

實際上,情況更為複雜,尤其是對於Consul。etcd和Consul都根據Raft演算法,提供CP解決方案。然而,Consul有三種不同模式(“default”、“consistent"和"stale”),提供不同程度的一致性與可用性權衡。

佈署Consul

每個主機執行一個Consul代理例項,可以是伺服器模式或客戶端模式。代理可以檢查各種服務的狀態以及記憶體使用量等一般統計資料,將複雜性從客戶端應用中分離出來。通常會有3、5或7台主機(參見"最佳叢集大小")以伺服器模式執行代理,負責寫入和儲存資料,並且其他伺服器代理協作。客戶端模式的代理則將請求轉發給伺服器代理。

讓我們使用Docker容器開始設定Consul。首先,建立兩個新的虛擬機器:

docker-machine create -d virtualbox consul-1
docker-machine create -d virtualbox consul-2

現在啟動Consul容器,同樣將虛擬機器的IP儲存到變數中:

HOSTA=$(docker-machine ip consul-1)
HOSTB=$(docker-machine ip consul-2)
eval $(docker-machine env consul-1)
docker run -d --name consul -h consul-1 \
-p 8300:8300 -p 8301:8301 -p 8301:8301/udp \
-p 8302:8302/udp -p 8400:8400 -p 8500:8500 \
-p 172.17.42.1:53:8600/udp \
gliderlabs/consul agent -data-dir /data -server \
-client 0.0.0.0 \
-advertise $HOSTA -bootstrap-expect 2

這裡我們:

  • 以伺服器模式啟動Consul代理,並將資料儲存到/data目錄
  • 指定監聽客戶端API請求的地址,預設為127.0.0.1(僅容器內可存取)
  • 使用-advertise標記指定其他主機應該聯絡伺服器的地址,並設定-bootstrap-expect標記告訴Consul等待第二個伺服器加入叢集

在生產環境中,你應該使用無法從網際網路存取的私有地址。

啟動第二個容器,使用-join命令連線第一個伺服器:

eval $(docker-machine env consul-2)
docker run -d --name consul -h consul-2 \
-p 8300:8300 -p 8301:8301 -p 8301:8301/udp \
-p 8302:8302/udp -p 8400:8400 -p 8500:8500 \
-p 172.17.42.1:53:8600/udp \
gliderlabs/consul agent -data-dir /data -server \
-client 0.0.0.0 \
-advertise $HOSTB -join $HOSTA

我們可以使用Consul CLI檢查它們是否都已新增到叢集中:

docker exec consul consul members

探索Consul的鍵值儲存功能

我們可以透過設定和取得一些資料來瞭解鍵值儲存的工作原理:

curl -XPUT http://$HOSTA:8500/v1/kv/foo -d bar
curl http://$HOSTA:8500/v1/kv/foo | jq .

回傳的值"Value":“YmFy"看起來很奇怪,這是因為Consul會對資料進行base64編碼。我們可以使用jq和base64工具取得原始資料:

curl -s http://$HOSTA:8500/v1/kv/foo | jq -r '.[].Value' | base64 -d

這樣我們就能看到原始值"bar"了。

Consul有一個單獨的API用於新增服務,這與其服務發現和健康檢查功能相關。通常,鍵值儲存僅用於儲存設定詳細資訊和少量元資料。

使用Consul服務實作identidock跨主機工作

讓我們嘗試使用Consul服務在主機間佈署identidock。我們的目標是與之前相同的結構:Redis在consul-2上執行,identidock和dnmonster在consul-1上執行。

首先啟動Redis:

eval $(docker-machine env consul-2)
docker run -d -p 6379:6379 --name redis redis:3

然後透過/service/register端點告知Consul我們的Redis服務:

curl -XPUT http://$HOSTA:8500/v1/agent/service/register \
-d '{"name": "redis", "address":"'$HOSTB'","port": 6379}'
docker run amouat/network-utils dig @172.17.42.1 +short redis.service.consul

接下來,我們需要設定consul-1使用Consul進行DNS解析。與之前的etcd範例不同,我們將設定Docker守護程式而非主機的/etc/resolv.conf檔案。編輯/var/lib/boot2docker/profile檔案,包含–dns和–dns-search標記:

docker-machine ssh consul-1
sudo vi /var/lib/boot2docker/profile

設定內容應該類別似:

EXTRA_ARGS='
--label provider=virtualbox
--dns 172.17.42.1
--dns-search service.consul
'
CACERT=/var/lib/boot2docker/ca.pem
DOCKER_HOST='-H tcp://0.0.0.0:2376'
DOCKER_STORAGE=aufs
DOCKER_TLS=auto
SERVERKEY=/var/lib/boot2docker/server-key.pem
SERVERCERT=/var/lib/boot2docker/server.pem

–dns-search引數允許我們使用簡短名稱如"redis"而非完整名稱"redis.service.consul”。

完成後,重啟Docker守護程式並重新啟動Consul。最簡單的方法是重啟虛擬機器:

exit
eval $(docker-machine env consul-1)
docker start consul

進行簡單測試:

docker run redis:3 redis-cli -h redis ping

如果回傳"PONG",表示設定成功。

在consul-1上啟動dnmonster並新增服務:

docker run -d --name dnmonster amouat/dnmonster:1.0
DNM_IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} dnmonster)
curl -XPUT http://$HOSTA:8500/v1/agent/service/register \
-d '{"name": "dnmonster", "address":"'$DNM_IP'","port": 8080}'

最後,啟動identidock:

docker run -d -p 80:9090 amouat/identidock:1.0
curl $HOSTA

這樣我們就再次實作了不使用連結的identidock服務。

Consul的健康檢查功能

Consul最有趣的功能之一是支援健康檢查,確保系統各部分都正常執行。我們可以為主機節點本身(例如檢查磁碟空間或記憶體)或特定服務編寫測試。以下程式碼為dnmonster服務定義了一個簡單的HTTP測試:

curl -XPUT http://$HOSTA:8500/v1/agent/service/register \
-d '{"name": "dnmonster", "address":"'$DNM_IP'","port": 8080,
"check": {"http": "http://'$DNM_IP':8080/monster/foo",
"interval": "10s"}
}'

此檢查將確保容器以2xx狀態碼回應給定URL上的HTTP請求。注意,這個檢查必須在consul-1上執行才能透過。我們可以透過存取/health/checks/dnmonster/端點來檢查測試狀態:

curl -s $HOSTA:8500/v1/health/checks/dnmonster | jq '.[].Status'

如果回傳"passing",表示健康檢查透過。

測試也可以使用指令碼編寫,如果指令碼回傳0則透過,這允許進行任意複雜的檢查。透過將健康檢查與Consul的watches支援(用於監控資料更新)結合,可以相對容易地實作容錯轉移解決方案和/或自動通知管理員問題。

其他值得注意的功能包括對多資料中心的支援和網路流量加密。

服務註冊的自動化

在前面的例子中,我們手動執行了服務發現的最後一步——註冊;我們必須編寫curl請求,將Redis和dnmonster服務註冊到SkyDNS和Consul。在實際生產環境中,我們可以考慮將服務註冊邏輯新增到應用程式中,或使用專門的註冊工具來自動化這個過程。

在玄貓多年的容器化實踐中,我發現服務發現是構建可靠分散式系統的關鍵環節。無論是選擇根據etcd的SkyDNS還是功能更全面的Consul,都需要根據特定場景的需求權衡選擇。對於需要簡單DNS解析的環境,SkyDNS可能已經足夠;而對於需要健康檢查、多資料中心支援和更高階功能的大型佈署,Consul則提供了更完整的解決方案。

容器服務發現技術正在快速發展,理解這些基本概念和工具將幫助你設計更健壯、更靈活的容器化應用架構。隨著容器協調平台如Kubernetes的普及,服務發現機制也在不斷演變,但核心原理仍然適用。

Docker 服務發現與網路架構深度解析

在現代容器化環境中,服務發現與網路架構是構建可靠分散式系統的基礎。本文將探討Docker生態系統中的服務發現機制、DNS服務發現的優缺點,以及各種網路選項的實作方式。

容器外部服務的自動序號產生器制

如果我們無法或不想修改容器內部的服務,可以透過包裝指令碼(wrapper script)或稱為「副手程式」(side-kick process)的方式實作自動註冊。雖然dnmonster容器可以在啟動時自動執行此操作,但也可以開發一個服務,透過監控Docker事件來自動註冊新啟動的容器。

GliderLabs開發的Registrator正是為此目的而設計。Registrator可以與Consul、etcd或SkyDNS協同工作,提供容器的自動註冊功能。它的運作方式是監控Docker的事件流,當偵測到容器建立事件時,根據容器的元資料在底層框架中新增相關條目。

根據DNS的服務發現優缺點分析

在本章討論的許多解決方案中,都提供DNS介面作為服務發現機制。某些情況下,DNS是主要或唯一的介面;其他情況下,它作為API之外的便利功能存在。

DNS服務發現的優勢

  • 原生支援傳統應用 - 無需修改即可使用。雖然使用其他服務發現機制時可透過新增大使容器(ambassador container)來解決,但這需要開發和維運團隊投入額外精力。

  • 開發者無需學習新API - 使用DNS的應用程式不需修改即可在各種平台上執行,降低了學習成本。

  • 成熟與受信任的協定 - DNS擁有多種實作和廣泛支援,具有很高的可靠性。

DNS服務發現的劣勢

除了有人批評DNS速度慢(在服務發現場景中,我們談論的是快速的本地查詢,而非緩慢的遠端查詢),DNS服務發現還存在以下問題:

  • 缺乏埠號資訊 - 標準DNS查詢不會回傳埠號資訊,這要麼需要假設,要麼需要透過其他通路查詢(DNS SRV記錄確實包含埠號資訊,但應用程式和框架幾乎總是隻使用主機名)。

  • 可能存在快取延遲 - 應用程式和作業系統可能會快取DNS回應,導致服務移動時客戶端更新延遲。

  • 健康檢查和負載平衡支援有限 - 大多數DNS服務僅提供有限的健康檢查和負載平衡支援。負載平衡通常僅限於輪詢(round-robin)或隨機選擇,只能滿足部分使用者需求。健康檢查可以根據TTL構建,但更複雜的檢查通常需要額外的服務。

  • 客戶端實作複雜度增加 - 客戶端需要自由實作根據屬性(如可用API版本、容量等)選擇服務的邏輯。

其他服務發現解決方案

除了上述方案,還有幾種值得考慮的服務發現選擇:

ZooKeeper

ZooKeeper是一個集中式、可靠與隨時可用的資料儲存,用於協調Mesos和Hadoop中的服務。它使用Java撰寫,透過Java API存取,但也提供多種語言的繫結。客戶端需要維護與ZooKeeper伺服器的活動連線並執行保活(keep-alive)機制,這需要大量編碼工作(不過有Curator等函式庫幫助簡化這一過程)。

ZooKeeper的主要優勢是成熟、穩定與經過實戰檢驗。如果你已經有使用ZooKeeper的基礎設施,它可能是不錯的選擇。否則,整合和構建ZooKeeper的額外工作可能不值得,特別是當你不使用Java時。

SmartStack

Airbnb的服務發現解決方案,由兩個元件組成:

  • Nerve:負責健康檢查和註冊
  • Synapse:負責發現服務

Synapse在每個消費服務的主機上執行,為每個服務分配一個埠號,然後代理到實際服務。Synapse使用HAProxy進行路由,並在發生變更時自動更新和重啟HAProxy。它可以被設定為從儲存(如ZooKeeper或etcd)取得要代理的服務列表,或者監視Docker事件流以取得容器建立事件(類別似於Registrator)。

每個服務都會有相應的Nerve程式或容器,負責檢查服務的健康狀況並自動向Synapse使用的儲存(如ZooKeeper或etcd)進行註冊。

Eureka

Netflix為AWS環境設計的負載平衡和容錯移轉解決方案。它被設計為處理AWS節點短暫性的「中間層」解決方案。如果你打算在AWS基礎設施上執行大型服務,這絕對值得研究。

WeaveDNS

WeaveDNS是Weave網路解決方案的服務發現元件。容器在啟動時向WeaveDNS註冊其主機或容器名稱,提供完全自動化的解決方案。WeaveDNS作為Weave路由器的一部分在每個主機上執行,並且網路中的其他Weave路由器通訊,以便解析所有容器名稱。WeaveDNS還提供簡單的負載平衡形式。

docker-discover

本質上是SmartStack的Docker原生實作,使用etcd作為後端。與SmartStack一樣,它由兩個元件組成:docker-register(相當於Nerve,用於健康檢查和註冊)和docker-discover(相當於Synapse,用於發現)。與SmartStack一樣,docker-discover使用HAProxy處理路由。這是一個很有趣的專案,但缺乏更新和支援組織,意味著開發和支援可能不夠完善。

值得注意的是,Docker的新網路功能(「新Docker網路」)也透過服務物件提供有限形式的服務發現。這種服務發現依賴於Docker Overlay網路驅動或其他相容外掛(如Calico)。

Docker網路選項深度解析

正如我們所見,大使容器和服務發現解決方案都可用於在底層網路允許的情況下跨主機連線服務。然而,這需要透過主機暴露埠,這需要手動管理與擴充套件性不佳。更好的解決方案是提供容器之間的IP連線,這是本章描述的解決方案的重點。

在深入研究跨主機網路解決方案之前,瞭解預設Docker網路的工作方式和可用選項很重要。Docker提供四種基本網路模式:橋接(bridge)、主機(host)、容器(container)和無網路(none)。

橋接網路模式(Bridge)

預設的橋接網路在開發中非常出色,提供了讓容器相互通訊的無痛方式。但在生產環境中,它不那麼理想,因為背後所需的所有管道設定都有相當大的開銷。

Docker橋接網路通常名為docker0,執行在172.17.42.1上,用於連線容器。當容器啟動時,Docker會例項化一個veth對(本質上是乙太網電纜的軟體等效物),將容器中的eth0連線到橋接器。外部連線是透過IP轉發和設定IP偽裝的iptables規則提供的,這是網路地址轉換(NAT)的一種形式。

預設情況下,所有容器都可以相互通訊,無論它們是否被連線或是否匯出或發布埠號。你可以透過在啟動Docker守護程式時傳遞--icc=false標誌來阻止這一點,這將設定一個iptables規則。透過設定--icc=false--iptables=true,你可以只允許連線的容器通訊,這同樣是透過新增iptables規則實作的。

這一切都能工作,並且在開發中非常有用,但出於效率考慮,它可能不適合生產環境。

主機網路模式(Host)

使用--net=host執行的容器與主機分享網路名稱空間,完全暴露於公共網路。這意味著容器必須分享主機的IP位址,但也消除了橋接網路的所有管道設定,使其速度與正常主機網路一樣快。

由於IP位址是分享的,需要相互通訊的容器將不得不協調主機上的埠號,這需要一些思考,可能還需要修改應用程式。

這也有安全隱患,因為你可能無意中將埠號暴露給外部世界,這可以透過防火牆層管理。

顯著提高的效率意味著你可能需要考慮混合網路模式,其中導向外部和網路密集型容器(如代理和快取)使用主機網路,而其餘容器位於內部橋接網路上。請注意,你不能使用連結將橋接網路上的容器連線到主機網路上的容器,但橋接網路上的容器可以透過使用docker0橋接器的IP位址與主機上的容器通訊。

容器網路模式(Container)

使用另一個容器的網路名稱空間。這在某些情況下非常有用(例如,啟動一個預先設定了網路堆積積疊的容器,並告訴所有其他容器使用該堆積積疊)。這允許建立和輕鬆重用針對特定場景或資料中心架構專門設計的高效網路堆積積疊。缺點是所有分享網路堆積積疊的容器都需要使用相同的IP位址等。

這在某些情況下效果很好,尤其是被Kubernetes等系統採用。

無網路模式(None)

正如其名稱所示,完全關閉容器的網路。這對於不需要網路的容器非常有用,例如將輸出寫入卷的編譯器容器。

無網路模式也可用於需要從頭開始設定自己的網路時。如果是這種情況,你可能會發現像pipework這樣的工具對於在cgroups和名稱空間內使用網路非常有用。

新Docker網路架構

在撰寫本文時,Docker網路堆積積疊和介面正在經歷重大改造。這很可能在本章付印時完成,並代表Docker網路建立和使用方式的重大變化(雖然本章中的程式碼應該繼續正常工作)。本文根據實驗性Docker頻道發布的工作來討論這些變化。由於這尚未最終確定,因此這裡介紹的內容與最終進入穩定Docker發行版的內容可能存在細微差異。

最直接的變化是Docker獲得了兩個新的頂層「物件」:網路(network)和服務(service)。這允許「網路」與容器分開建立和管理。啟動容器時,可以將其分配給特定網路,它們只能直接連線到同一網路上的其他容器可以發布服務,允許透過名稱聯絡它們,取代了對連結的需求(連結仍然可以使用,但用處會減少)。

這透過一些例子可以更好地解釋。

network ls子命令將列出當前網路及其ID:

$ docker network ls
NETWORK ID          NAME                DRIVER
5e0c59170fdc        bridge              bridge              
0c0b2a0f7a87        host                host                
1e543e3ea359        none                null

在這個新的網路模型中,我們可以更靈活地定義容器間的通訊和隔離策略,為現代分散式系統的建構提供了更強大的基礎。

Docker的服務發現和網路架構提供了多種解決方案,從根據DNS的服務發現到專用工具如Registrator、ZooKeeper和SmartStack。每種方法都有其優缺點,選擇哪一種取決於特定的使用情境、基礎架構和技術要求。

網路模式方面,Docker提供了橋接、主機、容器和無網路四種基本選項,每種都適用於不同的場景。隨著Docker網路架構的演進,新增的網路和服務物件進一步增強了容器間通訊的靈活性和可管理性。

瞭解這些服務發現和網路選項的特性及其適用場景,對於設計和佈署可靠、高效的容器化應用至關重要。在規劃容器化架構時,應根據應用需求、安全考量和效能要求,選擇最合適的服務發現機制和網路設定。