👏

カオスエンジニアリングツールのPumbaを触ってみた

に公開

Pumbaでカオスエンジニアリングをはじめよう

はじめに

本記事では、Dockerコンテナ環境でカオスエンジニアリングツールPumbaを使って、システムの耐障害性をテストする方法を紹介します。

カオスエンジニアリングとはなんなのか等については前回の投稿に記載してあります。
https://zenn.dev/rocket21/articles/b4f87c4359d11c

Pumbaとは

Pumba は、Dockerコンテナ環境に特化したカオスエンジニアリングツールです。コンテナの停止、ネットワーク遅延の追加、リソース制限など、様々な障害シナリオを簡単にシミュレートできます。

Pumbaの特徴

  • Dockerコンテナに対して様々な障害を注入可能
  • コマンドラインから簡単に実行できる
  • 定期的な障害注入が可能
  • 正規表現でターゲットコンテナを指定可能

サンプルアプリケーション構成

今回使用するサンプルアプリケーションは、以下のコンテナで構成されています:

services:
  db:        # PostgreSQLデータベース
  web:       # Railsアプリケーション(メインのWebサーバー)
  pumba:     # カオスエンジニアリングツール

各コンテナの役割:

  • db: アプリケーションのデータを永続化(PostgreSQL)
  • web: ユーザーからのHTTPリクエストを処理(Rails)
  • pumba: 他のコンテナに障害を注入

Pumbaのインストール方法

今回はDockerイメージとして使用します。Docker Composeファイルに以下のように追加するだけで利用できます:

pumba:
  image: gaiaadm/pumba
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock

/var/run/docker.sockをマウントすることで、PumbaがホストのDockerデーモンにアクセスし、他のコンテナを操作できるようになります。

⚠️ セキュリティ上の注意
docker.sock はホスト上のDockerデーモンへの管理者アクセスを提供します。
これをマウントしたコンテナは、ホスト上の全てのコンテナを停止・削除・実行する権限を持つことになります。
そのため、本番環境での利用には注意してください。

Pumbaでwebコンテナを停止させる

1. 基本的な使い方

単発でwebコンテナを停止させる場合:

# コンテナ名を指定して停止
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock gaiaadm/pumba:0.8.0 kill test-pumba-web-1

# 正規表現で指定
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock gaiaadm/pumba:0.8.0 kill "re2:^/test-pumba-web-1$" 

"re2:^/test-pumba-web-1$" は、コンテナ名が完全に /test-pumba-web-1 に一致するものだけを対象にする指定です。

2. 定期的に停止させる

30秒ごとにwebコンテナを停止させる場合:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock gaiaadm/pumba:0.8.0 --interval 30s kill --signal SIGTERM "re2:^/test-pumba-web-1$"

3. Docker Composeに組み込む

pumba:
  image: gaiaadm/pumba
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
  command: --log-level info --interval 30s pause --duration 10s "re2:^/test-pumba-web-1$"
  depends_on:
    - web
  profiles:
    - chaos  # 通常時は起動しない

4. 実行方法

# 通常起動(Pumbaなし)
docker compose up

# カオスエンジニアリングモードで起動
docker compose --profile chaos up

動作確認

  1. アプリケーションを起動
docker compose up -d
  1. 別ターミナルでログを監視
docker compose logs -f web
  1. Pumbaを起動してwebコンテナの停止を観察
docker compose --profile chaos up pumba

各コマンドを実行すると、30秒ごとにwebコンテナが10秒間停止していることが確認できます。

pumba-1    | time="2025-11-03T12:41:12Z" level=info msg="pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:41:22Z" level=info msg="stop pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:41:42Z" level=info msg="pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:41:52Z" level=info msg="stop pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:42:12Z" level=info msg="pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:42:22Z" level=info msg="stop pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:42:42Z" level=info msg="pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:42:52Z" level=info msg="stop pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:43:12Z" level=info msg="pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1
pumba-1    | time="2025-11-03T12:43:22Z" level=info msg="stop pausing container" dryrun=false id=2117a10b882f28ed7a4541d53b7278b76a0e51ad94cee2b68871a2d245816191 name=/test-pumba-web-1

動作確認:一時停止による障害注入

簡単なGETのAPIを用意

def index
  @tasks = Task.all
  render json: @tasks
