「ロードバランサ配下でページが時々404になる…」
そんな不思議な現象に遭遇したことはないでしょうか?

ロードバランサ(LB)が2台のWebサーバーにトラフィックを振り分けているとき、
もしサーバーAに test.html が存在し、サーバーBには存在しない場合、
クライアントからアクセスするとどうなるのか?
この記事では AWS ALB や NGINX を例に、404になる仕組みと実際の対策方法 を解説します。

実際の挙動:なぜ「表示されたり404になったり」するのか

結論から言うと、ロードバランサはファイルの有無までは管理しないためです。
ALBもNGINXのupstreamも、単にリクエストを「どのサーバーに送るか」だけを決定します。

そのため、

  • サーバーAに振られた場合 → test.html が返る
  • サーバーBに振られた場合 → 404 Not Found が返る
    という挙動になります。

実際に確認してみると、curlで以下のように結果が変わります。

# Aに振られたとき
$ curl -I https://example.com/test.html
HTTP/1.1 200 OK

# Bに振られたとき
$ curl -I https://example.com/test.html
HTTP/1.1 404 Not Found

振り分け方式による違い

もう少し正確に説明すると、ロードバランサのアルゴリズムによって、挙動は変わります。

1. ラウンドロビン

リクエストごとにA → B → A → Bと順番に振り分ける
test.html の結果は交互に「表示」「404」になる

2. 最小コネクション方式

空いている方のサーバーに振る
→ 負荷状況によって表示されたい、表示されなかったりする

3. スティッキーセッション(同じクライアントは同じサーバーへ)

最初にAを引けばずっと表示できる、Bを引けばずっと404になる

💡補足 ロードバランサ(特にAWS ALBなど)では、デフォルトで「ラウンドロビン + 重み付きランダム」方式が使われることが多いです。 Sticky sessionをONにしない限り、リクエストごとに異なるサーバーへルーティングされるため、 ファイル不一致があると404が断続的に発生します。

L4 / L7 の違いにも注意

ロードバランサは「通信のどの層(レイヤ)まで理解して処理するか」によって、
L4(レイヤ4)とL7(レイヤ7)の2種類に大きく分かれます。
簡単に言えば、L4は“宛先の箱を見て送る”、L7は“箱の中身まで読んで判断する”仕組みです。

L4ロードバランサ(例:AWS NLB)

L4は、TCPやUDPといったトランスポート層の情報(IPアドレスやポート番号)だけを見て、
どのサーバーに転送するかを決めます。
たとえば「このポートの通信はAサーバーへ」「次はBサーバーへ」といった具合です。

非常にシンプルな仕組みのため、処理が軽く、高速でスループットも高いのが特徴です。
一方で、HTTPのリクエスト内容やヘッダーなどの“中身”までは理解しません。
そのため、「特定のURLにアクセスされたら別のサーバーに送る」といった制御はできません。

AWSでは Network Load Balancer(NLB) がこのタイプにあたります。
L4はアプリケーションの構造に依存せず、
ネットワーク的に安定した負荷分散を行いたい場合に適しています。

L7ロードバランサ(例:AWS ALB、NGINX)

一方でL7は、アプリケーション層(HTTP/HTTPS)のデータを理解して処理します。
つまり、「リクエストのパス」「Hostヘッダー」「Cookie」「HTTPメソッド」などの情報を読み取り、
より細かい条件でルーティングを行うことが可能です。

