返回文章列表

容器化應用程式擴充套件性與可處置性實踐

本文探討如何利用 Docker 建構具備擴充套件性和可處置性的強韌容器化應用程式,涵蓋系統資源監控、突破容器數量限制、健康檢查、日誌管理以及開發與生產環境一致性等關鍵導向,並輔以程式碼範例與實務操作說明。

Web 開發 系統設計

在容器化時代,應用程式的擴充套件性和可處置性是確保系統強韌性的關鍵。本文探討如何運用 Docker 和相關技術,實作應用程式在面對高負載和故障時的穩定性和可靠性。首先,透過監控系統資源,例如記憶體和磁碟空間,可以預先評估系統容量,並針對潛在瓶頸,例如網路驅動程式限制,提出解決方案,確保應用程式能夠順利擴充套件。接著,文章討論瞭如何實踐容器的可處置性,包含手動刪除容器觀察 Docker Swarm 的自動還原機制,以及使用標籤和 Docker Chaos Monkey 進行容錯測試。此外,文章也強調了健康檢查的重要性,說明如何利用 HEALTHCHECK 指令確保容器在啟動後能正常提供服務,並探討瞭如何在 Dockerfile 中設定健康檢查選項。最後,文章強調了開發與生產環境一致性的重要性,並提供程式碼範例,展示如何測試應用程式相依性和組態,以及如何使用 Docker Compose 簡化多容器應用程式的管理。

擴充套件性與可處置性:建構強韌的容器化應用

在現代化的軟體開發與佈署中,擴充套件性和可處置性是確保應用系統強韌性的兩個關鍵因素。本文將探討如何利用 Docker 與其相關技術來實作這兩個目標。

VIII. 平行性 - 透過程式模型實作擴充套件

為了實作應用的橫向擴充套件,我們需要有效地利用平行處理能力。在 Docker 環境中,這意味著要能夠啟動和管理大量的容器。

系統資源監控

在進行擴充套件之前,首先需要監控系統的資源使用情況。我們可以使用 free -h 指令來檢查記憶體的使用狀態。

root@swarm1:~# free -h
              total        used        free      shared  buff/cache   available
Mem:           7.8G        3.9G        1.6G         62M        2.3G        3.6G
Swap:          2.0G         18M        2.0G

同時,也需要檢查磁碟空間的使用情況,可以使用 df 指令。

root@swarm1:~# df /dev/sda1 -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1       156G  2.2G  146G   2% /

突破容器數量限制

當需要執行超過 1000 個容器時,預設的網路驅動程式可能會成為瓶頸。這時,可以考慮使用不同的虛擬網路驅動程式,如 OpenVSwitch,或者重新編譯 Linux 核心以修改相關限制。

IX. 可處置性 - 最大化強韌性,實作快速啟動與優雅關閉

十二因子應用(12FA)的原則之一是應用的程式應該是可處置的,這意味著它們可以隨時被啟動或停止。這種設計哲學促進了彈性擴充套件、快速佈署和生產環境的強韌性。

處置容器的實踐

為了測試容器的可處置性,我們可以手動刪除正在執行的容器,並觀察 Docker 的反應。

首先,使用 docker psgrep 命令找出特定的容器,並使用 xargsdocker rm -f 強制刪除它們。

# docker ps -a | grep 'gotwitter\.[0-9]' | head -n 3 | awk '{print $1}' | xargs docker rm -f

接著,使用 docker service ls 命令檢查服務的狀態。

# docker service ls
ID            NAME        MODE        REPLICAS  IMAGE
j204e4p7q70q  gotwitter   replicated  997/1000  titpetric/gotwitter:latest
syhtbxnz8zhh  redis       replicated  1/1       redis:3.2-alpine

Docker 將會自動檢測到容器的失敗,並在一段時間後重新啟動新的容器,以維持服務的預期副本數量。

利用標籤過濾服務

為了更好地管理可處置的容器,我們可以在建立服務時新增特定的標籤,如 role=disposable

root@swarm3:~# docker service create \
-e PORT=81 \
--replicas 3 \
--network party-swarm \
-l role=disposable \
--name gotwitter-disp \
-p 81:81 titpetric/gotwitter

然後,使用 docker service ls 命令並結合 --filter 引數來篩選具有特定標籤的服務。