end

実際の動作ログ

クライアント側(curlコマンド)

# 正常時
$ curl http://localhost:3000/api/v1/tasks
[{"id":1,"title":"テストタスク","description":"動作確認","completed":true,"created_at":"2025-11-02T12:13:32.173Z","updated_at":"2025-11-02T12:13:32.388Z"}]

# タイムアウト1秒で実行(pause中)
$ curl -m 1 http://localhost:3000/api/v1/tasks
curl: (28) Operation timed out after 1006 milliseconds with 0 bytes received

$ curl -m 1 http://localhost:3000/api/v1/tasks
curl: (28) Operation timed out after 1005 milliseconds with 0 bytes received

# pause解除後
$ curl -m 1 http://localhost:3000/api/v1/tasks
[{"id":1,"title":"テストタスク","description":"動作確認","completed":true,"created_at":"2025-11-02T12:13:32.173Z","updated_at":"2025-11-02T12:13:32.388Z"}]

サーバー側ログ

web-1      | Started GET "/api/v1/tasks" for 172.18.0.1 at 2025-11-02 12:16:58 +0000
web-1      | Processing by Api::V1::TasksController#index as */*
web-1      |   Task Load (0.5ms)  SELECT "tasks".* FROM "tasks"
web-1      | Completed 200 OK in 2ms (Views: 1.1ms | ActiveRecord: 0.5ms)
web-1      | 
pumba-1    | time="2025-11-02T12:16:58Z" level=info msg="pausing container" name=/test-pumba-web-1
pumba-1    | time="2025-11-02T12:17:08Z" level=info msg="stop pausing container" name=/test-pumba-web-1
web-1      | Started GET "/api/v1/tasks" for 172.18.0.1 at 2025-11-02 12:17:09 +0000
web-1      | Processing by Api::V1::TasksController#index as */*
web-1      |   Task Load (0.4ms)  SELECT "tasks".* FROM "tasks"
web-1      | Completed 200 OK in 2ms (Views: 1.4ms | ActiveRecord: 0.4ms)
web-1      | 
pumba-1    | time="2025-11-02T12:17:28Z" level=info msg="pausing container" name=/test-pumba-web-1
pumba-1    | time="2025-11-02T12:17:38Z" level=info msg="stop pausing container" name=/test-pumba-web-1

このログから、以下の動作が確認できます:

  1. Pumbaが30秒ごとにwebコンテナを一時停止(pause)
  2. pause中のリクエストはタイムアウト
  3. 10秒後にpause解除され、正常にレスポンスが返る

このように、Pumbaを使うことで簡単にDockerコンテナ環境でカオスエンジニアリングを実践できます。
今回はpauseコマンドを使った一時停止のシナリオを紹介しました。
これにより、実際のシステムで起こりうる「一時的な応答不能状態」をシミュレートできました。

Pumbaでは他にも以下のような障害注入が可能です:

  • ネットワーク遅延・パケットロス
  • CPU/メモリの制限(※macOSでは制限あり)
  • ランダムな停止タイミング
  • 複数コンテナの同時障害

DBコンテナへのネットワーク障害テスト

次に、PumbaのNetemコマンドを使用してDBコンテナへのネットワーク接続に障害を注入し、Railsアプリケーションのエラーハンドリングを検証します。

今回は下記2パターンについて動作を確認してみます。

  • パケットロスによる接続不安定
    • DBコンテナへのネットワーク接続でパケットを100%ロスさせて、完全な接続断を再現します。
  • ネットワーク遅延による接続タイムアウト
    • 遅延(3秒)を追加して、接続タイムアウトをシミュレートします。

登録用のAPIを用意

  def create
    @task = Task.new(task_params)
    
    if @task.save
      render json: @task, status: :created
    else
      render json: { errors: @task.errors }, status: :unprocessable_entity
    end
  end

実行手順

1. 通常のアプリケーション起動

docker compose up -d db redis web

2. 正常動作確認

# タスクを作成
curl -X POST http://localhost:3000/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"DB Test","description":"Testing DB connection"}'

3. DB接続障害のテスト

別のターミナルでログを監視:

docker compose logs -f web

障害を注入:

docker compose --profile chaos up

障害注入後、APIを打鍵:

