-
[ASP.NET Core] SignalR 분산 환경 구축: Redis 백플레인(Backplane)과 웹소켓 스케일아웃Microsoft .NET/ASP.Net & ASP.Net Core 2026. 11. 11. 12:13

SignalR은 연결이 인스턴스에 묶여 있어, 서버를 여러 대로 늘리면 다른 인스턴스의 클라이언트에게 메시지가 전달되지 않습니다. Redis 백플레인을 붙여 인스턴스 간 메시지를 중계하고, 로드 밸런서에서 웹소켓이 끊기지 않게 구성하는 절차입니다.
절차
- 증상 확인
- Redis 기동
- 백플레인 등록
- 허브와 클라이언트 연결
- 로드 밸런서 설정
- 운영에서 조정할 값
- 점검 항목
1. 증상 확인
- 인스턴스 2대를 띄우고 같은 그룹에 두 클라이언트를 붙입니다.
- A 인스턴스에 붙은 클라이언트가 보낸 메시지가 B 인스턴스의 클라이언트에 도착하지 않습니다.
- 로그에는 오류가 없습니다. 브로드캐스트가 프로세스 안에서만 처리되기 때문입니다.
백플레인 없이 스케일아웃하면 이 상태가 정상 동작으로 보입니다. 연결 수는 늘고 실시간 기능만 조용히 깨집니다.
2. Redis 기동
docker run -d --name signalr-redis \ -p 6379:6379 \ -v $(pwd)/redis-data:/data \ redis:7-alpine redis-server --appendonly yes백플레인은 pub/sub만 사용하므로 영속성은 필수가 아닙니다. 다만 재연결 폭주 시 부하를 견디도록
maxmemory정책과 타임아웃을 함께 봅니다.docker exec -it signalr-redis redis-cli ping docker exec -it signalr-redis redis-cli config get maxmemory3. 백플레인 등록
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedisusing StackExchange.Redis; var redisConnection = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379"; builder.Services .AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.MaximumReceiveMessageSize = 64 * 1024; options.KeepAliveInterval = TimeSpan.FromSeconds(15); options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); }) .AddStackExchangeRedis(redisConnection, options => { options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp"); options.Configuration.AbortOnConnectFail = false; });ChannelPrefix는 같은 Redis를 여러 애플리케이션이 공유할 때 메시지가 섞이지 않게 합니다. 서비스마다 다르게 지정합니다.AbortOnConnectFail = false로 두면 Redis가 잠깐 끊겨도 애플리케이션 시작이 실패하지 않습니다. 백플레인 연결이 복구되면 자동으로 다시 붙습니다.MaximumReceiveMessageSize는 기본값이 작습니다. 큰 payload를 주고받으면 조용히 연결이 끊기므로 업무 메시지 크기에 맞춰 올립니다.
4. 허브와 클라이언트
public class ChatHub : Hub { public Task JoinRoom(string room) => Groups.AddToGroupAsync(Context.ConnectionId, room); public Task SendToRoom(string room, string message) => Clients.Group(room).SendAsync("message", message); } app.MapHub<ChatHub>("/hubs/chat");const connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/chat", { accessTokenFactory: () => token }) .withAutomaticReconnect() .build(); await connection.start(); await connection.invoke("JoinRoom", "notice"); connection.on("message", (msg) => console.log(msg));백플레인이 연결되면
Clients.Group,Clients.User,Clients.All메시지가 Redis를 거쳐 모든 인스턴스로 전달되고, 각 인스턴스는 자기에게 붙은 연결에만 실제로 씁니다.5. 로드 밸런서 설정
SignalR은 협상(negotiate) 후 연결을 유지합니다. 연결이 끝난 뒤 다른 인스턴스로 보내면 세션이 깨지므로, 웹소켓 연결에 대한 고정(sticky)이 필요합니다.
upstream signalr_backend { ip_hash; server 10.0.0.11:8080; server 10.0.0.12:8080; } server { listen 443 ssl; server_name chat.example.com; location /hubs/ { proxy_pass http://signalr_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; } }ip_hash(또는 쿠키 기반 고정)가 없으면 협상과 연결이 다른 인스턴스로 흩어집니다.proxy_read_timeout이 짧으면 유휴 연결이 끊깁니다.KeepAliveInterval보다 넉넉하게 잡습니다.proxy_buffering off로 두어야 메시지가 즉시 전달됩니다.- 웹소켓을 쓰지 않는 폴백(SSE, Long Polling)에서도 같은 고정이 필요합니다.
쿠키 기반 고정을 쓰는 로드 밸런서라면 애플리케이션에서 쿠키를 발급하지 않아도, LB가 붙인 세션 쿠키로 같은 인스턴스에 고정됩니다.
6. 운영에서 조정할 값
KeepAliveInterval과ClientTimeoutInterval은 2배 이상 차이가 나야 합니다. 클라이언트가 ping을 놓쳐도 즉시 끊기지 않게 하기 위해서입니다.- 연결 수가 많은 서비스는 인스턴스당 연결 수와 메모리를 함께 봅니다. 연결 하나가 차지하는 메모리는 메시지 크기와 버퍼 설정에 좌우됩니다.
- Redis 단일 인스턴스는 처리량 한계가 있습니다. 초당 메시지가 많은 서비스는 Redis Cluster나 Azure SignalR Service 같은 관리형 백플레인을 검토합니다.
- 재연결 폭주를 막기 위해 클라이언트
withAutomaticReconnect([0, 2000, 5000, 10000])처럼 지연을 단계적으로 둡니다.
7. 점검 항목
- 인스턴스별 연결 수: 로그에
OnConnectedAsync/OnDisconnectedAsync카운트를 남깁니다. - Redis pub/sub 채널 구독 여부:
redis-cli pubsub channels로 채널이 보이는지 확인합니다. - 로드 밸런서의 업그레이드 헤더 누락: 400이나 502가 반복되면
Upgrade/Connection헤더를 먼저 확인합니다. - 프록시 뒤 클라이언트 IP: 인증·레이트 리밋이 IP 기반이면
X-Forwarded-For처리(UseForwardedHeaders)를 함께 설정합니다.
정리
AddStackExchangeRedis로 백플레인을 붙이면 인스턴스 간 브로드캐스트가 Redis를 거쳐 전달됩니다. 로드 밸런서에서는 웹소켓 고정과 업그레이드 헤더, 넉넉한 read timeout을 함께 맞추고, 채널 프리픽스와 연결 파라미터를 서비스 단위로 고정하면 스케일아웃 후에도 실시간 기능이 유지됩니다.'Microsoft .NET > ASP.Net & ASP.Net Core' 카테고리의 다른 글
[ASP.NET Core] Serilog 구조화 로깅(Structured Logging)과 Seq 연동 실무 세팅 (0) 2026.11.11 [.NET 11] Native AOT 기본 활성화와 컨테이너 이미지 초경량화 세팅 (0) 2026.11.11 [.NET 11] .NET 11 GA 정식 릴리즈 핵심 변경점과 실무 마이그레이션 가이드 (0) 2026.11.11 [ASP.NET Core] Swagger UI에 JWT 인증(Authorize) 추가하기 (0) 2026.11.10 [ASP.NET Core] CORS 오류 "blocked by CORS policy" 원인과 설정 (0) 2026.11.10