# Chaos for Docker - Pumba(a?)

# Chaos Engineering

> *「混沌不是深淵，而是發現系統韌性的鏡子」*
> 
> ![](https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=1350&q=80 align="left")

## 混沌工程是什麼，不是什麼？

最常見的誤解是*混沌工程只是隨機地破壞營運環境中的事物*。

只要可以幫助我們確信系統可以抵禦突發事件的方法其實都可以稱為混沌工程。混沌工程主要*透過實驗性的方法，從而建立對系統抵禦營運環境中突發事件能力信心的工程*。換句話說，混沌工程是一種透過主動注入故障來驗證系統韌性的方法，目標是在真實故障發生前暴露系統脆弱性。

### 混沌工程 v.s. 故障演練 v.s. 測試 的差別

混沌工程實驗聽起來與另外兩個很像, 但目的其實差異很大。

* **混沌工程 v.s. 測試**
    

測試目的是關注**已知的**斷言是否通過. 混沌工程關注的是**發現更多未知的**暗債。

* **混沌工程 v.s. 故障演練**
    

故障演練側重於操練**已知的**故障應對流程, 混沌工程側重透過實驗發現更多**未知的暗債**。

### 在營運環境上執行實驗, 會有風險

其實任何有人在使用的系統都會有所謂的暗債, 未知的意外可能發生, 這些都是不可預測的. 即使我們不主動引入故障風險, 其實這些風險本來就存在著, 不是不會發生只是時候未到. 所以混沌實驗要定義最小化爆炸半徑來進行實驗.

其次團隊文化也不該一昧要求工程團隊不能也不應該出現任何問題或失誤. 一但有這樣強硬的規範定案下去, 以後在未知的故障出現時, 大部分會出現甩鍋情況, 耽誤了更多發現更多暗債以及設計防範措施的時間.

### 嘗試看看

團隊如果有以下理由的話，不坊考慮實踐混沌工程試試看︰

* 確定風險與成本，並設定 SLI、SLO 與 SLA。
    
* 在整體上測試系統（相比於端對端的測試，更加全面）。
    
* 找到團隊忽略的\*\*湧現性（Emergence）\*\*特性。在複雜系統中，服務與組件之間的複雜交互可能導致意想不到的系統行為。例如，多個服務自己正常運作，但組合在一起時於高負載下出現極大的延遲或故障發生。
    

實驗的 4 個步驟︰

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1739893197712/ac293dd8-e92b-426d-a9e2-996b623aecf5.gif align="center")

**定義穩態指標**

定義什麼是正常的，這取決於該系統跟目標。

假設在測試車輛，車輛的穩態可能涉及速度、機械狀態（如無刮傷）、路線等。這說明穩態指標需根據具體系統的目標而定，不同系統有不同的關鍵參數。我們目標可以是輛在無刮傷的情況下以80公里/小時的速度完成指定路線，也能是其他的條件視為正常。為此我們可以定義幾個要素：**可測量性**、**目標相關性**、**容許範圍**、**時間持續性**。例如，車輛的例子中，速度需可測量，80公里是目標值，允許一定波動範圍，並需在整個行駛時間內維持。

**穩態指標的四大要素**

| **要素** | **定義說明** | **車輛測試案例** | **系統測試案例** |
| --- | --- | --- | --- |
| **可測量性** | 指標是否可以被量化和測量，並具有客觀數據支持 | 速度可由傳感器測量；車體狀態可由影像辨識檢測 | API延遲時間可由監控工具測量；錯誤率可通過日誌分析獲取 |
| **目標相關性** | 指標是否與系統的業務目標或測試目標直接相關 | 車輛需在無刮傷的情況下以80公里/小時完成指定路線 | 99%的請求需在500ms內完成；系統需保持99.95%的可用性 |
| **容許範圍** | 指標允許的波動範圍，超出範圍則視為異常 | 速度允許±5公里/小時；刮傷深度需≤2毫米 | 延遲時間P99需≤500ms；錯誤率需≤0.1% |
| **時間持續性** | 指標需要在多長時間內保持穩定，才能被認為系統處於穩態 | 需在整個行駛過程中（如1.5小時）保持穩定 | 每月需保持99.95%的可用性，或在1分鐘內恢復異常 |