# タスク作成(エラーになるはず)  
curl -X POST http://localhost:3000/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"DB Error Test","description":"Should fail"}'

パケットロスによる接続不安定

障害を設定

pumbaのコンテナのcommandを下記に変更

command: --log-level info netem --duration 20s loss --percent 100 "re2:^/test-pumba-db-1$"

実際のテスト結果

実際にDBコンテナへのパケットロス100%を注入してテストを実行した結果、以下の動作を確認しました。

Pumbaのログ:

pumba-1  | time="2025-11-03T05:59:30Z" level=info msg="running netem on container" command="[loss 100.00]" duration=20s name=/test-pumba-db-1
pumba-1  | time="2025-11-03T05:59:50Z" level=info msg="stopping netem on container" name=/test-pumba-db-1
pumba-1 exited with code 0

観察された挙動:

web-1 | Started POST "/api/v1/tasks" for 172.18.0.1 at 2025-11-03 05:59:33 +0000
web-1 | Processing by Api::V1::TasksController#create as */*
web-1 | TRANSACTION (27575.3ms)  BEGIN
web-1 | Task Create (27580.9ms)  INSERT INTO "tasks" ... 
web-1 | TRANSACTION (1.0ms)  COMMIT
web-1 | Completed 201 Created in 27619ms

タイムライン:

05:59:30 - Pumba障害注入開始(20秒間のパケットロス100%)
05:59:33 - POSTリクエスト受信(障害開始から3秒後)
05:59:50 - Pumba障害解除(20秒経過)
06:00:00.8 - リクエスト完了(処理時間: 27.6秒)
  1. 障害期間: 実際の障害は17秒間(リクエスト時には残り17秒)
  2. 障害解除後の遅延な: 10.8秒
  3. 合計処理時間: 27.8秒

今回、パケットロスの障害は20秒間のみに設定していたが、実際の合計処理時間は27.8秒でした。
この原因について考察してみました。

TCP再送の詳細分析(ss -tiコマンドによる観察)

TCP(Transmission Control Protocol)は、インターネット通信で最も広く使われている信頼性のある通信プロトコルです。

信頼性とは、次の3つを保証することを意味します:

  • データが欠けずに届く
  • 送った順番どおりに届く
  • 届かなかったら自動で送り直す

このうち「3番目の送り直し(再送)」が、**TCP再送(TCP Retransmission)**です。

今回は、この TCP再送の過程で遅延が発生していた可能性があると考え、
実際の通信状態を ss -ti コマンドで観察し、詳細を調査しました。

調査方法

TCP再送の挙動を観察するために、ssコマンドを使用しました。

1. webコンテナに必要なツールをインストール

# webコンテナに入る
docker exec -it test-pumba-web-1 bash

# ssコマンドを含むiproute2パッケージをインストール
apt update && apt install -y iproute2

2. TCP接続を継続的に監視

# 1秒ごとにPostgreSQL(ポート5432)への接続状態を記録
while true; do
  date
  ss -ti dst :5432
  echo "------------------------------"
  sleep 1
done | tee ss_log.txt

3. 別のターミナルで障害を注入しながら観察

# 障害注入
docker compose --profile chaos up pumba

# 同時にタスク作成リクエストを送信
curl -X POST http://localhost:3000/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"TCP観察テスト","description":"指数バックオフを観察"}'

ssコマンドの出力の見方

State Recv-Q Send-Q Local Address:Port  Peer Address:Port
ESTAB 0      74     172.18.0.4:40006   172.18.0.3:postgresql
	 cubic wscale:7,7 rto:402 backoff:1 rtt:0.607/0.499 ...

重要な項目:

  • rto: Retransmission Timeout(再送タイムアウト、ミリ秒)
  • backoff: バックオフ回数(再送の失敗回数)
  • retrans: 再送回数
  • Send-Q: 送信キューに溜まっているバイト数(74は送信待ちデータがある)

指数バックオフの実際の動作

今回得られたログの一部を抜粋して記載します。

Mon Nov  3 06:35:17 UTC 2025
State Recv-Q Send-Q Local Address:Port  Peer Address:Port      Process
ESTAB 0      0         172.18.0.4:40006   172.18.0.3:postgresql
	 cubic wscale:7,7 rto:201 rtt:0.607/0.499 ato:40 mss:1448 pmtu:1500 
	 rcvmss:1448 advmss:1448 cwnd:3 ssthresh:7 bytes_sent:4534 bytes_retrans:592 
	 bytes_acked:3943 bytes_received:8594 segs_out:45 segs_in:32 
	 retrans:0/8 dsack_dups:1 rcv_rtt:1 rcv_space:14480 
