기본으로 돌아가기: 웹소켓 대신 롱폴링을 선택한 이유

13 hours ago 3

  • Node.js/TypeScript 기반 백엔드에서 대규모 실시간 업데이트를 처리해야 하는 상황이었음
  • PostgreSQL을 백엔드로 사용하여 수백 개의 워커 노드가 새로운 작업을 지속적으로 확인하고, 에이전트가 실행 및 채팅 상태 업데이트를 받아야 함
  • 웹소켓에 대한 탐구로 시작했지만, 놀랍도록 효과적인 '구식' 솔루션으로 귀결
    → "Postgres를 사용한 HTTP Long Polling"

문제 상황: 대규모 실시간 업데이트

  • 워커 노드 업데이트 :
    • Node.js/Golang/C# SDK를 실행하는 수백 개의 워커 노드가 있음
    • 새로운 작업이 제공되는 즉시 이를 알아야 했기 때문에 Postgres 데이터베이스를 다운시키지 않는 쿼리 전략이 필요
  • 에이전트 상태 동기화 :
    • 에이전트는 실행 및 채팅 상태에 대한 실시간 업데이트가 필요했고, 이를 효율적으로 스트리밍해야 함

롱 폴링과 WebSocket 비교

  • 숏 폴링은 시간표에 따라 엄격하게 출발하는 기차와 같아서 승객이 있는지 여부에 관계없이 정해진 간격으로 출발함
  • 롱 폴링은 서버가 응답을 기다리다 데이터가 생기면 바로 반환하고, 일정 시간이 지나면 타임아웃으로 응답을 돌려줌
    • 즉, “기다리다가 데이터가 생기면 출발”하는 기차와 같음. 특정 시간(TTL)내에 승객이 나타나지 않을때만 비어 있는 상태로 출발
    • 데이터(승객)가 있을 때는 즉시 출발하고 없을 때는 리소스를 효율적으로 사용할 수 있는 두 가지 장점을 모두 제공
  • WebSocket은 연결을 상시 유지해 양방향으로 데이터를 주고받는 방식임
    • 조직 환경, 인프라, 파이어월 문제 등으로 WebSocket 구성보다 롱 폴링이 더 단순하고 호환성 높음

롱 폴링 구현 세부 내용

  • getJobStatusSync 함수가 중요한 역할을 담당함
    • jobId, owner, ttl 등의 파라미터를 받아 특정 작업 상태를 특정 시간 동안 반복 조회함
  • 다음 조건 중 하나가 충족될 때까지 반복 조회를 수행함
    • 작업 상태가 success 또는 failure가 됨
    • ttl(타임아웃) 경과
  • 500ms 간격으로 데이터베이스를 조회하고, 결과가 확정되지 않았으면 기다렸다가 다시 조회함
  • 타임아웃 초과 시 에러를 던지고, 성공 시 결과를 반환함

데이터베이스 최적화

  • Postgres에 적절한 인덱스를 두어 조회 비용을 최소화함
  • 예: CREATE INDEX idx_jobs_status ON jobs(id, cluster_id);

롱 폴링의 이점

  • 모니터링 유지 용이성 : 기존 HTTP 기반 로깅, 모니터링 스택을 그대로 활용 가능함
  • 인증 단순성 : 새 인증 방식을 구현할 필요 없이 기존 HTTP 인증을 그대로 사용 가능함
  • 인프라 호환성 : 파이어월이나 로드 밸런서에 별도 설정이 필요 없고, 일반 HTTP 트래픽으로 취급됨
  • 운영 단순성 : 서버 재시작 시에도 연결 상태를 별도로 처리할 필요가 없고, 디버깅이 용이함
  • 클라이언트 구현 간편성 : 표준 HTTP 요청-응답 구조에 재시도 로직만 추가하면 동작 가능함

ElectricSQL과의 비교

  • ElectricSQL은 Postgres 데이터를 프론트엔드와 동기화하는 솔루션임
  • WebSocket 대신 HTTP를 쓰면서도 실시간성을 보장해주는 구조를 갖추고 있음
  • 실제로 실시간 업데이트를 처리하기 위해 극단적인 제어나 낮은 수준의 구조가 필요하지 않은 경우 ElectricSQL을 권장

우리가 Raw Long Polling을 선택한 이유

  • 메시지 전달 메커니즘은 단순한 구현 세부사항이 아니라 제품의 핵심 요소
  • 핵심 기능을 타사 라이브러리에 의존할 수 없음 (아무리 우수한 라이브러리라도)
  • 요구사항
    • 핵심 제품 제어 : 메시지 전달 메커니즘을 완전히 제어해야 함. 인프라 수준이 아니라 제품 자체임
    • 외부 의존성 제거 : 셀프 호스팅을 단순화하기 위해 외부 의존성을 최소화
    • 저수준 제어 : 폴링 메커니즘 및 연결 관리를 직접 제어
    • 최대 제어 가능성 : 동적 폴링 간격 구현 등 세부사항을 세밀하게 조정할 수 있어야 함
    • 코드 단순성 : 사용자들이 코드베이스를 쉽게 이해하고 수정할 수 있도록 간단하게 설계
  • 결론적으로 간단한 HTTP Long Polling 구현을 선택함으로써 직접 제어단순성을 확보

롱 폴링 구현 시 주의사항

  • TTL 설정 : 서버 쪽에서 반드시 최대 TTL을 강제하고, 클라이언트가 요청한 TTL이 이를 넘지 않도록 처리함
  • 인프라 타임아웃 고려 : 로드 밸런서, 엣지 서버, 프록시 등의 타임아웃 설정보다 충분히 짧은 TTL이어야 함
  • DB 폴링 간격 : 500ms 정도로 딜레이를 주어 DB 부하를 줄임
  • 백오프 전략(옵션) : 점진적으로 폴링 간격을 늘리는 방식으로 시스템 자원을 더 효율적으로 사용 가능함

WebSocket을 고려해야 할 상황

  • WebSocket 자체가 잘못된 것은 아니며, 다른 측면에서는 유용함
    • 상태가 많은 연결을 모니터링하고, 복잡한 이벤트를 상시 주고받아야 하는 경우
    • 인증, 인프라, 관측 문제를 해결할 리소스와 시간이 충분한 경우
  • 운영 및 로깅, 재연결 처리, 인증 메커니즘 등을 직접 구축해야 하는 복잡성이 존재함

WebSockets: 또 다른 선택지에 대한 이야기

  • Long Polling이 우리의 요구에 적합했지만, WebSockets도 충분히 고려할 가치가 있음
  • WebSockets 자체가 나쁜 것은 아니며, 많은 주의와 관리가 필요할 뿐
  • WebSockets의 주요 과제와 해결 방향
    • 가시성 : WebSockets는 상태 기반이므로, 지속적인 연결에 대한 로깅과 모니터링 추가 필요
    • 인증 : WebSocket 연결을 위한 새로운 인증 메커니즘 구현 필요
    • 인프라 : WebSocket을 지원하기 위해 로드 밸런서, 방화벽 등의 인프라를 적절히 구성해야 함
    • 운영 관리 : WebSocket 연결 및 재연결 관리. 연결 타임아웃 및 오류 처리
    • 클라이언트 구현 : 클라이언트 측 WebSocket 라이브러리 구현. 재연결 및 상태 관리 기능 포함

Read Entire Article