
Redis + Socket.IO + Docker + fetchSockets() Timeout 이슈
배경
새로운 회사에 이직하며 프론트엔드 보다 백엔드에 집중할 수 있는 기회가 생겼습니다.
자주 사용하던 Spring 이 아닌 평소 흥미를 가지고 있던 Typescript 기반의 nestjs를 도입,진행하며 다시금 코딩의 재미를 쌓아가던 중 발생한 이슈가 있어 이를 기록하고자 합니다.
최근 프로젝트에서 웹소켓 요구가 생기면서, 이미 운영 중인 백엔드 서버를 Auto Scaling을 비롯한 다중화 환경에서 안정적으로 운영해야 하는 상황이 있었습니다.
더불어 구현해놓은 웹소켓 서비스 또한 일반 HTTP 요청과 달리 다중화 환경에선 서버 간 세션 공유가 필요하기 때문에, 공식 문서를 참고하여 Socket.IO + Redis adapter를 통한 Pub/Sub 기반 세션 공유 방안을 적용하였습니다.
레디스 관련 설정을 진행하는 과정에서 처음엔 분명 캐시나 일반 Redis 명령어(get, set)는 모두 정상적으로 동작했습니다.
(cache-manager의 cache store를 redis로 설정해서 쓰고 있습니다. )
그러나 Socket.IO의 핵심 기능 중 하나인 fetchSockets()를 통해 방에 있는 소켓 목록을 가져오려고 하면, timeout이 걸리는 이슈가 발생하였습니다.
이슈
사실 로컬 개발 환경에서는 아무 문제 없이 동작했기 때문에 처음에는 코드 문제로 생각했지만, 아무리 보고 또 봐도 코드의 문제는 아니라는 생각이 들어 이번엔 다양한 환경에서 테스트를 진행하게 되었습니다.
사내던 집이건 Proxmox 을 통한 VM/컨테이너는 이젠 제겐 필수입니다.
| 백엔드 APP | REDIS | 결과 |
| 로컬 IDE (env: 개발) | 일반 설치 (Proxmox 컨테이너) | 이상 없음 |
| 도커 배포 (env: 개발, Proxmox 컨테이너) | 일반 설치 (Proxmox 컨테이너) | fetchSockets 함수 Timeout 발생 |
| 도커 배포 (env: 스테이징, Aws EC2) | 도커 배포 (Aws EC2 내 docker-compose) | fetchSockets 함수 Timeout 발생 |
| 로컬 IDE (env: 스테이징) | 도커 배포 (Aws EC2 내 docker-compose) | fetchSockets 함수 Timeout 발생 |
| 일반 배포 (env: 개발, Proxmox 컨테이너) | 일반 설치 (Proxmox 컨테이너) | 이상 없음 |
| 일반 배포 (env: 개발, Proxmox 컨테이너) | 도커 배포 (Proxmox 컨테이너) | fetchSockets 함수 Timeout 발생 |
위 표에 따르면 이건 도커의 문제로 보였습니다.
가장 의심이 가는건 도커 자체보다는, Docker 네트워크 모드로 인한 문제로 보였습니다.
제 경우 일반적으로 도커의 네트워크는 브릿지로 사용하고 있었는데 별도의 설정을 하지 않고 사용하고 있었기 때문에 기본설정인 브릿지 네트워크가 사용되고 있었습니다.
일반 TCP 요청은 브릿지 네트워크에서도 잘 동작하지만, Socket.IO Redis adapter가 사용하는 Pub/Sub 브로드캐스트 메시지는 브릿지 네트워크에서 제대로 전달되지 않았음을 확인했습니다.
즉, fetchSockets()는 Redis Pub/Sub를 이용해 “다른 노드의 소켓 정보”를 조회하는데, 브릿지 모드에서는 이 브로드캐스트 메시지가 컨테이너 밖으로 나가거나 돌아오는 과정에서 전달되지 않았던 것이죠.
Docker 브릿지 네트워크 동작 방식
기본 구조
컨테이너가 생성되면 보통 docker0라는 가상 브릿지 인터페이스에 연결되며 컨테이너는 이더넷 가상(virtual ethernet, veth) 인터페이스를 통해 172.17.0.0/16 대역과 같이 프라이빗 IP를 받습니다. 아이피는 바꿀 수 있지만 보통 저 대역 아이피로 부여받는 것으로 확인됩니다.
이때 외부 네트워크와 통신할 때는 호스트 iptables의 NAT(MASQUERADE) 규칙을 통해 패킷이 변환되어 나갑니다.
컨테이너에서 외부 네트워크와 통신할 때는 docker0 브릿지 -> iptables의 NAT(MASQUERADE) 규칙 -> 호스트의 실제 네트워크 인터페이스(eth0 등)를 거쳐 외부로 전달됩니다.
# 마스커레이드(Masquerade)
하나의 공인 IP 주소를 여러 사설 IP 주소에 할당하여 내부 네트워크의 여러 장치가 외부 네트워크에 동시에 접속할 수 있도록 하는 네트워킹 기술으로 Docker가 브릿지 네트워크에서 외부 통신을 가능하게 해주는 핵심이 IP 마스커레이드(MASQUERADE) 규칙 입니다.
NAT의 한 방식으로, 여러 사설 IP가 하나의 공인 IP를 공유해서 외부로 나가는 것을 뜻 합니다.
도커 컨테이너에 브릿지 네트워크 사용 시에 iptables에 아래와 같은 규칙을 추가합니다.
# 사설망(172.17.0.0/16) → 외부로 나갈 때 공인 IP로 변환
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
Pub/Sub 문제가 되는 이유는 무엇일까?
fetchSockets()는 socket.io-redis adapter가 Redis Pub/Sub 채널을 이용해 동작합니다.
위에서도 말씀드렸듯 Redis의 일반적인 요청/응답(Get/Set)은 NAT 환경에서도 문제없이 동작하지만, Pub/Sub은 성격이 조금 다릅니다.
Pub/Sub은 Redis 서버가 모든 구독자와 맺은 TCP 세션을 유지하면서 이벤트를 Push 하는 방식입니다.
이처럼 지속적인 세션 스트림이 필요한 경우, 브릿지 네트워크와 NAT 경로를 거치는 과정에서 세션이 정상적으로 유지되지 않거나 이벤트 전달이 기대대로 이루어지지 않는 문제가 발생할 수 있었다고 보고 있습니다.
즉, 브릿지 네트워크의 NAT 구조 때문에 컨테이너 내부에서 Redis 구독 채널의 콜백 이벤트가 올바르게 전달되지 못한 것으로 추측됩니다.
사실 정답인진 모르겠습니다.
해결 방법
네트워크 모드를 host로 변경
docker (예시)
# 포트 바인딩 제거 후, 네트워크 host 명시
docker run -d \
--network host \
--name ${APP_NAME} \
-e NODE_ENV=${CURRENT_ENV} \
-v /root/mw_env/${ENV_NAME}:/app/${ENV_NAME} \
${APP_NAME}:latest
docker-compose (예시)
services:
mysql:
image: mysql:8.4.6
environment:
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: test1@3#
MYSQL_USER: test
MYSQL_PASSWORD: test1@3#
TZ: Asia/Seoul
network_mode: host # 포트 바인딩 제거, host 모드 명시
volumes:
- ./database/mysql/data:/var/lib/mysql
- ./database/mysql/init:/docker-entrypoint-initdb.d
- ./database/mysql/my.cnf:/etc/my.cnf
restart: unless-stopped
redis:
image: redis:7
container_name: redis
network_mode: host # 포트 바인딩 제거, host 모드 명시
command: redis-server --requirepass test1@3#
restart: unless-stopped'이슈' 카테고리의 다른 글
| GCP 의문의 오류, 코드가 아니라 결제가 문제 (0) | 2025.09.26 |
|---|---|
| OpenAI API Structured model outputs 사용시 zod 관련 이슈 (0) | 2025.09.05 |
| 9600X 9700X 5070TI 등 장치 인식 불가 블랙스크린 현상 (3) | 2025.04.18 |
| Nvidia CUDA toolkit 설치 불가 현상 및 ComfyUI 작업 불가 해결 방안 (0) | 2025.04.15 |