------------------------------
Mon Nov  3 06:35:18 UTC 2025
State Recv-Q Send-Q Local Address:Port  Peer Address:Port      Process
ESTAB 0      74        172.18.0.4:40006   172.18.0.3:postgresql
	 cubic wscale:7,7 rto:402 backoff:1 rtt:0.607/0.499 ato:40 mss:1448 pmtu:1500 
	 rcvmss:1448 advmss:1448 cwnd:1 ssthresh:2 bytes_sent:4756 bytes_retrans:740 
	 bytes_acked:3943 bytes_received:8594 segs_out:48 segs_in:32 
	 unacked:1 retrans:1/10 lost:1 dsack_dups:1 rcv_rtt:1 rcv_space:14480 
------------------------------
Mon Nov  3 06:35:31 UTC 2025
State Recv-Q Send-Q Local Address:Port  Peer Address:Port      Process
ESTAB 0      74        172.18.0.4:40006   172.18.0.3:postgresql
	 cubic wscale:7,7 rto:12864 backoff:6 rtt:0.607/0.499 ato:40 mss:1448 pmtu:1500 
	 rcvmss:1448 advmss:1448 cwnd:1 ssthresh:2 bytes_sent:5126 bytes_retrans:1110 
	 bytes_acked:3943 bytes_received:8594 segs_out:53 segs_in:32 
	 unacked:1 retrans:1/15 lost:1 dsack_dups:1 rcv_rtt:1 rcv_space:14480
------------------------------

上記のログから以下が分かります:

  • Send-Qが0→74: データが送信待ちキューに溜まった
  • rtoが201→402: 再送タイムアウトが2倍になった
  • backoffが0→1: 最初の再送試行
  • retransが0/8→1/10: 再送が発生

ログの中身から各backoffの時間をまとめると下記の通りでした。

時刻        Backoff  RTO(ms)   累積待機時間
06:35:18    1        402       0.4秒
06:35:19    2        804       1.2秒(0.4 + 0.8)
06:35:20    3        1608      2.8秒(1.2 + 1.6)
06:35:21    4        3216      6.0秒(2.8 + 3.2)
06:35:25    5        6432      12.4秒(6.0 + 6.4)
06:35:31    6        12864     25.3秒(12.4 + 12.9)

なぜ27秒もかかるのか:

  1. TCP再送による遅延

    • 再送間隔は指数的に増加(0.4秒→0.8秒→1.6秒→3.2秒→6.4秒→12.8秒)
  2. 障害解除タイミングとのミスマッチ

    • 障害は20秒で解除される
    • しかし、最後の再送試行が12.8秒間隔
    • 最悪の場合、障害解除後も12秒待つ必要がある

以上のことから、TCP再送時の指数バックオフにより待機時間が伸びたことによる影響と考えられます。

ネットワーク遅延テストの実施

障害を設定

pumbaのコンテナのcommandを下記に変更

command: --log-level info netem --duration 30s delay --time 3000 "re2:^/test-pumba-db-1$"

実際のテスト結果

遅延の詳細を観察するため、1秒以上かかったSQLクエリをログ出力する設定を追加しました:

# config/initializers/database_logging.rb
ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
  duration = (finish - start) * 1000
  if duration > 1000
    Rails.logger.warn "=== DB Query took #{duration.round(2)}ms ==="
    Rails.logger.warn "SQL: #{payload[:sql]}"
  end
end

結果:

web-1 | Started POST "/api/v1/tasks" for 172.18.0.1 at 2025-11-03 07:04:34 +0000
web-1 | === DB Query took 3010.44ms ===
web-1 | SQL: BEGIN
web-1 | === DB Query took 6021.15ms ===
web-1 | SQL: INSERT INTO "tasks" ... RETURNING "id"
web-1 | === DB Query took 3009.41ms ===
web-1 | SQL: COMMIT
web-1 | Completed 201 Created in 9056ms

観察された挙動:

  • BEGIN: 3秒
  • INSERT: 6秒
  • COMMIT: 3秒
  • 合計: 約12秒

