세션 어피니티 (Session Affinity, 일명 Sticky Sessions)
특정 요청이 항상 같은 Machine으로 라우팅되도록 보장해야 하는 상황이 있어요. 이 페이지에서는 사용자의 전체 세션을 같은 Machine이 처리하도록 하는 두 가지 방법, 즉 클라이언트 측 헤더 fly-force-instance-id와 서버 측 헤더 fly-replay를 설명해요.
출처: 문서
본문
개요 (Overview)
여러 시나리오에서 특정 요청이 특정 Machine으로 라우팅되도록 보장하는 것이 중요해요. 이는 주로 사용자의 세션 전체를 같은 Machine이 처리하도록 하려는 형태로 나타나요.
Fly.io에서 이를 해결하는 방법은 두 가지예요:
- fly-force-instance-id - 클라이언트 측 요청 헤더
- fly-replay - 서버 측 응답 헤더
Fly-Force-Instance-Id
이 방식은 클라이언트, 보통 브라우저를 제어할 수 있어야 하지만, 추가 홉(hop) 없이 즉시 라우팅할 수 있어요. 아래 예시는 Rails의 Hotwire Turbo와 Stimulus를 사용해 필요한 헤더를 보내요.
이 방식이 동작하려면 클라이언트가 어떤 Machine으로 요청을 라우팅해야 하는지 알 수 있는 방법이 필요해요. 이는 HTML 응답의 <body> 태그에 속성을 추가해서 구현할 수 있어요. Rails에서는 그 태그를 app/views/layouts/application.html.erb에서 찾을 수 있어요.
<body> 태그 예시:
<body data-instance="<%= ENV['FLY_MACHINE_ID'] %>"
data-controller="sticky-session">
이 속성들은 향후 요청을 보낼 Machine 인스턴스를 식별하고, 이를 수행할 Stimulus 컨트롤러의 이름을 나타내요.
다음을 app/javascript/controllers/sticky-session.js에 넣으세요:
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="sticky-session"
export default class extends Controller {
connect() {
document.documentElement.addEventListener(
'turbo:before-fetch-request',
this.beforeFetchRequest
)
}
disconnect() {
document.documentElement.removeEventListener(
'turbo:before-fetch-request',
this.beforeFetchRequest
)
}
beforeFetchRequest = event => {
event.detail.fetchOptions.headers['fly-force-instance-id'] =
this.element.dataset.instance;
}
}
이 코드는 turbo:before-fetch-request 이벤트를 구독/구독 해제하고 요청에 fly-force-instance-id 헤더를 넣어요.
Fly-Replay
이 방식은 전적으로 서버에서 구현돼요. 애플리케이션이 각 요청을 검사해 어떤 Machine이 처리할지 결정하고, fly-replay 응답 헤더를 반환해 그에 맞게 요청을 라우팅해요. 최초 요청은 라우팅을 위해 추가 "홉(hop)"이 필요하고, 요청은 1메가바이트 페이로드로 제한돼요.
아래 예시는 세션 쿠키를 기반으로 요청을 라우팅하는 Express Middleware를 만들어요:
// Import required modules
import express from "express";
import cookieParser from "cookie-parser";
// Create an Express app
const app = express();
// Middleware to parse cookies
app.use(cookieParser());
app.use((request, response, next) => {
// This assumes your application has already established a session using a session_id cookie via your session middleware.
const sessionId = request.cookies["session_id"];
if (!sessionId) {
// No session - let any machine handle it
next();
return;
}
// Determine which machine should handle this session
// This could be based on:
// - Database lookup (session -> machine mapping)
// - Consistent hashing of session ID
// - Regional routing logic
// - Load balancing algorithm
// You'll need to implement determineTargetMachine() based on your routing strategy
const targetMachineId = determineTargetMachine(sessionId);
if (targetMachineId === process.env.FLY_MACHINE_ID) {
// This is the correct machine - handle the request
next();
} else {
// Route to the correct machine
response.set('Fly-Replay', `instance=${targetMachineId}`);
response.status(307);
response.send();
}
});
// Start the Express server
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
리플레이 캐싱으로 최적화하기
프로덕션 워크로드에서 애플리케이션이 모든 요청을 리플레이하게 하는 것은 불필요한 부하와 지연 시간을 만들 수 있어요. Fly Proxy가 리플레이 결정을 캐시하도록 구성하면, 세션의 첫 요청만 애플리케이션을 거치면 돼요.
fly.toml에 리플레이 캐시 규칙을 추가하세요:
[http_service]
internal_port = 8080
force_https = true
[[http_service.http_options.replay_cache]]
path_prefix = "/"
ttl_seconds = 300
type = "cookie"
name = "session_id"
이 구성에서:
- 주어진
session_id가 있는 첫 요청이 여러분의 앱에 도달해요 - 여러분의 앱이
fly-replay응답을 반환해요 - Fly Proxy가 해당
session_id값에 대한 결정을 캐시해요 - 다음 5분 동안 같은
session_id의 요청은 여러분의 앱을 거치지 않고 자동으로 라우팅돼요
캐싱은 최적화이지 보장이 아니에요. 그래서 원본 앱은 캐시된 리플레이로 라우팅되지 않는 요청에 대한 리플레이 처리도 여전히 해야 해요.
리플레이는 기존 쿠키나 요청 헤더를 기준으로 캐시할 수 있어요. 예를 들어 API에서의 스티키 세션은 Authorization 헤더를 기준으로 캐시하도록 선택할 수 있어요.
리플레이 캐싱 구성의 전체 세부 사항은 세션 기반 리플레이 캐싱을 참고하세요.
더 알아보기 (Learn more)
- 동적 요청 라우팅 with
fly‑replay—fly‑replay가 요청을 특정 Machine이나 리전으로 라우팅하게 하는 방법을 설명해요. 스티키 세션의 핵심 기법이에요. - 로드 밸런싱 — Fly Proxy가 Machine 간 트래픽을 라우팅하는 방법, 즉 세션 어피니티 결정의 기반이 되는 내용을 자세히 다뤄요.
- 네트워킹 — Fly가 공용/사설 네트워킹, Anycast IP, WireGuard 메시, 도메인 라우팅 등을 처리하는 방법에 대한 폭넓은 개요예요.