root@swarm3:~# docker service ls --filter=label=role="disposable"
ID            NAME           MODE        REPLICAS  IMAGE
nfnaya4wqj67  gotwitter-disp replicated  3/3       titpetric/gotwitter:latest

Docker Chaos Monkey

為了進一步測試系統的強韌性,可以使用類別似 Netflix 的 Chaos Monkey 的工具。我們開發了一個名為 docker-chaos-monkey 的工具,它能夠隨機終止標記為可處置的容器中的一個,以測試系統的容錯能力。

使用 docker-chaos-monkey 可以幫助我們確保系統在面對容器故障時的穩定性和可靠性。

IX. 可丟棄性 - 透過快速啟動與優雅關閉提升強健性

在建置應用程式的過程中,系統需要處理新應使用案例項的啟動與舊例項的淘汰。為了將可能的故障降到最低,新服務的啟動時間必須盡可能縮短。Docker Swarm 能夠處理滾動升級,但 Docker 直到最近才加入了對容器健康檢查的支援。要使用此功能,至少需要 Docker 1.12。

健康檢查

撰寫 HEALTHCHECK 指引可以讓你檢查 Docker 容器的狀態,並向 Docker 提供額外的資訊,以判斷你的應用程式是否已準備好處理請求。例如,啟動 MySQL 例項可能需要超過 10-20 秒的時間,容器才會準備好為客戶端提供查詢服務。在某些情況下,即使是像 nginx 這樣的網頁伺服器也可能需要一些時間才能準備就緒,Go 應用程式也是如此。

測試應用程式可連線性的一種簡單方法是對應用程式的服務埠發出 HTTP 請求。這是另一個組態需要來自環境的原因——健康檢查不會意識到任何可能傳遞給應用程式的命令列選項,而且如果你正在使用 godotenv 的 .env 檔案,你需要使用類別似簡單的 bash 指令碼來實作從該檔案讀取組態。或者,就像你應該做的那樣,直接使用環境變數!

一個簡單的 curl 檢查應該足以判斷你的應用程式是否已準備好提供請求。使用 -f 選項(簡寫為 --fail),curl 將產生一個非零的離開程式碼,這將告訴 Docker 該容器尚未準備就緒。

HEALTHCHECK --interval=60s --timeout=60s CMD curl -f http://localhost:$PORT/api/health || exit 1

內容解密:

  1. HEALTHCHECK:定義容器的健康檢查指令,讓 Docker 可以監控容器的健康狀態。
  2. --interval=60s:設定健康檢查的執行間隔為 60 秒,也決定了第一次健康檢查的時間。Docker 將來可能會改進此選項,以在健康檢查透過後實施某種形式的退避機制。
  3. --timeout=60s:設定健康檢查的超時時間為 60 秒。如果檢查在這段時間內未完成,將被視為失敗。
  4. CMD curl -f http://localhost:$PORT/api/health || exit 1:定義具體的健康檢查命令。使用 curl 對指定 URL 發起請求,並透過 -f 引數確保在請求失敗時傳回非零離開碼。如果 curl 命令失敗,則執行 exit 1 以傳回預期的離開碼,表示健康檢查失敗。

健康檢查選項

  • --interval:指定健康檢查的執行頻率,預設為 30 秒。
  • --timeout:指定健康檢查的超時時間,預設為 30 秒。
  • --retries=N:指定在將容器標記為不健康之前,健康檢查失敗的次數,預設為 3 次。

這些選項可以在 docker service createdocker service update 命令中控制,從而允許你在不同環境中調整健康檢查的行為。

X. 開發與生產一致性 - 盡可能保持開發、預發布和生產環境的一致性

Docker 映象包含了執行應用程式所需的一切。應用程式可能會依賴外部資料,這些資料可以透過環境、檔案系統或後端服務傳遞。無論在哪個環境中執行容器,這些外部來源都可能會有所不同。但有一點是相同的——Docker 映象。使用相同的映象可以在所有環境中執行。

為什麼要有這條規則?

Docker 基本上立即解決了這條規則的問題,但回顧這條規則存在的原因仍然是有益的。

“它在我的機器上能執行!”

通常,最常見的藉口是:“它在我的機器上能執行!”但是你的機器上安裝了你認為合適的軟體,要使用 Vagrant 或其他工具來管理開發環境需要很大的自律。