3秒の遅延を入れたが、INSERTに6秒(3秒*2?)が発生しているのが気になりました。
ここについても調査をしてみました。

tcpdumpによる詳細分析

Railsのログだけを見ると、各SQLの処理時間(BEGIN: 3s / INSERT: 6s / COMMIT: 3s)は分かるものの、
なぜINSERTだけ倍の時間がかかっているのかまでは特定できません。

RailsやActiveRecordのログはアプリケーション層の計測値であり、その下のTCP通信レベルで何が起きているのかは見えません。

そこで、本当にネットワーク遅延が3秒ずつ発生しているのか? あるいは、TCPの再送や再交渉など別の要因で6秒になっているのか?
を確認するために、tcpdumpでパケットレベルの通信を直接観察しました。

調査方法

# webコンテナに入る
docker exec -it test-pumba-web-1 bash

# tcpdumpをインストール
apt update && apt install -y tcpdump

# PostgreSQL通信をキャプチャ
tcpdump -i eth0 port 5432 -n -tt

キャプチャ結果の分析

# BEGINクエリ
1762154323.094706 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 74
1762154324.538449 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 74  # 再送
1762154326.101317 IP 172.18.0.3.5432 > 172.18.0.4.39656: Flags [P.], length 17  # 応答(3秒後)

# INSERTクエリ
1762154326.106329 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 271
1762154328.121200 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 271 # 再送
1762154329.114740 IP 172.18.0.3.5432 > 172.18.0.4.39656: Flags [P.], length 63  # DataRow応答(3秒後)
1762154332.135720 IP 172.18.0.3.5432 > 172.18.0.4.39656: Flags [P.], length 18  # ReadyForQuery応答(さらに3秒後)

# COMMITクエリ
1762154329.123545 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 75
1762154331.708959 IP 172.18.0.4.39656 > 172.18.0.3.5432: Flags [P.], length 75  # 再送
1762154332.135720 IP 172.18.0.3.5432 > 172.18.0.4.39656: Flags [P.], length 18  # 応答(3秒後)

なぜINSERTが6秒かかるのか

PostgreSQLのフロントエンド/バックエンド間プロトコルでは、サーバー(バックエンド)はクライアントからクエリを受け取ると、1つ以上の応答メッセージを返した後、最後に ReadyForQuery メッセージを送信します。
参考:PostgreSQL公式ドキュメント

フロントエンドがQueryメッセージをバックエンドに送信することで、簡易問い合わせサイクルが開始されます。 このメッセージには、テキスト文字列で表現されたSQLコマンド(またはコマンド)が含まれます。 そうすると、バックエンドは、問い合わせコマンド文字列の内容に応じて1つ以上の応答を送信し、最終的にReadyForQueryを応答します。 ReadyForQueryは、新しいコマンドを安全に送信できることをフロントエンドに知らせます。

この仕組みから、今回のようなINSERTのクエリを送信した場合、以下の応答が発生したと考えられます:

  • DataRow: INSERT結果
  • ReadyForQuery: 次のクエリ受け付け準備完了

今回のテストでは、ネットワークに片方向3秒の遅延を挿入していました。
この遅延が各応答メッセージに独立して影響した結果、次のような流れになったと考えられます。

[Web] --INSERT--> [3秒遅延] --> [DB]
[Web] <-- [3秒遅延] <--DataRow-- [DB]        # 1つ目の応答
[Web] <-- [3秒遅延] <--ReadyForQuery-- [DB]  # 2つ目の応答

各応答がそれぞれ3秒の遅延を受けるため、合計6秒になります。

タイムアウトとエラーハンドリング

これまでのテストから、ネットワーク障害は「完全な切断」だけでなく「遅延」や「パケットロス」など多様な形で発生し得ることがわかりました。
一見、通信が「繋がっているように見えても」内部ではTCPレベルで再送やバックオフが発生しており、
結果としてアプリケーション層ではリクエストが数十秒ハングするような事象が起こります。

このような状況では、

ユーザーから見るとレスポンスが返らない

サーバー側では接続プールが枯渇して新しい接続を確立できない

障害復旧後も、再送や遅延の影響で処理が遅延する

といった副作用が生じます。

