Skip to main content 집밥서선생

r/place

Published: 2025-09-16

레딧블로그 (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

전체 보드 조회

  • 앞의 캐싱 관련 내용에서 대부분 설명

타일 색칠

  1. 카산드라에서 마지막 타일 배치 timestamp를 읽음. 5분 이내이면 400
  2. 레디스와 카산드라에 write (현재 timestamp, 메타데이터, 색상 등등)
  3. 웹소켓으로 이벤트 브로드캐스트
카산드라의 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

© 2026 JHSeo