Efficiency trades off against resiliency

Nelson Elhage

効率はレジリエンスとトレードオフする

原文は Nelson Elhage により に公開されました。 このブログを購読する

サーバーにとって「適切な」CPU使用率とはどのくらいでしょうか。適切に設計され、適切に運用されているサービスの監視ダッシュボードを見たとき、1〜2日平均でどの程度のCPU使用率を見るのが望ましいのでしょうか。

これは非常に抽象的な問いであり、単一の答えがあるのかすら定かではありません。とはいえ、長い間、私は基本的に「高ければ高いほど良い」と考えていました。できる限り100%に近い使用率を目指すべきだ、と。なぜなら、100%未満はハードウェアの能力を使い切っていないことを意味し、すなわちリソースの無駄だからです。あるサービスがCPUを使い切っていないのであれば、より小さなインスタンスに移すか、そのノードで別の処理を走らせることもできるはずです。

しかし、この単純な直感は、実際のところ、ほとんどの場合正しくありません。

仮にその理想を達成し、サービスがほぼ100%の使用率で稼働しているとしましょう。そこで突如バズって予想外のトラフィックが押し寄せたらどうなるでしょうか。あるいは、リクエストごとにわずかにCPUを余計に消費する新機能をデプロイしたくなったらどうでしょうか。

もし100%の使用率で稼働している状態で、負荷が増えるような出来事が起これば、たちまち困ったことになります。100%で回しているということは、増えた負荷を吸収する余裕がまったくないということです。何らかの形で性能が劣化するか、慌てて緊急のキャパシティを追加するか、あるいはその両方を強いられることになります。

このおもちゃのような例は、非常に一般的な現象の一例です。効率の向上はしばしばレジリエンスとトレードオフの関係にあり、システムを最適化すればするほど、このトレードオフは深刻になる傾向があります。

ある時点を越えると、システムをより効率的にすることはレジリエンスを低下させることを意味し、逆に、頑健性を組み込むことはシステムの効率を下げることになります(少なくとも短期的には)。もちろん、まったくwin-winがないと言いたいわけではありません。時にはパレートフロンティア自体を外側へ押し広げることも可能です。単純なパフォーマンスバグを修正するような場合は、まさにそうした効果をもたらします。しかし、ある程度以上突き詰めれば、必ずトレードオフを迫られることになります。

ここで言う「レジリエンス」は、単なる「信頼性」や「稼働し続ける能力」よりもはるかに広い意味であることに注意してください。私が言いたいのは、もっと一般的な「変化を吸収し、対応する能力」です。バグや障害といった変化だけでなく、プロダクトの要件の変化、市場の変化、組織やチーム構成の変化など、あらゆる種類の変化を含みます。

このトレードオフの具体例

このトレードオフは、キャパシティプランニングの領域に限った話ではありません。ほとんどあらゆる技術システムや組織の、ほぼすべてのレイヤーで当てはまります。私が実際に観察した他の例をいくつか挙げてみます。

冗長性

サービスの複数インスタンスを稼働させ、どれか一つが障害を起こしても負荷が透過的に他のインスタンスへ振り分けられるようロードバランサーを設定するのは、ごく一般的な構成です。洗練された組織では、このパターンはデータセンター全体のレベルで適用され、データセンター丸ごとがダウンしても、その負荷を他のデータセンターに振り向けられるようなアーキテクチャが組まれています。

これを機能させるためには、各インスタンスがフェイルオーバーしてきた分の負荷を吸収できるだけの余剰キャパシティを持っていなければなりません。定常状態で、障害が起きていないとき、そのキャパシティはアイドル状態で待機しているか、せいぜい、いつでも中断できる低優先度の処理をこなしているだけです。求める冗長性が高ければ高いほど、定常時にアイドル状態で確保しておかなければならないキャパシティも大きくなります。

最適化

高度なパフォーマンス最適化は、多くの場合、問題領域の特定の性質や構造をうまく利用することで実現されます。不変条件をデータ構造やコード構成に焼き込むことで、しばしば大幅な性能向上が得られます。しかし、特定の前提に深く依存すればするほど、その前提を変更することは困難になり、高度に最適化されたコードは進化させたり機能を追加したりすることが往々にしてはるかに難しくなります。

