アプリケーションスケーリングとは?アプリ層で水平スケーリングを効かせる設計手法
アプリケーションスケーリングは、サーバーの台数やスペックではなくアプリケーション側の実装で処理能力の上限を押し上げる設計作業です。垂直スケーリングと水平スケーリングそのものの違いや使い分けはスケールアウトとスケールアップとは?水平・垂直スケーリングの違いと実装・使い分けを実装目線で解説【2026年版】で扱っているため、本記事ではインフラを増やしたあとにアプリ側で何が詰まるかに絞ります。台数を2倍にしても処理量が2倍にならない原因は、セッション・キャッシュ・データベース接続・同期処理の4か所にほぼ集中します。
まとめ:台数を増やす前に4か所から状態を剥がす
- アプリケーションスケーリングの実体は、1台のサーバーが抱え込んでいる状態をアプリの外へ出す作業である
- セッションをRedisへ逃がすなら、phpredisの
read_timeoutが既定で0(無制限)である点を最初に潰す - 共有キャッシュは
maxmemoryを設定した環境でmaxmemory-policyが既定のnoevictionのままだと、上限到達時に書き込みがエラーを返し全台が同時に失敗する - HikariCPの
maximumPoolSizeは既定10。台数だけ増やすとデータベースの接続上限を先に食い潰す - Amazon SQSを挟む非同期化は、可視性タイムアウト既定30秒と再配信を前提にした冪等化までがセットになる
優先順位は上から順です。状態を剥がさないまま台数を増やしても効果は出ないため、セッション外部化が常に最初に来ます。
アプリ層が担うスケーリングの守備範囲
スケーラビリティとスケーリングの語の使い分け
スケーラビリティは「負荷が増えたときに資源を足せば処理量が伸びる性質」を指す評価軸で、スケーリングはその性質を実際に使って資源を増減させる操作を指します。サーバーを増やせる状態にあることと、増やして実際に処理量が伸びることは別物です。水平分散システムを組んだのに1台あたりのスループットが落ちる場合、スケーラビリティの側に欠陥があります。
台数を増やしても伸びない原因が集まるアプリ層の4か所
アプリケーションを水平分散したときに詰まるのは、ほぼ次の4か所です。いずれも「全台で共有している1つの何か」に負荷が集まる構造をしています。
| 詰まる場所 | 症状 | 直す対象 |
|---|---|---|
| セッション | ログインが切れる/特定サーバーに偏る | 保存先の外部化 |
| 共有キャッシュ | 上限到達で書き込みエラー | maxmemory-policy |
| DB接続 | 接続数上限に到達 | プールサイズ |
| 同期処理 | 外部API待ちでスレッド枯渇 | キューへの退避 |
垂直分散(機能ごとにサーバーを分ける)でしのぐ手もありますが、切り出した機能の内側で結局この4か所に突き当たります。垂直分散より先にここを潰すほうが投資効率は高くなります。
状態をアプリから剥がすセッション外部化の実装
PHPのセッションをRedisへ逃がす設定と読み取りタイムアウトの既定値
PHPでセッションを共有ストアへ移すには、php.iniでsession.save_handlerとsession.save_pathを切り替えます。phpredisのREADMEが警告しているとおり、接続文字列にread_timeoutを書かない場合の既定値は0、つまり無制限です。Redisが応答しなくなるとPHPプロセスが無期限に待ち続け、Webサーバーのワーカーが順に埋まります。
; php.ini
session.save_handler = redis
session.save_path = "tcp://cache.internal:6379?read_timeout=2.5&prefix=app_sess:"
READMEは本番環境でread_timeoutを2.5秒程度の低い値にすることを強く推奨しています。prefixを省略した場合のキー接頭辞はPHPREDIS_SESSION:です。複数アプリで同じRedisを共有するとキーが衝突するため、アプリごとに別の接頭辞を割り当ててください。
セッションロックが同時リクエストを直列化する条件
phpredisのセッションロックはredis.session.locking_enabledで切り替え、既定値は0=無効です。有効にすると同一セッションIDへの同時リクエストが直列化されます。データ競合は防げる代わりに、スループットは落ちる仕組みです。待ち時間の既定はredis.session.lock_wait_timeが20000マイクロ秒、リトライ回数redis.session.lock_retriesが100回なので、単純計算で最大2秒ほど待つ設定になっています。redis.session.lock_expireを明示しない場合はPHPのmax_execution_timeが使われます。
この設定はphp.iniのグローバル設定で、有効化するとアプリ全体の全リクエストに効きます。画像の遅延読み込みやポーリングで同一セッションの並行リクエストが多いフロントエンドでは、有効にした瞬間に応答時間が跳ねます。ロックが要るのは、同一セッションへ書き込む処理が複数同時に走るアプリだけ。読み取り主体のアプリまで一律にロックする設定は過剰で、水平スケーリングの効果を自分で打ち消します。
ip_hashによるスティッキーセッションを逃げ道にしない判断
セッション外部化を避けて、ロードバランサー側で同じクライアントを同じサーバーへ固定する方法もあります。nginxのip_hashはIPv4なら先頭3オクテット、IPv6ならアドレス全体をハッシュキーにして振り分け先を決める方式です。サーバーを1台増減させるだけで多数のクライアントの割り当てが変わり、その分のセッションが失われます。公式ドキュメントは、一時的に外すサーバーにはdownパラメータを付けて現在のハッシュを保つよう指示していますが、これは計画的な切り離しにしか使えません。
モバイル回線や企業のプロキシ経由では、多数の利用者が同じ先頭3オクテットに集まります。結果として振り分けが偏り、水平スケーリングで増やした台数が遊びます。スティッキーセッションは移行期間の暫定策であって、恒久的な設計に据えるものではありません。
アプリ層キャッシュの配置と無効化ポリシーの設計
キャッシュの5層と共有キャッシュを置く位置
本記事ではキャッシュを、ブラウザ、CDN、アプリのプロセス内メモリ、共有キャッシュ(RedisやValkey)、データベースのバッファプールという5層に整理します。水平スケーリングで問題になるのはプロセス内メモリの層です。台数が増えるほど同じデータのコピーが増え、更新のたびに全台へ無効化を伝える必要が出てきます。伝播の手段はRedisのPub/Subで無効化通知を配るか、キー名にデータの更新世代番号を含めて旧世代を参照させない設計のどちらかになります。
経験則としての目安ですが、台数が3台を超えたあたりから、更新頻度のあるデータのキャッシュはプロセス内から共有キャッシュへ移したほうが管理は楽になります。プロセス内に残してよいのは、設定値やマスタデータのように、デプロイのタイミングでしか変わらないデータだけです。ディスク層とメモリ層の役割分担はディスクキャッシュとは|仕組み・メモリキャッシュとの違い・確認方法をわかりやすく解説で整理しています。
maxmemory-policyの既定値noevictionが書き込みエラーを返す条件
Redisのmaxmemory-policyは既定がnoevictionです。redis.confの記述どおり、この設定ではメモリ上限に達したときにキーを退避せず、書き込み系コマンドがエラーを返します。SETやINCR、HSETのほか、メモリを必要とするコマンドを含むEXECも対象。キャッシュ用途で既定のまま運用すると、上限到達の瞬間に全アプリケーションサーバーが同時にキャッシュ書き込みへ失敗します。
ただしこれはmaxmemoryを明示した環境か、ElastiCacheのように自動設定される環境での話です。公式ドキュメントによれば64ビット環境の既定値は0=無制限で、上限が無ければエラーの代わりにOSがメモリを使い切ります。
# redis.conf = キャッシュ用途の設定
maxmemory 4gb
maxmemory-policy allkeys-lru
選べるポリシーは、Redis 8.6.0(GA 2026-02-10)でvolatile-lrmとallkeys-lrmが加わって10種類になりました。LRMは最終「更新」時刻で退避対象を選ぶ方式です。参照のたびにタイムスタンプが動くLRUと違い、読み取りが多くても更新が止まったキーから消えていきます。公式ドキュメントはallkeys-lrmの使いどころを「よく読まれるデータを残しつつ、最近更新されていないデータを退避したい読み取り主体のワークロード」と説明しています。RedisとValkeyの分岐はValkeyとは?Redisとの違い・導入方法・データ型を2026年最新版で解説にまとめました。
CacheHitRateが0.8を下回ったときの調べ方
ヒット率はAWSのCloudWatchメトリクスCacheHitRateで見られます。算出式はcache_hits / (cache_hits + cache_misses)で、AWS公式ドキュメントは0.8を下回った状態を「相当数のキーが退避・期限切れ・不存在になっている」と説明しています。
特定のキーへアクセスが集中して1ノードだけが飽和している場合は、Redis 8.6.0で追加されたHOTKEYSで実測できます。
HOTKEYS START METRICS 2 CPU NET COUNT 10 DURATION 60
HOTKEYS GET
HOTKEYS GETはCPU時間順(by-cpu-time-us)とネットワーク転送量順(by-net-bytes)の上位キーを返します。ADMIN権限が必要なコマンドなので、ADMINを与えていない一般のアプリ用ACLユーザーでは実行できません。ホットキーが特定できたら、そのキーだけプロセス内キャッシュへ二重化するか、キー名にシャード番号を付けて分散させます。
TTLの偏りが生むキャッシュスタンピードの回避
同じ時刻に大量のキーを生成すると、TTLも同時に切れます。切れた瞬間に全台が同じ再計算をデータベースへ投げ、キャッシュを入れる前より重い負荷が一度にかかる。これがキャッシュスタンピードです。台数が多いほど被害が大きくなるため、水平スケーリングを進めた環境ほど対策が要ります。
実装としては、TTLに乱数の幅を持たせて期限を散らすのが最も安価です。経験則の目安として、基準600秒に対して0から60秒程度の乱数を足せば同時失効はほぼ解消します。再計算コストが高いキーには、再計算中の1リクエストだけがロックを取り、残りは期限切れの値を返す方式を併用してください。
キューで負荷を平準化する非同期処理の設計
Amazon SQSの可視性タイムアウト30秒と12時間の上限
外部API呼び出しやメール送信をリクエスト処理の中で同期実行していると、その待ち時間の分だけワーカーが占有されます。キューへ退避すれば、Webサーバーの台数と処理能力を切り離せる構成になる。Amazon SQSの可視性タイムアウトは既定30秒で、この時間内にメッセージを削除しないと別のコンシューマーへ再配信されます。
処理が30秒を超えるジョブでは、キューの設定値を実処理時間より長くするか、ChangeMessageVisibilityで都度延長します。ただし延長には上限があり、AWSのドキュメントは最初の受信から12時間を超えられないと明記しています。延長を繰り返してもこの12時間は再設定されません。12時間を超える処理は、AWS Step Functionsへ移すか、より小さい単位へ分割する必要があります。
1MiBのメッセージ上限とペイロードの分離
SQSのメッセージ最大サイズは1,048,576バイト(1MiB)です。AWSは2025年8月4日にこの上限を256KiBから1MiBへ引き上げており、標準キューとFIFOキューの両方が対象になりました。それ以前の256KiBを前提に設計すると、不要な分割処理を書くことになります。1MiBを超える場合はAmazon SQS Extended Client Library(JavaとPython向け)を使い、本体をAmazon S3へ置いて参照だけをメッセージに載せます。
画像やCSVのような大きなデータは、サイズが上限内に収まっていてもメッセージ本体には入れず、保存先のキーだけを渡します。キューは処理の指示を運ぶ経路であって、データの保管場所ではありません。
再配信を前提にした冪等化と同時実行数の上限
SQSの標準キューは同じメッセージが2回以上配信されます。コンシューマー側は、メッセージIDや業務上の一意キーを処理済みテーブルへ挿入し、一意制約違反なら黙って捨てる形で冪等化します。
もう1つ必要なのが同時実行数の上限です。キューを挟むと、コンシューマーの台数を増やした分だけ下流のデータベースや外部APIへ負荷が素通りします。キューは負荷を平準化する仕組みであって、下流を保護する仕組みではありません。コンシューマー側で同時実行数を明示的に絞り、下流の処理能力に合わせます。言語ごとの非同期実装の選択肢はPHPの非同期処理とは|Fibers・ReactPHP・AMPHP・Swooleの違いと実装方法で比較しています。
データベース接続を枯渇させないコネクションプールの上限設計
HikariCPの既定値10とプールサイズの算出式
HikariCPのmaximumPoolSizeはREADMEに記載のとおり既定10です。この値を触らないまま台数を増やすのが、水平スケーリングで最も多い事故です。HikariCPのwiki「About Pool Sizing」は、PostgreSQLプロジェクト提供の式を出発点として示しています。
connections = ((core_count * 2) + effective_spindle_count)
ここでのcore_countはハイパースレッディングの論理コアを含まない物理コア数、effective_spindle_countはデータセットが完全にキャッシュに載っていれば0です。同wikiによると、PostgreSQLのベンチマークではおよそ50接続でTPSが頭打ちになり、Oracleの事例では2048接続から96接続まで削減しています。プールを大きくすれば速くなるという直感は、接続数についてはあてはまりません。設定パラメータの詳細はHikariCPとは?設定パラメータとSpring Bootでのプール設定・リーク対策を解説で解説しています。
台数×プールサイズがDBの接続上限を超える計算
プールサイズはアプリケーションサーバー1台あたりの値です。実際にデータベースへ張られる接続の合計は、台数を掛けた数になります。
spring.datasource.hikari.maximum-pool-size=12
spring.datasource.hikari.minimum-idle=12
spring.datasource.hikari.connection-timeout=3000
この設定で20台へスケールアウトすると、常時240接続がデータベースへ張られます。minimum-idleをmaximum-pool-sizeと同値にしてプールを固定にしているため、負荷が低い時間帯でも接続は解放されません。オートスケールで台数が変動する構成では、想定される最大台数を掛けた値がデータベースの接続上限に収まるかを先に計算します。収まらない場合、増やすべきはプールではなく接続の多重化層です。
RDS Proxyのピン留めが多重化を止める条件
Amazon RDS Proxyは接続プールを保持して接続を再利用し、すぐに捌けないアプリケーション接続をキューまたはスロットルします。台数が変動する構成でデータベースの接続数を一定に保つ用途に向く仕組みです。ただし効果はトランザクション単位の多重化が働いている間に限られます。
接続の状態を変える操作が入ると、その接続はセッションにピン留めされ、多重化が止まります。AWSのドキュメントはPostgreSQLについて、変数を設定するとセッションがピン留めされると明記。MySQL系ではセッションピン留めフィルターを適用して特定の操作を除外できます。ピン留めの頻度はCloudWatchのDatabaseConnectionsCurrentlySessionPinnedで監視し、値が張り付いているならRDS Proxyは接続数削減に効いていません。
リクエストを配る負荷分散方式の選び分け
重み付けラウンドロビンの重みを実測スループットから決める手順
nginxのupstreamは、指定がなければラウンドロビンで順番に振り分けます。weightパラメータの既定値は1。これを変えると重み付けラウンドロビンになります。世代の違うインスタンスが混在する構成や、新しいサーバーへ段階的にトラフィックを寄せる場面で使う方式です。
upstream app_backend {
server app1.internal:8080 weight=5 max_fails=2 fail_timeout=10s;
server app2.internal:8080 weight=2;
keepalive 64;
}
重みの決め方には手順があります。旧世代と新世代のインスタンスが混ざった構成なら、まず同じシナリオをwrkやabで各インスタンスへ直接当て、1台ずつのRPSを測ります。仮に新世代が1,250RPS、旧世代が500RPSなら比は2.5対1なので、weight=5とweight=2を割り当てる、という決め方です。CPUコア数の比をそのまま重みに使うと、メモリ帯域やストレージの世代差を拾えず外れます。
max_failsとfail_timeoutの既定は1回・10秒で、単発のタイムアウトでもサーバーが切り離されます。デプロイ直後の一瞬の失敗で台数が減るのを避けるため、例ではmax_fails=2にしています。
least_connが効く処理時間のばらつき
リクエストごとの処理時間が均一なら、ラウンドロビンで十分です。重い集計処理と軽い参照が混在し、処理時間が数十倍ばらつく場合は、順番に配るだけでは重い処理を掴んだサーバーに次の処理が積み上がります。nginxのleast_connはアクティブな接続数が最も少ないサーバーへ回す方式です。
ここで注意したいのがweightとの関係です。公式ドキュメントはleast_connについて「サーバーの重みを考慮し、該当するサーバーが複数あれば重み付けラウンドロビンで順に試す」と書いています。つまりleast_connを併記した時点で、重みが効くのは接続数が並んだときのタイブレークだけになり、上の例のような5対2のリクエスト比は保証されません。両方を同時に狙うことはできないため、どちらの基準で配るかを先に決めます。
切り替えの目安として使えるのは、レスポンスタイムの95パーセンタイルと中央値の開きです。経験上、95パーセンタイルが中央値の3倍を超えたあたりからleast_connが有利になります。ばらつきが小さいうちに導入しても、接続数を数えるコストの分だけ損をします。
nginx 1.29.7で変わったupstreamキープアライブの既定
アプリケーションサーバーとの接続を毎回張り直すと、TCPハンドシェイクとTLSネゴシエーションの分だけ遅延が乗ります。keepaliveディレクティブは公式ドキュメント上のDefaultがkeepalive 32 local;。1.29.7以降は接続のキャッシュが既定で有効になり、異なるlocation間では共有されない挙動に変わりました。2026年8月7日時点の最新はmainlineが1.31.3、stableが1.30.4です。
1.29.7より前のバージョンでは、proxy_http_version 1.1;とproxy_set_header Connection "";を書かないとキープアライブが効きません。移行時に効いてくるのは上流側の設定です。アプリケーションサーバーのアイドルタイムアウトがnginx側より短いと、閉じられた接続を掴んで502が出ます。上流のタイムアウトを先に長くしてからバージョンを上げます。
アプリケーションスケーリングに投資すべきでない場面
ここまでの作業には相応の工数がかかります。次の3条件に当てはまるなら、着手を見送るほうが合理的です。
1台で捌けている段階で垂直スケーリングを選ぶ条件
ピーク時のリクエストが1台で捌けているうちは、垂直スケーリングでインスタンスのサイズを上げるほうが安く済みます。セッション外部化はネットワーク往復を1回増やすため、単体の応答時間はむしろわずかに悪化する。冗長化が目的なら2台構成にすれば足り、キャッシュ戦略の作り込みまでは要りません。
単一の書き込みトランザクションがボトルネックの場合の打ち手
在庫の引き当てや採番のように直列化が業務要件になっている処理は、アプリを何台に増やしても待ち行列が伸びるだけです。手を入れるべきはアプリ層ではなく、テーブル設計とトランザクション境界。ここを飛ばして台数を増やすと、接続数だけが増えてデータベースの負荷が悪化します。
セッション外部化がサポート対象外になる既製パッケージ
ファイルシステム上のセッションや一時ファイルを前提に作られた製品では、外部化が製品サポートの対象外になることがあります。対象外であれば垂直スケーリングで上限まで引き上げ、その先は製品の刷新を検討します。無理に水平分散すると、障害時にベンダーの支援を受けられません。
よくある質問
アプリケーションスケーリングと水平スケーリングは同じ意味ですか?
異なります。水平スケーリングはサーバーの台数を増やす操作を指し、アプリケーションスケーリングはその操作で実際に処理量が伸びるようアプリ側の実装を整える作業を指します。台数を増やしても伸びない状態は、水平スケーリングだけが済んでアプリケーションスケーリングが済んでいない状態です。垂直スケーリングとの比較や使い分けそのものはスケールアウトとスケールアップとは?水平・垂直スケーリングの違いと実装・使い分けを実装目線で解説【2026年版】で扱っています。
Redisに置いたセッションの有効期限はどこで設定しますか?
phpredisのREADMEによれば、セッションの寿命は秒単位でPHPのINI変数session.gc_maxlifetimeに保存され、ini_set()で変更できます。Redis側にTTLを直接書く設定項目はありません。maxmemory-policyにallkeys-lruを使っている場合、有効期限内でもメモリ逼迫でセッションキーが退避される点に注意が必要です。セッションとキャッシュを同じRedisインスタンスへ相乗りさせず、データベース番号かインスタンスを分けます。
オートスケールで台数が変わる構成ではコネクションプールをどう設定しますか?
minimumIdleをmaximumPoolSizeより小さくして、アイドル接続を返せる状態にします。HikariCPのminimumIdleは既定でmaximumPoolSizeと同値、つまりプールが固定サイズで、負荷が下がっても接続を解放しません。idleTimeoutの既定は600000ミリ秒(10分)なので、minimumIdleを下げれば10分後には余剰接続が返ります。台数のスケールインより接続の解放が遅れる前提で、上限は最大台数で計算してください。
SQSのFIFOキューを使えば冪等化は不要になりますか?
不要にはなりません。AWSのドキュメントが保証しているのは、5分間の重複排除間隔のなかでSendMessageを再試行しても重複がキューへ入らないことです。コンテンツベースの重複排除を有効にすると、メッセージ属性ではなく本文のSHA-256ハッシュから重複排除IDが生成されます。一方、コンシューマー側が可視性タイムアウト内に削除できなければ、FIFOキューでも同じメッセージが再配信されます。受信側の冪等化は標準キューと同じく必要です。
maxmemoryを設定していないRedisでも書き込みエラーは起きますか?
起きません。公式ドキュメントによればmaxmemoryを0にすると容量制限がなくなり、これが64ビット環境の既定の挙動です。32ビット環境では3GBの暗黙の上限があります。上限が無い場合、noevictionによる書き込みエラーの代わりに、OSがメモリを使い切ってプロセスが停止します。エラーで検知できるようにするため、キャッシュ用途ではmaxmemoryを明示してください。