가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 · 2장

주변 친구

1장의 근접성 서비스는 사업장이 거의 안 움직였지만, 이번엔 친구가 계속 움직인다. 그래서 “검색”이 아니라 “실시간 전달” 문제가 되고, 그 중심에 웹소켓과 레디스 펍/섭이 있다.

레디스 펍/섭유상태 서버

요구사항과 숫자

항목값 (책의 가정)
기능 사용자(DAU)1억 명
동시 접속자DAU의 10% = 1,000만 명
평균 친구 수400명
위치 갱신 QPS1,000만 ÷ 30초 ≈ 약 33만 4천
감 잡기초당 33만 건의 위치 갱신이 들어오고, 각 갱신은 그 사람의 “온라인 친구들”에게 퍼져야 한다. 이 팬아웃이 이 장의 진짜 부하다.

전체 구조

컴포넌트역할
API 서버 무상태친구 추가/삭제, 프로필 수정 같은 일반 요청. 오토스케일 쉬움.
웹소켓 서버 유상태클라이언트와 연결을 유지하며 위치 갱신을 주고받는다.
위치 캐시 (레디스)활성 사용자의 가장 최근 위치만 보관. TTL로 비활성 사용자 자동 제거.
사용자 DB프로필, 친구 관계.
위치 이력 DB (카산드라 등)쓰기가 매우 많으므로 쓰기에 강한 DB. 사용자 ID로 샤딩.
레디스 펍/섭가벼운 메시지 버스. 사용자 1명 = 채널 1개. 친구들이 그 채널을 구독.

펍/섭을 한 문장으로

민지가 움직이면 민지 채널에 새 위치를 발행한다. 민지 채널을 구독하고 있는 건 민지 친구들의 웹소켓 연결 핸들러. 레디스는 받은 메시지를 구독자에게 뿌리고 끝 — 메시지를 저장하지 않는다.

위치 정보 캐시

key:   user:1024
value: { lat: 37.4979, lng: 127.0276, timestamp: 1758000000 }
TTL:   10분 (갱신될 때마다 연장)

DB 대신 레디스를 쓰는 이유

영속성이 필요 없다

레디스 인스턴스가 죽으면 빈 인스턴스로 바꿔 끼우면 된다. 30초마다 갱신이 들어오니 캐시는 금방 다시 채워진다. 그 사이 갱신 주기 한두 번 동안 친구 위치 변경을 놓칠 수 있지만, 수용 가능한 트레이드오프다.

캐시 규모

동시 접속 1,000만 명 × 사용자당 약 100바이트면 메모리는 서버 한 대로 충분하다. 문제는 초당 33만 건 쓰기라 서버 한 대로는 버겁다는 것. 그래서 사용자 ID 기준으로 샤딩하고, 샤드마다 대기(standby) 복제본을 둔다.

위치 갱신 흐름

  1. 클라이언트가 위치를 보내면 로드밸런서가 이미 연결된 웹소켓 서버로 넘긴다.
  2. 웹소켓 서버가 위치 이력 DB에 저장한다.
  3. 위치 캐시를 갱신하고(TTL 연장), 연결 핸들러 변수에도 내 위치를 보관한다. 나중에 거리 계산에 쓴다.
  4. 내 채널에 새 위치를 발행한다.
  5. 레디스 펍/섭이 내 채널 구독자 전원(= 온라인 친구들의 연결 핸들러)에게 전달한다.
  6. 각 구독자 핸들러가 “새 위치 ↔ 자기가 보관한 내 친구 위치” 거리를 계산한다.
  7. 반경 안이면 친구 클라이언트로 전송, 밖이면 버린다.

위치 갱신 따라가 보기

