본문 바로가기

Project/기록

K8s 환경의 실시간 소켓 서버 최적화: 스케일아웃 이슈와 백엔드/인프라 설계 전략

상태를 가지는(Stateful) 연결과 스케일아웃의 본질적 한계

쿠버네티스(K8s) 환경에서 REST API와 같은 Stateless 애플리케이션은 HPA(Horizontal Pod Autoscaler)를 통한 수평 확장이 자연스럽게 이루어집니다. 하지만 웹소켓(WebSocket) 기반의 실시간 서버는 영구적인 TCP 연결을 유지해야 하는 Stateful 특성상, 인프라 확장에 따른 트래픽 분산이 의도대로 동작하지 않는 경우가 많습니다.

특히 서버 배포나 파드 축소 시 수만 개의 커넥션이 일시에 끊어지고 재접속되는 현상은 시스템 전체를 마비시킬 수 있는 가장 큰 리스크입니다. 본 글에서는 백엔드와 데브옵스의 관점에서 K8s 기반 Centrifugo 소켓 서버를 안정적으로 운영하기 위한 커넥션 관리, 자원 효율화, 그리고 L4/L7 라우팅 최적화 전략을 정리합니다.


대규모 재접속(Thundering Herd) 방어와 Graceful Shutdown

소켓 서버 운영 시 가장 경계해야 할 상황은 파드(Pod)가 종료될 때 기존에 연결되어 있던 수많은 클라이언트가 한 번에 끊어지며 동시에 재접속을 시도하는 현상입니다. 이를 방어하기 위해 K8s 인프라 레벨에서 커넥션 드레이닝(Connection Draining)을 구현했습니다.

  • 안전한 종료 유예 (terminationGracePeriodSeconds): 파드 종료 시 즉각적으로 프로세스를 죽이지 않고 30초의 유예 시간을 부여했습니다. 이 시간 동안 서버는 클라이언트들에게 순차적으로 종료 프레임(Close Frame)을 전송하여, 재접속 요청이 한 번에 몰리지 않고 완만하게 분산되도록 유도합니다.
  • 상태 저장소(Redis) 보호: 대규모 트래픽 환경에서 채팅/알림 이력이 무한정 쌓여 Redis 메모리가 고갈(OOM)되는 것을 막기 위해 보관 기준을 엄격히 제한했습니다.
# 1. 고가용성 유지 및 안전한 파드 종료 설정
replicaCount: 3

podDistruptionBudget:
  minAvailable: 2 # 배포 및 노드 장애 시에도 최소 2대의 파드 가용성 보장

terminationGracePeriodSeconds: 30 # 커넥션 드레이닝을 위한 종료 유예

config:
  engine:
    type: "redis" # 상태 관리를 중앙화하여 Stateless 파드 구성
  channel:
    namespaces:
      - name: "user"
        history_size: 10   # Redis OOM 방지: 채널당 보관 메시지 수 제한
        history_ttl: "600s" # Redis OOM 방지: 10분 후 자동 만료

인프라 자원 최적화: Bin-packing과 Anti-Affinity

새로운 서버 컴포넌트를 추가할 때, 불필요한 EC2 워커 노드 증설을 막고 기존 클러스터의 가용 자원을 최대한 활용하도록 스케줄링 전략을 구성했습니다.

  • 초소형 리소스 할당 (Bin-packing): Centrifugo 파드의 자원 요청량(Requests)을 CPU 100m, Memory 256Mi로 설정했습니다. 실제 노드 상태 모니터링 결과 기존 워커 노드들의 CPU 예약률은 40~50% 수준이었으며, 이 잉여 공간에 파드가 배치되므로 추가적인 인프라 비용이 발생하지 않습니다.
  • SPOF(단일 장애점) 방지: 특정 물리 노드가 다운되었을 때 소켓 서비스 전체가 중단되는 것을 막기 위해 podAntiAffinity를 적용했습니다. 이를 통해 파드들이 동일한 EC2 노드에 중복 배포되지 않고 고르게 분산되도록 강제했습니다.
# 자원 할당량(Bin-packing) 및 분산 배치(Anti-Affinity) 설정
resources:
  requests:
    cpu: "100m"
    memory: "256Mi" # 기존 워커 노드의 잉여 자원에 안착 가능한 초소형 리소스
  limits:
    memory: "1Gi"

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                  - centrifugo
          topologyKey: kubernetes.io/hostname # 동일 물리 노드(EC2) 중복 배치 방지

네트워크 중간 장비의 연결 해제 방어 및 보안 (Zero-Trust)

클라우드 환경의 로드밸런서(ALB)나 CDN을 거치는 웹소켓 연결은, 데이터가 흐르지 않으면 유휴 시간 초과(Idle Timeout, 보통 60초)로 인해 네트워크 장비가 임의로 TCP 연결을 끊어버리는 이슈가 발생합니다.

  • Ping/Pong 주기를 통한 Timeout 방어: 서버 설정에 ping_interval: "20s"를 명시하여 20초마다 가벼운 패킷을 주고받도록 했습니다. 이는 ALB의 60초 타이머를 지속적으로 리셋하여 비정상적인 연결 종료를 방지합니다. 반대로 실제 응답이 없는 좀비 커넥션은 즉시 메모리에서 회수됩니다.
  • 인가되지 않은 연결 차단: jwks_public_endpoint를 연동하여 올바른 JWT를 가진 요청만 소켓 레이어에서 수락합니다.
  • 단방향 통신 강제: allow_publish_for_client: false 설정을 통해 클라이언트가 서버로 메시지를 발행하지 못하고 오직 '구독'만 가능하게 제어하여, 악의적인 봇을 통한 스팸 공격을 원천 차단했습니다.
# 네트워크 타임아웃 방어 및 Zero-Trust 보안 설정
config:
  client:
    allowed_origins: ["*"] 
    ping_interval: "20s" # ALB/CDN의 60초 Idle Timeout 차단 및 좀비 커넥션 회수
    token:
      # 인증 서버의 JWKS를 연동하여 올바른 JWT를 가진 요청만 소켓 레이어에서 수락
      jwks_public_endpoint: "http://production-auto-deploy.memex-backend-auth-api.svc.cluster.local:8080/.well-known/jwks.json"
      issuer: "memex-auth-api"

  channel:
    namespaces:
      - name: "public"
        allow_subscribe_for_client: true
        allow_publish_for_client: false # 클라이언트의 직접 발행(스팸 공격) 원천 차단
        allow_presence_for_client: true
        history_size: 50
        history_ttl: "3600s"