最常見的目標大概是「9x %的使用者可以在 xxx ms 內訪問系統的 API」。要定義哪些穩態指標，應該直接由業務策略來驅動設計的。能參考 Alex 的 [Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets](https://www.amazon.com/Implementing-Service-Level-Objectives-Practical/dp/1492076813) 一書。

但我們前面有說混沌工程是更加全面的。其實很多因素都會影響這個指標。例如系統程序是否獲得足夠的 CPU time 與 memory？或者這些資源正在被其他程序佔用著。是不是 kernel 把 CPU 分配給令一個更高優先權的程序？諸如此類的。

**設計實驗假設**

在這階段，我們要把直覺塑造成一個可驗證的假設，在一個明確定義的問題出現時，你的系統會發生什麼情況的有根據的猜測。系統會繼續工作嘛？還是逐漸變慢？變慢了多少？

現實中容易出問題的場景大概常見的如下︰

* 環境事件（斷電、水災、地震…）
    
* 硬體故障（CPU, Mem, Disk, Switch、變壓器）
    
* 系統資源（CPU, Mem, 硬碟空間、頻寬）
    
* 軟體問題（Bug, Crash, Leak, Hack attack)
    
* 系統瓶頸
    
* 不可預期的湧現性特性
    
* 硬體的 bug
    
* 人為失誤（設定錯誤、不小心關機….）
    

**最簡案例**

o11y 顯示的是我們跟使用者是否能夠成功調用 API。穩態指標是 API 都能成功的回覆。實驗假設是如果 cache 失效了，依然能獲得回覆。執行實驗，發現現在版本存在缺陷，新版本修復後正常。

| **實驗階段** | **關鍵動作** | **量化指標範例** |
| --- | --- | --- |
| 定義穩態指標 | 確立系統正常行為基準 | API P99 延遲 &lt; 200ms，錯誤率 &lt; 0.1% |
| 設計實驗假設 | 明確預期故障影響範圍 | 當 30% 節點失效時，服務降級但不停機 |
| 執行故障注入 | 可控環境下觸發故障 | 隨機終止 Pod，網路延遲 500±50ms |
| 觀察與分析 | 比對穩態指標偏差 | 錯誤率飆升至 15%，自動擴容觸發延遲 |

### 常見誤區澄清表

| **迷思** | **現實** |
| --- | --- |
| 隨機製造混亂 | 精準注入故障模式 |
| 營運環境專用 | 建議先在預發環境驗證 |
| 一次性活動 | 需納入CI/CD常態流程 |
| 僅測試基礎架構 | 應包含業務邏輯驗證 |

# Pumba(a)