具体例として、私のSorbetに関する振り返りでは、型推論をローカルのみに限定し単一パスで行うという決定を下し、その前提をコードやデータ構造に組み込んだ経緯について書いています。この選択は大幅な効率向上をもたらしましたが、ある意味でシステムをもろくもしました。競合する他の型システムが備えている多くの機能が、Sorbetのコードベースでは、この前提のために実装が不可能か、途方もなく困難になってしまったのです。このプロジェクトにとっては今でも正しい選択だったと確信していますが、そのトレードオフは認識しておく価値があります。

Hillel Wayneは、同様の性質を「clever code(賢すぎるコード)」と呼んでいます。彼はそれを次のように定義しています。

問題についての知識を利用したコード

彼もまた、この意味でのclever codeは効率的である傾向がある一方で、時にもろくなりがちだと論じています。

シリアライゼーション形式

「メモリ上のstructsをそのままディスクにコピーする」以上に効率的なシリアライゼーション形式を見つけるのは困難です。ほとんどコードを書く必要がなく、データの保存や読み込みにかかるシリアライゼーションコストもほぼゼロです。

しかし、これはシステムをもろくします。新しいフィールドを一つ追加するだけでもデータの書き換えや何らかの特別な対応が必要になります。異なるエンディアンやワードサイズを持つマシン間でデータを共有することも難しくなります。

一方で、すべてのデータをJSONやそれに類する汎用的なコンテナ形式で書き出すようにすれば、新しいフィールドを追加したり、新しいモジュール同士が既存のコードに影響を与えずに通信したりする際の柔軟性は無限に高まります。ただし、その代償として、ネットワーク上でのオーバーヘッドやデータストリームのシリアライズ・デシリアライズにかかるCPUコストが大幅に増えます。

分散システム

私がこれまで読んだシステム論文の中でも特にお気に入りの一つが、COST論文です。この論文では数多くのビッグデータプラットフォームを検証し、その多くが利用可能なハードウェアに対して(ほぼ)線形にスケールするという望ましい特性を持つ一方で、チューニングされたシングルスレッド実装と比べてばかばかしいほど非効率であるという代償を払っていることを指摘しています。

これはよくあるトレードオフだと感じています。分散計算フレームワークは、スケールアップによってほぼ任意のワークロードを処理できるという意味で、柔軟でレジリエントです。誰かが非効率なコードをデプロイしてもスケールアウトで対応できますし、ハードウェア障害も透過的に処理できます。より多くのデータを処理する必要がある? ハードウェアを追加すればいいのです(多くの場合、オートスケーリングによって透過的に)。

一方で、丁寧に作り込まれたシングルノードのソリューションは、より高速になる傾向があります(時に10〜100倍も!)。しかし、はるかに脆いものでもあります。データセットが1ノードに収まらなくなったり、10倍もコストのかかる分析が必要になったり、チームの新しいエンジニアがうっかりタイトな内部ループの中に遅いコードをコミットしたりすれば、システム全体がダウンしたり、本来の役割を果たせなくなったりしかねません。

小規模チーム vs 大規模組織

小規模なチーム(一人だけの「チーム」を含めて)は、驚くほど生産的で効率的になりえます。チームが小さければ小さいほど、コミュニケーションのオーバーヘッドは少なくなり、豊かな共有コンテキストをすべてのエンジニアの頭の中に保っておくことが容易になります。ドキュメントを書く必要も少なく、変更について連絡し合う必要も少なく、新メンバーのオンボーディングやトレーニングに費やす時間も少なくて済みます。小規模チームは「慎重に考え、懸命に取り組む」といった戦略で大規模チームよりもはるかに遠くまで行くことができ、リンターや慎重な防御的抽象化の設計といったツールへの依存も少なくて済むことがよくあります。

条件さえ整えば、慎重な設計と経験豊富なエンジニアを擁する小規模チームが、その10倍の規模のチームの純粋なアウトプットにほぼ匹敵することもありえます。これは効率の面で途方もない向上です!

しかし、小規模チームははるかに脆く、組織や技術環境、あるいはプロジェクトのビジネスニーズの変化に対するレジリエンスは低くなります。4人チームから1人が抜ければ、単純に25%の戦力が失われます。さらに悪いことに、チームは新しいメンバーの採用やオンボーディングの経験が乏しく、大量の知識やドキュメントが残ったメンバーの頭の中にしか存在しないのです。