たとえば次のようなことができます:

  • /api/* はバックエンドAに、/static/* はバックエンドBに振り分ける
  • 特定のCookieを持つユーザーだけを特定サーバーへ固定する(スティッキーセッション)
  • リクエストヘッダーに応じてキャッシュや圧縮の有無を切り替える

つまり、L4が「通信路の橋渡し」なのに対して、L7は「Webアプリの意思決定者」といえます。
AWSでは Application Load Balancer(ALB)、またはNGINXのリバースプロキシ構成がこれに相当します。

ヘルスチェックの違いと注意点

L7ロードバランサでは、通常「ヘルスチェック」と呼ばれる機能でサーバーの健康状態を監視しています。
これは、指定されたURL(例:/healthz)に対して定期的にHTTPリクエストを送り、
ステータスコードが200なら「正常」と判断する仕組みです。

しかし注意が必要なのは、このチェックはサービス全体の稼働を確認するだけで、
すべてのファイルの存在を確認しているわけではない、という点です。

つまり、/healthz が200を返していればサーバーは“正常”とみなされますが、
その一方で /test.html が片方のサーバーに存在しない場合でも、
ロードバランサはそれを「異常」とは判断しません。
結果として、そのサーバーにもリクエストが振られ、404が発生するのです。

もし /test.html 自体をヘルスチェック対象にすれば検知できますが、
チェックパスを増やしすぎるとパフォーマンスや運用負荷が高まります。
また、デプロイ中に一時的な404が出ると「不健康」と判定され、
本来正常なサーバーまで外してしまうリスクもあります。

実際の設計で意識すべきこと

このように、L7ロードバランサはアプリケーションを深く理解して賢く動く反面、
“コンテンツの整合性までは保証しない” という性質があります。
したがって、問題の根本的な対策は「ロードバランサを賢く設定する」ことではなく、
すべてのサーバーで同一のファイルを確実にデプロイ・同期しておくことです。

ロードバランサは「どこへ送るか」を決める存在であって、
「何を送るか」「それが最新かどうか」までは関与しません。
本番運用ではCI/CDパイプラインや共有ストレージ(EFS/S3など)を活用し、
アプリケーション層の整合性を別のレイヤで担保する設計が求められます。

要するに、

  • L7は“リクエストの中身を理解する”が、“欠けているファイルを補うことはできない”
  • そのため、L7の力に頼るよりも、デプロイや同期の仕組みで「不一致を起こさない」構成を作ることが重要です。

このように「サーバーごとにファイルの有無が違う」状態は、ロードバランシング構成においては危険です。
ユーザー体験が不安定になるだけでなく、
「テストでは動いたのに本番では動かない」といった不具合の温床にもなります。

しかも、本番環境ではWebサーバーが2台あるのに、IT環境や検証環境ではサーバーが1台だけ、なんてケースも珍しくありませんので、 テストだと気づかないことがあります。そもそも特殊なテストケースを用紙しないと気付けないこともありますね。

💡なぜIT企業は、開発環境のこういう部分をケチってしまうんでしょうね。

解決策

対策 内容 メリット 注意点
✅ 全サーバーへ同一デプロイ CI/CD(GitHub Actions, CodeDeployなど)で自動配布 シンプル・確実 初期整備が必要
📁 共有ストレージ(NFS/EFS) 各Webサーバーから同じマウントを参照 変更が即時に全台へ反映 I/O/スループット上限に注意
☁️ S3 + CloudFrontで静的分離 静的はS3に一元化、LB配下は動的処理に専念 スケーラブル・キャッシュ最適化 構成見直しが必要
⚠️ Sticky Session(暫定) 同一ユーザーを同一サーバーへ固定 一時回避に有効 根本解決にならない

まとめ

ロードバランサ配下のWebサーバーでは、コンテンツを揃えておくことが大前提です。
片方にしかファイルがないと、ユーザー視点では「表示されたりされなかったりする」という不安定な体験になります。

負荷分散環境では、単に「サーバーを増やせば安心!」ではなく、
ファイル配置やセッションの扱いまで含めた設計が必要です。

シンプルな test.html の例でも、意外とシステム設計の奥深さが見えてきますね。

このサイトはまだアクセス数も大したことないのでLBは使っていませんが、
将来的にスケールする際はファイル同期や監視の仕組みを入れておきたいと思います。
もし別のファイルがアップロードされていたら検知できるように、
シンプルな監視バッチを組むのも良さそうですね。

保守性のためにバッチを作るけど、 バッチが増えるすぎると逆にバッチそのものが保守対象になる、、、

保守性って難しいですね。