![The Lion King Pumbaa and Simba Sticker - Sticker Mania](https://mystickermania.com/cdn/stickers/cartoons/lion-king-pumba-simba-512x512.png align="left")

[Pumba](https://github.com/alexei-led/pumba?tab=readme-ov-file) 是一個針對 Docker 容器進行混沌測試的工具，可以終止容器（kill、stop、remove），模擬網路問題（netem）、對容器的 cgroup 資源進行壓力測試（stress）等。

### **終止容器**

模擬隨機服務崩潰的情境，來測試系統的容錯能力（Fault Tolerance）與高可用性（High Availability）。

**Pause** 與 **Stop** 的區別是，`pause` 只是 **凍結程序**，不會讓容器真正停止或重啟。`stop` 會 **完全停止** 容器，需要手動或透過 `restart` 設定來啟動。

```bash
pumba --signal SIGKILL/SIGINT \
    kill \
    <container_name>

# 隨機 kill 開頭名為 "killme" 的容器 (每 30 秒一次)
pumba --random \
      --interval 30s \
      kill \
      're2:^killme*'

# 每 15 秒，會隨機挑選 名稱以 stopme 開頭 的容器，並執行 stop 命令讓它暫停運行。
# 容器會 停 7 秒，然後依據 Docker 的 restart 設定可能會自動重啟
# （如果 Docker 配置為 restart=always 或 restart=on-failure，則容器會重啟）。
pumba --interval=15s \
    --random \
    stop \
    --duration=7s \
    --restart \
    're2:^stopme*'

# 每 15 秒，pumba 會對名為 testme 的容器執行 pause 命令。
# 容器會 暫停 10 秒，10 秒後，容器會自動恢復執行。
pumba --interval=15s \
    pause \
    --duration=10s  \
    testme
```

### **模擬網路問題**

pumba **netem** 命令用來模擬不同的網路異常，例如 **延遲**（delay）、**封包丟失**（loss）、**封包重複**（duplicate）、**封包損壞**（corrupt） 以及 **頻寬限制**（rate）。這些功能對於 混沌工程（Chaos Engineering） 或 網路測試 非常有幫助。

但還是得看應用場景，決定合適的模擬數值。

| **應用場景** | **適用性** | **建議模擬設定** |
| --- | --- | --- |
| **衛星通訊模擬** | ★★★★★ | 維持 3000ms 延遲，抖動 ±50ms，封包丟失 2% |
| **物聯網（IoT）設備測試** | ★★★☆☆ | 延遲 100-500ms，封包丟失 1-3% |
| **金融交易系統（HFT）** | ☆☆☆☆☆ | 應確保延遲低於 50ms，避免網路異常 |
| **5G 通訊測試** | ★★★★☆ | 延遲 50ms，封包丟失 0.5%，模擬擁塞情境 |
| **邊緣運算（Edge Computing）** | ★★★☆☆ | 延遲 500ms，封包丟失 2-5% |
| **遊戲伺服器壓測** | ★★★★☆ | 延遲 100ms，封包丟失 1%，模擬真實網路環境 |

```bash
# 每20秒觸發一次對 ping 名稱容器執行 Delay 動作，
# 每次模擬持續 10 秒，增加基礎延遲 3000 ms，延遲抖動± 20ms
# 針對Alpine Linux ，--tc-image=gaiadocker/iproute2，pumba 需要 tc（Traffic Control）來執行網路模擬，
# 但 tc 可能沒有內建在 Docker 容器裡。告訴 pumba 使用 gaiadocker/iproute2 這個 image 來執行 tc 命令。
# correlation 20，代表 20% 的相關性，用來控制 隨機延遲的變動程度。
# 如果沒有 correlation，每次延遲程度都是隨機的。
# 有設定的話，多少會受到上一個封包的延遲影響。這讓延遲變動較為平滑，而不會完全隨機跳動。
pumba --interval=20s \
    netem \
    --tc-image=gaiadocker/iproute2 \
    --duration=10s delay \
    --time=3000 \
    --jitter=20 \
    --correlation 20 \
    ping

# 每20秒觸發一次對開頭是 payment-service 名稱容器執行 Loss 動作，
# 讓 15% 的封包在傳輸過程中被丟棄。
# duplicate 與 corrupt 用法與 loss 一樣。
pumba --duration 20s \
    netem \
    --tc-image gaiadocker/iproute2 \
    loss --percent 15 \
    're2:^payment-service'

# 限制名為 mycontainer 的容器出站流量，最高頻寬為 512 kbit/s。
pumba netem rate \
    --rate 512kbit \
    mycontainer

# 限制開頭名為 web 的容器出站流量，最高頻寬為 1 Mbit/s。
# 每個封包額外增加 50 bytes 的開銷，模擬實際網路傳輸中的額外payload。
pumba netem rate \
    --rate 1mbit \
    --packetoverhead 50 \
    're2:^web'
```

### 壓力測試

`pumba stress` 是 Chaos Engineering 工具 Pumba 中用於對容器施加壓力測試的命令，主要透過 `stress-ng` 工具模擬 CPU、記憶體、I/O 等資源的高負載情境，也能驗證集群自動擴展能力。或者測試記憶體有無洩漏問題，監控 GC 的行為。

```bash
# 對名為 web-server 的容器施加 4 個 CPU 核心的壓力，持續 30 秒
pumba stress \
    --duration 30s \
    --stressors "--cpu 4" \
    web-server

# 對開頭名為 web 的容器們進行 4 核心 CPU 和
# 啟用 2個獨立的記憶體壓力執行緒 ，各自分配 2GB 虛擬記憶體空間做測試，持續 1 分鐘
pumba stress \
    --duration 1m \
    --stressors "--cpu 4 --vm 2 --vm-bytes 2G" \
    "re2:^web"
```

# 案例整合 OpenTelemetry

使用上次 [DevOps meetup的 OpenTelemetry Demo專案](https://github.com/tedmax100/devops_meetup)來演示。只是加入 Pumba 。

### 對指定容器注入 delay

```yaml
  # Pumba
  pumba:
    image: gaiaadm/pumba:latest
    container_name: pumba
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command:
      - --log-level=info
      - --interval=30s
      - netem
      - --tc-image=gaiadocker/iproute2
      - --duration=10s
      - delay
      - --time=3000
      - --jitter=500
      - checkout-service
```

注入前

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740151775018/71e7dcaf-4b82-4422-a409-9829082a646b.png align="center")

注入後

可以看見，延遲突然提高到 5秒以上，但其實也不是全部請求都這麼久。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740151824527/b83b0f28-18d6-4b2e-b778-7d9307184549.png align="center")

點擊圖上的 data point，來看該 request 的 tracing，能發現瓶頸是卡在 checkout service，因為 pumba 就是針對 checkout service 做注入咩。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740152087088/f0dd7869-ae5d-4031-9972-af24823c639e.png align="center")

