本番環境でしか出なかったバグ
半年前、私のチームはマイクロサービスのアップデートをステージング環境で一度も失敗せずに通過させました。負荷テストも通過。結合テストも通過。そして本番にデプロイした瞬間、20 分以内に東南アジアのユーザーからタイムアウトや API コールの失敗が相次ぎました。
原因は明快でした。アプリケーションが現実のネットワーク条件下でまったくテストされていなかったのです。ステージングはギガビット LAN の上に乗っていましたが、本番トラフィックは海を越えていました。シンガポールのノードと東京バックエンドの間の RTT は常時 80ms 前後で、200ms を超えるスパイクも散発的に発生し、上流リンクの 1 つでは 1〜2% のパケットロスが常態化していました。
この障害が突きつけた問題は根本的なものでした。コードをリリースする前に、劣悪なネットワーク条件をローカルで確実に再現するにはどうすればいいか? たどり着いた答えが tc netem、Linux のトラフィックコントロール・ネットワークエミュレーターです。それ以来、デプロイ前のパイプラインに組み込んで運用しています。結果として、デプロイ後のタイムアウト発生率は 2 スプリント以内に 80% 以上低下しました。
tc netem とは何か
tc はトラフィックコントロールの設定を操作する Linux コマンドです。iproute2 パッケージの一部として提供されているため、現代的な Linux サーバーであればほぼ標準で利用できます。netem(Network Emulator)は、ネットワークインターフェースにアタッチして人工的な障害を注入するキューイングディシプリン(qdisc)です。
アプリケーションとネットワークスタックの間に挟まる、プログラマブルな劣化レイヤーとイメージしてください。カーネルに対して「このインターフェースからパケットが出るとき、ランダムに 2% を破棄し、20ms のジッターを伴う 80ms の遅延を加え、時々順序を入れ替えてくれ」と指示します。カーネルはその通りに動作します。アプリケーション側の変更は一切不要です。
iperf3 や mtr は既存のネットワーク状態を計測するツールです。tc netem はその状態をオンデマンドで作り出すツールです。この違いが本質です。
知っておくべき 4 つの障害種別
- 遅延(Delay) — 送信パケットに固定または可変のレイテンシを付加する
- パケットロス(Packet loss) — 一定割合のパケットをランダムに破棄する
- ジッター(Jitter) — 遅延に変動を加える(リアルタイムアプリケーションの大敵)
- パケットの順序入れ替えと重複(Reordering and duplication) — 不安定なリンクを再現する
最初の netem ルールを設定する
まず tc が使えるか確認します:
tc -V
多くのディストリビューションには同梱されています。なければ apt install iproute2 または yum install iproute でインストールしてください。
インターフェースに netem qdisc を追加する基本的な構文:
tc qdisc add dev eth0 root netem delay 100ms
eth0 の送信トラフィックすべてに 100ms の片道遅延が加わります。適用を確認するには:
tc qdisc show dev eth0
次のような出力が表示されます:
qdisc netem 8001: root refcnt 2 limit 1000 delay 100ms
テストが終わったら削除します:
tc qdisc del dev eth0 root
テスト後は必ずクリーンアップしてください。消し忘れた netem ルールが原因で、私のチームでも何度か混乱したデバッグセッションが発生しました。
実践:現実的なテストシナリオ
シナリオ 1:リージョン間レイテンシの再現(80ms + ジッター)
私たちが痛い目を見た東南アジア〜東京パスを正確に再現する設定:
tc qdisc add dev eth0 root netem delay 80ms 20ms distribution normal
2 つ目の 20ms がジッターの幅です。distribution normal を指定すると、純粋なランダムではなく正規分布に従った変動になります。実際のネットワーク挙動により近い形です。ほとんどのパケットは 80ms 前後に収まりつつ、スパイク時は 100ms 以上に達することもあります。
対象ホストへの ping で素早く確認できます:
ping -c 20 your-backend-host.example.com
シナリオ 2:パケットロス
リトライロジックが堅牢でなければ、1% のパケットロスでもアプリケーションのパフォーマンスは崩壊します。まずは控えめな値から始めましょう:
tc qdisc add dev eth0 root netem loss 1%
実際のリンク劣化時のように、パケットロスが連続して発生する相関ロスを再現するには:
tc qdisc add dev eth0 root netem loss 2% 25%
25% は相関係数です。あるパケットが破棄されたとき、次のパケットも破棄される確率が 25% になります。相関ロスはランダムロスよりもはるかに速くリトライ予算を使い果たします。上流障害時に実際に起きる現象です。
シナリオ 3:完全劣化リンク
混雑した 4G 上のモバイルユーザーや、飽和した ISP 経由の VPN トンネルを再現するには、3 つの障害をすべて組み合わせます:
tc qdisc add dev eth0 root netem \
delay 150ms 50ms distribution normal \
loss 3% \
duplicate 0.5% \
reorder 5% 50%
組み合わせ内容:平均 150ms で高ジッター、3% パケットロス、0.5% パケット重複、5% 順序入れ替え。この環境でアプリケーションを 10 分間動かせば、誤ったタイムアウト設定が必ず姿を現します。
シンガポール〜東京パスを模した環境にこれを適用したところ、結合テスト開始から 10 分以内に、リトライポリシーが未設定の gRPC クライアントが 3 つ見つかりました。それらは何週間もサイレントな本番障害として潜んでいたのです。
シナリオ 4:netem + tbf による帯域制限
netem は遅延とロスを担当しますが、帯域幅の上限には 2 つ目の qdisc として tbf(Token Bucket Filter)を連結する必要があります:
# まず root qdisc として netem を追加する
tc qdisc add dev eth0 root handle 1: netem delay 80ms loss 1%
# 次に子として tbf を追加し、帯域を約 1Mbit/s に制限する(モバイルを再現)
tc qdisc add dev eth0 parent 1:1 handle 10: tbf rate 1mbit burst 32kbit latency 400ms
この 2 つの qdisc を連結することで、遅延・ロスに加えて帯域上限も設定できます。電波の弱いモバイルユーザーの環境をかなり忠実に再現できます。
ルールを削除せずに変更する
テスト中にすべてを一度削除することなくパラメーターを調整したい場合は change を使います:
# 既存の遅延ルールを維持しながらロスを 5% に引き上げる
tc qdisc change dev eth0 root netem delay 80ms 20ms loss 5%
デプロイ前チェック用のテストスクリプトを作る
ad hoc な netem 使用を数週間続けた後、よく使うシナリオを小さなシェルスクリプトにまとめました。このスクリプトはリポジトリに置かれ、デプロイ前チェックリストの一部として実行されています:
#!/bin/bash
# network-chaos.sh — デプロイ前テスト用の netem 障害を適用・解除する
# 使い方: ./network-chaos.sh [apply|remove] [scenario]
IFACE="eth0"
apply_scenario() {
case "$1" in
regional)
echo "適用: 80ms 遅延 + 20ms ジッター + 1% ロス"
tc qdisc add dev $IFACE root netem delay 80ms 20ms distribution normal loss 1%
;;
degraded)
echo "適用: 150ms 遅延 + 50ms ジッター + 3% ロス + 順序入れ替え"
tc qdisc add dev $IFACE root netem \\
delay 150ms 50ms distribution normal \\
loss 3% reorder 5% 50%
;;
mobile)
tc qdisc add dev $IFACE root handle 1: netem delay 120ms 40ms loss 2%
tc qdisc add dev $IFACE parent 1:1 handle 10: tbf rate 2mbit burst 32kbit latency 400ms
echo "適用: モバイル環境(2Mbit、120ms、2% ロス)"
;;
*)
echo "不明なシナリオ: $1"
exit 1
;;
esac
}
remove_all() {
tc qdisc del dev $IFACE root 2>/dev/null && echo "netem ルールを削除しました" || echo "削除するルールがありません"
}
case "$1" in
apply) apply_scenario "$2" ;;
remove) remove_all ;;
*) echo "使い方: $0 [apply|remove] [regional|degraded|mobile]" ;;
esac
root 権限または CAP_NET_ADMIN ケーパビリティが必要です。Docker の場合、テスト環境を実行するコンテナに --cap-add NET_ADMIN を渡してください。
設定内容を検証する
ルール適用後、結果を信頼する前にサニティチェックで確認します:
# 現在の qdisc を表示する
tc qdisc show dev eth0
# ping の統計で実際のレイテンシを計測する
ping -c 50 -q 8.8.8.8
# または TCP レベルの統計には hping3 を使う(アプリテストにより適している)
hping3 -c 100 -S -p 80 your-backend.example.com
実際に何が見つかるか
6 ヶ月間、すべてのデプロイ候補を少なくとも regional シナリオで通してきた結果、本番前に検出できた問題のパターンが見えてきました:
- タイムアウトが設定されていない HTTP クライアント — パケットロス発生時に無限にハングする
- ジッタースパイクで TCP RST がトリガーされた後の再接続を処理できないデータベース接続プール
- メッセージ再構成ロジックのエッジケースに順序入れ替えパケットがヒットすると、サイレントにメッセージ受信を停止する WebSocket クライアント
- プリフェッチウィンドウが小さすぎて、高レイテンシ下で飢餓状態になるキューコンシューマー
これらは LAN 環境のテストでは 1 件も表面化しませんでした。劣化ネットワークシナリオでは、すべてが数分以内に現れました。
いくつかの実践的な注意点
netem が影響するのはインターフェースの送信トラフィックのみです。両方向に障害を加えるには、接続の両端にルールを設定するか、中継役として専用のネットワーク名前空間や VM を使用する必要があります。
コンテナベースのセットアップでは、コンテナのネットワーク名前空間内で netem を実行するか、サイドカーコンテナをネットワークプロキシとして使いそのインターフェースに障害を適用します。
もう一点:netem ルールはプロセスの再起動では消えませんが、再起動後は消えます。テスト中に VM が再起動するとルールがなくなります。適用・削除スクリプトはわかりやすい場所に置いておきましょう。
標準プロセスに組み込む
本当の変化はツールの導入ではありませんでした。劣化したネットワーク条件をエッジケースではなくテストマトリクスの通常項目として扱うようになったことです。コードベース内のネットワークに関わる非自明な変更はすべて、デプロイ承認を得る前に少なくとも regional シナリオを通過させています。
初回セットアップに必要な時間は約 5 分です。それで見つかるバグは、本番で診断しようとすれば何時間もかかるものばかりです。再現できるかどうかさえ怪しいものも多い。1 時間の事前テストは、深夜 2 時の緊急対応より必ず割に合います。
まず 80ms の遅延ルールを設定し、アプリケーションを動かして、ログを眺めてみてください。必ず何かが壊れます。そして今ここで見つかってよかったと思うはずです。

