14P by GN⁺ 5달전 | ★ favorite | 댓글 5개
  • 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 라이브러리 구현. 재연결 및 상태 관리 기능 포함

ML 모델 서빙에 여기서 말하는 "숏폴링" 구조를 사용하고 있는데 뭐가 효율적일지 고민이 많네요. 나름대로 여기 저기 알아본 바로는 웹소켓이나 SSE 등의 재연결 처리에 대한 큰 비용때문에 숏폴링이 일반적으로 더 안전하다는 이야기가 있어서 숏폴링을 선택하긴 했는데.. 😭

Hacker News 의견
  • Long polling은 자체적인 문제를 가지고 있음

    • Second Life는 클라이언트와 서버 간에 HTTPS long polling 채널을 사용함
    • 클라이언트 측에서는 libcurl을 사용하며, 타임아웃이 발생할 수 있음
    • 서버가 타임아웃과 다음 요청 사이에 메시지를 보내려 하면 경합 조건이 발생하여 메시지가 손실될 수 있음
    • Apache 서버가 앞단에 위치하여 불필요한 요청을 차단하지만, 타임아웃이 발생할 수 있음
    • 중간 박스와 프록시 서버가 long polling을 싫어할 수 있음
    • HTTP 연결을 오래 유지하는 것을 싫어하는 요소들이 많음
    • 결과적으로 신뢰할 수 없는 메시지 채널이 되어 중복을 감지하기 위해 시퀀스 번호가 필요하고 메시지를 잃을 수 있음
    • 원래 기사에서 "loop"로 표시된 차트 섹션은 타임아웃 처리를 언급하지 않음
    • long polling을 사용할 경우 몇 초마다 데이터를 보내 연결을 유지해야 함
  • Phoenix와 LiveView를 매일 사용하는 것이 기쁨

    • WebSockets를 사용하여 신경 쓸 필요가 없음
  • 서버 전송 이벤트(SSE)를 사용하는 것보다 기술적인 이점이 있는지 궁금함

    • 둘 다 HTTP 연결을 열어두고 간단한 HTTP라는 장점이 있음
    • SSE는 업데이트나 결과를 스트리밍할 수 있는 경우에 더 적합해 보임
    • 적합한 사용 사례는 특정 클라이언트를 대신하여 모든 작업 ID를 모니터링하는 경우일 수 있음
  • 이 기사는 "Websocket"과 "Long-polling"을 독립적인 결정으로 연결하고 있음

    • long-polling 서버는 약간의 추가 작업으로 websocket 클라이언트를 처리할 수 있음
    • 기존 아키텍처가 websocket인 경우 long-polling 클라이언트를 지원하려면 두 개의 서버 계층이 필요함
  • Node.js에서 setTimeout을 사용하는 더 쉬운 방법

    • import { setTimeout } from "node:timers/promises"; await setTimeout(500); 사용
  • long polling을 좋아함, 이해하기 쉽고 클라이언트 관점에서 매우 느린 연결처럼 작동함

    • 재시도와 클라이언트 측 취소된 연결을 추적해야 함
    • 코드 예제에서 반복적으로 데이터를 쿼리하는 루프가 어색해 보임
  • 서버 전송 이벤트나 WebSockets가 long polling의 모든 사용 사례를 대체하지 못함

    • SSE의 연결 제한이 자주 문제로 등장함
    • WebSockets는 대부분의 환경에서 신뢰할 수 없음
    • 백엔드에서 변경 사항을 감지하고 적절한 클라이언트로 전파하는 문제는 여전히 해결되지 않음
  • Postgres의 비동기 알림 기능을 사용하는 것이 좋음

    • 서버가 채널을 LISTEN하고 데이터 변경 시 PG가 TRIGGER 및 NOTIFY할 수 있음
  • 짧은 타임아웃과 우아하게 종료된 요청을 가진 long polling의 의미가 여전히 있는지 모르겠음

    • HTTP/2나 QUIC이 사용되지 않는 경우 이 트릭이 여전히 의미가 있을 수 있음
  • WebSockets의 상대적으로 간단한 대안을 상기시키는 것이 상쾌함

    • WebSockets를 선택한 스타트업에서 일했었고, 호텔과 레스토랑 와이파이에서 테스트가 어려웠음

Elixir, Phoenix framework, LiveView를 통해서 WebSockets를 써보고 싶네요.

Long polling 은 좀 hacky 하게 느껴져서 꺼려지는 것 같네요. 브라우저에선 아마 계속 요청이 완료되지 않은 것으로 뜰 것같고요. 종종 로딩이 끝나지 않는 사이트들이 있던데 저는 컨텐츠가 전부 로드되지 못한건가? 싶어서 별로더라구요.
어플리케이션에서도 결국 어느부분에 hang 을 걸고 응답을 대기하는 상태가 될텐데,, 좀 어색하게 보이네요.

"에이전트가 실행 및 채팅 상태 업데이트를 받아야 함"
이거보고 바로 sse를 떠올렸는데 역시 해커뉴스 의견에서 sse 언급이 많군요.