Web リクエストの解剖 基礎
20 の primer がスタックを構築した。これはそれらを 1 つの物語に縫い合わせる:curl https://api.example.com/user/42 をキーストロークから stdout 上の JSON まで。5 セクションで全体像を組み立てる:セットアップ —— 1 つの curl、~250 ms、20 レイヤ;トレース —— 5 つのスイムレーンにわたる 12 ステージ、 対話的ウォーターフォール、各 primer をそのステージで交差リンク;レイテンシ予算 —— 時間が実際にどこへ行くか、 再利用が何を節約するか;故障モード —— レイヤごとにどこで何が壊れるか;そしてクイックリファレンス。 CS 基礎シリーズの集大成。
セットアップ —— 1 つの curl、~250 ms、20 レイヤ
curl https://api.example.com/user/42 と打って、 半秒もしないうちに JSON オブジェクトが返ってきた。 20 個の異なるサブシステムがたった今、それを実現するために協力したばかりだ。 この primer はインデックス —— タイムライン全体を一度歩き、 シリーズ内のすべての primer を詳細のために指し示す。
登場人物:あなたのラップトップ(curl プロセス、libc、カーネル、NIC)、 その間にあるパブリックインターネット(~30 ms RTT、十数個の中継ルータ、 1〜2 個の CDN エッジ)、そしてどこかのサーバ —— nginx がバックエンドプロセスをフロントし、そのバックエンドが Postgres や MySQL と話し、 データベースは NVMe 上のファイルシステムに支えられている。 どの部品も真空中で設計されたものではない;各層は下層が解決できなかった問題への意図的な応答か、 上層が要求した抽象のいずれかだ。
カバーした 4 レイヤ
CS 基礎シリーズは 20 の primer でスタックを積み上げた。 各々がこの同じリクエストの 1 切片だった:
- ハードウェア(5 primer)。ビットと数の表現、 1 つの CPU コアが実際に何をするか、メモリレイテンシの 6 桁、 マルチコアコヒーレンス、コンパイラが出すアセンブリ。CPU 体系構造から。
- オペレーティングシステム(8 primer)。プロセスとスレッド、 誰が走るかを選ぶスケジューラ、仮想メモリとページフォルト、アロケータ、 syscall / 割り込みの境界、ファイルシステムとページキャッシュ、 ブロッキングから io_uring までの I/O モデル、並行プリミティブ。プロセスとスレッドから。
- ネットワーク(4 primer)。パケットの NIC からの旅、 TCP の信頼性機構、頂上の DNS と HTTP、すべてを機密性で包む TLS。ネットワークスタックから。
- ストレージ & データ(3 primer)。ディスクの物理、 データベースが構築される B-tree と LSM-tree、 アプリが頼ってよいトランザクション保証。ディスクストレージから。
この集大成が何をするか
5 セクション、順序通り。セクション 2 は背骨 —— 1 つの curl 呼び出しの 12 ステージ、 端から端まで歩き、各 primer をそのステージに交差リンクする。 埋め込まれたウォーターフォールデモで視覚的にステップできる。セクション 3 は同じトレースを取って時間がどこに行くかを問う —— レイテンシ予算。セクション 4 はどこで何が壊れるかを問う —— 各レイヤのお気に入りの故障モードを primer にマップ。セクション 5 はクイックリファレンス Q&A。
教育契約は、シリーズ内のすべての primer が本文のどこかからリンクされる —— 例外なし。 個々を理解したならこの primer はそれらを 1 つの物語に結ぶ; いくつかを流し読みしたなら、交差リンクはちょうど糸を見失ったその場所で深く入る招待状だ。
要点。「1 つの curl リクエスト、エンドツーエンドで ~250 ms、 ハードウェア、OS、ネットワーク、ストレージにわたって 20 のサブシステムを動かす。 各サブシステムには独自の primer がある; この集大成はタイムラインを一度歩き、各 primer をそれが所有するステージでリンクする。」
トレース —— 20 primer、1 タイムライン
1 リクエスト、5 つのスイムレーン、12 ステージ。 まずウォーターフォールをステップし、それから散文で各ステージを歩く —— primer リンクを行内に入れて、各詳細を所有するレイヤにいつでも飛び込めるように。
ステージ 1 —— キーストロークとプロセス生成
Enter を押す;bash が子を fork し curl を exec する。 カーネルが新しいアドレス空間、新しいページテーブルセット、curl ELF バイナリの text と data セグメントの ロードを構築する。glibc がコンストラクタを走らせる;ヒープが新しい brk から始まる。 最初のネットワークバイトより前に、OS レベルのセットアップで既に ~5 ms 費やした。 詳細:プロセスとスレッド で fork / exec 機構、仮想メモリ でアドレス空間構築とデマンドページングロード、メモリ割り当て で glibc ヒープの裏側。
ステージ 2 —— DNS 解決
curl が getaddrinfo("api.example.com", ...) を呼ぶ。 glibc のスタブリゾルバが /etc/nsswitch.conf を参照し、 通常 systemd-resolved に渡し、設定された上流(よく 1.1.1.1 や 8.8.8.8)へ UDP クエリを送る。 リゾルバがキャッシュしていなければ、root → TLD → authoritative を歩く。 初回ミスで典型 ~30 ms;ウォームキャッシュなら 1 ms 未満。 詳細:DNS と HTTP。
ステージ 3 —— socket() と最初の SYN
curl が socket(AF_INET, SOCK_STREAM, 0) 次に connect() を呼ぶ。 最初の syscall がカーネル struct sock を割り当てる; 2 つ目はカーネルに IP ヘッダ + TCP SYN セグメントを構築させ、 適切な qdisc にキューさせ、NIC ドライバに渡し、デバイスのリングバッファに TX ディスクリプタを書かせる。 NIC がパケットを DMA し完了を通知する。 詳細:システムコールと割り込みでユーザ→カーネル遷移、ネットワークスタックでヘッダ構築とドライバパス。
ステージ 4 —— 3-way ハンドシェイク
SYN が出る、サーバが listen() バックログから SYN-ACK を返す、クライアントが ACK を送る。 1 つの完全な RTT —— 典型的なインターネットパスで ~30 ms。両側で ESTABLISHED; シーケンス番号合意、ウィンドウサイズ交渉、MSS と selective-ACK オプション交換完了。 詳細:TCP ディープダイブ。
ステージ 5 —— TLS ハンドシェイク
https:// では、TCP ハンドシェイクの上に 2 つ目のハンドシェイクがある。 TLS 1.3 ではクライアントが ClientHello をキーシェア(エフェメラル ECDHE 公開鍵) とサポートする暗号スイートと共に送る;サーバが ServerHello、X.509 証明書チェーン、Finished メッセージを返す —— すべて 1 RTT で。 クライアントがシステムトラストストアでチェーンを検証しセッション鍵を導出する。さらに ~30 ms。 詳細:TLS とセキュリティ。
ステージ 6 —— HTTP リクエスト送出
今 GET /user/42 HTTP/1.1 を標準ヘッダと共に送る。 バイトはまず TLS レコードに暗号化され、TCP セグメント、IP パケット、Ethernet フレームに包まれ、 NIC に TX ディスクリプタとして投稿され、ワイヤに DMA され、インターネット越しにサーバへルーティングされる。 ハーフ RTT —— 典型 ~15 ms。詳細:DNS と HTTPでリクエスト形式、ネットワークスタックでカプセル化、CPU 体系構造で NIC のディスクリプタリングと DMA が 必要以上に CPU を巻き込まずバイトを動かす方法。
ステージ 7 —— サーバ NIC、IRQ、softirq
サーバ側で、NIC が入ってきたパケットを RX リングに DMA し、MSI-X 割り込みを上げる。 ドライバの IRQ ハンドラが softirq をスケジュールする —— NAPI ポーリングが 1 バッチで できるだけ多くのパケットを読む。各パケットはスタックを登る:IP 層(再)組み立て、 TCP セグメント処理、ペイロードをソケットの受信バッファに追加、リスニング fd を可読としてマーク。 詳細:システムコールと割り込みで IRQ → softirq の機構、ネットワークスタックでレイヤを登り返す。
ステージ 8 —— epoll がワーカーを起こす
サーバワーカースレッドが epoll_wait() でパークされていた。 ソケットが可読に切り替わると、カーネルが epoll の準備リストを歩き、スレッドを実行可能としてマークし、 CFS スケジューラがそれを CPU に乗せる。起床自体は裏で futex を使う —— 非競合時は安く、多数のスレッドが積み重なると高い。 詳細:I/O モデルで epoll vs io_uring と なぜこれが高スループットサーバの構造か、CPU スケジューリングで「実行可能」が CFS にとって何を意味するか、並行プリミティブで futex 起床自体。
ステージ 9 —— HTTP パースとディスパッチ
ワーカースレッドがリクエストバイトを読み、HTTP リクエスト行とヘッダをパースし、/user/:id ハンドラにディスパッチする。 パースループは完全に L1/L2 で生きるたぐいのもの —— 小さなホットバッファ、ほぼ予測可能なブランチ、 「素朴」と「丁寧」の差が分岐予測器が内ループを掴むかどうかになるコード。 詳細:CPU 体系構造でパイプラインが実際に何をしているか、メモリ階層で L1 に留まることが速い遅いの差である理由、キャッシュコヒーレンスで接続テーブルが複数コアから触られたとき何が起こるか、アセンブリ & ISAで JIT または AOT コンパイラがホットパスに何を出したか。
ステージ 10 —— データベースクエリ
ハンドラがプールされた Postgres 接続上に SELECT * FROM users WHERE id = 42 を発行する。 DB が主キーインデックスで B+tree 下降する:ルートページ、内部ページ、リーフページ; リーフがヒープタプルポインタを与える;buffer pool が既にそのヒープページを持つか (キャッシュヒット、合計 ~50 μs)、ファイルシステムと NVMe を通してディスクから取得する (キャッシュミス、+50 μs から数 ms)。行は MVCC 可視性ルールの下で読まれる。 詳細:データベースストレージで B+tree と buffer pool、メモリ階層で buffer pool が存在する理由、ファイルシステムで buffer pool の下のページキャッシュ層、ディスクストレージで 4 KB ランダム読みで NVMe が何をするか。
ステージ 11 —— レスポンスをシリアライズ
ハンドラがレスポンスオブジェクトを構築し、JSON シリアライザに渡す、 それはアロケータに 2 KB バッファを要求し UTF-8 バイトを書き込む。 TLS ライブラリが結果の HTTP レスポンスを TLS レコードに暗号化する; カーネルがそれを TCP、IP、ドライバ、NIC へと送り下ろす。 詳細:メモリ割り当てでアロケータ (大半のサーバで jemalloc / tcmalloc)、バイナリと数システムで シリアライザが生成する UTF-8 エンコーディング、TLS とセキュリティで各レコードの AEAD 暗号化。
ステージ 12 —— 帰路
パケットがインターネットを横断する(~30 ms RTT、レスポンス帰路は ~15 ms 追加)。 クライアント側で、NIC RX パスはステージ 7 を逆さまに反映する:NIC DMA → softirq → TCP 再構成 → ソケット受信バッファ。 curl がソケットから読み、TLS ライブラリがレコードを復号し、curl が平文 JSON を stdout に書く。 一方、サーバのハンドラが COMMIT でデータベーストランザクションを閉じる —— 可視効果は、選んだ分離レベルで順序付けられた、サーバ上の N 個の並行トランザクションのうちの 1 つ。 詳細:TCP ディープダイブで再構成、ネットワークスタックでレイヤを下る、I/O モデルで curl がソケットから読む方法、トランザクションでサーバの COMMIT が実際に保証するもの。
要点。「12 ステージ、5 レーン、2 エンドポイント、~250 ms の壁時計。 各ステージは詳細を 1 つの primer が所有し、クロスレイヤ関心事のために 2〜3 個を参照する。 ウォーターフォールは孤立したイベントの連なりではない —— それぞれが互いに自分の仕事をする前提で設計された 20 のサブシステムを流れる同じパケットだ。」
レイテンシ予算 —— 250 ms が実際にどこへ行くか
同じ 12 ステージ、別のレンズ。ウォーターフォールは順序を示す; このセクションはコストを示す。多くのエンジニアの 「HTTP 呼び出しのコストは?」の心的モデルは、少なくとも 1 ステージで 1〜2 桁ずれている。
単一バックエンドへの典型的な初回 HTTPS リクエスト、~30 ms RTT、ウォーム DB キャッシュ、接続再利用なし:
ステージ 典型 主因 ───────────────────────────────── ─────── ────────────────────────── 1. プロセス生成 + glibc 初期化 ~5 ms fork、ELF load、ページテーブル 2. DNS ルックアップ(初回) ~10–50 ms 上流リゾルバ + auth NS 3. TCP 3-way ハンドシェイク ~30 ms 1 × RTT 4. TLS 1.3 ハンドシェイク ~30 ms 1 × RTT + 証明書検証 5. リクエスト送出(ハーフ RTT) ~15 ms ワイヤ + 中継ルータ 6. サーバ NIC RX + ディスパッチ ~1 ms softirq + epoll 起床 7. HTTP パース + ルーティング ~1 ms L1/L2 ホット、ほぼ分岐 8. DB クエリ(ウォーム) ~1–50 ms buffer pool vs ディスク 9. シリアライズ + TLS 暗号化 ~1–5 ms アロケータ + AEAD 10. レスポンス(ハーフ RTT) ~15 ms ワイヤ 11. クライアント NIC RX + TLS 復号 ~1 ms softirq + AEAD 12. curl が stdout に書く ~50 μs 1 つの write() syscall ───────────────────────────────── ─────── 合計 ~110–250 ms
余地が隠れている場所
その表の 3 つの数字がゲームの全てだ。DNS: 初回ルックアップ後はキャッシュされ、後続リクエストではゼロに償却される —— が初回リクエストは常にそれを食う。DNS と HTTPでキャッシュ層 (ブラウザ、OS、リゾルバ)を見る。TCP ハンドシェイク: ~30 ms 一度、その後は keep-alive する限り接続の寿命の間ゼロ。TLS ハンドシェイク:さらに ~30 ms 一度、再利用セッションでは再びゼロ。 接続を再利用するとリクエストあたり 60 ms 節約できる —— HTTP/2 マルチプレキシングと接続プールがそれほど重要な理由。 HTTP/3(QUIC)は TLS と転送ハンドシェイクを融合してさらに 1 RTT 節約する。TCP ディープダイブとTLS とセキュリティで keep-alive と 0-RTT の機構を見る。
驚かされる 2 つ
初回リクエストの DNS は予想より遅いことが多い、なぜならリゾルバチェーンが深いから: あなたのマシン → systemd-resolved → 上流 → root → TLD → authoritative。 各ホップが本物の UDP 往復で、authoritative サーバは別の大陸にあるかもしれない。 初回ルックアップが 80 ms は珍しくない;200 ms も起こる。
WAL 上の fsync はサーバ上で起こっている最も長い単一のことであることが多い —— 上のトレースには見えないのは SELECT をしているからで、書き込みではないため。 これを last_seen を更新する POST にすると、サーバの COMMIT が 先行書き込みログに fsync を発行する。それは NVMe で 5〜15 ms、 劣化ディスクのクラウドブロックストレージで 50+ ms、あらゆる耐久書き込みのレイテンシを支配する。データベースストレージで WAL、トランザクションで COMMIT が実際に何を待つか、ディスクストレージで fsync が SSD に何をさせるか (揮発キャッシュをフラッシュ)を見る。
P99 スパイク —— メンタル プレイブック
平均レイテンシは上の表;P99 レイテンシは別の話を語る。よくある 4 つの容疑者:
- GC ポーズまたはコンパクション。ほとんどのマネージドランタイム(JVM、Go、.NET)は周期的に stop-the-world でコンパクトする。メモリ割り当てで 世代別 GC がなぜ存在しコストが何かを読む。
- スケジューラ遅延。CPU プレッシャー下では、ワーカーは スケジューラが選ぶまで runqueue に数十ミリ秒座っているかもしれない。CPU スケジューリングを見る。
- コールドページのページフォルト。ワーカーのスタックやホットデータページがスワップアウトされた(またはページインされなかった)場合、 最初のタッチはディスク往復。仮想メモリで メジャー vs マイナーフォルトを見る。
- fsync 停止。ディスクまたはその書き込みキャッシュが競合状態に達した; コミットがブロックする。ファイルシステムで ジャーナルコミットパス、ディスクストレージで SSD が時々 NAND を GC するためにすべてを一時停止する理由を見る。
要点。「典型的な ~250 ms のうち、1 ラウンドトリップ分(~60 ms)は TCP + TLS ハンドシェイクで、再利用接続では消える。ネットワーク移動はさらに ~30 ms で、 サーバを動かさずには避けられない。残りはサーバ側で、主に DB クエリと WAL 上の fsync で 境界される。P99 は別物 —— GC、スケジューラ遅延、ページフォルト、または fsync 停止。」
故障モード —— どこで何が壊れるか
各レイヤにはお気に入りの間違い方がある。症状(タイムアウト、遅いテール、接続切断、500)を 故障を所有するレイヤにマップできれば、推測をやめて修正を始められる。
- DNS サーバ到達不可。クエリがタイムアウト、glibc が数秒後に
EAI_AGAINを返し、 アプリが紛らわしい「name resolution failed」を露出する。 systemd-resolved がスタックしているか、リトライ予算のないネットワークで UDP パケットが落ちたのが原因のことが多い。 修正:短いリゾルバタイムアウト、1 回リトライ、リクエストハンドラを同期 DNS でブロックしない。DNS と HTTPを見る。 - TCP SYN が静かに落とされる。ファイアウォール、パス MTU ディスカバリが壊れている、 または過度に攻撃的なクラウドセキュリティグループ。クライアントは指数バックオフ(1 秒、3 秒、7 秒…)で SYN を再送する —— これが説明不能な「5 秒ハング」の正体。TCP ディープダイブとネットワークスタックを見る。
- TLS 証明書期限切れまたはチェーン破損。ハンドシェイクが HTTP を送る前に失敗する。症状:接続が開き、すぐに TLS アラートでクローズ。 本番で最も一般的な原因は期限切れリーフ証明書、欠落した中間証明書、 または関連ルートを欠くシステムトラストストア。TLS とセキュリティを見る。
- エフェメラルポート枯渇。クライアントが ~50K の接続をクリーンアップ可能より速く開く; 新しい
connect()呼び出しがEADDRNOTAVAILを返す。 通常、リクエストあたりの TCP 接続(プールなし)が、それを 60+ 秒TIME_WAITに 保持するロードバランサーにヒットすることが原因。TCP ディープダイブとネットワークスタックで接続状態の寿命を見る。 - サーバ NIC RX オーバーラン。ドライバがリングを排出できるより速くパケットが到着;
ethtool -Sでrx_droppedとして可視。TCP 再送が最終的にそれを隠すが、テールレイテンシが屋根を突き抜ける。ネットワークスタックでリングバッファ、システムコールと割り込みで 排出率を決定する softirq 予算を見る。 - アプリワーカーが CPU プレッシャーで飢える。負荷下(または共有ホスト上の暴走する隣人)で、ワーカーが runqueue に数十ミリ秒座る。 テールレイテンシがすべてのハンドラで一様に上がる、特定のものではない —— 明白なサイン。CPU スケジューリングを見る。
- リクエスト中のページフォルト。メモリプレッシャーで ワーカーのスタックやホットデータページがスワップアウトされた;最初のタッチはディスク読み(5-15 ms)。
vmstatが非ゼロsi/soを示すことで診断。仮想メモリとメモリ割り当てを見る。 - データベース行ロック競合。2 つのトランザクションが同じ行を UPDATE したい; 1 つが
lock_timeoutまで待つ。シリアライザブル分離では、敗者がリトライ可能エラーを得る。トランザクションで分離、並行プリミティブで 根底のロック機構を見る。 - COMMIT 時の fsync 停止。NVMe 劣化、ディスクフル、または揮発キャッシュフラッシュが遅い。 COMMIT が数百ミリ秒ブロックする。スロットルされた IOPS のクラウドブロックストレージは これを見る典型的な環境。ファイルシステムで ジャーナルコミットパス、ディスクストレージで NAND が時々ガベージコレクトのためにすべてを一時停止する理由を見る。
- キャッシュコヒーレンス偽共有。2 つのコアが同じ 64 バイト struct の 隣接フィールド(カウンタ、ロック、ホットテーブルスロット)を繰り返し書く。 MESI がコア間でラインをバウンスする;スループットが静かに 2-4× 下がる。キャッシュコヒーレンスを見る。
- HTTP/2 ストリーム ID 枯渇または HOL ブロック。1 つの接続がストリーム ID を使い切る(2^31、だが高変動の長寿接続でヒットしうる)、 または 1 つの遅いストリームが TCP head-of-line で接続全体をブロックする。 HTTP/3 は QUIC へ移行することで HOL を修正する。DNS と HTTPを見る。
要点。「サーバからの 500 はほとんど決して全部の物語ではない。 症状はレイヤを上って旅する;原因は 1 つに住む。 各共通故障をどのレイヤが所有するかを知ることで、最初に正しい質問ができる —— このシリーズの 20 primer はまさにそのマッピングだ。」
クイックリファレンス
冷たく歩ける価値のある 6 つの大局的問題と、本番システムで見抜くべき 5 つの赤旗。
1 つの curl https://... GET のライフサイクルをスケッチして —— 12 ステージを名前付ける。
(1) プロセス生成、(2) DNS ルックアップ、(3) ソケット + 最初の SYN、 (4) TCP 3-way ハンドシェイク、(5) TLS 1.3 ハンドシェイク、(6) HTTP リクエスト送出、 (7) サーバ NIC RX + softirq、(8) epoll がワーカーを起こす、 (9) パース + ディスパッチ、(10) DB クエリ、(11) シリアライズ + TLS 暗号化、 (12) 帰路 + stdout。5 つのスイムレーン (クライアントアプリ、クライアントカーネル、ワイヤ、サーバカーネル、サーバアプリ)、 初回リクエストで典型 ~250 ms の壁時計。
ライフサイクルで最も長い典型的なレイテンシはどこで、何が支配する?
TCP + TLS ハンドシェイクとネットワーク往復が合わせて ~90 ms を消費する (~60 ms ハンドシェイク + 各方向 ~30 ms のネットワーク移動)—— 予算の 3 分の 1 以上。 残りはすべてサーバ側で、通常データベースクエリ(キャッシュ vs ディスクで ~1–50 ms)、 書き込みでは COMMIT 時の WAL 上の fsync(NVMe で 5–15 ms)で境界される。 初回 DNS ルックアップもコールドキャッシュ時はハンドシェイクと対抗できる。
接続が再利用される(keep-alive / HTTP/2 / HTTP/3)場合、何が違うか?
ステージ 3、4、5 が消える —— TCP ソケットは既に ESTABLISHED、TLS セッションは既にネゴ済み。 「リクエスト送出を決める」から「HTTP バイトがワイヤに乗る」へ直行、リクエストあたり ~60 ms 節約。 HTTP/2 は同じ接続上で多くのリクエストをマルチプレキシングする; HTTP/3(QUIC)はすべてを UDP 上でストリームごとの信頼性で実行することにより TCP レベルの head-of-line ブロックも避ける。初回リクエストは変わらない; 後続はすべて劇的に安くなる。
P99 レイテンシが突然スパイクしたら、メンタル プレイブックは?
診断しやすい順に 4 つのよくある容疑者をチェック。(1) GC / コンパクション —— ランタイムメトリックがレイテンシと整合するポーズスパイクを示す? (2) スケジューラ遅延 —— runqueue 深度が上昇、またはクラウドで steal time が非ゼロ? (3) ページフォルト —— vmstat が非ゼロ si/so またはメジャーフォルト率の上昇を示す? (4) fsync 停止 —— ディスクレベルの書き込みレイテンシ上昇、 または WAL 自体が長いコミット時間を報告? 多くの場合、答えは単に共有ホストの「うるさい隣人」。
暗号化はどこでレイテンシを追加し、どこでしないか?
新規接続では TLS ハンドシェイクが TCP ハンドシェイクの上に丸ごと 1 RTT(~30 ms)。 その後、レコードあたりの暗号化(AES-NI / 専用命令付き AES-GCM または ChaCha20-Poly1305)は キロバイトあたり 1 桁マイクロ秒で、本質的に無料。コストはハンドシェイク、継続的な暗号化ではない。 接続を再利用すると TLS の限界コストはゼロに近い。
本番で最適化を頼まれる可能性が最も高いステージを 1 つ選んで、余地を説明。
データベースクエリ(ステージ 10)。最も変動の大きい単一ステージ —— 50 μs (buffer-pool ヒット、完全に L2 内の B+tree 下降)から数百ミリ秒 (キャッシュミス、ロック待ち、フルテーブルスキャン)まで。 余地は通常インデックス(正しいものが欠けている)、クエリプラン(予期しない順次スキャン)、 または分離レベル(シリアライザブルが維持できる以上のリトライを強いる)にある。 ネットワークは最適化が難しい;勝ちはほぼ常にデータベースにある。
本番の赤旗
- 「もっとレプリカを投げ込めばいい」。レイテンシが実際にどこにあるかを測定せず、水平スケーリングするのは 同じ無駄なハンドシェイクと fsync 待ちを倍にするだけ。 先にプロファイル、後でスケール。
- 外部 HTTP 呼び出しをリクエストハンドラ内で安いと扱う。各リクエストでの同期 DNS + TCP + TLS ハンドシェイクは呼び出しあたり 60–90 ms を燃やす。 ユーザリクエストあたり N 回そのような呼び出しをするなら、N 倍払う。 接続をプール、DNS をキャッシュ、可能ならバッチ。
- プーリングなしのリクエストごとのデータベース接続。各クエリで新しい TCP + TLS ハンドシェイクは接続プールが 1 行の設定で修正する種類の自傷。 PgBouncer-または同等のルール。
- 「リクエストが十分速かった」からコミット時に fsync をスキップ。時に正しい(キャッシュ、ログ);しばしば実際に電源を失う唯一の機械でのみ表面化する データロスバグの始まり。どの書き込みに耐久性が必要かを明示的に決める。
- 分散トレーシングなし。20 レイヤのうちどれが責任あるかを推測して遅いリクエストをデバッグするのは、 本番エンジニアリングで最大の単一の生産性シンク。各レイヤ境界にスパン —— DNS、TCP、TLS、サーバエントリ、DB クエリ、ダウンストリーム呼び出し —— を打つことで「時間はどこへ行く?」を数日の調査から 1 画面に変える。