- 良いシステム設計とは、複雑に見えず、長期間にわたって特に問題が発生しない形を指す
- システム設計で最も難しいのは 状態(state) を扱うことであり、可能な限り状態を保存するコンポーネント数を減らす方向が重要
- データベースは主に状態が保管される場所であり、スキーマ設計とインデックス設計、ボトルネック解消に重点を置いたアプローチが必要
- キャッシュ、イベント処理、バックグラウンド作業 などは性能と保守性のために慎重に導入すべきであり、乱用は避けたほうがよい
- 複雑な設計よりも、十分に検証されたシンプルなコンポーネントや方法論を適切に使うことが、持続可能で安定したシステム構築の鍵
システム設計の定義と全体的なアプローチ
- ソフトウェア設計がコードの組み立てだとすれば、システム設計はさまざまなサービスを組み合わせるプロセス
- システム設計の主な構成要素には、アプリサーバー、データベース、キャッシュ、キュー、イベントバス、プロキシ などがある
- 良い設計は「特に問題がない」「思ったより簡単に終わる」「ここは気にしなくてよい」といった反応を引き出す
- 逆に、複雑で目立つ設計は、根本的な問題を隠していたり、過剰設計を示していたりする可能性がある
- 複雑なシステムは最初から導入するよりも、最小限で動作する単純な構造から徐々に発展させる方が有利
状態(state)とステートレス(stateless)の区別
- ソフトウェア設計で最も厄介なのが 状態管理
- 情報を保存せず即座に結果を返すサービス(GitHubのPDFレンダリングのようなもの)はステートレスである
- 一方で、データベースへの書き込みを行うサービスは状態を管理している
- システム内の状態保存コンポーネントは、できる限り減らしたほうがよい。これはシステムの 複雑性と障害発生可能性 を下げる
- 状態管理は1つのサービスだけが担い、他のサービスはAPI呼び出しやイベント発行など ステートレスな役割 に集中する構造が推奨される
データベース設計とボトルネック
スキーマとインデックスの設計
- データ保管のためには、人が読みやすい形のスキーマ設計 が必要
- 柔軟すぎるスキーマ(例: 全体をJSONカラムに保存する方式)は、アプリケーションコードや性能に負担を与える可能性がある
- クエリが頻繁に発生するカラムを基準に 適切なインデックスを設定 すべきであり、何にでもインデックスを張るのはむしろ無駄なオーバーヘッドになる
ボトルネックの解決方法
- データベースアクセスはしばしば重いボトルネック になる
- 可能な限り複雑なデータ処理はアプリケーションではなくデータベース内で JOIN などを使って処理 するほうが性能面で有利
- ORM使用時には、ループ内でクエリを発行してしまうミスに注意すべき
- 必要に応じてクエリを分割し、データベースの負荷やクエリの複雑さを調整するのも1つの方法
- 読み取りクエリはレプリカ(read-replica)に分散 し、主たる書き込みノードの負荷を下げる戦略が有効
- 大量のクエリが集中すると、トランザクションや書き込み処理はデータベースを簡単に過負荷にしうるため、クエリスロットリング(制限) の検討が必要
遅い処理と速い処理の分離
- ユーザーが操作する処理は、数百ミリ秒以内の応答 が必要
- 時間のかかる処理(例: 大容量PDF変換など)は、最小限の作業だけをフロントで即時提供 し、残りはバックグラウンドへ回すパターンが有効
- バックグラウンド処理 は、一般にキュー(例: Redis)とジョブランナーが組み合わさって動作する
- 遠い将来に予約された処理は、RedisよりもDBテーブルを別途作って管理し、スケジューラで実行する形が実用的
キャッシュ
- キャッシュ は、同一または高コストな計算を繰り返す場合に、コスト削減と性能向上に貢献する
- たいてい、キャッシュを最初に学んだ ジュニアエンジニアは何でもキャッシュ したがり、経験豊富なエンジニアほどキャッシュ導入には慎重
- キャッシュは新たな状態を導入するため、同期の問題・エラー・古いデータ(stale data)などのリスク がある
- まずクエリへのインデックス追加のような性能改善を試したうえで、キャッシュを適用するのが望ましい
- 大容量キャッシュはRedis/Memcachedではなく、S3/Azure Blob Storageのような オブジェクトストレージに定期保存 する方法も活用できる
イベント処理
- ほとんどの企業は イベントハブ(例: Kafka) を備え、さまざまなサービスがイベントベースで分散処理を行っている
- イベントを乱発するより、シンプルなリクエスト–レスポンスAPI設計のほうが ロギングや問題解決の面で有用
- イベントベース処理は、送信側が受信側の動作を気にしなくてよい場合、あるいは 高スループット・遅延許容シナリオ に適している
データ伝達方式: PushとPull
- データ伝達には Pull(要求後に応答) と Push(変更時に自動配信) の2つの方式がある
- Pull方式は単純だが、繰り返しリクエストや過負荷の問題が発生する
- Push方式はサーバーでデータ変更が起きた際にクライアントへ即座に配信するため、効率的で最新データの維持に有利
- 大量クライアントを扱うには、それぞれの方式に応じてインフラ(イベントキュー、複数のキャッシュサーバーなど)の拡張が必要
ホットパス(Hot Paths)への集中
- ホットパス とは、システム内で最も重要で、データが多く流れる経路を意味する
- ホットパスは選択肢が少なく、設計に失敗すると サービス全体に深刻な問題 を引き起こしかねないため、慎重な設計が必須
- 選択肢の多い小規模機能よりも、ホットパスに集中して設計とテスト にリソースを配分するほうが効果的
ロギング、メトリクス、トレーシング
- 障害発生時の原因診断のため、異常系(unhappy path)に対する詳細ログ を積極的に記録すべき
- システム資源(CPU/メモリ)、キューサイズ、リクエスト/処理時間など、基本的な 可観測性指標の収集が必要
- 平均値だけでなく、p95、p99レイテンシのような分布指標 も必ず観測すべき。上位少数の遅いリクエストが主要ユーザーの問題かもしれない
キルスイッチ、リトライ、障害復旧
- キルスイッチ(システムの一時停止) と、リトライの戦略的活用が重要
- むやみなリトライは他サービスに負担をかけるだけで、事前にサーキットブレーカー(circuit breaker) などでリクエストを制御してこそ効果がある
- Idempotency Key(冪等キー) を導入すれば、同一リクエスト再処理時の重複作業を防げる
- 一部の障害状況では フェイルオープン(fail open) またはフェイルクローズド(fail closed) の選択が必要。たとえばRate Limitingはfail open(許可)のほうがユーザー影響が小さい。一方で認証はfail closedが必須
まとめ
- サービス分離、コンテナ、VM導入、トレーシングなど一部の話題は省かれているが、十分に検証されたコンポーネント を適材適所で使うことが、長期的に最も安定したシステム構築につながる
- 技術的に特別な設計は実際には非常にまれであり、退屈なくらいシンプルな設計 が、むしろ実務で最もよく使われる
- 本質的に良いシステム設計とは、目立たず、十分に実証された方法論を安全に組み合わせるプロセスである
まだコメントはありません。