사람을 누르면 그 사람이 위치를 발행한다. 끌어서 옮기면 옮긴 위치로 다시 발행한다. 빈 동그라미는 오프라인 — 웹소켓 연결이 없으니 구독자도 아니다. 거리는 대칭이라 발행자 기준 원으로 표시했다.

    앱을 켰을 때 (클라이언트 초기화)

    웹소켓 연결을 맺고 첫 위치를 보내면 서버가 이 일을 한다.

    1. 위치 캐시에 내 위치 저장, 연결 핸들러 변수에도 저장.
    2. 사용자 DB에서 내 친구 목록(평균 400명)을 가져온다.
    3. 위치 캐시에 친구들 위치를 한 번에(batch) 요청한다. TTL이 지난 비활성 친구는 결과에 없다.
    4. 받은 위치마다 거리를 계산해 반경 안인 친구만 클라이언트에 보낸다.
    5. 친구 전원의 채널을 구독한다 — 온라인이든 아니든. 채널은 싸고, 발행이 없는 채널은 CPU를 쓰지 않는다.
    6. 내 채널에 현재 위치를 발행한다.

    친구 추가/삭제 시에는 클라이언트가 웹소켓 서버에 알리고, 서버가 그 친구 채널을 구독하거나 해지한다.

    웹소켓 서버 확장

    유상태 서버라 제거할 때 조심서버를 그냥 내리면 붙어 있던 연결이 한꺼번에 끊긴다.
    1. 로드밸런서에서 해당 서버를 드레인(draining) 상태로 표시 — 새 연결을 보내지 않는다.
    2. 기존 연결이 모두 닫힐 때까지(또는 충분히 오래) 기다린다.
    3. 그 뒤에 서버를 제거한다. 새 버전 배포도 같은 방식.

    비유

    문 닫을 식당이 “신규 손님 안 받음” 팻말을 걸고, 이미 앉은 손님이 다 먹고 나갈 때까지 기다리는 것.

    레디스 펍/섭은 몇 대가 필요한가

    메모리

    채널 1억 개 × 채널당 구독자 약 100명 × 구독자당 포인터 약 20바이트

    ≈ 200GB → 서버 2대

    CPU

    초당 갱신 33.4만 × 온라인 친구 40명(400명의 10%) = 초당 1,400만 건 전달. 서버 1대가 초당 약 10만 건 처리한다고 보면

    ≈ 140대

    결론병목은 메모리가 아니라 CPU다. 서버 한두 대로는 안 되고, 분산 클러스터가 필요하다.

    분산 레디스 펍/섭 클러스터

    채널을 여러 서버에 나눈다. 기준은 발행자의 사용자 ID. 어느 채널이 어느 서버에 있는지는 안정 해시(consistent hashing)로 정한다.

    서비스 탐색 — 주키퍼(ZooKeeper), etcd

    “지금 살아있는 펍/섭 서버 목록”과 해시 링 설정을 한곳에 두고, 바뀌면 구독자(웹소켓 서버)에게 알려주는 컴포넌트.

    /config/pub_sub_ring  →  ["p_1", "p_2", "p_3", "p_4"]
    1. 웹소켓 서버들은 이 해시 링을 구독하고 메모리에 캐시한다.
    2. 민지가 위치를 발행하려 하면, 해시 링에서 민지 채널 담당 서버(예: p_3)를 찾는다.
    3. p_3에 발행한다. 구독할 때도 같은 방식으로 서버를 찾는다.

    해시 링에 서버를 넣고 빼 보기

    바깥 점 = 채널(사용자 240명), 네모 = 펍/섭 서버. 채널은 시계 방향으로 처음 만나는 서버에 속한다. 테두리가 굵은 점이 방금 서버가 바뀐 채널이다.

    펍/섭 클러스터 규모 확장

    펍/섭은 메시지를 저장하지 않으니 무상태처럼 보이지만, “누가 어떤 채널을 구독 중인가”라는 상태를 들고 있다. 그래서 유상태 클러스터로 취급한다.

    서버 수를 바꾸면 벌어지는 일해시 링이 바뀌어 일부 채널이 다른 서버로 옮겨진다 → 그 채널의 구독자 전원이 새 서버로 재구독해야 한다 → 재구독 요청이 한꺼번에 몰리고, 그동안 일부 위치 갱신을 놓친다.

    장애 서버 교체는 상대적으로 덜 위험

    죽은 서버 자리를 대기 서버로 바꾸면 링 구조는 그대로라, 그 서버의 채널 구독자만 재구독하면 된다. 링 크기를 바꾸는 것보다 영향 범위가 훨씬 작다.

    친구가 아주 많은 사용자

    친구 수에 상한이 있고(예: 페이스북 5,000명) 친구 관계는 양방향이라, 팔로워 수백만인 셀럽 문제와는 다르다. 그런 사용자의 구독자들은 여러 웹소켓 서버에 흩어져 있어 부하도 분산된다.

    추가 — 친구가 아닌 주변 사용자도 보여주기

    친구 채널 대신 지오해시 격자별 채널을 만든다. 1장의 지오해시가 여기서 다시 나온다.

    1. 사용자는 자기 위치의 지오해시 채널을 구독한다.
    2. 격자 경계 문제 때문에 주변 8격자 채널까지, 총 9개를 구독한다.
    3. 위치가 바뀌면 자기가 속한 지오해시 채널에 발행 → 그 격자와 이웃 격자에 있는 사람들이 받는다.

    예시 — 강남역에 있는 사용자가 구독하는 채널

    geo:wydm67geo:wydm6egeo:wydm6g geo:wydm66geo:wydm6dgeo:wydm6f geo:wydm63geo:wydm69geo:wydm6c

    역삼역 쪽(wydm6f)으로 걸어가 격자를 넘으면, 구독 목록도 wydm6f 중심의 9개로 바꿔야 한다.

    대안 — 얼랭(Erlang)

    레디스 펍/섭 대신 얼랭/엘릭서로 구현할 수도 있다. 얼랭 프로세스는 수백 바이트 수준으로 가벼워서, 사용자 한 명을 프로세스 하나로 두고 서버 한 대에 수백만 개를 띄울 수 있다. 다만 얼랭 개발자를 구하기 어렵다는 현실적 제약이 있다.