14P by neo 6일전 | ★ 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 라이브러리 구현. 재연결 및 상태 관리 기능 포함

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

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

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

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를 써보고 싶네요.