Designing Data-Intensive Applications の第1章の読書ノートを公開する。主題はシステムの信頼性、保守性、スケーラビリティについてである。
個別の手法や技術というよりは、システム設計や技術選定の際に大事にすべきマインドセットに重点をおいた論文である。おおいに同意、納得しながら読めたし、これからも長く大切にしたい視座だとしみじみ味わっている。いつもよりもよく咀嚼しながら読み進めたのに加えて、ダメ押しで記憶に焼き付けるために、サマリとして記録しておく。
現代のシステム開発のパラダイムは、計算能力よりもデータの量、データの複雑度、データの変化の速さを問題にする。このようなシステムをデータシステムと呼ぶことにする。
データシステムに必要な構成要素はおおかた世の中に出揃っている。データベース、キャッシュ、検索インデックス、メッセージキュー、バッチ処理、などの諸要素である。とはいえ、現実の技術選定のあり方は、プロジェクトの特性によるとしか言いようがない。実際、 Redis はデータベースだがメッセージキューとしても使えるし、Apache Kafka はメッセージキューだがデータベースにもなる。そして往々にして、「どのツールを採用するか?」というよりも「どのツールをどう組み合わせるか?」ということが問題になる。
そうしてデータシステムを設計するとき、主要な関心ごとは例えば次のような項目となる:
- どうやってデータの一貫性を保証する?
- どうやってデータシステムの一部が壊れても主要機能が動き続けられるようにする?
- どうやって負荷に耐えてスケールさせる?
- どんな API を用意すれば使いやすいサービスになる?
もちろん糸口は様々にあるが、多くの場合で有効な、3つの基本的な考え方がある。それが表題に掲げた「信頼性、保守性、スケーラビリティ」であり、これらの要素はいずれも非機能要件に分類されるものである。それぞれについて簡単に要約するとこうなる。
- 信頼性: ハードやソフトの障害、あるいはユーザーエラーへの耐用性
- 保守性: 新しいメンバーが既存の振る舞いを守りながら開発に参入していけるか?
- スケーラビリティ: データ量やトラフィックの増大に耐えられる仕組み
以下に見出しを立ててより詳しく検討してみよう。
信頼性
システムの信頼性とは、なにかおかしなことが起きても機能し続けることである。部分的な欠陥が存在する可能性をあらかじめ織り込むことで、「部分で障害が発生しても全体は機能し、要件を満たし続けることができる」という状態を作り出すメカニズムが要請される。たとえばカオスエンジニアリングは有効なアプローチといえるだろう。
ハードウェア障害
耐用年数など、確実な対応が求められる問題は存在する。しかしクラウドコンピューティングの時代であるから、個々のシステム設計者がこのレイヤの問題に精通している必要は必ずしもない。
ソフトウェアエラー
ハードウェア障害によって全てのマシンが連鎖的に落ちることは稀である。しかしソフトウェアエラーにはそれがありうる。テストや監視といったプラクティスの小さな積み重ねで予防できるから、手を抜かずに基盤を整備しよう。
人的エラー
操作ミスや想定外のユースケースによってシステムが破綻することはありうる。そうしたエラーを起こさせないための制約を、ユーザーが不自由を感じない範囲で考えて設計しよう。たとえば自動テストはとりわけコーナーケースの振る舞いを調べるのに有効であるし、本番環境相当のサンドボックスがあればなおよい。明快な監視メトリクスを定義して、問題を予兆することも可能である。
以上が信頼性の議論である。これらは開発コストとのトレードオフで犠牲にされることも少なくない。絶対悪だとはいわないが、その判断は決して軽い意思決定ではないことはよく認識しておこう。
保守性
保守性を高めるにはどうすればよいか? 逆を考えてみよう。保守性が低くならざるを得ないシステムとはどんなものだろうか? それは、運用が難しく、新入社員には扱いづらく、変更のするのが困難なシステムのことだ。
ここでは「保守性が高い」という状態を運用が容易で、シンプルで、進化の余地が用意されたシステムと定義しよう。
運用が容易であること
悪いソフトの欠陥を運用でカバーすることはできる。しかし悪い運用はどれほどよいソフトも安全に扱うことはできない。
できることは、ここでも小さなプラクティスの積み重ねしかない。たとえば、個別のマシンに依存せず、システムを停止せずにメンテナンスできるようにすること。あるいは、規定の振る舞いには制約を与えつつ、管理者権限で柔軟に操作できるようにすること。それから、予想可能な振る舞いを定義し、驚きを最小にすること。人に優しいシステムを設計しよう。
シンプルであること
小さくシンプルなシステムが、やがて大きくなるほどに複雑性を高めていく。これは当然の摂理である。しかし、大きなシステムでも複雑性を下げることは十分に可能である。
複雑性には「本質的な複雑性」と「偶発的な複雑性」がある。後者は「事故的な複雑性」といってもよいかもしれない。これを削減することを考えよう。
要するに、「単純な要件を複雑に実装してしまっている」ような箇所を見つけ出し、そこにアプローチしよう。単にテストとリファクタリングが有効なこともある。あるいは適切な抽象化が要請されることもあるだろう。これらの営みによって、この種の複雑性は排除できるはずである。
小さなプログラムならともかく、分散システムにおいてどう抽象化を実装するかを考えるのは難しい。これはより大きな問題であるから、のちの章に話題を譲ろう。
進化の余地が用意されていること
システムの要件は常に変わりうるものである。まずはこの真理に向き合おう。変わり続ける要件に対応できないシステムは本質的に脆弱である。
アジャイルとは組織論だが、方法論としてこの問題に対して有効である。大規模データシステムにおいてどのようにアジャイルを実践するか? これもまたより大きなトピックである。組織のシンプルさ、システムの抽象化にも密接に関係する問題提起として、これも詳しい議論はのちの章に議論を譲ろう。
スケーラビリティ
システムをミクロに観察して「これはスケールする/しない」と評価することは本質的にはできないはず。より適切な問いは「システムが成長したときに対応できるか?」や「さらに計算資源を投じるときはどうするか?」といった事柄である。これらの問いに答えるには、まず「負荷」を定義しなければならない。
「負荷」の定義はプロダクトの性質によってさまざまである。まず次のような単純な命題から考え始めてみよう。
- 読み込み負荷が大きい? or 書き込み負荷が大きい?
- データ量が大きい? or データの複雑性が高い?
- 多量の小さなリクエストが与えられる? or 少量の大きなリクエストが与えられる?
「負荷」を定義し、それに影響を与えるパラメータを定義できて、はじめてパフォーマンスを語ることができる。2012年のTwitterにとって、それは「ユーザーあたりのフォロワー数の分散」であった。リソース量を変えずに負荷を増やすとどうなる? あるいは、負荷が増えてもパフォーマンスが変わらないためにはどれだけリソースが必要になる?
パフォーマンスの代表的な指標であるレスポンスタイムを例にとってみよう。最大値や最小値をターゲットにすることにはあまり意味はない。結果が分散していることを前提に、パーセンタイルで評価が行われることが多い。95%, 99%, 99.9%あたりをメトリクスとすることが多い。具体例として、 Amazon では 99.9% の厳しいパーセンタイルをレスポンスタイムの重要指標としている。レスポンスに多くのデータが含まれるのはしばしば大のお得意様であるから、もっとも読み込み負荷の高い上客をこそ満足させることが重要なのである。そのうえでこれより厳しい99.99%のパーセンタイルに投資することは効果に見合わないとも結論づけられている。
「負荷」の定義を誤ったままパフォーマンス改善に着手してしまうと、大きな機会損失につながってしまう可能性が高い。ゆえにスタートアップなどではしばしば、負荷増大を心配するよりも機能の柔軟性を高めるほうが有効である。