GN⁺: 기본으로 돌아가기: 웹소켓 대신 롱폴링을 선택한 이유
(inferable.ai)- 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 등의 재연결 처리에 대한 큰 비용때문에 숏폴링이 일반적으로 더 안전하다는 이야기가 있어서 숏폴링을 선택하긴 했는데.. 😭
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를 선택한 스타트업에서 일했었고, 호텔과 레스토랑 와이파이에서 테스트가 어려웠음