Sandcorp샌드코프 · 웹·AI 제품 개발

새 프로젝트 받는 중 · 갱신 2026.11sandcorp.co.krGitHub

← 팁

성능웹소켓·실시간

같은 상태를 보는 연결은 한 프로세스로 모으고, 휘발성 이벤트는 묶어 보낸다

스티키 라우팅으로 같은 문서의 연결을 한 프로세스에 모으고, 프로세스 안에서는 메모리로 나눠 주고, 커서처럼 마지막 값만 중요한 이벤트는 모아 보낸다.

공동 편집 서버에서 같은 문서를 보는 연결들이 여러 백엔드 프로세스에 흩어지면, 연결마다 상태를 따로 들고 모든 변경이 브로커(Redis)를 거쳐 오간다. 사람이 늘수록 브로커 왕복과 중복 상태가 곱으로 늘고, 커서 움직임 하나하나가 메시지가 된다.

네 가지를 함께 쓴다.

  • 스티키 라우팅: 로드밸런서가 문서 ID(경로)를 해시해 같은 문서의 연결을 같은 백엔드로 보낸다. Caddy라면 lb_policy uri_hash.
  • 프로세스 안 전달: 프로세스 안에서는 문서당 세션 하나가 상태를 들고 연결들에 메모리로 나눠 준다. 브로커는 프로세스 사이에만 쓴다.
  • 휘발성 이벤트 묶기: 커서처럼 자주 오고 마지막 값만 중요한 이벤트는 50~100ms씩 모아 한 번에 보낸다.
  • 연결별 송신 큐: 느린 클라이언트 하나가 다른 연결의 송신을 막지 않게 한다.
import asyncio

class CursorBatcher:
    def __init__(self, broadcast, interval: float = 0.05) -> None:
        self.broadcast = broadcast
        self.interval = interval
        self.latest: dict[str, dict] = {}
        self.task: asyncio.Task | None = None

    def push(self, user_id: str, cursor: dict) -> None:
        self.latest[user_id] = cursor  # 사용자마다 마지막 값만 남긴다
        if self.task is None:
            self.task = asyncio.create_task(self._flush_later())

    async def _flush_later(self) -> None:
        await asyncio.sleep(self.interval)
        batch, self.latest, self.task = self.latest, {}, None
        await self.broadcast({"type": "cursors", "items": batch})

재배포 중에는 한 문서의 연결이 옛 프로세스와 새 프로세스에 잠시 나뉠 수 있다. 프로세스 사이의 브로커 중계 경로는 없애지 말고 남겨 둔다.

문의 →