이번 장에서는 주변 친구(nearby friends)라는 모바일 앱 기능을 지원하는 규모 확장이 용이한 백엔드 시스템을 설계해본다.
앱 사용자 가운데 본인 위치 정보 접근 권한을 허락한 사용자에 한에 인근의 친구 목록을 보여주는 시스템
1장 근접성 서비스와 주변 친구 는 비슷해 보이지만 큰 차이가 있다.
- 근접성 서비스의 사업장 주소는 정적이다.
- 주변 친구 위치는 자주 바뀐다.
1단계: 문제 이해 및 설계 범위 확정
페이스북 규모를 지원하는 백엔드 시스템은 복잡하다. 따라서 설계 시작 전 질문을 통해 설계 범위를 좁혀야한다.
- Q. 지리적으로 얼마나 가까워야 주변에 있다고 할 수 있는가?
- A. 5마일. 이 수치는 설정 가능해야한다.
- Q. 두 사용자 사이의 직선 거리라고 가정?
- A. Y
- Q. 얼마나 많은 사용자가 이 앱을 사용하는가? 10억명을 가넝하고, 그 가운데 10% 정도가 이 기능을 활용한다고 생각해도 괜찮은가?
- A. Y
- Q. 사용자의 이동 이력을 보관해 둬야하는가?
- A. Y
- Q. 친구 관계에 있는 사용자가 10분 이상 비활성 상태면 해당 사용자를 주변 친구 목록에서 사라지도록 해야하는가? 마지막 확인 위치를 표시하는가?
- A. 사라지게 할 것
- Q. GDPR, CCPA 같은 사생활 및 데이터 보호법도 고려해야하는가?
- A. 과정이 너무 복잡해질 수 있으므로 배제
기능 요구사항
- 사용자는 모바일 앱에서 주변 친구를 확인할 수 있어야 한다.
- 해당 친구까지의 거리 표시
- 정보가 마지막으로 갱신된 시간이 함께 표시
- 이 친구 목록은 몇 초마다 한번 씩 갱신
비기능 요구사항
- 낮은 지연 시간(low latency)
- 주변 친구의 위치 변화가 반영되는 데 너무 오랜 시간이 걸리지 않아야함
- 안정성
- 전반적으로 안정적이어야 하지만, 가끔 몇 개 데이터가 유실되는 것 정도는 용인 가능
- 결과정 일관성
- 위치 데이터를 저장하기 위해 강한 일관성을 지원하는 데이터 저장소를 사용할 필요는 없다.
- 복제본의 데이터가 원본과 동일하게 변경되기까지 몇 초 정도 걸리는 것은 용인
개략적 규모 추정
제시할 솔루션이 풀어야 할 도전적 과제를 결정하기 위해, 개략적으로 문제 규모를 추정한다.
- 주변 친구는 5마일(8km) 반경 이내 친구로 정의
- 위치 정보는 30초 주기로 갱신
- 사람의 걷는 속도가
4-6km/h
정도로 느림 - 30초 주기라면 주변 친구 검색 결과가 크게 달라지지는 않음
- 사람의 걷는 속도가
- 평균적으로 매일 주변 친구 검색 기능을 활용하는 사용자는 1억명으로 가정
- 동시 접속 사용자의 수는 **DAU의 10%**로 가정
- 천만명이 동시에 시스템을 이용한다고 가정
- 평균적으로 한 사용자는 400명의 친구를 갖는다고 가정
- 모든 친구가 주변 친구 검색 기능을 활용한다고 가정
- 이 기능을 제공하는 앱은 한 페이지당 20명의 주변 친구를 표시
- 사용자의 요청이 있다면 더 많은 주변 친구를 노출
QPS 계산
- 1억 DAU
- 동시 접속 사용자:
10% * 1억 = 천만
- 사용자는 30초마다 자기 위치를 시스템에 전송
위치 정보 갱신 QPS = 천만 / 30 = ~334,000
2단계: 개략적 설계안 제시 및 동의 구하기
위치 정보를 모든 친구에게 전송해야 한다는 요구사항으로 인해 클라이언트와 서버 사이의 통신 프로토콜로 단순한 HTTP 프로토콜을 사용하지 못하게 될 수 있음을 감안해야한다.
개략적 설계안
이번 문제는 메시지의 효과적 전송을 가능케 할 설계얀을 요구한다.
개념적으로 보면 사용자는 근방의 모든 활성 상태 친구의 새 위치 정보를 수신하고자 한다. 이론적으로 순수한 P2P(peer-to-peer) 방식으로도 해결 가능한 문제이다.
다시 말해, 활성 상태인 근방 모든 친구와 항구적 통신 상태를 유지하면 되는 것이다.
P2P 통신은 실용적인 아이디어는 아니지만 추구해야 할 설계 방향에 대한 통찰은 얻을 수 있다.
이보다 조금 더 실용적인 설계안은 공용 백엔드를 사용하는 것이다.
백엔드는 다음과 같은 역할을 담당한다.
- 모든 활성 상태 사용자의 위치 변화 내역을 수신
- 사용자 위치 변경 내역을 수신할 때마다 해당 사용자의 모든 활성 상태 친구를 찾아 그 친구들의 단말로 변경 내역을 전달
- 두 사용자 사이의 거리가 특정 임계치보다 먼 경우 변경 내역을 전송하지 않음
간단해 보이지만 문제의 가정이 활성 상태의 동시 접속 사용자 천만 명이므로 큰 규모에 적용하기가 쉽지 않다.
- 천만명이 자기 위치 정보를 30초마다 갱신하면 334,000번의 위치 정보 갱신 처리가 필요하다.
- 사용자 1명은 400명의 친구를 가지고, 그 중 10%가 인근에서 활성화 상태라면, 초당
334,000 * 400 * 10% = 1400만
건의 위치 정보 갱신 요청을 처리해야한다. - 엄청난 양의 갱신 내역을 사용자 단말로 보내야한다.
설계안
우선 소규모 백엔드를 위한 개략적 설계안에서 시작하여 더 큰 규모에 맞게 확장해나간다.
로드 밸런서
RESTful API 서버 및 양방향 유상태 웹소켓 서버 앞단에 위치한다.
부하를 고르게 분산하기 위해 트래픽을 서버들에 배분하는 역할을 한다.
RESTful API 서버
- 무상태 API 서버의 클러스터
- 통상적인 요청/응답 트래픽을 처리
친구를 추가/삭제하거나 사용자 정보를 갱신하는 등의 부가적인 작업을 처리한다.
웹소켓 서버
친구 위치 정보 변경을 거의 실시간에 가깝게 처리하는 유상태 서버 클러스터이다.
- 각 클라이언트는 클러스터 내 한 대 서버와 웹소켓 연결은 지속적으로 유지
- 검색 반경 내 친구 위치가 변경되면 해당 내역은 이 연결을 통해 클라이언트로 전송
- 주변 친구 기능을 이용하는 클라이언트의 초기화 담당
- 온라인 상태인 모든 주변 친구 위치를 해당 클라이언트로 전송
레디스 위치 정보 캐시
활성 상태 사용자의 가장 최근 위치 정보를 캐시하는 데 사용한다.
- 레디스에 보관하는 캐시 항목에는 TTL(Time-To-Live) 필드가 있어, 설정한 기간이 지나면 해당 사용자는 비활성으로 바뀌고 위치 정보는 캐시에서 삭제된다.
- 캐시에 보관된 정보를 갱신할 때 TTL도 갱신한다.
레디스가 아니더라도 TTL을 지원하는 키-값 저장소는 캐시로 활용될 수 있다.
사용자 데이터 베이스
사용자 데이터 및 사용자의 친구 관계 정보를 저장한다.
관계형 데이터베이스, NoSQL 어느 쪽이든 사용 가능하다.
위치 이동 이력 데이터베이스
사용자의 위치 변동 이력을 보관한다.
주변 친구 표시와 직접 관계된 기능은 아니다.
레디스 펍/섭 서버
레디스 펍/섭은 초경량 메시지 버스(message bus)다.
- 레디스 펍/섭에 새로운 채널을 생성하는 것은 아주 값싼 연결이다.
- 기가바이트급 메모리를 갖춘 최신 레디스 서버에는 수백만 개의 채널(토픽)을 생성할 수 있다.
- 웹소켓 서버를 통해 수신한 특정 사용자의 위치 정보 변경 이벤트는 해당 사용자에게 배정된 펍/섭 채널에 발행한다.
- 해당 사용자의 친구 각각과 연결된 웹소켓 연결 핸들러는 해당 채널의 구독자로 설정되어 있다.
- 특정 사용자의 위치가 바뀌면 해당 사용자의 모든 친구의 웹소켓 연결 핸들러가 호출된다.
- 핸들러는 위치 변경 이벤트를 수신할 친구가 활성 상태면 거리를 다시 계산한다.
- 검색 반경 이내면 갱신된 위치와 갱신 시간을 웹소켓 연결을 통해 해당 친구의 클라이언트 앱으로 보낸다.
다른 메시지 버스 기술도 경량의 통신 채널만 제공한다면 같은 형태로 사용 가능하다.
주기적 위치 갱신
모바일 클라이언트는 항구적으로 유지되는 웹소켓 연결을 통해 주기적으로 위치 변경 내역을 전송한다.
- 모바일 클라이언트가 위치가 변경된 사실을 로드밸런서에 전송
- 로드밸런서는 그 위치 변경 내역을 해당 클라이언트와 웹소켓 서버 사이에 설정된 연결을 통해 웹소켓 서버로 보냄
- 아래 작업을 병렬로 수행
- 웹소켓 서버는 해당 이벤트를 위치 이동 이력 데이터베이스에 저장
- 웹소켓 서버는 새 위치를 위치 정보 캐시에 보관
- TTL도 새롭게 갱신
- 웹소켓 연결 핸들러 안의 변수에 해당 위치를 반영(거리 계산에 활용)
- 웹소켓 서버는 레디스 펍/섭 서버의 해당 사용자 채널에 새 위치를 발행
- 레디스 펍/섭 채널에 발행된 새로운 위치 변경 이벤트는 모든 구독자(웹소켓 이벤트 핸들러)에게 브로드캐스트된다.
- 구독자는 위치 변경 이벤트를 보낸 사용자의 온라인 상태 친구들
- 구독자의 웹소켓 연결 핸들러는 친구의 위치 변경 이벤트를 수신
- 메시지를 받는 웹 소켓 서버, 즉 웹 소켓 연결 핸들러가 위치한 웹 소켓 서버는 새 위치를 보낸 사용자와 메시지를 받은 사용자 사이의 거리를 새로 계산
- 검색 반경을 넘지 않는다면 새 위치 및 해당 위치로의 이동이 발생한 시각을 나타내는 타임스탬프를 해당 구독자의 클라이언트 앱으로 전송
- 넘은 경우에는 보내지 않음
- 사용자 1의 위치가 변경되면 그 변경 내역은 사용자 1과의 연결을 유지하고 있는 웹소켓 서버에 전송됨
- 해당 변경 내역은 레디스 펍/섭 서버 내의 사용자 1 전용 채널로 발행
- 레디스 펍/섭 서버는 해당 변경 내역을 모든 구독자에게 브로드캐스트한다.
- 구독자는 사용자 1과 친구 관계에 있는 모든 웹소켓 연결 핸들러
- 위치 변경 내역을 보낸 사용자와 구독자 사이의 거리(사용자 1과 2 사이의 거리)가 검색 반경을 넘지 않을 경우 새로운 위치는 사용자 2의 클라이언트로 전송
이 계산 과정은 해당 채널의 모든 구독자에게 반복 적용된다.
- 한 사용자의 위치가 바뀔 때마다 이치 정보 전송은 40건 정도 발생할 것이다.
API 설계
필요한 API를 나열해본다.
웹소켓
사용자는 웹소켓 프로토콜을 통해 위치 정보 변경 내역을 전송하고 수신하므로, 최소한 다음 API는 구비되어야 한다.
- [서버 API] 주기적인 위치 정보 갱신
- 요청
- 위도, 경도, 시각 정보
- 응답
- 없음
- 요청
- [클라이언트 API] 클라이언트가 갱신된 친구 위치를 수신하는 데 사용할 API
- 전송되는 데이터
- 친구 위치 데이터와 변경된 시각을 나타내는 타임스탬프
- 전송되는 데이터
- [서버 API] 웹소켓 초기화 API
- 요청
- 위도, 경도, 시각 정보
- 응답
- 자기 친구들의 위치 데이터
- 요청
- [클라이언트 API] 새 친구 구독 API
- 요청
- 친구 ID
- 응답
- 가장 최근의 위도, 경도, 시각 정보
- 요청
- [클라이언트 API] 구독 해지 API
- 요청
- 친구 ID
- 응답
- 없음
- 요청
HTTP 요청
API 서버는 친구를 추가/삭제하거나 사용자 정보를 갱신하는 드으이 작업을 처리할 수 있어야 한다.
아주 흔한 종류의 API이므로 상세한 내용은 다루지 않는다.
데이터 모델
살펴봐야하는 중요한 주제중 하나는 데이터 모델이다.
위치 정보 캐시와 위치 이동 이력 데이터베이스만 살펴본다.
위치 정보 캐시
위치 정보 캐시는 주변 친구기능을 켠 활성 상태 친구의 가장 최근 위치를 보관한다.
설계안에서는 레디스를 사용해 이 키새를 구현하며, 해당 캐시에 보관될 키/값 쌍은 아래와 같다.
키 | 값 |
---|---|
사용자 ID | {위도, 경도, 시각} |
위치 정보 저장에 데이터베이스를 사용하지 않는 이유는?
주변 친구 기능은 사용자의 현재 위치만 사용하므로, 사용자 위치는 하나만 보관하면 충분하다.
- 읽기 및 쓰기 연산 속도가 매우 빠르다.
- TTL을 지원하여 활성 상태가 아닌 사용자 정보를 자동으로 제거할 수 있다.
- 활용하는 위치 정보는 영속성을 보장할 필요가 없다.
- 장애 발생시 새 서버로 바꾼 후 갱신된 위치 정보가 캐시에 채워지기만 하면 충분하다.
- 캐시가 데워질 동안은 갱신 주기가 한두 번 정도 경과하여 변경 내역을 놓칠 수도 있지만 수용 가능하다.
위치 이동 이력 데이터베이스
위치 이동 이력 데이터베이스는 사용자의 위치 정보 변경 이력을 다음 스키마를 따르는 테이블에 저장한다.
user_id | latitude | longitude | timestamp |
---|
필요로 하는 것은 막대한 쓰기 연산 부하를 감당할 수 있고, 수평적 규모 확장이 가능한 데이터베이스다.
카산드라(Cassandra)는 이러한 요구에 잘 부합한다.
- 관계형 데이터 베이스도 사용할 수는 있으나 이력 데이터의 양이 서버 한 대에 보관하기에는 너무 많을 수 있으므로 샤딩이 필요하다.
- 사용자 ID를 기준 삼는 샤딩 방안이 가장 기본
- 부하를 모든 샤드에 고르게 분산시킬 수 있고, 데이터베이스 운영 관리도 간편하다.
3단계: 상세 설계
개략적 설계안은 대부분의 경우 통하지만, 주어진 문제의 규모를 감당하기는 어려울 것이다.
이번 절에서는 규모를 늘려 나가면서 병목 및 해결책을 찾는데 집중한다.
중요 구성요소별 규모 확장성
API 서버
RESTful API 서버의 규모 확장 방법은 널리 알려져있다.
본 설계안의 API 서버는 무상태로 이런 서버로 구성된 클러스터의 규모를 CPU 사용률이나 부하, I/O 상태에 따라 자동으로 늘리는 방법은 다양하다.
웹소켓 서버
웹소켓 클러스터도 사용률에 따라 규모를 자동으로 늘리는 것은 그다지 어렵지 않다.
하지만 웹소켓 서버는 유상태 서버라 기존 서버를 제거할 때는 주의가 필요하다.
- 노드를 제거하기 전 기존 연결부터 종료될 수 있도록 해야한다.
- 로드 밸런서가 인식하는 노드 상태를 **연결 종료 중(draining)**으로 변경해둔다.
- 해당 서버로는 새로운 웹소켓 연결이 만들어지지 않는다.
- 모든 연결이 종료되면(충분한 시간이 흐른 후) 서버를 제거한다.
웹소켓 서버에 새로운 버전의 애플리케이션 소프트웨어를 설치할 때도 마찬가지로 유의해야한다.
유상태 서버 클러스터의 규모를 자동으로 확장하려면 좋은 로드밸런서가 있어야한다.
대부분의 클라우드 로드밸런서는 이런 일을 잘 처리한다.
클라이언트 초기화
모바일 클라이언트는 기동되면 웹소켓 클러스터 내의 서버 가운데 하나와 지속성 웹소켓 연결을 맺는다.
- 연결이 오랜 시간 유지된다.
- 현대적 프로그래밍 언어는 이런 연결 유지에 많은 메모리를 필요로 하지 않는다.
웹소켓 연결이 초기화되면 클라이언트는 해당 모바일 단말의 위치, 즉 해당 단말을 이용중인 사용자의 위치 정보를 전송한다.
그 정보를 받은 웹 소켓 연결 핸들러는 다음 작업을 수행한다.
- 위치 정보 캐시에 보관된 해당 사용자의 위치를 갱신
- 해당 위치 정보는 뒤이은 계산 과정에 이용되므로, 연결 핸들러 내의 변수에 저장해둔다.
- 사용자 데이터베이스를 뒤져 해당 사용자의 모든 친구 정보를 조회
- 위치 정보 캐시에 일괄(batch) 요청을 보내 모든 친구의 위치를 한번에 조회
- 비활성화 친구의 위치는 캐시에 없을 것
- 친구 위치 각각에 대해, 해당 친구와 사용자 사이의 거리를 계산
- 검색 반경 이내면 해당 친구의 상세 정보, 위치, 마지막으로 확인된 사각을 웹소켓 연결을 통해 클라이언트에 반환
- 각 친구의 레디스 서버 펍/섭 채널을 구독
- 채널 생성 및 구독 비용이 저렴하므로 활성 상태에 관계없이 모든 친구 채널을 구독할 수 있음
- 비활성화 친구의 채널을 유지하기 위해 메모리가 필요하지만 매우 적고, 활성 전까지 CPU나 I/O를 전혀 이용하지 않음
- 사용자의 현재 위치를 레디스 펍/섭 서버의 전용 채널을 통해 모든 친구에게 전송
사용자 데이터베이스
사용자 데이터베이스에는 두 가지 종류의 데이터가 보관된다.
- 사용자 ID, 사용자명, 프로파일 이미지의 URL 등 사용자 상세 정보(프로필 데이터)
- 친구 관계 데이터
이번 장에서 다루는 설계안의 규모를 감안하면 한 대의 관계형 데이터베이스 서버로는 감당할 수 없으나, 사용자 ID를 기준으로 데이터를 샤딩하면 관계형 데이터베이스라 해도 수평적 규모 확장이 가능하다.
설계하고 있는 규모의 시스템을 실제로 운영하려면 사용자 및 친구 데이터를 관리하는 팀이 따로 필요할 것이다.
웹소켓 서버는 데이터베이스를 직접 질의하는 대신 API를 호출하여 사용자 및 친구 관계 데이터를 가져와야 한다.
위치 정보 캐시
활성화 상태 사용자의 위치 정보를 캐시하기 위해 레디스를 활용한다.
- 각 항목의 키에는 TTL을 설정한다.
- 사용자의 위치 정보가 갱신될 때마다 초기화된다.
- 따라서 최대 메모리 사용량은 일정 한도 아래로 유지된다.
하지만 천 만명의 활성 사용자가 대략 30초마다 변경된 위치 정보를 전송한다고 가정하면 레디스 서버가 감당해야 하는 갱신 연산의 수는 초당 334K에 달하게되는데, 최신 고사양 서버를 쓴다 해도 부담되는 수치이다.
다행히도 각 사용자의 위치 정보는 서로 독립적인 데이터이므로 사용자 ID를 기준으로 여러 서버에 샤딩하면 부하 또한 고르게 분배할 수 있다.
- 가용성을 높이려면 각 샤드에 보고나하는 위치 정보를 대기(standby) 노드에 복제해 두면 된다.
레디스 펍/섭 서버
본 설계안에서 펍/섭 서버를 모든 온라인 친구에게 보내는 위치 변경 내역 메시지의 라우팅(routing)계층으로 활용한다.
레디스 펍/섭 서버를 사용하는 이유는 채널을 만들고 유지하는 비용이 매우 저렴하기 때문이다.
- 새 채널은 구독하려는 채널이 없을 때 생성한다.
- 구독자가 없는 채널로 전송되 메시지는 그대로 버려지는데, 서버에 가해지는 부하는 거의 없다.
- 채널 하나를 유지하기 위해서는 구독자 관계를 추적하기 위한 해시 테이블과 연결 리스트가 필요한데 아주 소량의 메모리만 사용한다.
- 오프라인 사용자라 어떤 변경도 없는 채널의 경우에는 생성된 이후에 CPU 자원은 전혀 사용하지 않는다.
본 설계안은 그 점을 활용하여 주변 친구 기능을 활용하는 모든 사용자에 채널 하나씩을 부여한다.
- 해당 기능을 사용하는 사용자의 앱은 초기화 시에 모든 친구의 채널과 구독 관계를 설정한다.(친구가 비활성이라도)
활성화 상태로 바뀐 친구의 채널을 구독하거나 비활성 상태가 된 친구의 채널을 구독 중단하는 작업이 필요 없어지므로 설계가 간단해진다.
더 많은 메모리를 사용하게 되지만, 메모리가 병목이 될 가능성은 매우 낮다. 아키텍처를 단순하게 만들 수 잇다면 더 많은 메모리를 투입할 가치는 충분하다.
얼마나 많은 레디스 펍/섭 서버가 필요한가?
메모리 사용량
- 주변 친구 찾기를 사용하는 모든 사용자에게 채널 하나씩 할당하면 채널 수는 1억개
- 구독자 한 명을 추적하기 위해 내부 해시 테이블과 연결 리스트에 20바이트 상당의 포인터들을 저장해야함
1억 * 20바이트 * 100명 친구 / 10^9 = 200GB
100GB 메모리 서버 2대로 충분하다.
CPU 사용량
펍/섭 서버가 구독자에게 전송해야하는 위치 정보 업데이트 양은 초당 1400만 건에 달한다.
최신 레디스 서버 한 대로 얼마나 많은 메시지를 전송할 수 있는지 정확히 알 수는 없지만, 서버 한 대로는 곤란할것이다.
- 보수적으로 기가비트 네트워크 카드를 탑재한 현대적 아키텍처의 서버 한 대로 감당 가능한 구독자의 수는 100,000이라고 가정
이 추정치에 따르면 필요한 레디스 서버의 수는 1400만 / 100,000 = 140
이다.
위 계산 결과를 통해 다음과 같은 결론을 내릴 수 있다.
- 레디스 펍/섭 서버의 병목은 메모리가 아니라 CPU 사용량이다.
- 문제의 규모를 감당하려면 분산 레디스 펍/섭 클러스터가 필요하다.
분산 레디스 펍/섭 서버 클러스터
모든 채널을 서로 독립적이므로, 메시지를 발행할 사용자 ID를 기준으로 펍/섭 서버들을 샤딩한다.
하지만 현실적으로는 수백 대의 펍/섭 서버가 관련된 문제이므로 동작 방식을 상세하게 집어볼 필요가 있다.
- 서버에는 필연적으로 장애가 생기게 마련이므로, 매끄러운 운영을 위해 필요하다.
본 설계안에서는 서비스 탐색(Service discovery) 컴포넌트를 도입하여 이 문제를 푼다.
- etcd
- 주키퍼(ZooKeeper)
서비스 탐색 컴포넌트의 아래 기능을 활용한다.
- 가용한 서버 목록을 유지하는 기능 및 해당 목록을 갱신하는 데 필요한 UI나 API
- 서비스 탐색 소프트웨어는 설정 데이터를 보관하기 위한 소규모의 키-값 저장소라고 보면 된다.
- 키:
/config/pub_sub_ring
- 값:
["p_1", "p_2", "p_3", "p_4"]
- 클라이언트(웹소켓 서버)로 하여금 값에 명시된 레디스 펍/섭 서버에서 발생한 변경 내역을 구독할 수 있도록 하는 기능
키에 매달린 값에는 활성 상태의 모든 레디스 펍/섭 서버로 구성된 해시 링을 보관한다.
레디스 펍/섭 서버는 메시지를 발행할 채널이나 구독할 채널을 정해야 할 때 이 해시링을 참조한다.
웹소켓 서버가 특정 사용자 채널에 위치 정보 변경 내역을 발행하는 과정은 아래와 같다.
- 해시 링을 참조하여 메시지를 발행할 레디스 펍/섭 서버를 선정한다.
- 정확한 정보는 서비스 탐색 컴포넌트에 보관되어 있으나 해시 링 사본을 웹소켓 서버에 캐시한다면 성능을 높힐 수 있다.
- 그 경우 웹소켓 서버는 해시 링 원본에 구독 관례를 설정하여 사본을 원본과 동일하게 유지하도록 해야한다.
- 웹소켓 서버는 해당 서버가 관리하는 사용자 채널에 위치 정보 변경 내역을 발행한다.
구독할 채널이 존재하는 레디스 펍/섭 서버를 찾는 과정도 이와 동일하다.
레디스 펍/섭 서버 클러스터의 규모 확장 고려사항
무상태 서버라면 트래픽 피턴에 따라 크기를 늘리거나 줄이는 방법이 위험성이 낮고 비용을 절감하기도 좋아 널리 활용되지만, 레디스 펍/섭 서버 클러스터 특성으로 인해 고려하기 어렵다.
펍/섭 채널에 전송되는 메시지는 메모리나 디스크에 지속적으로 보관되지않지만, 채널에 대한 상태 정보를 보관한다.(각 채널의 구독자 목록)
따라서 특정한 채널을 담당하던 펍/섭 서버를 교체하거나 해시 링에서 제거하는 경우 기존 채널에 대한 구독 관계를 해지하고 새 서버에 마련된 대체 채널을 다시 구독하기 위해 채널을 다른 서버로 이동시켜야 하고, 해당 채널의 모든 구독자에게 그 사실을 알려야한다.
유상태 서버 클러스터의 규모를 늘리거나 줄이는 것은 운영 부담과 위험이 큰 작업이므로 주의 깊게 계획하고 진행해야한다.
- 유상태 서버 클러스터는 혼잡 시간대 트래픽을 무리 없이 감당하고 불필요한 크기 변화를 피할 수 있도록 어느 정도 여유를 두고 오버 프로비저닝(over provisioning)하는 것이 일반적이다.
불가피하게 규모를 늘려야 할 때는 다음과 같은 문제가 발생할 수 있음에 유의하여야 한다.
- 클러스터의 크기를 조정하면 많은 채널이 같은 해시 링 위의 다른 여러 서버로 이동한다.
- 서비스 탐색 컴포넌트가 모든 웹소켓 서버에 해시 링이 갱신되었음을 알리면 엄청난 재구독 요청이 발생할 것이다.
- 재구독 요청을 처리하다 보면 클라이언트가 보내는 위치 정보 변경 메시지의 처리가 누락될 수 있다.
- 어느 정도는 허용할 수 있으나 빈도는 반드시 최소화해야한다.
- 서비스의 상태가 불안정해질 가능성이 있으므로 클러스터 크기 조정은 하루 중 시스템 부하가 가장 낮은 시간을 골라 시행한다.
클러스터의 크기 조정 자체는 꽤 간단하다.
- 새로운 링 크기를 계산한다.
- 크기가 늘어난다면 새 서버를 준비한다.
- 해시 링의 키에 매달린 값을 새로운 내용으로 갱신한다.
- 대시보드를 모니터링한다.
- 웹소켓 클러스터의 CPU 사용량이 어느 정도 튀는 것이 보여야 한다.
p_5
, p_6
두 노드가 추가된다면 해시 링은 다음과 같이 바뀐다.
- 변경 전
[p_1, p_2, p_3, p_4]
- 변경 후
[p_1, p_2, p_3, p_4, p_5, p_6]
운영 고려사항
기존 레디스 펍/섭 서버를 새 서버로 교체할 때는 교체되는 서버의 채널만 처리하므로 운영 문제가 발생할 가능성은 클러스터 크기를 조정할 때보다 훨씬 낮다.
펍/섭 서버에 장애가 발생하면 모니터링 소프트웨어는 온콜(on-call) 엔지니어에게 경보를 발송하며, 온콜 담당자는 아래와 같은 처리를 수행한다.
- 서비스 탐색 컴포넌트의 해시 링 키에 매달린 값을 갱신하여 장애가 발생한 노드를 대기 중인 노드와 교체
- 교체 사실은 모든 웹소켓 서버에 통지되고, 각 웹소켓 서버는 실행 중인 연결 핸들러에게 새 펍/섭 서버의 채널을 다시 구독하라고 알림
- 각 연결 핸들러는 구독 중인 채널의 목록을 유지하고 있으므로, 모든 채널을 해시 링과 대조하여 새 서버로 구독 관계를 다시 설정해야 하는지 검토
친구 추가/삭제
새 친구를 추가하면 해당 클라이언트에 연결된 웹소켓 서버의 연결 핸들러에 그 사실을 알려 새 친구의 펍/섭 채널을 구독할 수 있도록 해야한다.
- 주변 친구 기능은 큰 앱의 일부로, 새 친구가 추가되면 호출될 콜백을 해당 앱에 등록해 둘 수 있다.
- 콜백이 호출되면 웹소켓 서버로 새 친구의 펍/섭 채널을 구독하라는 메시지를 보낸다.
- 이 메시지를 처리한 웹소켓 서버는 해당 친구가 활성화 상태인 경우 가장 최근 위치 및 시각 정보를 응답 메시지에 담아 보낸다.
마찬가지로 친구가 삭제되면 호출될 콜백도 앱에 등록해 둘 수 있다.
- 콜백이 호출되면 해당 친구의 펍/섭 채널 구독을 취소하라는 메시지를 웹소켓 서버로 보낸다.
친구가 위치 정보 정보 전성을 허가/취소하는 경우의 처리에도 활용될 수 있다.
친구가 많은 사용자
친구가 많은 사용자가 시스템 성능 문제를 야기할 가능성이 있는지 논의해볼 만한 주제이다.
- 친구 수의 상한이 있다고 가정(페이스북은 5,000명)
- 친구 관계는 양방향
- 팔로워 모델 같은 단방향 관계는 배제
수천 명의 친구를 구독하는 데 필요한 펍/섭 구독 관계는 클러스터 내의 많은 웹소켓 서버에 분산되어 있을 것이다.
- 따라서 친구들의 위치 변경에서 오는 부하는 각 웹소켓 서버가 나누어 처리하므로 핫스팟 문제는 발생하지 않을 것 이다.
다만 많은 친구를 둔 사용자의 채널이 존재하는 펍/섭 서버의 경우는 조금 더 많은 부하를 감당하게 될 수 있지만, 클러스터 안에 100대가 넘는 펍/섭 서버가 있고, 그런 헤비 유저들의 채널들이 모든 펍/섭 서버에 분산된다는 점을 감안하면, 특정 서버에 막대한 부담을 줄 일은 없을 것이다.
주변의 임의 사용자
정보 공유에 동의한 주변 사용자를 무작위로 보여줄 수 있도록 한다면 어떻게 해야할까?
기존 설계안을 크게 훼손하지 않으면서 해당 기능을 지원하는 한 가지 방법은 지오해시에 따라 구축된 펍/섭 채널 풀을 두는 것 이다.
- 지오 해시 격자로 나눈 다음 격자 마다 채널을 하나씩 만들어 둔다.
해당 격자 내의 모든 사용자는 해당 격자에 할당된 채널을 구독한다.
- 사용자의 위치가 변경되면 웹소켓 연결 핸들러는 해당 사용자의 지오해시 ID를 계산한 후, 해당 지오해시 ID를 담당하는 채널에 새 위치를 전송한다.
- 근방에 있는 사용자 가운데 해당 채널을 구독하고 있는 사용자는 사용자 2의 위치가 변겨오디었다는 메시지를 수신한다.
경계에 위치한 사용자를 잘 처리하기 위해 모든 클라이언트는 사용자가 위치한 지오해시 뿐 아니라 주변 지오해시 격자를 담당하는 채널도 구독한다.
레디스 펍/섭 외의 대안
얼랭(Erlang)은 이문제에 특히 유용한 해결책이 될 수 있다.
- 오히려 더 좋은 솔루션이 될 수 있다.
- 얼랭은 사용자가 적어 좋은 개발자를 구하기 어렵다
- 얼랭 전문가가 있다면 좋은 선택지가 될 수 있다.
얼랭은 고도로 분산된 병렬 애플리케이션을 위해 고안된 프로그래밍 언어이자 런타임 환경이다.
4단계: 마무리
개념적으로 보자면 어떤 사용자의 위치 정보 변경 내역을 그 친구에게 효율적으로 전달하는 시스템을 설계했다.
설계안의 핵심 컴포넌트는 아래와 같다.
- 웹소켓
- 클라이언트와 서버 사이의 실시간 통신을 지원한다.
- 레디스
- 위치 데이터의 빠른 읽기/쓰기를 지원한다.
- 레디스 펍/섭
- 한 사용자의 위치 정보 변경 내역을 모든 온라인 친구에게 전달하는 라우팅 계층
소규모 트래픽에 적합한 개략적 설계안부터 시작하여, 규모가 커짐에 따라 발생 가능한 도전적 문제들을 살펴보았다.
- RESTful API 서버
- 웹소켓 서버
- 데이터 계층
- 레디스 펍/섭 서버 클러스터
- 레디스 펍/섭 서버의 대안
친구가 많은 사용자에게 발생할 수 있는 잠재적 성능 병목문제와, 주변의 임의 사용자를 보여주는 기능의 설계안도 살펴보았다.
요약
flowchart LR nearby(주변 친구) --> first((1단계)) nearby --> second((2단계)) nearby --> third((3단계)) first --> giyogu[기능적 요구사항] giyogu --> display[주변 친구 목록 표시] giyogu --> update[주변 친구 목록 갱신] first --> biyogu[비 기능적 요구사항] biyogu --> low_latenty[낮은 지연 시간] first --> choo[추정] choo --> mile[5마일 검색 반경] choo --> interval[위치 갱신 주기: 30초] choo --> updatedata[위치 정보 갱신 QPS: 334k/s] second --> rufh[개략적 설계안] rufh --> rest[RESTful API 서버] rufh --> websocket[웹소켓 서버] rufh --> redis[레디스 위치 정보 캐시] rufh --> casandra[위치 이동 이력 데이터베이스] rufh --> pubsub[레디스 펍/섭 서버] second --> intervalpos[주기적 위치 갱신] second --> api[API 설계] second --> datamodel[데이터 모델] datamodel --> position[위치 정보 캐시] datamodel --> positionlog[위치 이동 이력 데이터베이스] third --> components[각 컴포넌트의 규모 확장] components --> apiComponent[API 서버] components --> websocket_cluster[웹소켓 서버 클러스터] components --> userDB[사용자 정보 데이터베이스] components --> positionCache[위치 정보 캐시] components --> pubsubCluster[레디스 펍/섭 서버 클러스터] components --> erlang[레디스 펍/섭 외 대안] third --> friendUD[친구 추가/삭제] third --> manyFriend[친구가 많은 사용자] third --> random[주변 임의 사용자]