良いシステム設計とは — 目立たない設計こそ最高の設計
状態管理、DB設計、キャッシュ、イベント処理、障害復旧まで — 実証済みのシンプルさの力
Sean Goedeckeの記事をベースにまとめたシステム設計の原則だ。
良い設計は目立たない
ソフトウェア設計がコードの組み立てなら、システム設計はサービスの組み合わせだ。アプリサーバー、データベース、キャッシュ、キュー、イベントバス、プロキシ — これらのコンポーネントをどう配置するかの問題。
良い設計に出会うと「特に問題なかった」「思ったより簡単だった」という反応が出る。逆に複雑で目立つ設計は根本的な問題を隠しているか、過剰設計の表れだ。最初から派手な構造を入れるより、最小限のシンプルな構造から始めて段階的に発展させる方がいい。
状態(State)が最も難しい
システム設計で最も厄介なのが状態管理だ。情報を保存せず結果だけ返すサービス(GitHubのPDFレンダリングのような)はステートレスだ。DBに書き込むサービスは状態を管理する。
状態を保持するコンポーネントを最大限減らすべきだ。 複雑さと障害可能性が同時に下がる。状態管理は1つのサービスだけが行い、残りはAPI呼び出しやイベント発行のようなステートレスな役割に集中する構造が良い。
データベース — スキーマとインデックス
人が読みやすいスキーマが良い。柔軟すぎるスキーマ(全部JSONカラムに入れる)はアプリケーションコードに負担をかける。インデックスは頻繁なクエリのカラムにだけ張る — 全カラムにインデックスを張ると書き込みオーバーヘッドが増えるだけだ。
DBがボトルネックになったらJOINを積極活用する。アプリケーションで複数回クエリして合わせるよりDBでJOINする方がほとんどの場合速い。ORM使用時はループ内クエリのN+1問題に注意。
読み取りはレプリカに分散し、書き込みはスロットリングを検討する。
遅い作業と速い作業の分離
ユーザーインタラクションは数百ミリ秒以内の応答が必要。時間のかかる作業(大容量PDF変換等)は最小限の応答だけフロントで返し、残りはバックグラウンドに回す。キュー(Redis等)+ジョブランナーの組み合わせが一般的。
遠い将来のスケジュール処理はRedisよりDBテーブルで管理してスケジューラで実行する方が実用的。
キャッシュは慎重に
キャッシュは高コストな繰り返し演算を減らせる。でもジュニア時代は何でもキャッシュしたくなり、経験を積むほどキャッシュ導入に慎重になる。
キャッシュは新しい状態を導入する。同期問題、ステイルデータ、無効化バグ — 全部キャッシュから生まれる。まずインデックス追加のようなクエリ最適化を試み、それでもダメなら初めてキャッシュを付ける。
イベント処理
ほとんどの企業がKafkaのようなイベントハブを持っている。でもイベントを乱発すると追跡が難しくなる。シンプルなリクエスト・レスポンスAPIの方がロギングとデバッグで有利だ。
イベント駆動処理は、送信者が受信者の動作を気にしなくていい時、あるいは大量・遅延許容シナリオに適している。
PushとPull
Pullはシンプルだが繰り返し要求が問題になる。Pushは変更時に即時配信するので効率的で最新データの維持に有利。大量クライアントの処理にはどちらの方式でもインフラ拡張が必要。
ホットパスに集中
最もトラフィックが流れる経路がホットパスだ。ここの設計失敗はサービス全体に影響するので、マイナー機能よりホットパスに設計・テストリソースを集中すべきだ。
ロギングと観測性
異常経路(unhappy path)に詳細ログを積極的に残す。平均値だけ見ずにp95、p99レイテンシも必ず観察する。最も遅いリクエストが最も重要なユーザーの問題かもしれない。
キルスイッチと障害復旧
闇雲なリトライは他のサービスに負担をかけるだけだ。サーキットブレーカーで要求を制御し、冪等キー(idempotency key)で重複処理を防止する。
障害時にfail open(許可)かfail closed(遮断)かの選択が必要。Rate limitingはfail openがユーザー影響が少ない。認証はfail closedが必須。
退屈なほどシンプルな設計が本番で生き残る
技術的に特別な設計は非常にまれだ。実証済みのコンポーネントを適材適所に配置するのが長期的に最も安定する。目立たず、十分に実証された方法論を安全に組み合わせること — それが良いシステム設計だ。
動作フロー
状態を保持するコンポーネント数を最小化する
DBスキーマは読みやすく、インデックスは必要な箇所だけ
遅い処理はバックグラウンドに分離する
キャッシュはクエリ最適化の後、最終手段として導入
ホットパスに設計・テストリソースを集中する
サーキットブレーカー+冪等キーで障害に備える