スケーリング
Scaling ・ すけーりんぐ
負荷の増大に処理能力を追随させること。1台を強くする垂直と、台数を増やす水平の二方向がある。
✦ 30秒で試す — 航海1台を強くするか、台数を増やすか。伸ばし方は2通りある4台にすると、1秒にさばける要求の数は?予想してみる概要
スケーリングとは、利用者やデータ量の増加に合わせて、システムの処理能力を引き上げていくことです。サービスが成長すると、最初は余裕だったサーバがやがて悲鳴を上げます。応答が遅くなり、ピーク時にはエラーが返り始める。そのときに取れる手段がスケーリングで、その手段には大きく二つの方向があります。
一つは、サーバそのものをより強力な機械に替える垂直スケーリング(スケールアップ)。CPU を増やし、メモリを積み、より速いディスクにする方向です。もう一つは、同じ役割のサーバの台数を増やす水平スケーリング(スケールアウト)。1台でさばけないなら10台で分担しよう、という方向です。負荷に合わせて台数を自動で増減させる仕組みはオートスケールと呼ばれます。
「スケーラビリティ(拡張性)」という言葉は、いま速いかどうかではなく、「負荷が10倍・100倍になったとき、資源を足すだけで追随できる設計になっているか」を指します。そして実際に資源を足してみると、たいていは期待どおりに伸びません。どこか1か所が先に詰まり、そこが全体の上限を決めるからです。スケーリングの実務は、この「先に詰まる1か所」(ボトルネック)を見つけて広げ、次に詰まる場所を探す作業の繰り返しです。
なぜ生まれたか
Web 以前のシステムは利用者数がほぼ固定で、必要な性能を見積もって機械を買えば済みました。ところがインターネットのサービスは、利用者が一晩で何倍にもなりえます。テレビで紹介された瞬間、セールの開始時刻、SNS での拡散。負荷は予測を裏切って跳ね上がります。
当初の答えは垂直スケーリングでした。しかし1台の機械の性能には物理的な上限があり、上位機ほど価格が非線形に跳ね上がり、そして何より「その1台が壊れたら全部止まる」という単一障害点の問題が残ります。Google や Amazon が示したのは逆の道でした。安価な普通のサーバを大量に並べ、故障を前提に設計する。この水平スケーリングの思想が、クラウドコンピューティングの「必要なときに必要な台数を借りる」モデルと噛み合い、2009年に AWS が提供を始めた Auto Scaling のように、台数の増減そのものを自動化する仕組みへとつながりました。
ただし、台数を増やせば比例して速くなるわけではないことは、もっと前から知られていました。1967年に Gene Amdahl は、処理のうち並列にできない部分の割合が、台数(プロセッサ数)をいくら増やしても超えられない速度向上の上限を決めると指摘しています。これがアムダールの法則で、水平スケーリングの設計は、この「並列にできない部分」をどこまで小さくできるかとの戦いでもあります。
詳細
垂直と水平の使い分け
垂直スケーリングの利点は単純さです。アプリケーションを一切変更せずに性能が上がり、分散に伴う複雑さがありません。小〜中規模のシステムや、分散が難しい RDBMS では今でも第一選択です。欠点は上限があること、費用対効果が悪化していくこと、機械の交換に停止を伴いがちなことです。
水平スケーリングは、うまく作れば台数に応じて能力を伸ばせ、1台の故障がサービス全体の停止にならない冗長性も同時に得られます。その代わり「複数台で正しく分担する」ための設計コストを支払います。そして「台数に応じて」には但し書きがつきます。アムダールの法則によれば、処理の割合 p だけが並列にでき、残りの 1 − p はどうしても1か所で順番にこなすしかないとき、N 台での速度向上は 1 ÷ ((1 − p) + p ÷ N) にとどまります。並列にできない部分がたった5%でも、何台並べても20倍が上限です。Web システムでいえば、全員が書き込む1台の DB や、全員が取り合う1つのロックが、この「並列にできない部分」にあたります。
水平スケーリングの前提: 分散とステートレス化
台数を増やすには、まずリクエストを各サーバへ振り分ける入口が必要です。それがロードバランサで、定期的なヘルスチェックによって故障したサーバを自動的に振り分け先から外す役割も担います。
もう一つの前提が「どのサーバに当たっても同じ結果になる」こと、つまりアプリケーションのステートレス化です。ログイン状態などのセッション情報をサーバのメモリに置いてしまうと、次のリクエストが別のサーバに振られたときに「ログインしていない」ことになってしまいます。そこでセッションは Redis のような共有ストアに外出しするか、JWT のようにクライアント側へ持たせます。「状態をサーバから追い出す」ことが水平スケーリングの合言葉で、12 Factor App はこれを「ステートレスなプロセス」と「プロセスを増やして並行性を得る」という原則にまとめています。
ボトルネックは移動する
アプリケーションサーバを並べても、全員が同じデータベースに問い合わせるなら、次に詰まるのは DB です。ボトルネックは解消されるのではなく移動する、というのがスケーリングの実感的な法則です。行列は必ず、いちばん処理能力の低い段の手前にできます。アプリ層を広げれば、その手前の行列は消えますが、アプリ層が DB に流し込む量が増えるので、今度は DB の手前に行列ができます。
DB 側の打ち手は段階的に投入されます。まず遅い SQL を見直してインデックスを足す。次に、同じ質問に DB を起こさず答えるキャッシュで読み取りを手前で吸収する。さらに、レプリケーションで読み取り専用の写し(リードレプリカ)を作り、読み取りをそちらへ逃がす。ただし、ここまでの打ち手はどれも読み取りのためのもので、書き込みは相変わらずプライマリの1台に集まります。書き込みまで分けるには、データそのものを複数の DB に分割するシャーディングが必要ですが、トランザクションが複数の DB をまたげなくなるなど複雑さの代償が大きく、最後の手段に近い位置づけです。水平分割を最初から前提にした NoSQL が選ばれる場面もあります。
下の部品で、アプリサーバの箱をアプリ層へドラッグして台数を増やしてください。行列がどこに移るかを見て、キャッシュやリードレプリカを置き、流量をピークに上げてみてください。
要求が毎秒6件届いていますが、アプリサーバ1台では毎秒約4件しかさばけず、アプリ層の手前の行列があふれています(朱の✕)。下の「+アプリサーバ」をアプリ層へドラッグしてください。
オートスケーリングと現実の落とし穴
クラウドでは、CPU 使用率やリクエスト数といったメトリクスに応じて台数を自動で増減させるオートスケーリングが使えます。Kubernetes の Horizontal Pod Autoscaler も同じ考え方です。負荷の波に合わせて夜間は縮小し、ピーク時だけ拡大する、といった費用の最適化ができます。
ただし自動化には時間の遅れがつきまといます。メトリクスが閾値を超えたと判断されるまでに数分、新しいサーバが起動して温まり、ロードバランサに組み込まれるまでにさらに数分かかることがあります。数秒で跳ね上がるアクセスの急増には間に合わないので、開始時刻の分かっているセールには前もって台数を増やしておく、平常時から余裕を持たせておく、といった備えが要ります。逆に、閾値の近くで増やしては減らすを繰り返す揺れ(フラッピング)を防ぐため、縮小側には待ち時間(クールダウン)を置くのが普通です。起動の遅れを限りなく小さくした形がサーバレスで、そこでは同じ問題がコールドスタートとして現れます。
実務での最大の落とし穴は、自動化そのものより手前にあります。負荷の原因を特定せずに台数を増やすと、遅い SQL や外部 API の応答待ちのような「増台では解決しない問題」に金だけを注ぐことになります。部品で見たとおり、ボトルネックが DB にあるときにアプリサーバを増やしても、増えた台数は DB の前で待つだけです。それどころか、DB への同時接続が増えて DB をさらに苦しめることさえあります。まず監視とログで、どの段の手前に行列ができているかを特定する。インデックスの追加やキャッシュのような安い打ち手を先に検討する。スケーリングは常に「計測してから」が鉄則です。
スケールしない自由もある
最後に、すべてのシステムが水平スケーリングを必要とするわけではありません。ステートレス化や分散設計には確かなコストがかかり、社内システムや初期のプロダクトでは、1台の強いサーバと丁寧なチューニングのほうが総合的に安いことも多いのです。台数を増やすほど台数間の調整(ロック、データの同期、合意)に払う手間も増え、ある台数を超えると全体の処理量がかえって下がることさえあります。「将来必要になるかもしれない」だけで分散の複雑さを先払いしない、というのも立派な設計判断です。
試問
利用者の要求(金=読み取り、紫=書き込み)が、アプリ層の手前の行列 → アプリサーバ → DB の手前の行列 → データベースと流れます。いまはピーク(毎秒12件)で、アプリサーバは1台。下の置き場から、アプリサーバ(何台でも、最大6台)・キャッシュ(読み取りの一部を DB に行かずに返す)・リードレプリカ(読み取り専用の DB の写し)を運んで置けます。置いた台やキャッシュは押すと外せます。
ピーク(毎秒12件)の要求が届いています。アプリサーバ1台では毎秒約4件しかさばけず、行列があふれています(朱の✕)。下の置き場から、アプリサーバ・キャッシュ・リードレプリカを運んで置けます(タップでも置けます)。
