[Book - 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 2. 주변 친구
앱 사용자 가운데 본인 위치 정보 접근 권한을 허락한 사용자에 한해 인근의 친구 목록을 보여주는 시스템이다.
1단계: 문제 이해 및 설계 범위 확정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
지리적으로 얼마나 가까워야 '주변에 있다'고 할 수 있나요?
>> 5마일
>> 이 수치는 설정 가능해야 함
실제 상황에서는 직선 이동이 힘들텐데, 거리는 두 사용자 사이의 직선거리라고 가정해도 될까요?
>> yes
얼마나 많은 사용자가 이 앱을 사용하나요?
10억 명을 가정하고, 그 가운데 10% 정도가 이 기능을 활용한다고 생각해도 될까요?
>> yes
사용자의 이동 이력을 보관해 두는 것이 좋을까요?
>> yes
친구 관계에 있는 사용자가 10분 이상 비활성 상태면 해당 사용자를 주변 친구 목록에서 사라지도록 해야 할까요?
아니면 마지막으로 확잉ㄴ한 위치를 표시하도록 할까요?
>> 사라지게
사생활 및 데잍어 보호법도 고려해야 할까요?
>> 풀이 과정이 복잡해질 수 있으니 생략
기능 요구사항
- 사용자는 모바일 앱에서 주변 친구를 확인할 수 있어야 한다.
- 주변 친구 목록에 보이는 각 항목에는 해당 친구까지의 거리, 그리고 해당 정보가 마지막으로 갱신된 시각이 함께 표시되어야 한다.
- 이 친구 목록은 몇 초마다 한 번씩 갱신되어야 한다.
비기능 요구사항
- 낮은 지연 시간
- 주변 친구의 위치 변화가 반영되는 데 너무 오랜 시간이 걸리지 않아야 한다.
- 안정성
- 시스템은 전반적으로 안정적이어야 하지만, 때로 몇 개 데이터가 유실되는 것 정도는 용인할 수 있다.
- 결과적 일관성
- 위치 데이터를 저장하기 위해 강한 일관성을 지원하는 데이터 저장소를 사용할 필요는 없다.
- 복제본의 데이터가 원본과 동일하게 변경되기까지 몇 초 정도 걸리는 것은 용인할 수 있다.
개략적 규모 추정
- ‘주변 친구’는 5마일(8km) 반경 이내 친구로 정의
- 친구 위치 정보는 30초 주기로 갱신
- 평균적으로 매일 주변 친구 검색 기능을 활용하는 사용자는 1억 명으로 가정
- 동시 접속 사용자의 수는 DAU 수의 10%로 가정
- 평균적으로 한 사용자는 400명의 친구를 갖는다고 가정하고, 그 모두가 주변 친구 검색 기능을 활용한다고 가정
- 이 기능을 제공하는 앱은 페이지당 20명의 주변 친구를 표시하고, 사용자의 요청이 있으면 더 많은 주변 친구를 보여 준다.
QPS 계산
QPS = 1억 DAU X 10% / 30초 =~ 334,000
2단계: 개략적 설계안 제시 및 동의 구하기
개략적 설계안
메시지를 효과적으로 전송하기 위해 백엔드는 어떤 역할을 해야할까?
- 모든 활성 상태 사용자의 위치 변화 내역을 수신한다.
- 사용자 위치 변경 내역을 수신할 때마다 해당 사용자의 모든 활성 상태 친구를 찾아서 그 친구들의 단말로 변경 내역을 전달한다.
- 두 사용자 사이의 거리가 특정 임계치보다 먼 경우에는 변경 내역을 전송하지 않는다.
여기에는 큰 규모에 적용하기 쉽지 않다는 문제가 있다.
주어진 문제의 가정은 활성 상태의 동시 접속 사용자가 천만 명 정도라는 것이다.
그 모두가 자기 위치 정보를 30초마다 갱신한다고 하면 초당 334,000번의 위치 정보 갱신을 처리해야 하고,
사용자 1명이 400명의 친구를 갖고 그 가운데 10%가 인근에서 활성화 상태라고 가정하면,
334,000 X 400 X 10% = 1400만 건의 위치 정보 갱신 요청을 처리해야 한다.
즉 엄청난 양의 갱신 내역을 사용자 단말로 보내야 한다.
설계안
우선 소규모 백엔드를 위한 개략적 설계안부터 만들어 보자.
로드밸런서
RESTful API 서버 및 양방향 유상태(stateful) 웹소켓 서버 앞단에 위치한다.
부하를 고르게 분산하기 위해 트래픽을 서버들에 배분하는 역할을 한다.
RESTful API 서버
무상태(stateless) API 서버의 클러스터로서, 통상적인 요청/응답 트래픽을 처리한다.
이 API 계층은 친구를 추가/삭제하거나 사용자 정보를 갱신하는 등의 부가적인 작업을 처리한다.
웹소켓 서버
친구 위치 정보 변경을 거의 실시간에 가깝게 처리하는 유상태 서버 클러스터다.
각 클라이언트는 그 가운데 한 대와 웹소켓 연결을 지속적으로 유지한다.
검색 반경 내 친구 위치가 변경되면 해당 내역은 이 연결을 통해 클라이언트로 전송된다.
주변 친구 기능을 이용하는 클라리언트의 초기화도 담당한다.
모바일 클라이언트가 시작되면, 온라인 상태인 모든 주변 친구 위치를 해당 클라이언트로 전송하는 역할을 하는 것이다.
레디스 위치 정보 캐시
Redis는 활성 상태 사용자의 가장 최근 위치 정보를 캐시하는 데 사용한다.
레디스에 보관하는 캐시 항목에는 TTL 필드가 있다.
이 기간이 지나면 해당 사용자는 비활성 상태로 바뀌고 그 위치 정보는 캐시에서 삭제된다.
캐시에 보관된 정보를 갱신할 때는 TTL도 갱신한다.
레디스가 아니라도 TTL을 지원하는 키-값 저장소는 캐시로 활용할 수 있다.
사용자 DB
사용자 데이터 및 사용자의 친구 관계 정보를 저장한다.
위치 이동 이력 DB
사용자의 위치 변동 이력을 보관한다.
Redis Pub/Sub 서버
Redis Pub/Sub은 초경량 메시지 버스로, 새로운 채널을 생성하는 것은 아주 값싼 연산이다.
본 설계안의 경우 웹소켓 서버를 통해 수신한 특정 사용자의 위치 정보 변경 이벤트는 해당 사용자에게 배정된 Pub/Sub 채널에 발행한다.
해당 사용자의 친구 각각과 연결된 웹소켓 ㅇ녀결 핸들러는 해당 채널의 구독자로 설정되어 있다.
따라서 특정 사용자의 위치가 바뀌면 해당 사용자의 모든 친구의 웹소켓 연결 핸들러가 호출된다.
각 핸들러는 위치 변경 이벤트를 수신할 친구가 활성 상태면 거리를 다시 계산한다.
새로 계산한 거리가 검색 반경 이내면 갱신된 위치와 갱신 시각을 웹소켓 연결을 통해 해당 친구의 클라이언트 앱으로 보낸다.
주기적 위치 갱신
모바일 클라이언트는 항구적으로 유지되는 웹소켓 연결을 통해 주기적으로 위치 벼경 내역을 전송한다.
- 모바일 클라이언트가 위치가 변경된 사실을 로드밸런서에 전송
- 로드밸런서는 그 위치 변경 내역을 해당 클라이언트와 웹소켓 서버 사이에 설정된 연결을 통해 웹소켓 서버로 전송
- 웹소켓 서버는 해당 이벤트를 위치 이동 이력 DB에 저장
- 웹소켓 서버는 새 위치를 위치 정보 캐시에 보관
- 웹소켓 서버는 Redis Pub/Sub 서버의 해당 사용자 채널에 새 위치를 발행 (3~5까지의 각 당계는 병렬로 수행)
- Redis Pub/Sub 채널에 발행된 새로운 위치 변경 이벤트는 모든 구독자에게 broadcast
- 메시지를 받은 웹소켓 서버, 즉 상기 웹소켓 연결 핸들러가 위치한 웹소켓 서버는 새 위치를 보낸 사용자와 메시지를 받은 사용자 사이의 거리를 새로 계산
- 7에서 계산한 거리가 검색 반경을 넘지 않는다면, 새 위치 밋 해당 위치로의 이동이 발생한 시각을 해당 구독자의 클라이언트 앱으로 전송
아래 사례를 통해 흐름을 다시 살펴보자.
- 사용자 1의 위치가 변경되면 그 변경 내역은 사용자 1과의 연결을 유지하고 있는 웹소켓 서버에 전송
- 해당 변경 내역은 Redis Pub/Sub 서버 내의 사용자 1 전용 채널로 발행
- Redis Pub/Sub 서버는 해당 변경 내역을 모든 구독자에게 브로드캐스트
- 위치 변경 내역을 보낸 사용자와 구독자 사이의 거리, 즉 이 경우에는 사용자 1과 2 사이의 거리가 검색 반경을 넘지 않을 경우 새로운 위치는 사용자 2의 클라이언트로 전송
이 계산 과정은 해당 채널의 모든 구독자에게 반복 적용된다.
한 사용자의 위치가 바뀔 때마다 한 사용자당 평균 400명의 친구가 있으면 10%(가량이 주변에서 온라인 상태일 것으로 가정)인 40건의 위치 정보 전송이 발생할 것이다.
API 설계
웹소켓: 사용자는 웹소켓 프로토콜을 통해 위치 정보 변경 내역을 전송하고 수신한다.
최소한 아래 API는 구비되어야 한다.
- [서버 API] 주기적인 위치 정보 갱신
- 요청: 클라이언트는 위도, 경도, 시각 정보를 전송
- 응답: 없음
- [클라이언트 API] 클라이언트가 갱신된 친구 위치를 수신하는 데 사용할 API
- 전송되는 데이터: 친구 위치 데이터와 변경된 시각을 나타내는 타임스탬프
- [서버 API] 웹소켓 초기화 API
- 요청: 클라이언트는 위도, 경도, 시각 정보를 전송
- 응답: 클라이언트는 자기 친구들의 위치 데이터를 수신
- [클라이언트 API] 새 친구 구독 API
- 요청: 웹소켓 서버는 친구 ID 전송
- 응답: 가장 최근의 위도, 경도, 시각 정보 전송
- [클라이언트 API] 구독 해지 API
- 요청: 웹 소켓 서버는 친구 ID 전송
- 응답: 없음
HTTP 요청: API 서버는 친구를 추가/삭제하거나 사용자 정보를 갱신하는 등의 작업을 처리할 수 있어야 한다.
데이터 모델
위치 정보 캐시와 위치 이동 이력 DB만 살펴보자.
위치 정보 캐시
위치 정보 캐시는 ‘주변 친구’ 기능을 켠 활성 상태 친구의 가장 최근 위치를 보관한다.
| 키 | 값 |
|---|---|
| 사용자 ID | {위도, 경도, 시각} |
위치 정보 저장에 DB를 사용하지 않는 이유는?
‘주변 친구’ 기능은 사용자의 현재 위치만을 이용한다.
따라서 사용자 위치는 하나만 보관하면 충분하다.
읽기 및 쓰기 연산 속도가 빠른 Redis는 이런 목적에 아주 적합하다.
또한 TTL을 지원하므로 더 이상 활성 상태가 아닌 사용자 정보를 자동으로 제거할 수도 있다.
‘주변 친구’ 기능이 활용하는 위치 정보에 대해서는 영속성을 보장할 필요가 없으므로,
Redis 서버에 장애가 발생하면 다른 새 서버로 바꾼 다음 갱신된 위치 정보가 캐시에 채워지기를 기다려도 충분하다.
위치 이동 이력 DB
사용자의 위치 정보 변경 이력을 다음 스키마를 따르는 테이블에 저장한다.
| user_id | latitude | longitude | timestamp | |———|———-|———–|———–|
우리가 필요로 하는 것은 막대한 쓰기 연산 부하를 감당할 수 있고, 수평적 규모 확장이 가능한 DB다.
Cassandra는 그런 요구를 잘 부합한다.
RDB도 사용할 수는 있으나 이력 데이터의 양이 서버 한 대에 보관하기에는 너무 많을 수 있으므로 sharding이 필요하다.
3단계: 상세 설계
규모를 늘려 나가면서 병목 및 그 해결책을 찾는 데 집중해 보자.
중요 구성요소별 구모 확장성
API 서버
본 설계안의 API 서버는 무상태 서버로,
이런 서버로 구성된 클러스터의 규모를 CPU 사용량이나 부하, I/O 상태에 따라 자동으로 늘리는 방법은 다양하다.
웹소켓 서버
웹소켓 서버는 유상태 서버라, 기존 서버를 제거할 때는 주의해야 한다.
노드를 실제로 제거하기 전에 우선 기존 연결부터 종료될 수 있도록 해야 한다.
이를 위해, 로드밸런서가 인식하는 노드 상태를 ‘연결 종료 중(draining)’으로 변경해 둔다.
그러면 그 서버로는 새로운 웹소켓 연결이 만들어지지 않는다.
그러고 나서 모든 연결이 종료되면 서버를 제거하면 된다.
유상태 서버 클러스터의 규모를 자동으로 확장하려면 좋은 로드밸런서가 있어야 한다.
클라이언트 초기화
모바일 클라이엉ㄴ트는 기동되면 웹소켓 클러스터 내의 서버 가운데 하나와 지속성 웹소켓 연결을 맺는다.
웹소켓 연결이 초기화되면 클라이언트는 해당 모바일 단말의 위치,
즉 해당 단말을 이용 중인 사용자의 위치 정보를 전송한다.
그 정보를 받은 웹소켓 연결 핸들러는 다음 작업을 수행한다.
- 위치 정보 캐시에 보관된 해당 사용자의 위치를 갱신
- 해당 위치 정보는 뒤이은 계산 과정에 이용되므로, 연결 핸들러 내의 변수에 저장
- 사용자 DB를 뒤져 해당 사용자의 모든 친구 정보를 조회
- 위치 정보 캐시에 일괄(batch) 요청을 보내어 모든 친구의 위치를 한번에 조회
- 캐시에 보관하는 모든 항목의 TTL은 비활성화 타임아웃 시간과 동일한 값으로 설정되어 있으므로, 비활성화 친구의 위치는 캐시에 없음
- 캐시가 돌려준 친구 위치 각각에 대해, 웹소켓 서버는 해당 친구와 사용자 사이의 거리를 계산한다.
- 그 거리가 검색 반경 이내이면 해당 친구의 상세 정보, 위치, 그리고 해당 위치가 마지막으로 확인된 시각을 웹소켓 연결을 통해 클라이언트에 반환
- 웹소켓 서버는 각 친구의 Redis Pub/Sub 채널을 구독
- 사용자의 현재 위치를 Redis Pub/Sub 서버의 전용 채널을 통해 모든 친구에게 전송
사용자 DB
사용자 DB에는 두 가지 종류의 데이터가 보관된다.
- 사용자 ID, 사용자명, 프로파일 이미지의 URL 등을 아우르는 사용자 상세 정보 데이터
- 친구 관계 데이터
사용자 ID를 기준으로 데이터를 샤딩하면 RDB라 해도 수평적 규모 확장 가능
위치 정보 캐시
캐시할 데이터는 쉽게 샤딩할 수 있다.
각 사용자의 위치 정보는 서로 독립적인 데이터이므로, 사용자 ID를 기준으로 여러 서버에 샤딩하면 부하 또한 고르게 분배할 수 있다.
가용성을 높이려면 각 샤드에 보관하는 위치 정보를 대기(standby) 노드에 복제해 두면 된다.
주(primary) 노드에 장애가 발생하면 대기 노드를 신속하게 주 노드로 승격시켜 장애시간을 줄인다.
Redis Pub/Sub 서버
Redis Pub/Sub 서버를 모든 온라인 친구에게 보내는 위치 변경 내역 메시지의 routing 계층으로 활용한다.
새 채널은 구독하려는 채널이 없을 때 생성한다.
한편 구독자가 없는 채널로 전송된 메시지는 그대로 버려지는데, 그 과정에서 서버에 가해지는 부하는 거의 없다.
채널 하나를 유지하기 위해서는 구독자 관계를 추적하기 위한 hash table과 linked list가 필요한데 아주 소량의 메모리만을 사용한다.
오프라인 사용자라 어떤 변경도 없는 채널의 경우에는 생성된 이후에 CPU 자원은 전혀 사용하지 않는다.
아래와 같이 활용해 보자.
- ‘주변 친구’ 기능을 활용하는 모든 사용자에 채널 하나씩을 부여
- 해당 기능을 사용하는 사용자의 앱은 초기화 시에 모든 친구의 채널과 구독 관계를 설정(이때 친구의 상태는 개의치 않음)
- 설계 단순해짐
- 유의할 점: 더 많은 메모리를 사용하게 됨
- 메모리가 병목이 될 가능성은 낮음
- ‘주변 친구’ 기능의 경우, 아키텍처를 단순하게 만들 수 있다면 더 많은 메모리를 투입할 만한 가치 충분
얼마나 많은 Redis Pub/Sub 서버가 필요한가?
- 메모리 사용량
- 10억 사용자의 10% = 1억 개의 채널
- 1억 개의 채널 X 20바이트 상당의 포인터 X 주변 친구 기능을 사용하는 100명 / 10^9 = 200GB
- 모든 채널을 저장하는 데는 200GB의 메모리 필요
- CPU 사용량
- Pub/Sub 서버가 구독자에게 전송해야 하는 위치 정보 업데이트 양 = 초당 1400만 건
- 서버 한 대로 감당 가능한 구독자 수 = 100,000
- 필요한 Redis 서버 수 = 1400만 / 100,000 = 140대
가정한 수의 계산 결과로 다음과 같은 결론을 내릴 수 있다.
- Redis Pub/Sub의 병목은 메모리가 아니라 CPU 사용량이다.
- 규모를 감당하려면 분산 Redis Pub/Sub 클러스터가 필요하다.
분산 Redis Pub/Sub 서버 클러스터
모든 채널을 서로 독립적이므로, 메시지를 발행할 사용자 ID를 기준으로 Pub/Sub 서버들을 샤딩하여 채널을 분산한다.
서비스 탐색 컴포넌트로 etcd, ZooKeeper 등을 도입하여 아래 두 가지 기능을 사용하자.
- 가용한 서버 목록을 유지하는 기능 및 해당 목록을 갱신하는 데 필요한 UI나 API
- 서비스 탐색 소프트웨어는 설정 데이터를 보관하기 위한 소규모의 키-값 저장소
- 클라이언트로 하여금 ‘값’에 명시된 Redis Pub/Sub 서버에서 발생한 변경 내역을 구독할 수 있도록 하는 기능
1번 항목에서 언급한 ‘키’에 매달린 ‘값’에는 활성 상태의 모든 Redis Pub/Sub 서버로 구성된 해시 링을 보관한다.
Redis Pub/Sub 서버는 메시지를 발행할 채널이나 구독할 채널을 정해야 할 때 이 해시 링을 참조한다.
웹소켓 서버가 특정 사용자 채널에 위치 정보 변경 내역을 발행하는 과정이 어떻게 처리되는지 보자.
- 웹소켓 서버는 해시 링을 참조하여 메시지를 발행할 Redis Pub/Sub 서버를 선정
- 정확한 정보는 서비스 탐색 컴포넌트에 보관되어 있으나 성능 효율을 높이고 싶다면 해시 링 사본을 웹소켓 서버에 캐시
- 다만 그 경우, 웹소켓 서버는 해시 링 원본에 구독 관계를 설정하여 사본의 상태를 항상 원본과 동일하게 유지하도록 해야 함
- 웹소켓 서버는 해당 서버가 관리하는 사용자 채널에 위치 정보 변경 내역을 발행
구독할 채널이 존재하는 Redis Pub/Sub 서버를 찾는 과정도 이와 동일하다.
Redis Pub/Sub 서버 클러스터의 규모 확장 고려사항
Redis Pub/Sub 서버 클러스터의 규모를 늘리려면 어떻게 해야 할까?
트래픽 패턴에 따라 크기를 늘리거나 줄이는 것이 좋을까?
이 전략은 무상태 서버로 구성된 클러스터에 널리 사용되는데, 위험성이 낮고 비용을 절감하기도 좋기 때문이다.
정확한 답을 얻기 위해 Redis Pub/Sub 서버 클러스터의 몇 가지 속성을 살펴보자.
- Pub/Sub 채널에 전송되는 메시지는 메모리나 디스크에 지속적으로 보관되지 않는다.
- 채널의 모든 구독자에게 전송되고 나면 바로 삭제된다.
- 구독자가 아예 없는 경우에는 그냥 지워진다.
- 이런 관점에서 보면 Pub/Sub 채널을 통해 처리되는 데이터는 무상태라고 할 수 있다.
- Pub/Sub 서버는 채널에 대한 상태 정보를 보관한다.
- 각 채널의 구독자 목록은 그 상태 정보의 핵심적 부분이다.
- 특정한 채널을 담당하던 Pub/Sub 서버를 교체하거나 해시 링에서 제거하는 경우 채널은 다른 서버로 이동시켜야 하고, 해당 채널의 모든 구독자에게 그 사실을 알려야 한다.
- 그래야 기존 채널에 대한 구독 관계를 해지하고 새 서버에 마련된 대체 채널을 다시 구독할 수 있다.
- 그런 관적에서 보면 Pub/Sub은 유상태 서버다.
이런 이유로 Redis Pub/Sub 서버 클러스터는 유상태 서버 클러스터로 취급하는 것이 바람직하다.
유상태 서버 클러스터의 규모를 늘리거나 줄이는 것은 운영 부담과 위험이 큰 작업이다.
따라서 혼잡 시간대 트래픽을 무리 없이 감당하고 불필요한 크기 변화를 피할 수 있도록 어느 정도 여유를 두고 over provisioning하는 것이 보통이다.
하지만 불가피하게 그 규모를 늘려야 할 때는 다음과 같은 문제가 발생할 수 있음에 유의해야 한다.
- 클러스터의 크기를 조정하면 많은 채널이 같은 해시 링 위의 다른 여러 서버로 이동한다.
- 서비스 탐색 컴포넌트가 모든 웹소켓 서버에 해시 링이 갱신되었음을 알리면 엄청난 재구독 요청이 발생할 것이다.
- 이 재구독 요청을 처리하다 보면 클라이언트가 보내는 위치 정보 변경 메시지의 처리가 누락될 수 있다.
- 서비스의 상태가 불안정해질 가능성이 있으므로 클러스터 크기 조정은 하루 중 시스템 부하가 가장 낮은 시간을 골라서 시행해야 한다.
클러스터 크기는 다음 절차대로 조정하면 된다.
- 새로운 링 크기를 계산한다.
- 그 크기가 늘어나는 경우에는 새 서버를 준비한다.
- 해시 링의 키에 매달린 값을 새로운 내용으로 갱신한다.
- 대시보드를 모니터링한다.
- 웹소켓 클러스터의 CPU 사용량이 어느 정도 튀는 것이 보여야 한다.
운영 고려사항
- Pub/Sub 서버에 장애 발생
- 모니터링 소프트웨어는 on-call 엔지니어에게 경보를 발송
- on-call 담당자는 경보를 받으면 서비스 탐색 컴포넌트의 해시 링 키에 매달린 값을 갱신하여 장애가 발생한 노드를 대기 중인 노드와 교체
- 교체 사실은 모든 웹소켓 서버에 통지
- 각 웹소켓 서버는 실행 중인 연결 핸들러에게 새 Pub/Sub 서버의 채널을 다시 구독하도록 알림
- 각 연결 핸들러는 구독 중인 채널의 목록을 유지하고 있으므로, 웹소켓 서버로부터 통지를 받으면 그 모든 채널을 해시 링과 대조하여 새 서버로 구독 관계를 다시 설정해야 하는지 검토
서버 p_1에 장애가 발생하여 p_1_new로 교체해 보자.
친구 추가/삭제
새 친구를 추가하면 해당 클라이언트에 연결된 웹소켓 서버의 연결 핸들러에 그 사실을 알려야 새 친구의 Pub/Sub 채널을 구독할 수 있다.
‘주변 친구’ 기능은 큰 앱의 일부로 들어가는 기능이므로, 새 친구가 추가되면 호출될 콜백을 해당 앱에 등록해 둘 수 있다.
이 콜백은 호출되면 웹소켓 서버로 새 친구의 Pub/Sub 채널을 구독하라는 메시지를 보낸다.
이 메시지를 처리한 웹소켓 서버는 해당 친구가 활성화 상태인 경우 그 가장 최근 위치 및 시각 정보를 응답 메시지에 담아 보낸다.
친구가 삭제되면 호출될 콜백도 앱에 등록해 둘 수 있다.
이 콜백은 호출되면 해당 친구의 Pub/Sub 채널 구독을 취소하라는 메시지를 웹소켓 서버로 보낼 것이다.
친구가 많은 사용자
헤비 유저들의 채널들이 모든 Pub/Sub 서버에 분산된다는 점을 감안하면, 부하 증가가 특정 서버에 막대한 부담을 줄 일은 없을 것이다.
주변의 임의 사용자
기존 설계를 좀 고쳐서 정보 공유에 동의한 주변 사용자를 무작위로 보여 줄 수 있도록 해 보자고 한다면 어떻게 해야 할까?
기존 설계안을 크게 훼손하지 않으면서 해당 기능을 지원하는 한 가지 방법은 지오해시에 따라 구축된 Pub/Sub 채널 풀을 두는 것이다.
표시된 지역은 네 개의 지오해시 격자로 나눈 다음, 격자마다 채널을 하나씩 만들어 두면 된다.
해당 격자 내의 모든 사용자는 해당 격자에 할당된 채널을 구독한다.
- 사용자 2의 위치가 변경되면 웹소켓 연결 핸들러는 해당 사용자의 지오해시 ID를 계산한 다음, 해당 지오해시 ID를 담당하는 채널에 새 위치를 전송
- 근방에 있는 사용자 가운데 해당 채널을 구독하고 있는 사용자는 사용자 2의 위치가 변경되었다는 메시지를 수신
격자 경계 부근에 있는 사용자를 잘 처리하기 위해 모든 클라이언트는 사용자가 위치한 지오해시뿐 아니라 주변 지오해시 격자를 담당하는 채널도 구독한다.
따라서 모든 지오해시를 포함하는 아홈 개 격자를 구독하게 된다.
4단계: 마무리
어떤 사용자의 위치 정보 변경 내역을 그 친구에게 효율적으로 전달하는 시스템을 설계한 설계안의 핵심 컴포넌트는 아래와 같다.
- 웹소켓: 클라이엉ㄴ트와 서버 사이의 실시간 통신을 지원
- Redis: 위치 데이터의 빠른 읽기/쓰기를 지원
- Redis Pub/Sub: 한 사용자의 위치 정보 변경 내역을 몯든 온라인 친구에게 전달하는 라우팅 계층