Docker 映象可以包含你能想到的任何軟體包,並且製作 Docker 鴻像的所有步驟都被記錄下來。這是自動化的極致——它就像你設定虛擬機器所做的一切,除了最終結果是你可以在任何時候升級它,同時保留舊版本,並且“它在我的機器上能執行!”這種藉口再也不會被提起。

ENV PORT=3000

內容解密:

  1. ENV PORT=3000:在 Dockerfile 中宣告環境變數 PORT,並在環境變數不存在時將其設定為 3000。如果該變數已存在,則保持其原有值。這個變數隨後會被用於後續的命令中,以確保應用的連線埠組態正確。

開發與生產環境的一致性:確保開發、測試與生產環境盡可能相似

在開發與維運的過程中,保持開發、測試與生產環境的一致性是至關重要的。這不僅能夠減少因環境差異導致的錯誤,也能提高整體開發效率與系統的可靠性。

升級與測試

在虛擬機器或 VPS 上進行升級時,通常的做法是建立新的環境、安裝所需的軟體,然後測試哪些軟體會出現相容性問題。較好的專案至少會有單元測試,可以在出現問題時及時報錯。然而,除了軟體之外,基礎設施的測試也是必不可少的。

在建立 Docker 映像檔的過程中,一個常見的做法是測試軟體的功能是否正常。這些測試不僅僅侷限於版本限制,還包括了對特定功能的驗證。例如,檢查 LUA 是否正確安裝並且能夠正常運作。

#!/bin/bash
set -e
TEST=$(echo 'print("hello")' | lua -)
if [ "$TEST" == "hello" ]; then
    echo "Lua check passed OK"
else
    echo "Lua check failed, unexpected output: ${TEST}"
    exit 1
fi

上述指令碼檢查 LUA 是否能夠正確執行簡單的 print 命令。如果輸出不符合預期,則測試失敗並離開。

內容解密:

  1. set -e:此命令使指令碼在遇到錯誤時立即離開,避免繼續執行可能導致更多問題的命令。
  2. TEST=$(echo 'print("hello")' | lua -):此行將一個簡單的 LUA 指令碼傳遞給 lua 命令執行,並將輸出儲存在 TEST 變數中。
  3. if [ "$TEST" == "hello" ]; then:檢查 TEST 變數的值是否為 “hello”,如果是,則表示 LUA 執行正確。
  4. exit 1:如果測試失敗,則以非零狀態離開,表示指令碼執行過程中出現了錯誤。

同樣地,也可以對 NGINX 組態進行測試,確保其正確性。

#!/bin/bash
set -e
CONFIG_FOLDER=$(realpath -s $0 | cut -d'.' -f1)
CONFIG="$CONFIG_FOLDER/nginx.conf"
echo "NGINX Testing" $CONFIG
nginx -c ${CONFIG} -t

內容解密:

  1. CONFIG_FOLDER=$(realpath -s $0 | cut -d'.' -f1):取得指令碼檔案的路徑,並去掉副檔名,用於定位組態檔案。
  2. nginx -c ${CONFIG} -t:使用 nginx 命令測試指定的組態檔案是否正確。

對於更複雜的元件,如 ImageMagick、LUAJIT 等,進行測試尤為重要,因為它們可能會因為版本或相依性問題而導致功能失效。

#!/bin/bash
set -e
git clone https://github.com/titpetric/magick.git
cd magick
luarocks make
TEST_FOLDER=$(realpath -s $0 | cut -d'.' -f1)
cd $TEST_FOLDER
echo -n "LUA magick test: "
luajit test.lua
if [ ! -f "output.png" ]; then
    echo "LUA test failed, no output.png (magick)"
    ls -la .
    exit 1
fi
echo "PASS"

內容解密:

  1. git clone https://github.com/titpetric/magick.git:下載特定的 GitHub 專案。
  2. luarocks make:使用 luarocks 編譯專案。
  3. luajit test.lua:執行測試指令碼,檢查功能是否正常。
  4. if [ ! -f "output.png" ]; then:檢查測試是否產生預期的輸出檔案。

多個元件的協同運作

在真實世界的應用中,系統往往由多個元件組成,包括負載平衡器、網頁伺服器、資料函式庫伺服器、快取伺服器等。為了保持開發與生產環境的一致性,需要考慮這些元件之間的互動。

