今回の記事のテーマは、Retry Storms(再試行の嵐) です。
名前は少し物騒ですが、やっていることは単純で、「失敗したらもう一回投げる」をみんなが同時にやりすぎて、システム全体を押しつぶしてしまう、という話です。
これ、かなり“あるある”だと思います。
人間の感覚だと「1回失敗したなら、もう1回試せばいいじゃない」と思いがちです。実際、それで助かるケースは多い。でも、遅くなっている時に再試行を積み増すと、救済策が加害者に変わるんですよね。ここが本当に面白くて、怖いところです。
元記事では、API-led architecture(APIを中心にした構成)を前提に、次のような機能がそれぞれ“善意”で入っていると説明します。
どれも単体なら正しい。でも、ストレス下では同時に暴走しうる。
この記事の主張はそこに尽きます。
Retryは「一時的なタイムアウトなら、もう一度試す」という仕組みです。
たとえば、あるサービスが一時的に遅くなっただけなら、再試行でうまくいくことがあります。これは確かに便利です。
ただし問題は、失敗したリクエストが、失敗したままでは終わらないこと。
1回のリクエストが3回までRetryされると、単純に考えて負荷は3倍です。しかも、それが複数レイヤーで起きると掛け算になります。
元記事の例では、こんな流れが描かれています。
この構成で各層が独立にRetryすると、1つの遅延が上流まで増幅します。
たとえば下流のDBが少し遅くなっただけで、上流のAPIが「まだ待てる、もう1回」と再試行し、その再試行がさらに下流を圧迫する。まさに悪循環です。
個人的には、ここは分散システムの“性格の悪さ”がよく出る部分だと思います。
単体では良い挙動が、組み合わせると最悪になる。ソフトウェア設計って、こういうところが本当に油断できません。
記事では、Retryは次のようにbounded(上限付き)にすべきだとしています。
特にJitterは地味ですが重要です。
全員が同じタイミングで再試行すると、再び一斉攻撃みたいになるからです。
「ちょっと待ってから、しかも人それぞれズラして再挑戦する」という発想は、かなり賢いと思います。
Replicationは、データを複数の場所に複製することで、壊れにくくする仕組みです。
これも一見すると万能に見えます。データが飛んでも大丈夫そうですし、冗長性も上がる。いいことづくめに見えます。
でも元記事は、同期的なReplicationにはコストがあると指摘します。
書き込みのたびに複数レプリカへ同時反映するなら、そのぶん待ち時間も増えるし、調整の手間も増える。
つまり、耐久性を上げるつもりが、書き込みのボトルネックになるわけです。
記事の例では、書き込みが3つのReplicaにファンアウトし、トラフィックが増えるとレプリカ遅延が起き、クライアントがRetryし、結果として書き込み負荷がさらに増える、という流れが示されています。
これは、業務システムではかなり厄介です。
注文処理、請求、突合(reconciliation)みたいな領域では、「データが消えないこと」は大事ですが、同時に「処理が止まらないこと」も大事です。
どちらか片方だけを最大化すると、もう片方が死ぬことがある。ここは設計者泣かせですね。
記事は、全部のデータに同じレベルの耐久性を求めないことを勧めています。
これはかなり現実的です。
「全部を最高品質で」ではなく、「大事なものにだけ重い保証をかける」。
個人的には、こういう優先順位ベースの設計が、実運用では一番強いと思います。
Autoscalingは、アクセスが増えたらインスタンスを増やす仕組みです。
クラウド時代の希望みたいな機能ですが、元記事はここにも落とし穴があると言います。
問題は、Retryで増えた負荷を、本物の需要だと誤認することです。
たとえば本当は100件のアクセスしかないのに、Retryで300件に見えていたら、システムは「うわ、需要が急増してる!」と判断してスケールアウトするかもしれません。
でも新しいインスタンスを立ち上げると、今度はそれ自体がDBやキャッシュに負荷をかける。
その結果、さらに遅くなる。さらにRetryが増える。
つまり、スケーリングが不安定さを加速するわけです。
これ、なかなか残酷です。
“助けようとした仕組み”が“混乱を広げる仕組み”になる。
分散システムは、こういうフィードバックの連鎖が本当に怖い。
元記事では、スケール判断を次のような信号に寄せるべきだとしています。
要するに、見せかけのトラフィックではなく、本物のトラフィックを見るということです。
これ、当たり前のようで難しいんですよね。観測値はいつもノイズだらけなので。
この部分がこの記事の核心です。
Retry、Replication、Autoscaling、Circuit breakerは、それぞれ別の問題に反応しています。
でも実際の障害時には、これらが同じ原因から同時に揺れ始めることがあります。
すると、個別には正しい反応でも、全体としては不安定なループになる。
記事では、分散システムは「フィードバックシステム」だと表現しています。
これはかなり本質的だと思います。
ソフトウェアは、ただの箱の集まりではなく、互いの出力が互いの入力になる“生き物”みたいなものなんですよね。
元記事では、payment reconciliation API のシナリオが紹介されています。
流れはこんな感じです。
ここでERPが少し遅くなるとします。
すると:
結果、小さな遅延がプラットフォーム全体の障害に変わる。
これ、かなりリアルです。単なる「遅い」から始まって、全体が勝手に大騒ぎする感じ。運用担当からすると悪夢だと思います。
元記事は、信頼性を“最大化”するのではなく、bounded reliability(上限付きの信頼性)として設計すべきだと述べます。
要するに、「どこまでも頑張る」のではなく、「ここまでは守る、ここから先はあきらめる」を決めることです。
Retry Budgetは、Retryに使っていい“予算”のようなものです。
たとえば incoming RPS が 1,000、Retry count が 3 なら、effective load は 3,000 になります。
つまりRetryは、ただの保険ではなく負荷の増幅装置なんです。
だから、サービス全体で何回までRetryしてよいかを制限する必要があります。
元記事は、全部のエラーをRetryしてはいけないと強調しています。
これはかなり実務的です。
ValidationエラーをRetryしても、入力内容が間違っているだけなので直りません。
Authエラーも、再試行したところで認可が通るわけではない。
この当たり前を徹底するのが、意外と難しいんですよね。
Idempotency(冪等性)は、「何回やっても同じ結果になる」性質です。
Retryするなら、これがないと危険です。
たとえば決済や注文登録で、同じリクエストを2回送ったら2重処理になると困ります。
そのため、記事では transaction_id や correlation-id のような識別子を使う例が示されています。
ここは本当に重要です。
Retryは“同じことを何度もやる”仕組みなので、同じことを何度やっても安全である必要があります。
この原則がないと、リトライは保険ではなく事故の原因になります。
DLQは、失敗したメッセージを一旦避難させる場所です。
ただ置くだけでは足りず、観測が必要だと記事は言います。
見るべき指標はたとえば:
P95 latency は、ざっくり言うと「遅い側から数えて5%の手前の値」です。
平均値よりも、体感に近い“遅さ”を捉えやすいので、運用でよく使われます。
この記事を読んで強く感じたのは、信頼性の仕組みは、足し算すると安心ではなくなるということです。
RetryもReplicationもAutoscalingも、ひとつひとつは正義です。
でも、上限なく組み合わせると、システムは“守られる”どころか“追い込まれる”。
だから大事なのは、こんな発想だと思います。
個人的には、この文章のメッセージはかなり刺さりました。
「止まらない仕組みを作る」より、「止まり方を穏やかにする」ほうが、分散システムではずっと重要ではないかと思います。
理想論より現実論。派手さはないけれど、運用ではそっちが勝つ。そんな記事でした。