본문 바로가기
C.W.K.
Stream
Lesson 06 of 07 · published

오류 복구와 우아한 배포

~12 min · production, deploy, draining

Level 0Poller
0 XP0/60 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

연결 비우기

배포 중 새 서버는 연결 0개로 시작하지만 기존 서버에는 수천 개가 남아 있어. 기존 서버를 바로 죽이면 모두 코드 1006 으로 끊겨. 우아한 방식은 클라이언트에 재연결해 새 서버로 가라고 알리고, 스스로 끊을 시간을 잠깐 준 뒤, 남은 연결을 1001 ‘going away’ 코드로 닫는 거야. 트랙 4 레슨 5 의 cwkPippa lifespan 예시가 바로 이 패턴이야.

클라이언트 메시지 버퍼링

짧은 연결 단절 사이에도 클라이언트는 메시지를 보내려 할 수 있어. 견고한 클라이언트는 이를 로컬 버퍼에 넣었다가 다시 연결하면 비워. 트랙 5 의 ACK 패턴을 로컬에 적용한 셈이야. OPEN 이 아닐 때 대기열에 넣고 다음 OPEN 에 보내면 배포 때문에 사용자가 입력한 메시지를 잃지 않아.

순차 배포

고정 세션을 쓰는 부하 분산기 뒤에서 서버를 한 번에 하나씩 내려. 그 서버의 연결이 나머지 서버로 다시 연결되면 다음 서버에서 반복해. 전체 서비스 중단은 0이고 사용자마다 잠깐 재연결만 겪어.

Code

서버: SIGTERM 에서 우아하게 연결 비우기·python
from contextlib import asynccontextmanager
import asyncio, signal

shutdown_event = asyncio.Event()

@asynccontextmanager
async def lifespan(app):
    # Trap SIGTERM so we can drain instead of crashing.
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGTERM, signal.SIGINT):
        loop.add_signal_handler(sig, shutdown_event.set)
    yield
    # Drain phase
    log.info('draining %d connections', manager.total_connections)
    for room in list(manager.rooms):
        await manager.broadcast(room, {
            'type': 'system.reconnect',
            'data': {'reason': 'deploy'},
        })
    await asyncio.sleep(2.0)  # let clients reconnect on their own
    close_tasks = [
        ws.close(code=1001, reason='deploy')
        for ws in list(manager.ws_meta)
    ]
    await asyncio.gather(*close_tasks, return_exceptions=True)
    log.info('drain complete')

app = FastAPI(lifespan=lifespan)
클라이언트: 버퍼링한 전송·javascript
class BufferedSocket {
  constructor(url) {
    this.url = url;
    this.buffer = [];
    this._connect();
  }
  _connect() {
    this.ws = new WebSocket(this.url);
    this.ws.addEventListener('open', () => {
      while (this.buffer.length) this.ws.send(this.buffer.shift());
    });
    this.ws.addEventListener('close', () => {
      // schedule reconnect (Track 2 backoff pattern)
      setTimeout(() => this._connect(), 1_000);
    });
  }
  send(msg) {
    const wire = JSON.stringify(msg);
    if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(wire);
    else this.buffer.push(wire);
  }
}

External links

Exercise

lifespan 연결 비우기와 BufferedSocket 을 구현해. 시험 클라이언트 세 개를 연결한 채 kill -TERM <pid> 로 배포를 일으켜. (a) 클라이언트가 system.reconnect 를 받고, (b) 버퍼링한 메시지가 재연결 뒤 전송되며, (c) 새 서버가 약 2초 안에 재연결을 받고, (d) 사용자에게 미치는 전체 영향이 1초 미만인지 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.