サーバーサイドセッションストアのスケールアウト時に同期遅延が起きる理由と、単純レプリケーション設計の落とし穴
複数サーバー運用時に「ログイン直後に認証が通らない」が起きる背景
ユーザーがログインしてセッションが作成されたのに、別のサーバーへのリクエストで認証が通らない──こういった間欠的なエラーは、スケールアウトしたシステムで必ず誰かが経験する問題です。原因は単純です。セッションストアがサーバー間で同期されていないか、同期のタイミングと実際のリクエスト到達のタイミングがズレているのです。
現場では、この問題に直面してから「では複数サーバーで動かそう」と気づくことが多い。最初は開発環境で1台のサーバーで十分だからです。しかし本番環境で負荷が増えたり、可用性を上げるために冗長化したりする段階で、セッション同期の設計がいかに重要か思い知ることになります。
この記事では、サーバーサイドセッションストアをスケールアウト環境で運用するときの同期パターンを、検証を通じて整理します。単純なレプリケーション設計がなぜ落とし穴になるのか、そして実務ではどういう判断をするべきかを、実装レベルで見ていきましょう。
検証の前提:どの同期パターンを比較するのか
スケールアウト時のセッション同期には、大きく3つのアプローチがあります。
1. インメモリストア(各サーバーで独立)
- Redis や Memcached を各サーバーに配置し、ロードバランサーでセッションアフィニティを固定
- 同期なし。ノード障害時にセッションが失われる
2. 共有外部ストア(集約型)
- Redis クラスタ、RDS、Memcached など、複数サーバーから共通で参照する1つのストア
- 同期の概念がない。すべてのサーバーが同じデータソースを見る
3. マスター・レプリカ型(または相互レプリケーション)
- セッションストアをサーバー間でレプリケーション
- 各サーバーがマスターを持つか、1つのマスターに複数のレプリカが従う構成
今回の検証では、3番目のレプリケーション型に焦点を当てます。理由は、このパターンが「安そうに見えて実装が厄介」だからです。共有外部ストアは単純で堅牢ですが、SPOFのリスクがあります。レプリケーション型は分散性が高そうに見えますが、同期遅延が認証エラーに直結しやすい。
検証環境と実装パターン
環境構成
ロードバランサー
├─ アプリサーバーA (セッションストア付き)
├─ アプリサーバーB (セッションストア付き)
└─ アプリサーバーC (セッションストア付き)
各サーバーは定期的に他のサーバーへセッション情報をレプリケーション
検証に使ったのは、Node.js + Express + 簡易的なインメモリセッションストアです。セッションデータを JSON 形式で定期的に HTTP POST で他のサーバーへ送信し、受け取ったサーバーがマージするシンプルな実装です。
パターン1:定期的なバッチレプリケーション(5秒間隔)
// サーバーA での実装例
const sessionStore = {};
// ログイン時
app.post('/login', (req, res) => {
const sessionId = generateSessionId();
sessionStore[sessionId] = {
userId: req.body.userId,
loginTime: Date.now(),
lastAccess: Date.now()
};
res.json({ sessionId });
});
// 5秒ごとにレプリケーション
setInterval(async () => {
const otherServers = ['http://server-b:3000', 'http://server-c:3000'];
for (const server of otherServers) {
try {
await fetch(`${server}/replicate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sessionStore)
});
} catch (err) {
console.error(`Replication to ${server} failed:`, err.message);
}
}
}, 5000);
// レプリケーション受け取り
app.post('/replicate', (req, res) => {
const incomingData = req.body;
Object.assign(sessionStore, incomingData);
res.json({ ok: true });
});
結果:ログイン直後に別サーバーへリクエストが到達すると、セッションが見つからない
ユーザーがサーバーA でログインしてセッションID を取得してから、ロードバランサーがサーバーB へリクエストを振ると、そこではセッションがまだ存在しません。次のレプリケーション周期(最大5秒)まで待つ必要があります。
パターン2:イベント駆動型レプリケーション(ログイン・セッション更新時)
// セッション作成・更新時に即座にレプリケーション
async function replicateSession(sessionId, data) {
const otherServers = ['http://server-b:3000', 'http://server-c:3000'];
const promises = otherServers.map(server =>
fetch(`${server}/sync-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, data })
}).catch(err => {
console.error(`Sync to ${server} failed:`, err.message);
// レプリケーション失敗時の処理
})
);
await Promise.allSettled(promises);
}
app.post('/login', async (req, res) => {
const sessionId = generateSessionId();
sessionStore[sessionId] = {
userId: req.body.userId,
loginTime: Date.now(),
lastAccess: Date.now()
};
// 即座にレプリケーション
await replicateSession(sessionId, sessionStore[sessionId]);
res.json({ sessionId });
});
結果:ネットワーク遅延やレプリケーション先の障害に左右される
レプリケーション処理が成功するまで待つと、ログインレスポンスが遅くなります。逆に非同期で発火させると、レプリケーション中にユーザーが別サーバーへリクエストすれば、やはりセッションが見つかりません。
実装上の判断ポイント:結局何が起きるのか
両パターンとも、根本的な問題は同じです。
- セッション作成と同期の間に時間差がある
- その間にロードバランサーが別サーバーへリクエストを振る可能性がある
- セッションが見つからず、認証エラーになる
さらに実務的な課題が重なります。
ネットワーク分断やレプリケーション先障害への対応
複数サーバーへのレプリケーションで、1台が応答しない場合、どうするか。全台への同期を待つと、1台の障害で全体が遅くなります。部分的な失敗を許容すると、どのサーバーがどのセッションを持っているか追跡が難しくなります。
セッション削除やタイムアウト処理の同期
セッション作成だけでなく、削除やタイムアウトも同期する必要があります。各サーバーが独立してタイムアウト判定をすると、あるサーバーではセッションが有効、別のサーバーでは無効という矛盾が起きます。
データベース的な一貫性の問題
複数サーバーが同時にセッションを更新する場合(例:複数タブから同時アクセス)、更新順序が異なるとサーバー間でセッション内容がズレます。
実務投入前に確認すべきこと
単純なレプリケーション設計を採用する場合、以下を明確にしておく必要があります。
1. セッション同期の遅延をアプリケーション側で吸収できるか
- ログイン直後は同じサーバーへリクエストを固定する(セッションアフィニティ)
- クライアント側で再試行ロジックを入れる
- 認証エラー時に再ログインを促す
2. レプリケーション失敗時の動作を定義しているか
- 失敗したサーバーのセッションは古いままになる
- そのサーバーへのアクセスが来たとき、どう判定するか
3. セッション削除やタイムアウトの一貫性をどう保証するか
- マスターサーバーを決めて、そこでのみタイムアウト判定を行う
- 全サーバーが同じタイムアウトルールを持つ
4. スケールするか
- サーバー台数が増えるたびに、レプリケーション対象も増える
- 10台以上になると、各サーバーが他の9台へレプリケーションするため、ネットワーク負荷が二次関数的に増える
実務ではどう判断するか
正直に言えば、スケールアウト環境ではレプリケーション型セッションストアは避けるべきです。理由は上記の複雑性です。
代わりに、以下の選択肢のほうが実装と運用が単純です。
- 共有外部ストア(Redis など):複数サーバーから同じストアを参照。同期遅延がない。単一障害点のリスクは冗長化で対応。
- ステートレス設計(JWT など):セッション情報をクライアント側に持たせ、サーバーは検証のみ。スケールが最も簡単。
ただし、既存システムの制約や、セッション情報の機密性の理由から、サーバーサイドセッションストアが必須な場合もあります。そのときは、レプリケーション型ではなく、共有ストアに一元化するほうが堅牢です。
現場では「複数台で動かしたいから、セッションも複数台に分散させよう」という発想が出やすいですが、セッションは集約すべきデータです。認証