リソース効率で考えるアーキテクチャ設計:機能要件を超えた技術選定の本質
はじめに
技術選定やアーキテクチャ設計は簡単な仕事ではありません。
資格の勉強を通して各クラウドサービスの機能がわかるようになったから・プログラミング言語を勉強してアプリコードが書けるようになったからといって、では次のステップとしてアーキテクチャ設計ができるか?と言われると必ずしもそうではないことの方が多いのではないでしょうか。
このサービスを用いることでシステムの機能要件を問題なく満たせるという理由で技術選定した結果、全然パフォーマンスが出ない・コストがかかり過ぎるといった問題が発覚して、運用やビジネス遂行に支障をきたしてしまうこともあります。
このような失敗を未然に防ぎ、システムがその役割を十分に果たすために必要となるアーキテクチャを先んじて確実に見通し設計するには、私たちは一体どのようなことを学び、気をつければいいのでしょうか。
この記事では、アーキ設計・技術選定において大きな失敗をしないために大事になるポイントを考察していきたいと思います。
対象読者
- システム設計や技術選定がうまくできずに悩んでいる方
- アーキテクチャ設計で痛い失敗をしたことがある(特にジュニアな)アーキテクト
- 非機能要件やパフォーマンス問題に直面しているエンジニア
目次
- アーキ設計失敗あるある
- 見落とされがちな制約の正体 - クオータとリソース
- システムを支える多次元のリソース
- リソース枯渇への段階的対処法
- リソース効率で測るアーキテクチャの良し悪し
1. アーキ設計失敗あるある
一つ例え話をします。
とあるシステム上でユーザーさんが行動したアクティビティの回数を集計するAPIエンドポイントを実装していました。
そのシステムはリソースEntityごとにデータベースが設計されており、イベントEntityを収めているテーブルはまだありませんでした。
その代わりといってはなんですが、ユーザーさんがその集計対象となるアクティビティを行なったという事実はアプリケーションのログに出力されるようになっていました。
その状況を見た担当者は、あろうことにも「CloudWatch Logs Insightで出力ログを検索してクエリし、その結果を元にレスポンスを返す」という処理を書いてしまいました。
CloudWatch Logs InsightはSQL文形式でログを検索することができるため、機能面だけ見るのであればまるでRDBのようにデータを取得することができます。
しかし、CloudWatch Logs Insightには「同時実行が可能なクエリは30個まで」というハードクオータが存在します。そのため、一般のtoCのWebシステムのユーザーリクエストほどの頻度でCloudWatch Logs Insightが絡んだ処理を起動させると、確実にこのハードクオータに引っかかることになります。
案の定全くパフォーマンスが出なかった上にコストもめちゃくちゃ嵩むことになりました。
結局その当該システムは、集計したいイベントEntityを記録するテーブルを新たに作成し、そのテーブルをクエリする形にすることで落ち着きました。
このように、機能用件だけ見て無邪気に利用するサービスや技術を選定したはいいものの、パフォーマンスやコストといった非機能面で致命的な課題が後から発覚して、結果うまくいかなかったということは誰しも経験があることかと思います。
2. 見落とされがちな制約の正体 - クオータとリソース
前述した失敗を避けるためには、私たちは何を知り、何を意識しないといけないのでしょうか。
「こういうユースケースのときはこのサービスを使うのだ」というパターンと覚えればいいのでしょうか?それでは私たちは一体いくつのパターンを覚えればいいのでしょうか?
「Lambdaと組み合わせるのはDynamoDBだよね」という決まったパターンに盲目的に従った選定というのはただの思考停止です。
パターンがある種のデファクトスタンダートとして確立しているからには、そのパターンが良いとされたそれ相応の理由があるはずです。私たちはアーキテクトとして、その背景・その理由を知っておくべきでしょう。
先ほどのCloudWatch Logs Insightを利用した失敗例を見返しています。
あれは、CloudWatchに設定された「同時に実行できるLogs Insightのクエリ数は30個まで」というハードクオータの存在を事前に知っていれば、「このシステムを同時に利用する人数は30人じゃ済まないな……じゃあLogs Insightでは無理だな」と気づくことができたはずです。
ここからわかることとしては、技術選定やインフラの本質はクオータの把握であるということです。
ソフトクオータはともかくハードクオータは把握しておかないと、そこがボトルネックとなり問題になったときに、その課題を解決するためには大幅なリアーキぐらいしか打ち手がないという悲惨な状況になってしまいます。
自分たちが利用しようとしているサービスに何か制限事項・クオータがないかどうかを事前にきちんと把握しておき、その範囲内で想定トラフィック・想定リクエストパターンを捌けるかどうかを判断することが大事になってきます。
それでは、よきアーキテクチャを設計するためにはよく利用するようなサービスのクオータを全て覚えるべきなのでしょうか?
試験勉強のようにAWSの公式Docのページを丸暗記していつでも頭の中から引き出せるというような状態になれば、よきアーキが組めるようになるのでしょうか?
そもそもサービスのクオータはアップデートによって変動する可能性があります。頻繁にリリースされるアップデートを逐一追って、クオータの引き上げ情報を追って知識を更新していくのはなかなか大変です。
クオータというのは、そのアーキテクチャ・そのサービスが持つある種の性能指標でしかありません。いついかなるときにも通用する技術選定やインフラアーキの不変な本質部分は、時とともに変わりゆく性能指標・クオータのさらに裏に埋まっています。それは一体何なのでしょうか?
筆者の結論を先に言ってしまうと、本当に気にするべき本質部分はリソースの有限性であり、クオータの存在はリソースの有限性の表れと考えます。
クオータでサービスの利用を制限するということは、ユーザーにそれ以上使われてしまっては困るというサービス提供者側の意図の表れです。そしてこれは往々にして、インフラを支える物理リソースの有限性が反映されているものです。
例えば、Azure OpenAIのgpt4oモデルは、Global StandardのオプションでデプロイするとTPM(Token Per Minute)は最大でも30Mまでしか割り当てることができません。ソフトクオータなので上限緩和申請をすることはできますが、モデルでの処理に必要なGPUインスタンスがデータセンターで確保できないと判断された場合にはクオータ引き上げ申請は通りません。
これは言い換えると、Azure OpenAIのTPMクオータというのは、Azureデータセンター内でのGPUインスタンスというリソースの有限性が反映されているということになります。
当該システムを稼働させるに必要なリソースが足りていないとパフォーマンス問題、ひいては障害の原因となります。
例えばKubernetesを想像してみてください。KubernetesはPodと呼ばれるコンテナをマシンクラスタ上にデプロイし、その管理を自動化するコンテナオーケストレーションツールです。
Kubernetesを使ってデプロイしたアプリケーションがうまく動いてくれない!というシチュエーションになったときに、原因としてよくあるのは「クラスタに属するNodeのスペックが足りてない(=CPU,メモリ,ディスク容量が枯渇しているなど)せいで、Podのプロビジョニングに失敗している」というパターンです。
Nodeには8GBしかディスクが搭載されていないのに、1GBのVolumeを要するPodをその上に10個デプロイするのは無理であるということは理解していただけると思います。このようなときはPodの立ち上げと異常終了が頻発するような状態に陥り、運用担当者からは「なんかよくわからないんだけどヘルスチェックが安定しないんだよなぁ……」という見え方になります。障害の一段前状態です。
ここからわかるのは、Kubernetesというコンテナオーケストレーションツールの実態は、Nodeに搭載されているリソースをPodにうまく配分し配置するアプリケーションであるということです。
効率の良い処理を行うために課されたAffinityやTaintなどの条件も加味した上で、クラスタがもつリソース総量の中から、Podが要求している分を確実に割り振るというのがKubernetesの責務なのです。
Kubernetesに限らず、システムを構築する我々アーキテクトに課せられた責務にも同じことが言えるでしょう。
アーキテクトによる技術選定やインフラアーキ選定というのは、自分たちが持つ有限のリソースを、システム上で発生する諸々の処理を全て捌ききれるように割り振るという行為なのです。
3. システムを支える多次元のリソース
ではここで割り振りの対象となっている「リソース」というのは具体的に一体何なのでしょうか。
OSのレイヤだと真っ先に思いつくのがCPU・メモリ・ディスクの3つでしょう。
AWSのCloudWatchしかりGoogle CloudのCloud Monitoringしかり、大体のパブリッククラウドプラットフォームにはメトリクス監視のツールがあり、VMに紐づくメトリクス項目としてこれらがあるのでイメージがつきやすいかと思います。
ネットワークの観点だと、例えばインスタンスに割り当て可能なENI=NICの数もハードクオータで数が限られています。
ネットワーク帯域も、深堀りしていくとハードウェアに繋がるケーブルのスペック・規格という物理層(=OSI参照モデルのL1)の話になるため、簡単に増強することはできません。
IPアドレスも、VPC内で利用可能なアドレスを全て使い切ってしまわないようにあらかじめシステムで必要になりそうなCIDRブロックを注意深く見積もる必要があります。特にKubernetesクラスタをネットワーク上でホストする場合、Node以外にもPodや ServiceなどのKubernetes内部リソースに割り当てるIPアドレスもVPCから払い出すことが多いためアドレス枯渇を招きやすく問題になりがちです。
簡単に増やすことができない、有限性・クオータがあるという視点で見ると、これらネットワークリソースも管理しなくてはならないリソースの一種と捉えるべきでしょう。
データベースですと、一度に接続可能なコネクション数もインスタンスサイズによって限られています。LLMモデルの観点だとトークン使用量=GPUインスタンス数も有限です。ウェブサーバーの観点でも、かつでのC10K問題で見られたようなコネクション数=OS内のプロセス数・ファイルディスクリプタ数も上限があります。
これらのリソースを、サービスを安定して提供するために必要な分だけ確実に確保する。それを可能にする構造的な解決策を考えるのがアーキテクトの仕事です。
4. リソース枯渇への段階的対処法
それではリソースが足りないとなったときに、アーキテクトはどのようにして手を打てばいいのでしょうか。
まず真っ先に考えられるのはスケールアップ・スケールアウトです。前者はリソースの性能スペックを上げる、後者はリソースの数を増やすという方針で、やり方こそ違えど、どちらもリソースが足りないなら追加で確保してくればいいじゃない・分割するリソースのパイを大きさを増やそうという思想です。
しかしこれにも限界があります。スケールアップに関しては単独のインスタンスで持てるリソース(CPU・メモリetc...)の数が限られており無限に増やせるわけではない、スケールアウトに関しても結局のところパブリッククラウドのリージョンデータセンター内にあるリソース数以上の数を確保することは物理的に不可能だからです。
そうなってくると次にやるべきは、同じ大きさのパイをどう効率よく分けるか=チューニングとなります。
例えば、セキュリティ観点で違う物理ホストマシン上にシステムを載せないといけないとかいう要件がないのであれば、異なるシステムコンテナを同じホスト上に展開して、浮いている余剰Nodeリソースを有効活用するような配置戦略・スケーリングポリシーを策定するなどです。
ただしここで言えることとしては、上記のようなインフラレイヤのチューニングだけで解決しないのであれば、最後に手をいれるべき箇所はアプリケーションレイヤになるということです。
リソース不足に起因したパフォーマンス問題を、アプリケーションレイヤのチューニングによって解決した例をいくつかご紹介します。
例えば先ほども少し述べたC10K問題について考えてみます。これは1リクエストごとにメモリやファイルディスクリプタ・プロセスを割り振るサーバーロジックだと、1サーバーインスタンスで捌けるリクエストコネクション数が10000個ほどで限界に達し、レスポンス遅延やConnection Refusedエラーが多発するという問題です。
これをインフラレイヤでどうにかすることは難しいでしょう。その代わりに、非同期・ノンブロッキングI/Oを利用することによって、1プロセスで複数リクエストを捌けるように構成を変えることによって問題を解決しました。これはサーバーロジックという一種のアプリケーションロジックに手を入れてチューニングしたと捉えることができます。
また、Javaのアプリケーションでメモリ使用量が嵩んできている場合、メモリリークを疑ってロジックを修正したり、ヒープサイズが大きいのであれば適正量に減らしたりといった、Javaソースコードの背景知識を前提としたチューニングをしなくてはならないかもしれません。
さらに、ネットワーク帯域の枯渇やディスクサイズが問題になっており節約したいという場合には、より軽量で小さなコンテナイメージになるようにイメージランタイムレイヤをリファクタするというのも、上に載せるアプリケーションが必要としているライブラリや言語ランタイムの互換性を正しく理解した上で行わなければならない、アプリケーションのチューニングです。
アーキテクトというと、一般的なイメージだとパブリッククラウドが提供する各種サービスをどう組み合わせてシステムを構築するかという、インフラとしての色が強い職種と見られることが多いかと思います。
しかし、システムに必要なリソースを確保する段階でクオータというボトルネックにあたってしまったときの解決策は、限界まで突き詰めると活路をアプリレイヤに求めることになります。
言い換えると、インフラレイヤの課題がアプリレイヤに侵食することになります。そのため、インフラエンジニアだからアプリ解らなくていいよねというのは今後は通用しなくなってくるでしょう。
余談ですがさらにいうと、チューニングのために施すアプリレイヤにおける改良策も突き詰めれば似たようなものになりがちです。
例えば、リクエストを非同期化するためにキューイングサービスを挟むだとか、対向サービスの障害発生時に追い打ちをかけないようにサーキットブレイカーを導入する、キャッシュを導入するといった解決策です。
これらの似偏った解決策を手軽に導入するために、パブリッククラウドベンダーが用意している各種マネージドサービスを利用するという選択も増えてくると思います。つまり、アプリレイヤの施策がインフラレイヤに侵食することになります。
これからの時代、アプリとインフラの境目はだんだん曖昧になってくるのかもしれません。
5. リソース効率で測るアーキテクチャの良し悪し
アーキテクトとして求められるスキルが「リソースの割り振り」であるとわかったところで、今度はより高みを目指すために「よきアーキテクチャとは何か?」を考えていきたいと思います。
例えば、ACIDトランザクションを要する処理をどのデータベースで処理するかを検討します。
普通に考えればリレーショナルデータベースにやらせるのがいいという結論になるでしょう。
しかし、NoSQLでも一応トランザクション処理をやろうと思えば実行することができます。現にDynamoDBはトランザション機能を有しています。
それでは、DynamoDBのようなNoSQLでトランザクション処理をRDBと同じノリでバンバン実行できるか?と聞かれたらどうでしょうか。AWSにそれなりに長けたアーキテクトであれば、間違いなくやめておいたほうがいいと答えるでしょう。
この理由を一言で説明するならば「ACIDトランザクションはNoSQLには不得意な処理であり、RDBのほうが得意である」ということになるのですが、この言外の得意・不得意という感覚をもう少し掘り下げてみたいと思います。
RDBのシステム構成は全データが一つのプライマリインスタンスの中に収まっておりその中でデータ処理を完結させるというのが基本的な考え方なので、異なるデータレコード間でリレーションを張り制約事項を設けることにそこまでの困難は生じません。トランザクションを張ったとしても、その影響が一つのインスタンスノードの中に閉じるからです。
しかし一般にNoSQLは複数のサーバーにデータを分散させて保存させる構成を取ります。異なるノードに配置されたデータ間でトランザクションを張るとなると、ロックやコミット処理などでノードを跨いだ複雑な協調処理が必要になってきます。
そしてこれは、ディスクやネットワークI/Oというリソース消費という形で顕在化することになります。つまり、NoSQLにとってトランザクション処理というのは、「できなくはないが、リソース効率が悪い重い処理」ということなのです。
AWSではDynamoDBの料金はCU(Capacity Unit)という処理単位の消費量に応じた従量課金体系をとっているのですが、通常のRead/Writeに対してトランザクションRead/Writeに要するCUの量は明らかに多く設定されています。
CUという単位を「DynamoDBで生じる処理を捌くのに必要となる、CPUやメモリ・各種IO等のリソース量を抽象化したもの」だと捉えるのであれば、AWSは「トランザクション処理には通常の処理よりもよりリソースを要する、重くコストのかかる処理です」ということを、料金体系で明示してくれているのです。
料金や利用体系に現れる部分ではありませんが、現にDynamoDBを利用したトランザクション処理はレイテンシが大きく性能問題に発展する率が高いと感じます。
ここからわかることとしては、技術選定やアーキテクチャの良し悪しというのは、そのリソース効率の良し悪しとイコールであるということです。
得意な処理・得意なアクセスパターンほど、それをリソース効率よく捌くことができるということです。
つまり、アーキ選定というのは「システムで発生する1リクエスト・1トリガで発生する諸々の処理を、一番リソース効率よく捌くシステム構成はどれですか?」という問いへの解答なのです。
よきアーキテクトとなるためには、その技法・機能が成り立つ原理原則を知った上で、それを成り立たせるためにはどれ程のリソース(CPU・メモリ・I/O etc...)を要するのかという隠蔽された低レイヤ構成まで見通し、「自分たちがやりたい処理にこの体制は重すぎる・軽すぎる」という感覚を身につけることが大事になってくるのかもしれません。
おわりに
同じ要件をインプットにしたとしても、それを満たすために組むアーキテクチャは人によって様々です。
新しいサービスや機能が次々とリリースされる日進月歩の技術の世界においては、今持ちうる技術選定の選択肢と一年後に持ちうる選択肢は異なるものなのかもしれません。そうなると、今ベストだと考えるアーキテクチャと、一年後に同じ設計をもう一度行ったときにベストだと考えるアーキテクチャも異なってくるでしょう。
人や時代によって本当に様々なアーキテクチャが考えられて、そのどれもが機能要件は満たしていたとしてもなお「良し悪し」というものは存在します。
社会人になりたての頃は、新たな技術を学び自分が持つ選択肢が増えていくのが楽しくて、同じ要件を別のサービス・別の技術でも実装できそうだと知るとつい何も考えずにその新サービスを使って性能問題にぶつかり失敗するということがありました。
そこで私は、機能要件を満たすためのアーキ設計・技術はたくさん存在するが、その全てが現実的に採用可能な良いアーキテクチャではないのだということを身をもって知ったのです。
確かに存在するアーキ設計の良し悪し、しかしそれを事前に見抜くためにはどうすればいいだろうか?というところについてはずっと答えがないままでした。
数々の試行錯誤と考察を繰り返し、キャリアも5年目に突入し、最近ようやっと自分なりの答えに辿り着いたため筆をとりそれをここに綴りました。
時代が変わって新しい技術やサービスが生まれて、それによって実現できるユースケースがどんなに増えたとしても、それを支える物理リソースの種別とその有限性という根本的な制約は変わりません。
どんなコンピューターを使いどんな処理を試みようとしたとしても、最終的にはCPU・メモリ・ネットワークといった低レイヤの話にたどり着きます。
そしてCPUのクロック数にもメモリ容量にも限界があり、ネットワーク帯域も無限ではない。
この当たり前の事実を、時代の波に翻弄されて新しい潮流に心奪われがちな私たちはつい見落としがちになってしまいます。
しかし、この物理的なリソース制約という不変の真実こそが、アーキテクチャ設計における重要なポイントとなるのです。
どれほど魅力的な新サービスが登場しようとも、どれほど革新的な技術が生まれようとも、その根底にはリソースの有限性という普遍的な法則が横たわっています。
優れたアーキテクトとは、この制約を深く理解し、限られたリソースの中で最大の価値を生み出す方法を見出すことができる人です。
新しい技術の表面的な機能に惑わされることなく、その技術が消費するリソースという本質を見抜き、アプリとインフラの垣根を超えて全体最適を図る視点を持つこと。
それが、時代を超えて通用するアーキテクトになるために必要な資質なのかもしれません。
Discussion
とても面白い記事で参考になりました。
記事にある内容は、アーキテクチャ選定の上で気をつけなきゃいけない重要なことだと思います。
一方で(細かいところですが)、
のところで、それもトレードオフだと思っていて、実装の容易性やアジリティを取ったほうがいい場面や、保守性・拡張性を考慮しないと痛い目にあう場面もあるなあ、と思いました。