- 米国のメディケア健康保険プラン比較システムを再構築し、**実績ある技術(Postgres、golang、React など)**だけで構成したシンプルな構造で、10億件を超えるWebリクエストを安定して処理した経験を共有する
- シンプルさと安定性を目標にアーキテクチャを設計し、平均10ms未満の応答速度と非常に低い障害率を達成した
- **イノベーション(innovation token)**は中核となる構造分離(3つの大型モジュール、gRPC通信)にだけ最小限で適用し、それ以外はすべて退屈でも信頼できる方法論を選んだ
- DBスキーマ管理、ETLパイプライン、テスト、ロギング、ドキュメント化、CLIツールまで、あらゆる運用要素を再現可能でシンプルな方法で構築し、チーム全体が容易に理解・保守できるシステムを完成させた
- 継続的な品質管理と強いチームワークが、大規模な政府プロジェクトでも通用することを生々しく示している
Serving a billion web requests with boring code
High level
- 2年半にわたり、米国政府のメディケアプラン比較・購入Webサイトのリード開発を担当した
- 1日平均500万件のAPIリクエストを処理し、平均応答速度は10ms未満、95%のリクエストは100ms以下を維持した
- 障害発生率が非常に低く、実際にエンジニアが深夜に呼び出された事例は片手で数えられるほどだった
- Postgres、golang、Reactなど、誰もが理解できる実績ある技術だけで構成し、着実に安定したシステムを構築した
Boring über alles
- 最優先の原則は、**「退屈で実証済みの技術」**を優先すること(Choose Boring Technology)
- 新しい試みは、本当に必要な箇所でのみinnovation tokenを節約して使う
- 複雑で派手なソリューションよりも、安定して明快な技術とプロセスを好む
The boring bits
- Postgres: データ保存の中核であり、信頼性と拡張性の両方を満たした。複雑な検索(ファセット検索など)もPostgresで解決した
- golang: ビルドとデプロイが速く、バイナリ成果物が明確。エラーハンドリングが直感的で、新しいチームメンバーもすぐに適応できた
- React: SPAフレームワークの中で最も実績があり、チームメンバーもすでに慣れていた。アクセシビリティと多様なデバイス対応も重要な考慮要素だった
- 長期的にはバンドルサイズと速度低下の問題が生じたが、当時の状況では期限内に結果を出すための最適な選択だった
The innovation tokens
- Modular backend: バックエンド全体を、マイクロサービスでもモノリスでもない3つの大型モジュール(druginfo、planinfo、beneinfo)で構成した
- 各モジュールは別々のPostgres DBを使い、データ共有はgRPCを通じてのみ行われる
druginfo: 薬局、保険、包装などの組み合わせで指数的に増える薬価情報を非常に精密にインデックス化し、複雑な前処理と性能最適化が必要だった
planinfo: 毎日新しいCMSデータを受信し、DB全体を作り直して使う(不変性を維持)
beneinfo: 実際の加入者情報を保管する唯一の部分で、機微なPII(個人情報)は最小限だけ保存した。データ流出リスクを最小化するため、設計と運用に注意を払った
- gRPC: モジュール間通信インターフェースをコードで明確に定義できる利点がある。自動化ツールとの連携性にも優れている
- ただし、ビルド・ツール群・デバッグは複雑で、JSON APIに比べて直感性が劣るという欠点もあった
- grpc-gatewayを通じてWebクライアントをサポートし、大量トラフィックも無理なく処理した
Strict backwards compatibility
- APIおよびデータベースの後方互換性維持を厳格に守った
- 公開APIのフィールドは決して削除せず、セキュリティ問題がない限り半永久的に維持する
- DBカラムも追加は自由だが、削除は複数段階の検証(参照除去→数週間待機→実際の削除)を経る
- この規律が、高い変更速度と安定したデプロイ・運用の中核的な土台になった
Faceted search
- ElasticSearchの代わりにPostgresだけでファセット検索を実装した
- 適切にインデックスされたplanテーブルに条件を組み合わせる250行の関数1つで、すべての検索ロジックを処理した
- ビジネス要件に集中し、不要な複雑さなしにシンプルに解決した
Database
-
creation
- DBスキーマは番号付きの.sqlファイルで管理し、順番に読み込むことで信頼性を担保した
planinfo/beneinfo DBは毎日再作成されるため、マイグレーションは不要。バージョン不一致などの設定ミスがある場合は、アプリ自体を起動しない設計にした
-
ETL
- データソースごとのシェルスクリプトでS3に投入し、cronでEC2インスタンスが最新のETLコード/データを取得して新しいRDS DBを作成する
- PostgresのCOPY文を積極的に活用し、INSERTの代わりに大量データ投入を効率的に処理した
- 毎日2〜4時間で数億行のデータを新しいDBへ切り替えられた
-
models
- xoライブラリでDBモデルを自動生成し、カスタムテンプレートでチーム向けのコードを生成した
-
testing
- 最大の失敗はsqlmockを使ったテストを作り込みすぎたことで、データが頻繁に変わる状況では保守が非常に煩雑だった
- 実際に不変DBを使うなら、実DBに対するテストのほうが効率的だったはずだ
-
Local database for development
- 各テーブルの部分データを自動生成するスクリプトにより、開発者ごとに小さなローカルDBで実データベースベースのテストと開発が可能だった
- DBが大きくなる前にこうしたツールを整備すると、チーム全体の開発効率を最大化できる
Miscellaneous tooling
- 各種の運用・観測自動化のため、CLIツールをシェルスクリプトで実装し、すべてのユーティリティ機能を1つにまとめて管理した
- SplunkのログをSlackコマンドで即座にグラフ化するなど、現場中心のツールを積極的に開発・活用した
Logging
- リクエスト受け付け時にrequest idを生成し、そのidをすべてのログコンテキストに付与して、どこからでも追跡できるようにした
- zerologで安全かつ体系的なロギングを設計した
- システムの入口・出口、例外状況など重要な時点では必須ログを残した
Documentation
- GitHub Markdown文書をsphinx-book-themeで変換し、ウィキブックとして運用した
- チームメンバー全員が積極的にドキュメント化に貢献し、あらゆるシステム文書を1か所で見つけられるようにした
- 優れたドキュメント文化が、チームの成長と保守、新人オンボーディングの効率を大きく高めた
Runtime integrations
- クライアントからの性能低下を招く要求(分析スクリプトの挿入など)は、できる限り説得して最小化した
- クエリもブラウザランタイムではなくビルド時処理へ移し、サービス性能を維持した
- 実際には顧客の要望をすべて防ぐことはできず、一部は性能低下につながった
And more
- 技術面以外にも、前向きで協力的なチームの雰囲気と強いモチベーションこそが、大規模システム成功の真の原動力だと強調している
- 些細だが重要な実務上の選択と、継続的な品質管理の力を実感した事例だった
18件のコメント
こんな退屈なものを2.5年も?!
Faceted Search が何か気になって追ってみたら、さらに読む価値のあるものがありました。
https://www.cybertec-postgresql.com/en/faceting-large-result-sets/
https://roaringbitmap.org/about/
https://github.com/cybertec-postgresql/pgfaceting
「退屈」に対する意見が面白いですね(笑)。別の言い方にすると、何がよいでしょうか? ありきたりな、よくある、などでしょうか?
boringを「退屈」と訳したのは、本来の意味をあまりうまく捉えられていませんね。boringnessは Go の設計哲学の一つです。退屈〜なふり…
韓国は何だかんだで Java 一色の国だから、なじみがないんだねw
golang も React も、どちらも新時代の無難な(boring な)エンタープライズ向けコーディング言語だと思います。
boring -> 「退屈」と 100% 正確に訳せるわけではないので、韓国の読者にはニュアンスがきちんと伝わっていないように見えます。
Postgres、golang、Reactのような退屈な世界で生きていたいですね
そうですね、タイトルを見て何の冗談かと思いましたね
海外では、それが退屈なスタックだと思われているようです。
実際、Goは単にWebサーバーを作るには最も簡単な選択ですし……
RustやFP系の言語のようなもので開発してこそ、退屈ではないと思われているみたいですね。
あまりにも当たり前な話ばかり……あまりにも当たり前すぎて見落としている大事なこと……
この程度なら、そこまで退屈に見えるスタックでもなさそうですね。本当に退屈だと言うなら、Java 1.8以下のバージョンやVBくらいは出てこないといけないのでは……という不謹慎な考えが浮かびます。
原文には
boringに関するリンクがありますが、内容を見ると、boring= 「あまりにも慣れ親しんでいる」という意味のようです。experienced、verified、skillfulのような、もっと適切な単語があるのに、あえてboringを使ったのは、注目を集めようという意図があるように思えますね。書くのが退屈ということではなく、あまりにも何度も使っていて退屈な定番スタック、という意味で表現したのではないでしょうか
Linuxカーネル 2.6.29あたり...
gRPCを使ったってこと自体が…笑
私も「golangが退屈だって?」という感想が最初に浮かびました
classic aspくらいなら退屈だと言えそうですが