r/place
레딧블로그 (2017) How We Built r/Place - Upvoted
레딧글 (2022) How we built r/place 2022. Backend. Part 1. Backend Design
레딧글 (2022) How we built r/place 2022 (Backend Scale)
유튜부 비디오(2022) How We Built r/place
1. 개요
레딧에서 만우절에 이벤트성으로 진행한 온라인 픽셀 아트 이벤트입니다
요구사항
- 1000x1000 타일
- 최소 10만명의 동접자
- 5분마다 한 번씩 타일을 바꿀 수 있음. (따라서 최소 333RPS)
- 모든 클라이언트가 실시간으로 동일한 화면을 볼 수 있도록 동기화해야 함 ㄷㄷㄷ
- 그 외 기타등등
DB 결정 과정
- 1트: Cassandra
- Column-Oriented
- 한 row안에 있는 모든 column에 모든 픽셀 정보를 다 때려넣자
- read 30초 걸려서 실패..
- 의문: 모든 픽셀을 왜 한 row에 다 때려 넣었을까..? 그냥 row로 나누면 안되는 거였을까
- 2트: Redis
- 레디스엔 BITFIELD라는 게 있다 (난생처음봄)
- 각 오프셋으로 좌표를 결정 (offset = x + 1000y)
- 저장된 각 4비트 정수의 값으로 색상 표시
4비트로 한다고....????
4비트로 어떻게 색상을 표현하는지에 대한 이미지가 안보임.. 아마 아래와 같은 느낌이 아닐까..

- 보드 전체를 읽거나(100ms) 특정 타일을 업데이트하는 게 가능했음
- 픽셀을 칠한 유저 정보 등등의 메타데이터는 Cassandra에 저장했다고 함
- 근데 모든 유저가 새로고침을 연타한다면 레디스가 터질 것임
- 보드는 전역적인 상태이므로 캐싱 가능함. Fastly라는 CDN을 사용했다고 함
- expiry 1초로 걸고, stale-while-revalidate 를 사용했다고 함.
- 🗒️stale-while-revalidate: 캐시가 만료된 이후 지정된 시간동안 요청이 오는 경우, 응담으로는 일단 만료된 데이터를 내려주지만 캐시 서버가 백그라운드에서 데이터를 최신화함
- 하여, RPS가 Fastly에서 유지하는 POPs의 개수만큼 줄어든다! (17년도 당시에는 33개쯤 있었던 모양)
API
전체 보드 조회
- 앞의 캐싱 관련 내용에서 대부분 설명
타일 색칠
- 카산드라에서 마지막 타일 배치 timestamp를 읽음. 5분 이내이면 400
- 레디스와 카산드라에 write (현재 timestamp, 메타데이터, 색상 등등)
- 웹소켓으로 이벤트 브로드캐스트
카산드라의 consistency level은 QUORUM이라고 함
사실 저도 이게 뭔지 잘 몰라서 GPT한테 물어봤습니다. 분산 DB이니 완전히 매칭시키긴 어렵지만, 얼추 READ COMMITTED 수준이라고 하네용

- 동일 타일에 대해서 동시에 업데이트하는 것에 대한 lock이 따로 없어서 race condition이 있긴 함
타일 정보
- 특정 타일을 업데이트한 유저, 시간 등등을 불러옴
- 카산드라에서 불러옴. CDN은 따로 안 탔고, 제일 많이 불렸다고 함
실시간 동기화
- 역시나 웹소켓 사용. 기존에 RMQ가 브로커 해주는 웹소켓 브로드캐스팅 서비스를 짜둔 게 있는 모양
- 피크 타임의 커넥션 수는 80,000, outgoing bytes는 4gbps였다고 함 (150Mbps * 24 인스턴스)
- 클라에서는 웹소켓 업데이트 이벤트를 쌓아두다가, 1프레임(대략 16ms)마다 렌더링
- 픽셀 업데이트 이벤트는 초당 최대 333개(실제로는 200개 정도)만 들어오기 때문에 터지지 않았다고 합니다..? 🤔
2022~
- 짜잔 앞서 쓴 내용은 모두 2017년에 적용된 내용이었습니다. 2022년 이벤트 재개장하면서 뭐가 바뀌었는지 알아보려 합니다
- redis 비트필드는 그대로 사용했지만, 사용 가능 색상을 32개로 늘림
- 실시간 동기화의 경우, 이벤트 참여자가 늘어서 이전처럼 모든 픽셀 변화를 스트리밍할 수 없다
- 웹소켓 서비스도 새로 팠음 (같이 보면 재밌을 법한 글)
- 참여자가 10배만 늘어도 웹소켓 서버에서 받는 로드가 100배는 증가한다고 함
캔버스를 실시간으로 png로 만들어 cdn에 올리고, 웹소켓은 png 경로만 알려주는 방식

- 모든 프레임 정보를 다 담은 PNG와, 이전 프레임의 델타 PNG를 각각 생성하고, realtime 서비스로 보냄
- GraphQL Subscription을 통해 클라에서 이벤트 수신
- 델타 메세지에 prevTimestamp와 curTimestamp를 둬서 일관성 유지
- 새로 받은 델타 메세지의 prevTimstamp와 현재 캔버스의 curTimestamp가 일치하지 않으면 I프레임부터 새로 불러온다
- PNG 저장 용으로 AWS Elemental MediaStore를 썼는데, 어차피 대부분의 PNG Get 요청은 CDN 타기 때문에 그냥 S3 쓸 걸~하고 생각했다고 함.
- CDN의 Shielding같은 것도 사용해서 메인 스토리지로 오는 요청 수를 줄였다고 함
로드 테스트
- 이번엔 목표 유저 수가 천만 명이므로, 웹소켓 서버가 10M개의 커넥션을 핸들링할 수 있는지 테스트해야 했음
- 리눅스에서 ephermeral port는 대략 60000개라서, box 하나당 커넥션을 60000만개까지 밖에 못 연다. 커넥션 10M개 열려면 185개의 box 필요
- 로드 밸런서가 두개 떠있다고 하니, box 당 연결은 12만개까지 늘어남. ip는 적당히 하드코딩해서 DNS 서버에 휴식을 주자..
- box 한개당 네트워크 인터페이스를 여러 개 붙였다고 함. 인스턴스는 튼튼한 c4.24xlarge를 사용
- 그렇게 하여 box 수를 5개까지 줄임
LiveOps
- 문제가 있어서 중간에 배포를 한 번 했는데, 기존 연결을 섞으면서 20분동안 파드를 다시 띄우는 배포 전략 사용
- 캔버스 크기를 확장한 이후,
- 일부 웹소켓 서버 파드의 p50이 10초까지 갔었음
- 일부 느린 클라에서 서순 깨지는 일이 발생했음 (diff 프레임의 timestamp가 일치하지 않음)
- 프레임 인터벌을 100ms에서 200ms까지 늘려서 어찌저찌 해결
Metric
6.63M req/s (max edge requests)
360.3B Total Requests
2,772,967 full canvas PNGs and 5,545,935 total PNGs (diffs + full) being served from AWS Elemental MediaStore
1.5PB Bytes Transferred
99.72% Cache Hit Ratio with 99.21% Cache Coverage
726.3TB CDN Logs