ストーリー
高橋アーキテクトが図を描いた。
ステートフルの問題
// ステートフルなサーバー(水平スケーリングに不向き)
class StatefulServer {
private sessions = new Map<string, UserSession>(); // メモリに保存
login(userId: string): string {
const sessionId = generateSessionId();
this.sessions.set(sessionId, { userId, createdAt: new Date() });
return sessionId;
}
getUser(sessionId: string): UserSession | undefined {
return this.sessions.get(sessionId); // このサーバーにしかない
}
}
// 問題: Server Aでログイン → Server Bにリクエスト → セッションがない
ステートレスの原則
各リクエストが独立して処理でき、サーバーが状態を保持しない設計です。
// ステートレスなサーバー
class StatelessServer {
constructor(
private sessionStore: ExternalSessionStore, // Redis等の外部ストア
private jwtVerifier: JWTVerifier,
) {}
// 方法1: 外部セッションストア(Redis)
async getUserBySession(sessionId: string): Promise<UserSession | null> {
return this.sessionStore.get(sessionId); // どのサーバーからでもアクセス可能
}
// 方法2: JWT(トークンに情報を含める)
getUserByToken(token: string): UserPayload {
return this.jwtVerifier.verify(token); // サーバーに状態を持たない
}
}
セッション管理の外部化
方法1: 外部セッションストア(Redis)
// Redis をセッションストアとして使用
class RedisSessionStore {
constructor(private redis: RedisClient) {}
async create(userId: string): Promise<string> {
const sessionId = crypto.randomUUID();
const session: Session = {
userId,
createdAt: new Date().toISOString(),
data: {},
};
await this.redis.setex(
`session:${sessionId}`,
1800, // 30分
JSON.stringify(session)
);
return sessionId;
}
async get(sessionId: string): Promise<Session | null> {
const data = await this.redis.get(`session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
async destroy(sessionId: string): Promise<void> {
await this.redis.del(`session:${sessionId}`);
}
}
方法2: JWT(JSON Web Token)
// JWT を使ったステートレス認証
class JWTAuthService {
constructor(private secret: string) {}
generateToken(user: User): string {
const payload = {
userId: user.id,
email: user.email,
role: user.role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // 1時間
};
return jwt.sign(payload, this.secret);
}
verifyToken(token: string): UserPayload {
return jwt.verify(token, this.secret) as UserPayload;
}
}
// JWTの特徴
// - サーバーに状態を保持しない(トークン自体に情報が含まれる)
// - DBやRedisへの問い合わせが不要
// - トークンの無効化が難しい(ブラックリスト管理が必要)
比較
| 観点 | Redis Session | JWT |
|---|---|---|
| 状態の保存場所 | Redis(サーバーサイド) | トークン(クライアントサイド) |
| スケーラビリティ | Redis依存 | 完全ステートレス |
| セッション無効化 | 即座に可能 | 困難(ブラックリスト必要) |
| データ量 | 大量のデータも可 | トークンサイズに制限 |
| セキュリティ | データがサーバーに保持 | ペイロードは読み取り可能 |
ファイルストレージの外部化
// ステートフル: ローカルファイルシステムに保存
class LocalFileStorage {
async save(file: Buffer, filename: string): Promise<string> {
const path = `/uploads/${filename}`;
await fs.writeFile(path, file);
return path; // このサーバーにしかない
}
}
// ステートレス: オブジェクトストレージ(S3等)に保存
class S3FileStorage {
constructor(private s3: S3Client) {}
async save(file: Buffer, filename: string): Promise<string> {
const key = `uploads/${Date.now()}-${filename}`;
await this.s3.putObject({
Bucket: 'my-app-uploads',
Key: key,
Body: file,
});
return `https://my-app-uploads.s3.amazonaws.com/${key}`;
// どのサーバーからでもアクセス可能
}
}
ステートレス設計のチェックリスト
const statelessChecklist = {
// セッション: メモリではなく外部ストアに保存
sessions: 'Redis or JWT',
// ファイル: ローカルではなくオブジェクトストレージ
files: 'S3 or Cloud Storage',
// キャッシュ: ローカルメモリではなく分散キャッシュ
cache: 'Redis or Memcached',
// 設定: ファイルではなく環境変数 or 設定サービス
config: 'Environment variables or Config service',
// スケジュールタスク: 各サーバーではなく専用ワーカー
scheduledTasks: 'Dedicated worker or cron service',
};
まとめ
| ポイント | 内容 |
|---|---|
| ステートフルの問題 | サーバー固有の状態が水平スケーリングを阻む |
| ステートレスの原則 | サーバーは状態を持たない。外部ストアを使用 |
| セッション | Redis(外部ストア)or JWT(クライアント保持) |
| ファイル | S3等のオブジェクトストレージ |
| 設計指針 | 全ての状態をサーバー外部に出す |
チェックリスト
- ステートフルの問題点を説明できる
- セッション管理の外部化方法を2つ知った
- JWTとRedis Sessionの使い分けを理解した
- ファイルストレージの外部化の必要性を把握した
次のステップへ
次は「データベーススケーリング」を学びます。アプリケーション層のスケーリングができても、データベースがボトルネックになるケースを解決しましょう。
推定読了時間: 40分