同様に、事業の方向転換や新製品の立ち上げによってチームのシステムに対してはるかに多くの機能や開発が求められるようになると、チームのキャパシティを簡単に超えてしまい、同じ理由からチームを急拡大することも困難になります。

自動化 vs 人手のプロセス

一般的に、タスクを人手で行うよりも機械にやらせる方が効率的です。より安く、より速く、そして多くの場合より信頼性も高くなります。

しかし、人間は限りなく適応力がありますが、機械(物理的な機械であれソフトウェアシステムであれ)ははるかにもろく、やり方も固定的です。人間がループの中にいるシステムは、状況の変化や予期せぬ出来事に対して、その場で対応するための選択肢をより多く持っています。

たとえ同じ人間をそのまま残しつつ、業務の一部を加速するために自動化で補強したとしても、オートメーション依存のリスクがあります。人間が自動化に過度に依存して不適切に信頼してしまったり、自動化なしで機能する能力が衰えてしまい、必要なときに適切に「手動」で介入できなくなったりするのです。

スラック(余裕)

ここまで挙げてきた具体例の多くは、突き詰めればスラック(余裕)(あのソフトウェア製品のことではありません)についての観察だと言えます。

健全な量のスラックを抱えたシステムは、少なくとも短期的かつ単純な分析では、定義上非効率ということになります。なぜなら、そのスラックは「アイドル状態」で費やされている時間やリソースであり、本来であればアウトプットを生み出すのに使えたはずのものだからです。

しかし、より広い視点で見れば、そのスラックこそがレジリエンスの鍵となります。システムに「遊び」があることで、小さな混乱やトラブルに対処できるようになるのです。開発者や運用者は、そのスラックを使って介入し、予期せぬ負荷に対処したり、問題が破滅的になったり外部から見える形で表面化する前に根本的な課題を解決したりすることができます。

スラックのないシステムは、うまく動いている間は効率的ですが、もろく、通常の動作モードに何らかの変化が生じるとすぐに破綻してしまいます。

結論

ここまで、効率とレジリエンスが対立し、トレードオフしたり、少なくとも互いに逆方向への圧力を生み出したりする具体例をいくつか取り上げてきました。これが幅広い現象であり、厳密なトレードオフがまだ存在しないケースでさえ、二つの価値は対立する傾向にあり、異なる意思決定を示唆することを、ご理解いただけたのではないでしょうか。

残念ながら、この観察だけで、特定のシステムについて何をすべきかが分かることはめったにありません。あるシステムを見て、最初の分析ではインプットを非効率に使っているように見えたとしても、より詳しく調べなければ、それが機能不全と稚拙な設計判断の巣窟なのか、それとも表面的な非効率が膨大な冗長性や柔軟性、スラックを支えており、それがあらゆる変化を乗り越える力になっているのかを判断することはできません。より注意深く見る必要があり、ほとんどの場合、そのチームや問題領域に特有のドメイン知識が不可欠です。

さらに、時にはタダ飯も存在します。パレートフロンティアに沿って動くのではなく、それ自体を外側へ押し広げるような設計上の選択や意思決定もあるのです。よく最適化されたシステムでは稀かもしれませんが、その可能性を無視することはできません。そして、多くのシステムはそもそもまだそこまで最適化されていないのです!

加えて、設計空間における最適なポイントはシステムによって異なります。信頼性やレジリエンスが極めて重要で、一次的に見れば甚だしい非効率を許容することが正しい場合もあります。一方で、極限まで効率を追求することが正しい目標となる場合もあります。利益率が薄く、それ以外に選択肢がないのかもしれませんし、あるいは自分たちの領域やシステムへの要求が十分に安定しており、どのような種類の劇的な変化にも見舞われることはないと自信を持てるのかもしれません。

結局のところ、私にできることのほとんどは、エンジニア、デザイナー、そしてシステムの観察者として、このトレードオフとその含意を意識して取り組むよう呼びかけることです。あるシステムを無駄が多く非効率だと非難するときは、一度立ち止まって、その「無駄」が何を買っているのかを問う価値があります。システムの最適化に着手するときは、現在のシステムのどこに継ぎ目や柔軟性があり、そのうちのどれが不可欠なのかを理解するために立ち止まり、それらをできる限り保つよう努めるべきです。システムやチーム、組織に対して効率を求める指標や目標を設定するときは、対抗する圧力がなければ、おそらく同時にシステムをより脆く、もろくするよう求めているのだということを意識しましょう。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント