深夜2時、メンタル崩壊の解剖
(zarar.dev)- jumpcomedy.comで夜10時ごろから RTK Query の HTTP POST 呼び出しがすべて失敗し、サイト機能が壊れた。ローカルでは正常だったため、原因追跡が難しかった
- 顧客の不満が積み上がる中、運営者は プロダクションサポート、SRE、シニアエンジニア、マネージャーなしで、ひとりで障害に対応しなければならなかった
- ブラウザの
fetch関連の TypeError は、GET・DELETE が正常な状況と相まって、直接的な手がかりにはならなかった - Sentry、プロダクション DB、Cloudflare、Chrome アップデート、過去バージョンへのロールバックを確認しても変化はなく、ローカルで空にしていた PostHog api_key を入れると問題が再現した
- PostHog を削除すると機能は正常化し、その後 PostHog と Redux Toolkit の GitHub イシューで同じ障害が確認され、外部ツールの影響であることが明らかになった
障害が生んだプレッシャー
- jumpcomedy.com は夜10時ごろから RTK Query ベースの HTTP POST 呼び出しがすべて失敗し、主要機能が正しく動作しなくなった
- 直近でデプロイした変更はあったが、原因と見なせる内容ではなく、ローカル環境では問題が再現しなかったため、追跡はいっそう難しくなった
- NextJS と Vercel の Discord に助けを求めたが応答はなく、障害対応を引き継げる プロダクションサポート組織もなかった
- 顧客からのメールは増え続けた
- イベント価格を変更できないという問い合わせ
- プロモーションコードを削除できないという問い合わせ
- 小規模ビジネスの顧客がサービスに依存しているという点から、運営者は恥ずかしさ、悲しみ、無力感、インポスター症候群を感じた
デバッグ過程と原因確認
- ブラウザのエラーは、
fetchがすでに使用済みの request object とともに実行されたという TypeError だったが、実際の原因を示すものではなかった - 複数の
console.log()とブレークポイントを追加し、ヘッダー、API トークン長、呼び出し順序などを確認したが、明確な原因にはつながらなかった - Chrome アップデートの可能性を疑ったが、Firefox と Edge でも再現したため、ブラウザ単独の問題ではなかった
- 過去バージョンへ戻しても失敗は続いた
- 1か月前のバージョンでも失敗
- 3か月前のバージョンでも失敗
- 1年前のバージョンでも失敗
- ローカルとプロダクションの差を減らすため、複数の候補を確認した
- プロダクションから Sentry を削除: 変化なし
- ローカルをプロダクション DB に接続: 変化なし
- Cloudflare を無効化: 変化なし
- ローカルではコスト削減のため PostHog api_key を空にしており、これを追加すると同じ問題が再現した
- 次のコミットで PostHog を削除すると、すべての機能が正常に動作した
- 同じ問題はその後 GitHub イシューでも確認された
1件のコメント
Hacker News のコメント
大手グローバル企業で1年間 SRE として働いたことで、記事でいう「パニック」モードから抜け出せるようになった
ビジネス側から見ると、あらゆる問題が世界の終わりのような出来事に見え、その状況ではパニックに陥りやすいが、実際にはそこまでひどいケースは少なく、たとえひどくてもたいていは無事に乗り切れる
こういう状況では、すぐ直そうとして手を出す前に 5〜10分立ち止まり、できるだけ明確に状況を描いてみることが核心になる。恐怖は理性的な判断を妨げるし、パニック状態でボタンをむやみに押すと、問題がさらにこじれることがある。顔と手にとても冷たい水をかけて恐怖の回路を断ち切るのが、自分のやり方だ
こうしたことを何度か経験すると、思ったより大丈夫だし、以前にも悪い状況を処理したことがあるという自信がついて、助けを求められる相手がいなくても対処できると分かるようになる
買ったソフトウェアが人員配置や設定の失敗で何の役にも立っていなかったり、従業員がひどいユーザー体験や無意味な要件のせいで毎年何千時間も失っていたり、何の機能も果たしていない能力が放置されていたり、役に立たない会議が毎日時間を浪費していたり、監査要件を満たすためだけに存在する機能があったり、経営陣が会社の金を浪費し続けていたりするようなことだ
ダウンタイムがこうした問題より悪く見えるわけではないが、はるかに多くの注目とパニックを集める。テロと心臓病の対比のように感じる。会社はあなたの睡眠やメンタルヘルスを気にせず、可能な限り押し込んでくる。悪意があるという意味ではないが、この点ではいじめっ子のように、あなたが引いた分だけさらに押してくる
私のプログラミング上の座右の銘の一つは「黒魔術禁止」だ。なぜ動くのか理解していないなら、それは終わっていない
障害対応も同じように見ている。誰かの提案がなぜ影響を与えるのかを一貫して説明できないなら、実行すべきではないと思う。いつかはただ引き金を引かなければならない時が来るのかもしれないが、振り返ると結局そんなケースはなかった気がする
普段は非常に落ち着いている上級幹部たちが、障害中に思いつきの修正案を投げ始めるのを見るのは、かなり衝撃的だった
誰かがこの1人開発者に「fetch を流行らせようとするな」と止めていたら、被害がどれほど大きくなっていたか想像してみてほしい
恐怖が理性的な判断を妨げるという話はその通りで、ここに、恐怖は非常に伝染しやすいという点も付け加えたい。実務担当者は、リーダーやマネージャー、同僚がパニックに陥るのを見ると、しばしば一緒にパニックになる。幸い、私の VP はいつも落ち着いていて、行動よりも明確さを優先していた
これが 精神的崩壊なのかはよく分からないし、技術関連のストレスで実際に崩壊を経験する人たちに誤った印象を与えるかもしれない
私の場合は一度だけあり、それは不安発作だった。妻がそばで状況を説明し、私が何を経験しているのか理解できるよう助けてくれたので、本当に運がよかった。妻は何度も経験していたが、私は初めてで、幸い最後でもあった
こういうことは人に起こり得るし、それ自体が間違っているわけではない。欠陥があるとか弱いという意味ではない、ということを内面化するのが本当に重要だ
私の場合、最終的に止めてくれたのは Xanax で、眠ることができたので、手の届くところに置いておく価値はあると思う
言いたいのは、侵入思考と、不安発作やパニック発作のように実際に制御できず機能を麻痺させる状態は違うということだ。そういうことが起きると仕事はできなくなるし、それでも問題ない
すべての崩壊がパニック発作や不安発作の形で来るわけではない。そういう形で現れることはあるが、それが唯一の形ではない。ストレスは人によって、さらにはストレス要因によっても非常に異なる形で現れる
その人の頭の中で実際に何を経験していたのかは分からないので、外から「診断」するのはほぼ不可能だ。完全なパニック発作ではなかったとしても、数時間にわたって機能的に麻痺していたようには聞こえる
オンラインで調べると、Xanax には依存性があり得るように見える
https://www.drugs.com/xanax.html
軽く服用してよい種類のものではなさそうだ
2000年代半ばにテック業界に入ってきた大きな集団が、ストレス関連疾患で亡くなっていくのを見る可能性は高い
生涯ずっと不安が強かった身としては、依存性のある錠剤一つでそれがすべて消えるという考え自体が怖い。私は一生それにしがみつくことになりそうだ
この人のストレスは PostHog のコード1行によって生じた。元に戻されたコミットはこちら: https://github.com/PostHog/posthog-js/pull/1371/commits/7598...
ここから二つの教訓が見える。第一に、デプロイしたなら所有したということだ。だからデプロイは少ないほどよく、依存関係は最小限にすべきだ。第二に、重要でないものは クリティカルパスの外に置くべきだ。エアコンのコンプレッサーが壊れたからといってエンジンが止まってはいけない。ブラウザでは実現が非常に難しいが、試す価値はある
さらに悪いことに、PostHog は自分たちのコードの一部を実行時に動的に更新しているように見え、ビルド時にまとめていないようです。
ドキュメントには、すべての依存関係をビルドに含める高度なオプションがあります。なぜそうしているのかは理解できますし、私が誤解している可能性もありますが、ユーザーの立場としては、実行コードの遅延ロードはデフォルトではなく最適化オプションであるべきだと期待します。完全なバンドルが深刻な配信遅延を生む場合にだけ使うべきだと思います。
バグは monkey-patch された
window.fetchの中にあったようです。https://github.com/PostHog/posthog-js/blob/759829c67fcb8720f...
ここで最大の教訓は、人気ライブラリを作りながらグローバル関数を monkey-patch するなら、テストが本当にしっかりしていなければならないということです。
「念のため PostHog 呼び出しを try/catch に入れよう」と、「PostHog のせいで文字どおり
fetch()で POST リクエストを送れない」は、まったく別物です。fetchを使えるさまざまな方法に対するテスト範囲が不足しているうえ、過剰なモック化も一因のようです: https://github.com/PostHog/posthog-js/blob/main/src/_tests...fetch と XHR 関数全体がモック化されて何もしないようになっているので、当然ながら下位のネイティブ実装や他のライブラリと相互作用するときに起きる問題は捕まえられません。Cypress も設定されているのに、なぜブラウザ API をモック化しようとするのか分かりません。
合理的に統合されていれば、最悪でも監視イベントの処理が失敗する程度で済むはずだと思っていました。
PostHog が非常に重要なグローバル関数をパッチするというのは、きちんと文書化されるべき機能です。そうすれば利用者がそれを把握でき、表面的には説明しにくい問題をデバッグするときに合理的に念頭に置けます。
たとえば Heap Analytics は、今月時点でもまだ Hotwire 内部の何かに触れて、Hotwire をランダムに完全に壊し、すべてのクリックをフルページロードにしてしまいます。私の経験ではページロードの 30〜60% に影響します。直すことはできますが、Heap がすべての Hotwire JavaScript の後にロードされるようにするまで、50 時間以上デバッグする必要がありました。
他の人たちが言っているように、この深夜のストレスにつながったバグは PostHog ライブラリ の 1 行の変更でした[0]。
私はこれを、変数に正確な名前を付けることの重要性を思い出させる出来事だと見ています。
res = await originalFetch(url, init)というコードは十分に無害に見えます。しかし TypeScript の宣言が示すように、urlパラメータは必ずしも URL ではありません:url: URL | RequestInfoURL ではなく RequestInfo オブジェクトの場合に問題が起きます。関数実装の前半で Request オブジェクトを作る際にすでに「消費」されており、ここで再利用できないためです。
パラメータ名が
urlOrRequestInfoのようにより正確であれば、この変更で問題を見落とすのはもっと難しかったはずです。さらに推測めいた考えですが、線形論理に由来する線形型で値が「消費される」ことを形式化できるので、適切な型システムならこの種のバグを防げるかもしれません。
[0] https://github.com/PostHog/posthog-js/pull/1351/commits/2497...
Rust のような言語の所有権セマンティクスを見るだけでも分かります。突破不可能なほどではなく、特に経験を積めば楽になりますが、学習者が最も不満を述べる点であるほど負担は大きいです。
ストレスは大きかったものの、笑える記事でもありました。ただ、自己非難の部分はあまりにも身に覚えがあります。
かなり成功している iOS/macOS アプリを運営していますが、リリースを押し出した結果、35 万以上のインストール済み環境を完全に壊してしまったことがあります。完全に私だけの責任ではありませんでしたが、私の製品なので大差はありませんでした。
あのときの冷や汗と羞恥心は本当にひどいものでした。しかも App Store なので修正版も審査プロセスを通る必要があり、時間が延びます。幸い、提出から 30 分後に審査に入り、数分で承認されました。
キャリアを積み、リーダーシップに移る中で、それが非常に価値のある経験だったと気づきました。当時はストレスだったかもしれませんが、今ではその記憶は遠くなりすぎて届きもしません。今は確かにストレスを感じません。
議論はあるかもしれませんが、私はときどきキャリア初期のチームメンバーが本番環境を壊すことを許します。事前に見えていて、私たちが素早く復旧できると確信している場合です。
失敗する余地を与えることが重要なのは常識ですが、多くのリーダーは実際の顧客に影響する失敗で線を引きます。飛行機を着陸させるソフトウェアのような重要なものを作っているわけではない、という非常によくある幸運な状況なら、ワシントン州スポケーンの誰かが数分間製品を使えないという代償を払ってでも、チームに本番障害を経験させるべきです。
こういう記事を書いてくれてありがとうございます。特にプレッシャーの下で、たいていは夜通しで、人々がこうした難題をどう乗り越えるのか読むのが好きです。
技術的な事後分析だけでなく、こうした話からは普通消されてしまう人間的な視点まで聞けるので、より良かったと感じます。こういう技術的な語りは、小規模/一人開発者や創業者だけが自由に共有できる種類のものです。
問題を追跡したやり方を見るだけでも、まずプログラマーなのだと分かる。自分のコードに向かい、ログに向かった。どちらも合理的で、どちらも原因になり得るが、手元にあった最も重要な手がかりである「localhost では動いていた」という点を見落としていた。
SRE、DevOps、プラットフォームエンジニア、その日その日で付く肩書が何であれ、私なら動くシステムと動かないシステムの差に注目しただろう。差分を一つずつ追加しては取り除く、あるいは取り除いてからまた追加し、何かが動くまで見ていったはずだ。
私が見ているのは二つ。1) 動く環境がある。2) 失敗する環境も、もともとは動いていて、ある時点から失敗し始めた。
私のやり方のほうが優れているという意味ではない。問題の見方の違いを示したいだけだ。どちらも自分の知っているものを中心に絞り込んでいく。私はシステムを知っていて、あなたはコードを知っている。
私は見込みがないと思ったが、年上で賢い技術者が方法を教えてくれた。
良品のボードをエクステンダーに挿し、失敗する診断プログラムをループで回す。オシロスコープでコネクタの全ピンを見て記録する。不良ボードに差し替えて繰り返す。
どの信号が違うのか? その信号をさかのぼる。回路図が合っていなければ、電圧計と目視で実際の配線を反映した回路図を描く。
彼はこれを「良いカード - 悪いカード」と呼んでいて、実際にうまくいった。費用対効果がよかったとは主張しないが、すべてのボードを直し、デジタル電子回路のトラブルシューティング能力は大きく伸びた。
これは一種の「消防士」のような仕事だった。システムが壊れるのを待つ仕事だったので、技術者 2 人が回路ボード 1 枚に 1 週間かけても問題なかった。
「1か月前のバージョンに戻してみよう。だめ。3か月前は? だめ。まだ失敗する。1年前は? まったく動かない。」
自分のコードだけを戻して、同じ日に壊れた PostHog の更新はそのまま使っていたのだろうか? 私が得た教訓は、依存関係まで含めてすべてを戻せるようにすべきだということだ。
https://github.com/PostHog/posthog/issues/24471#issuecomment...
自前でバンドルする選択肢も一応ある。
https://github.com/PostHog/posthog/issues/24471#issuecomment...
元記事の筆者はうまく対処した。こうした障害の良い面は、価値ある教訓が山ほど得られることだ。
サービスの裏側にいる人たちのことを改めて思い出させてくれる良い記事で、デバッグの過程もよく分かる。
現実的には、プレッシャーが問題をより早くデバッグさせてくれるわけではない。たいていは思考の邪魔になる。結果をできるだけ無視し、可能な限り落ち着いている必要がある。
私たちのほとんどは、程度の差こそあれ似たような状況を経験したことがあるはずだ。もちろん、自分の会社を運営するストレスはひときわ大きいだろう。