再仔細看，其實也不是 checkout service 每個都很慢，那到底問題出在哪？  
其實 pumba delay 是針對 **egress** 方向，也就是該容器對外發出的流量。所以 checkout service 要回應給 fronted 會變慢。然後下圖 checkout service 對 cartservice 的請求也是會受到影響，明明 cartservice 回應不到 1ms，但 checkut service 好幾秒才收到回應。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740152329950/16a07dfb-7e2d-4cfa-a627-f8080de5598d.png align="center")

### 對隨機容器進行 Stop

```yaml
  # Checkout service
  checkoutservice:
    restart: always
    labels:
      - "chaos=true"  

  # Cart service
  cartservice:
    restart: always
    labels:
      - "chaos=true"  

  # Pumba
  pumba:
    image: gaiaadm/pumba:latest
    container_name: pumba
    privileged: true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command:
      - --log-level=info
      - --random
      - --interval=15s      
      - --label=chaos=true
      - stop
      - --duration=5s
      - --restart
```

注入前，能看見好幾分鐘沒錯誤，請求也正常，API 請求沒有 500。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740155978799/d048b2f2-20be-4062-91d1-651941656c3f.png align="center")

注入後，首先能看到 pumba log，隨機挑選滿足 labels 條件的容器進行 stop， 5秒後啟動。

```yaml
pumba  | time="2025-02-21T16:38:52Z" level=info msg="stopping container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service signal=SIGTERM timout=5
pumba  | time="2025-02-21T16:38:57Z" level=info msg="starting container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service
pumba  | time="2025-02-21T16:39:07Z" level=info msg="stopping container" dryrun=false id=883cb3ae71fb89a48ab63601be61729e548781832cb318affdce441004d48a46 name=/cart-service signal=SIGTERM timout=5
pumba  | time="2025-02-21T16:39:12Z" level=info msg="starting container" dryrun=false id=883cb3ae71fb89a48ab63601be61729e548781832cb318affdce441004d48a46 name=/cart-service
pumba  | time="2025-02-21T16:39:22Z" level=info msg="stopping container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service signal=SIGTERM timout=5
pumba  | time="2025-02-21T16:39:27Z" level=info msg="starting container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service
pumba  | time="2025-02-21T16:39:37Z" level=info msg="stopping container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service signal=SIGTERM timout=5
pumba  | time="2025-02-21T16:39:42Z" level=info msg="starting container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service
pumba  | time="2025-02-21T16:39:52Z" level=info msg="stopping container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service signal=SIGTERM timout=5
pumba  | time="2025-02-21T16:39:57Z" level=info msg="starting container" dryrun=false id=5499b7067bd5f66e911970435dea064cd5e1f4fbd58c3f473f18c3fc20c9a8d2 name=/checkout-service
```

也能看見下圖，開始出現大量 500的請求，但其實也不是每筆都失敗的，甚至因為失敗的請求導致回應也提高，產生長尾效應。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740156197967/5e87a684-0fd9-4002-afd1-067de9fb31b0.png align="center")

# 總結

對於混沌工程的測試，我沒提到太多，畢竟我經驗也不多。但是有 Pumba、chaos monkey 或是 [xk6-disruptor](https://github.com/grafana/xk6-disruptor)，都蠻方便針對容器化環境進行實驗。只是大部分都需要 K8S 環境，但 K8s 我沒那麼熟稔，剛好 Pumba滿足我的需求又好上手。

但我還是多說幾句，給團隊一些時間來實驗，發現未知的暗債問題，並主動發起討論和應對。會比起趕鴨子上架，硬性要求不能出錯，長遠來看對團隊能力以及文化發展更佳的好。

可觀測性驅動工程搭配混沌工程實驗，可以讓團隊對於複雜系統的未知問題，很方便的在迭代中進行探索。

# **References**

[Pumba](https://github.com/alexei-led/pumba)

[Pumba Image](https://hub.docker.com/r/gaiaadm/pumba/)

[**CHAOS MONKEY ALTERNATIVES**](https://www.gremlin.com/chaos-monkey-alternatives/docker)

[Chaos Testing for Docker Containers - Alexei Ledenev (Codefresh)](https://youtu.be/68ZepHa5UVg?si=I1dXqkeGGBrnAMEX)

[Microservice resilience testing with Docker & Pumba](https://youtu.be/ihxzVNDNF18?si=6v0fsM6cbQsPQfMu)