したがって、アプリケーション側で適切なタイムアウト設定とエラーハンドリングを実装しておくことが極めて重要です。
これにより、通信障害が発生してもアプリケーション全体がブロックされず、
ユーザーへの応答やリトライ制御を安全に行えるようになります。

実装内容

今回は簡易的な例として、1秒のタイムアウトを設定し、適切なエラーハンドリングを実装しました。

# app/controllers/api/v1/tasks_controller.rb
def create
  @task = Task.new(task_params)
  
  begin
    # タイムアウトを1秒に設定
    Timeout.timeout(1) do
      if @task.save
        render json: @task, status: :created
      else
        render json: { errors: @task.errors }, status: :unprocessable_entity
      end
    end
  rescue Timeout::Error
    Rails.logger.error "Database timeout: Task creation took longer than 1 second"
    render json: { 
      error: 'Database timeout',
      message: 'The database is not responding. Please try again later.'
    }, status: :service_unavailable
  end
end

動作確認

動作確認用に例外情報も含めたログ出力を追加:

# config/initializers/database_logging.rb(追加分)
ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
  duration = (finish - start) * 1000
  if duration > 1000 || payload[:exception]
    Rails.logger.warn "=== DB Query took #{duration.round(2)}ms ==="
    Rails.logger.warn "SQL: #{payload[:sql]}"
    Rails.logger.warn "Exception: #{payload[:exception].inspect}" if payload[:exception]
  end
end

正常時(障害なし)

web-1 | Started POST "/api/v1/tasks" for 172.18.0.1 at 2025-11-03 07:46:18 +0000
web-1 | === PostgreSQL connection established in 13.11ms ===
web-1 | TRANSACTION (0.1ms)  BEGIN
web-1 | Task Create (2.5ms)  INSERT INTO "tasks" ... RETURNING "id"
web-1 | TRANSACTION (1.6ms)  COMMIT
web-1 | Completed 201 Created in 20ms

結果: 正常にタスクが作成される(20ms)

障害時(3秒遅延)

web-1 | Started POST "/api/v1/tasks" for 172.18.0.1 at 2025-11-03 07:46:36 +0000
web-1 | === DB Query took 997.78ms ===
web-1 | SQL: BEGIN
web-1 | Exception: ["Timeout::ExitException", "execution expired"]
web-1 | TRANSACTION (999.3ms)  BEGIN
web-1 | Database timeout: Task creation took longer than 1 second
web-1 | Completed 503 Service Unavailable in 1015ms

クライアントのレスポンス:

{
  "error": "Database timeout",
  "message": "The database is not responding. Please try again later."
}

改善効果

  1. ユーザー体験の向上

    • Before: 27秒(パケットロス)または12秒(遅延)待たされる
    • After: 1秒でエラーレスポンスが返る
  2. リソースの効率化

    • Webサーバーのワーカープロセスが長時間ブロックされない
    • 同時接続数の枯渇リスクが軽減
  3. 明確なエラーメッセージ

    • 技術的な詳細を隠蔽
    • ユーザーフレンドリーなメッセージ
    • 適切なHTTPステータスコード(503)

まとめ

本記事では、Pumba を用いて Docker 環境で実践的なカオスエンジニアリングを行い、
Webコンテナ停止/一時停止、DB へのネットワーク遅延/パケットロスを注入して、Rails アプリの振る舞いを観察しました。

本番環境での障害は「完全停止」よりも “遅い・不安定” といった中間的な状態で発生することが多く、
単なる冗長化や監視だけでは再現・検証が難しいケースが少なくありません。
そのため、Pumba のようなツールを使って 意図的に遅延や断を作り出し、実際にどう振る舞うかを検証することが、
サービスの可用性や回復力(Resilience)を高める上で非常に有効です。

小さな実験を繰り返すことで、

  • 適切な タイムアウト設定
  • 再接続やフォールバックの 設計の妥当性
  • 監視・アラートの 検知と通知のタイミング
  • 障害復旧後の システム挙動の安定性

といったポイントを“理論ではなく実際の挙動”として把握できます。

カオスエンジニアリングは、障害を起こすことが目的ではなく、
障害に強い設計を「安全に」育てるための実験文化を作るための手段です。
Pumba は、ローカル環境で小さな“カオス”を試すのにとても使いやすいツールだと個人的には思いました。

Discussion