Docker Compose 成為了一個非常有用的工具,它允許開發者定義和執行多容器的 Docker 應用程式。

version: '3'
services:
  web:
    build: .
    ports:
      - "8080:80"
    depends_on:
      - db
    volumes:
      - .:/app
  db:
    image: postgres
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

內容解密:

  1. version: '3':指定 Docker Compose 的版本。
  2. services:定義服務及其組態,如建置方式、連線埠對映、依賴關係等。
  3. volumes:掛載本地目錄到容器中,方便開發時直接修改程式碼並在容器中生效。

Docker Compose 使得在開發、測試和 CI 環境中啟動和管理多個容器變得更加簡單。然而,它主要用於單一主機環境,並不適合用於生產環境中的複雜佈署需求。

將日誌視為事件流(Logs - Treat logs as event streams)

12 因素應用程式的日誌處理原則

12 因素應用程式指出,一個遵循 12 因素原則的應用程式絕不應關注其輸出流的路由或儲存。應用程式不應嘗試寫入或管理日誌檔案。相反,每個執行的程式都應將其事件流以無緩衝的方式寫入 stdout。在本地開發過程中,開發人員可以在終端機的前台檢視此串流,以觀察應用程式的行為。

日誌處理的兩大關注點

  1. 即時檢視輸出:當在前台執行 Docker 容器時,應能即時檢視輸出內容。這可能包括存取日誌、操作稽核日誌等各種輸出。

    # docker run -it --rm debian:jessie bash -c 'for i in {1..5}; do echo $i; done'
    1
    2
    3
    4
    5
    

    這使得開發人員和維運人員能夠即時觀察應用程式的行為,無論是在開發環境還是在生產環境中。當應用程式以後台服務方式執行時,可以使用 docker logs [容器名稱] 命令檢視所有輸出,或使用 docker logs -f [容器名稱] 即時跟蹤輸出。

  2. 避免在應用程式中管理日誌路由和儲存:如果應用程式內部處理日誌的路由和儲存,這些功能可能會像應用程式的其他部分一樣發生故障。遵循單一職責原則,應用程式應專注於其主要功能,而非同時負責收集、儲存和管理日誌。這樣可以提高整體的可靠性,因為某一部分的功能故障不會影響到其他部分。

後端服務在日誌處理中的角色

後端服務可以用於儲存實時日誌串流於資料函式庫或專門設計用來處理大量資料的服務中。區分日誌儲存和 stdout 輸出的原因是為了控制輸出冗餘度。標準輸出串流通常用於系統操作日誌,而非整個服務的存取日誌。可以透過環境組態來決定是否將存取日誌輸出到 stdout(用於開發環境),或是儲存到獨立的後端服務中。

發生錯誤時的處理

當應用程式發生錯誤並離開時,這些事件應可見於 stdout/ stderr 事件串流中。這些事件應被轉發到專門處理維運相關事件的服務。同時,對於高流量的 API 請求日誌,可能不適合直接混入同一個輸出串流中,因為這會導致冗餘資訊過高,難以在其中找到異常資訊。結構化日誌(Structured Logging)旨在解決這類別問題。

使用 Papertrail 蒐集 Docker 日誌

為了能夠檢視 Docker 容器內應用程式產生的日誌,需要引入後端服務來處理這些日誌、儲存它們,並提供可視性。這裡介紹瞭如何使用 Papertrail 和 gliderlabs/logspout 蒐集和檢視 Docker 日誌。

設定步驟:

  1. 在 Papertrail 建立一個新的 Log Destination,例如命名為 “serenity”。

  2. 在主機上執行 logspout 容器,將日誌傳送到 Papertrail:

    # docker run --restart=always -d -h logspout \
      -v=/var/run/docker.sock:/var/run/docker.sock \
      gliderlabs/logspout \
      syslog://logs5.papertrailapp.com:41062
    

    需要將 logs5.papertrailapp.com:41062 替換為實際建立的 Log Destination 的位址。

Papertrail 的功能

  • 即時檢視日誌:Papertrail 提供了一個搜尋介面,可以過濾日誌。可以針對特定的容器或關鍵字進行搜尋。
  • 設定警示:Papertrail 允許設定警示,以便在偵測到特定日誌事件時通知相關人員。