GN⁺: Figma 데이터베이스 팀이 100배 규모 확장을 견뎌낸 방법
(figma.com)- Figma의 데이터베이스 팀이 Postgres 스택을 수평적으로 샤딩한 아홉 달간의 여정과 거의 무한한 확장성을 가능하게 한 방법을 정리
Figma의 Postgres 스택 수평 샤딩 여정
- Figma의 데이터베이스 스택 규모가 2020년 이후 거의 100배 증가함: 이는 비즈니스 확장을 의미하는 긍정적인 문제이지만, 동시에 기술적인 도전을 야기함. 2020년에는 AWS의 가장 큰 물리 인스턴스에서 단일 Postgres 데이터베이스를 운영했으며, 2022년 말까지 캐싱, 읽기 복제본, 그리고 수직 분할된 데이터베이스 다수를 포함하는 분산 아키텍처를 구축함.
- 수직 분할: 관련 테이블 그룹을 자체적인 수직 파티션으로 분리하여 점진적인 스케일링 이득을 얻고 성장을 앞서 나갈 수 있는 충분한 여유를 유지함. 예를 들어, “Figma 파일” 또는 “조직”과 같은 관련 테이블 그룹을 자체 수직 파티션으로 분할함.
- 수평 샤딩으로의 전환: 수직 분할만으로는 한계가 있음을 인식. CPU 사용량 감소에 초점을 맞춘 초기 스케일링 노력 이후, 더 크고 다양해진 플릿에서 다양한 병목 현상을 모니터링 시작. 데이터베이스 스케일링 한계를 CPU 및 IO부터 테이블 크기와 쓰기된 행 수까지 다양한 측면에서 정량화함. 이러한 한계를 식별하는 것은 샤드당 얼마나 많은 여유가 있는지 예측하는 데 중요함.
- 테이블 크기의 한계: 일부 테이블이 수테라바이트와 수십억 행을 담게 되어 단일 데이터베이스로는 다루기 어려운 크기에 도달함. 이러한 크기에서는 Postgres의 vacuum 작업(트랜잭션 ID가 고갈되어 중단되지 않도록 하는 필수 백그라운드 작업) 동안 신뢰성에 영향을 미침. 가장 많이 쓰기가 이루어지는 테이블들은 아마존의 관계형 데이터베이스 서비스(RDS)가 지원하는 최대 IOPS를 곧 초과할 것임. 수직 분할로는 해결할 수 없는 문제이며, 데이터베이스가 붕괴하는 것을 방지하기 위해 더 큰 해결책이 필요함.
규모 확장을 위한 발판 구축
- 개발자 영향 최소화: 복잡한 관계형 데이터 모델을 대부분 처리하여 어플리케이션 개발자가 코드베이스의 큰 부분을 리팩터링하는 대신, Figma에서 흥미로운 새 기능을 구축하는 데 집중할 수 있도록 함.
- 투명한 확장: 미래에 확장할 때 어플리케이션 계층에서 추가 변경을 하지 않아도 되도록 함. 즉, 테이블을 호환 가능하게 만들기 위한 최초의 사전 작업 후, 미래의 확장이 제품 팀에게 투명하게 이루어질 수 있도록 함.
- 비용이 많이 드는 백필(backfill) 피하기: Figma의 큰 테이블이나 모든 테이블에 대한 백필을 포함하는 해결책을 피함. 우리 테이블의 크기와 Postgres 처리량 제한을 감안할 때, 이러한 백필은 몇 달이 걸릴 것임.
- 점진적 진행: 주요 생산 변경 사항의 위험을 감소시키면서 점진적으로 출시될 수 있는 접근 방식을 식별함. 이는 주요 중단 위험을 줄이고, 데이터베이스 팀이 마이그레이션 동안 Figma의 신뢰성을 유지할 수 있도록 함.
- 일방향 마이그레이션 피하기: 물리적 샤딩 작업이 완료된 후에도 롤백할 수 있는 능력을 유지함. 이는 알려지지 않은 변수가 발생했을 때 나쁜 상태에 갇히는 위험을 줄임.
- 강력한 데이터 일관성 유지: 다운타임 없이 구현하기 어렵거나 일관성을 해치지 않으면서 복잡한 해결책, 예를 들어 더블-라이트(double-writes)를 피함. 거의 제로 다운타임으로 확장할 수 있는 해결책을 원함.
- 우리의 강점 활용: 타이트한 마감 압박하에 작업하는 동안, 가능한 한 점진적으로 출시할 수 있는 접근 방식을 선호함. 가장 빠르게 성장하는 테이블에 대해, 기존의 전문 지식과 기술을 활용하려고 함.
가능한 옵션 탐색
- 수평 샤딩 데이터베이스 옵션 검토: Postgres나 MySQL과 호환되는 수평 샤딩 데이터베이스를 위한 다양한 인기 있는 오픈 소스 및 관리형 해결책들이 있음. CockroachDB, TiDB, Spanner, Vitess를 평가하는 과정에서 검토함. 하지만, 이러한 대안 데이터베이스로 전환하는 것은 두 가지 다른 데이터베이스 저장소 간의 일관성과 신뢰성을 보장하기 위해 복잡한 데이터 마이그레이션을 요구했을 것임.
- 기존 전문성 활용: 최근 몇 년 동안 RDS Postgres를 안정적이고 효율적으로 운영하는 방법에 대한 많은 전문 지식을 개발함. 마이그레이션 중에는 도메인 전문 지식을 처음부터 다시 구축해야 했을 것임. 매우 공격적인 성장률을 고려할 때, 남아 있는 시간은 몇 달에 불과했음.
- NoSQL 데이터베이스 선택 배제: 회사가 성장함에 따라 일반적으로 선택하는 또 다른 확장 가능한 솔루션은 NoSQL 데이터베이스임. 그러나 현재 Postgres 아키텍처 위에 구축된 매우 복잡한 관계형 데이터 모델을 가지고 있으며, NoSQL API는 이러한 다양성을 제공하지 않음. 엔지니어들이 거의 모든 백엔드 어플리케이션을 재작성하는 대신 훌륭한 기능을 출시하고 새로운 제품을 구축하는 데 집중할 수 있도록 하고자 함; NoSQL은 실행 가능한 솔루션이 아님.
- 기존 RDS Postgres 인프라 위에 수평 샤딩 솔루션 구축 고려: 작은 팀이 일반적인 수평 샤딩 관계형 데이터베이스를 내부에서 재구현하는 것은 의미가 없음. 이렇게 하면 대규모 오픈 소스 커뮤니티나 전문 데이터베이스 벤더가 구축한 도구와 경쟁하게 될 것임. 그러나 Figma의 특정 아키텍처에 맞춰 수평 샤딩을 맞춤화하기 때문에, 훨씬 더 작은 기능 세트를 제공하는 것으로 충분할 수 있음. 예를 들어, 샤드 간 트랜잭션 실패를 해결할 수 있는 방법이 있기 때문에 원자성을 보장하는 샤드 간 트랜잭션을 지원하지 않기로 결정함. 어플리케이션 계층에서 필요한 변경을 최소화하는 코로케이션(colocation) 전략을 선택함. 이를 통해 제품 로직의 대다수와 호환되는 Postgres의 부분 집합을 지원할 수 있게 됨. 또한, 샤딩된 Postgres와 샤딩되지 않은 Postgres 간의 후방 호환성을 쉽게 유지할 수 있었음. 알려지지 않은 변수에 직면한 경우, 샤딩되지 않은 Postgres로 쉽게 롤백할 수 있었음.
수평 샤딩으로의 길
- 수평 샤딩의 도입: 수평 샤딩은 단일 테이블이나 테이블 그룹을 쪼개어 여러 물리적 데이터베이스 인스턴스에 데이터를 분할하는 과정임. 이 과정을 통해 어플리케이션 계층에서 수평 샤딩된 테이블은 물리 계층에서 어떤 수의 샤드도 지원할 수 있게 됨. 물리적 샤드 분할을 단순히 실행함으로써 항상 추가 확장할 수 있으며, 이러한 작업은 최소한의 다운타임과 어플리케이션 수준에서의 변경 없이 투명하게 배경에서 이루어짐. 이 기능을 통해 Figma는 남아 있는 데이터베이스 스케일링 병목 현상을 앞서나가고, Figma의 마지막 주요 스케일링 도전 과제 중 하나를 제거할 수 있게 됨. 수직 분할이 고속도로 속도로 가속할 수 있게 했다면, 수평 샤딩은 속도 제한을 제거하고 비행할 수 있게 함.
- 수평 샤딩의 복잡성: 수평 샤딩은 이전의 스케일링 노력보다 한 차원 더 복잡함. 테이블이 여러 물리적 데이터베이스에 걸쳐 분할될 때, ACID SQL 데이터베이스에서 당연하게 여겨지는 많은 신뢰성 및 일관성 속성을 잃게 됨. 예를 들어, 특정 SQL 쿼리는 지원하기 비효율적이거나 불가능해질 수 있고, 어플리케이션 코드는 쿼리를 가능한 한 올바른 샤드로 효율적으로 라우팅하기 위한 충분한 정보를 제공해야 업데이트되어야 함. 스키마 변경은 모든 샤드가 동기화되어 있도록 조정되어야 하며, 외래 키와 전역적으로 고유한 인덱스는 더 이상 Postgres에 의해 강제될 수 없음. 트랜잭션이 여러 샤드에 걸쳐 있게 되어, Postgres를 사용하여 트랜잭션을 강제할 수 없게 됨. 이제 일부 데이터베이스에 대한 쓰기 작업이 성공하는 동안 다른 작업이 실패할 수 있음. 제품 로직이 이러한 “부분 커밋 실패”에 대해 강건하도록 주의를 기울여야 함(예를 들어, 두 조직 간에 팀을 이동하는 경우, 데이터의 절반만 누락되었다고 상상해 보십시오!).
- 수평 샤딩을 향한 다년간의 노력: 전체 수평 샤딩을 달성하는 것은 다년간의 노력이 될 것임을 알고 있었음. 가능한 한 프로젝트의 위험을 줄이면서 점진적인 가치를 제공해야 함. 첫 번째 목표는 가능한 한 빨리 생산 환경에서 상대적으로 간단하지만 매우 높은 트래픽을 가진 테이블을 샤딩하는 것이었음. 이는 수평 샤딩의 실행 가능성을 증명할 뿐만 아니라 가장 많이 로드된 데이터베이스에 대한 우리의 여유를 연장할 수 있게 함. 그런 다음 더 복잡한 테이블 그룹을 샤딩하면서 추가 기능을 구축할 수 있음. 가장 간단한 가능한 기능 세트조차도 여전히 상당한 작업이었음. 처음부터 끝까지, 우리 팀은 첫 번째 테이블을 샤딩하는 데 대략 아홉 달이 걸림.
우리의 독특한 접근 방식
- 코로케이션(Colos): 관련된 테이블 그룹을 같은 샤딩 키와 물리적 샤딩 레이아웃을 공유하는 코로케이션(애정을 담아 “콜로”라고 함)으로 수평 샤딩함. 이는 개발자가 수평 샤딩된 테이블과 상호 작용할 수 있는 친근한 추상화를 제공함.
- 논리적 샤딩: 어플리케이션 계층의 “논리적 샤딩”과 Postgres 계층의 “물리적 샤딩” 개념을 분리함. 더 위험한 분산 물리적 장애 전환을 실행하기 전에 더 안전하고 비용이 적은 논리적 샤딩 출시를 수행하기 위해 뷰를 활용함.
- DBProxy 쿼리 엔진: 어플리케이션 계층에서 생성된 SQL 쿼리를 가로채고, 다양한 Postgres 데이터베이스로 쿼리를 동적으로 라우팅하는 DBProxy 서비스를 구축함. DBProxy에는 복잡한 수평 샤딩 쿼리를 파싱하고 실행할 수 있는 쿼리 엔진이 포함됨. DBProxy를 통해 동적 부하 분산 및 요청 헤징과 같은 기능을 구현할 수 있었음.
- 그림자 어플리케이션 준비성: 다양한 잠재적 샤딩 키 하에서 실제 생산 트래픽이 어떻게 동작할지 예측할 수 있는 “그림자 어플리케이션 준비성” 프레임워크를 추가함. 이는 제품 팀이 어플리케이션을 수평 샤딩에 준비하기 위해 어플리케이션 로직을 리팩터링하거나 제거해야 할 필요성에 대한 명확한 그림을 제공함.
- 전체 논리적 복제: 각 샤드로 데이터의 부분 집합만 복사하는 “필터링된 논리적 복제”를 구현할 필요가 없었음. 대신, 전체 데이터 세트를 복사한 다음, 주어진 샤드에 속하는 데이터의 부분 집합에 대해서만 읽기/쓰기를 허용함.
샤딩 구현
- 샤드 키 선택의 중요성: 수평 샤딩에서 가장 중요한 결정 중 하나는 어떤 샤드 키를 사용할 것인가임. 수평 샤딩은 샤드 키를 중심으로 여러 데이터 모델 제약을 추가함. 예를 들어, 대부분의 쿼리는 올바른 샤드로 요청을 라우팅하기 위해 샤드 키를 포함해야 함. 특정 데이터베이스 제약 조건, 예를 들어 외래 키는 외래 키가 샤딩 키일 때만 작동함. 샤드 키는 모든 샤드에 걸쳐 데이터를 고르게 분산시켜 신뢰성 문제를 일으키거나 확장성에 영향을 미치는 핫스팟을 피해야 함.
- Figma의 데이터 모델 맞춤형 접근: Figma는 브라우저에서 작동하며, 많은 사용자가 동시에 같은 Figma 파일에서 협업할 수 있음. 이는 파일 메타데이터, 조직 메타데이터, 코멘트, 파일 버전 등을 포착하는 비교적 복잡한 관계형 데이터 모델에 의해 구동됨을 의미함. 기존 데이터 모델에서 단일 좋은 후보가 없었기 때문에 모든 테이블에 대해 같은 샤딩 키를 사용하는 것을 고려했으나, 통합된 샤딩 키를 추가하기 위해 복합 키를 생성하고, 모든 테이블 스키마에 열을 추가하며, 이를 채우기 위해 비용이 많이 드는 백필을 실행하고, 그런 다음 제품 로직을 상당히 리팩터링해야 했을 것임. 대신, Figma의 고유한 데이터 모델에 맞게 접근 방식을 맞춤화하여 UserID, FileID, OrgID와 같은 소수의 샤딩 키를 선택함. Figma의 거의 모든 테이블은 이러한 키 중 하나를 사용하여 샤딩될 수 있음.
- 코로케이션(Colos)의 도입: 제품 개발자에게 친근한 추상화를 제공하는 코로케이션 개념을 도입함. 코로 내의 테이블은 단일 샤딩 키에 제한될 때 교차 테이블 조인과 전체 트랜잭션을 지원함. 대부분의 어플리케이션 코드는 이미 이 방식으로 데이터베이스와 상호 작용하고 있어, 어플리케이션 개발자가 테이블을 수평 샤딩에 대비해 수정해야 하는 작업을 최소화함.
- 데이터 분산의 균일성 보장: 샤딩 키를 선택한 후, 모든 백엔드 데이터베이스에 걸쳐 데이터의 균등한 분포를 보장해야 함. 불행히도, 선택한 많은 샤딩 키는 자동 증가 또는 Snowflake 타임스탬프 접두사 ID를 사용함. 이는 단일 샤드에 대부분의 데이터가 포함된 상당한 핫스팟을 초래했을 것임. 더 무작위화된 ID로의 마이그레이션을 탐색했으나, 이는 비용이 많이 들고 시간이 오래 걸리는 데이터 마이그레이션을 요구함. 대신, 라우팅을 위해 샤딩 키의 해시를 사용하기로 결정함. 충분히 무작위적인 해시 함수를 선택한다면, 데이터의 균일한 분포를 보장할 수 있음. 이의 한 가지 단점은 샤드 키에 대한 범위 스캔이 덜 효율적이라는 것이며, 연속적인 키가 다른 데이터베이스 샤드로 해시됨. 그러나, 이 쿼리 패턴은 우리 코드베이스에서 흔하지 않으므로, 우리가 수용할 수 있는 타협이었음.
"논리적" 해결책
- 수평 샤딩 출시의 위험 감소: 수평 샤딩 출시의 위험을 줄이기 위해, 샤드 분할을 실행하는 물리적 과정과 어플리케이션 계층에서 테이블을 준비하는 과정을 분리하고자 함. 이를 위해 "논리적 샤딩"과 "물리적 샤딩"을 분리함. 그러면 두 마이그레이션 부분을 분리하여 독립적으로 구현하고 위험을 줄일 수 있음. 논리적 샤딩은 저위험, 비율 기반 출시를 통해 서빙 스택에 대한 확신을 제공함. 버그를 발견했을 때 논리적 샤딩을 롤백하는 것은 간단한 구성 변경이었음. 물리적 샤드 작업을 롤백하는 것은 가능하지만, 데이터 일관성을 보장하기 위해 더 복잡한 조정이 필요함.
- 논리적 샤딩 후의 행동: 테이블이 논리적으로 샤딩되면, 모든 읽기와 쓰기 작업은 이미 수평 샤딩된 것처럼 작동함. 신뢰성, 지연 시간 및 일관성 측면에서 우리는 수평 샤딩된 것처럼 보이지만, 데이터는 여전히 단일 데이터베이스 호스트에 물리적으로 위치함. 논리적 샤딩이 예상대로 작동한다는 확신을 가지게 되면, 그때 물리적 샤딩 작업을 수행함. 이는 단일 데이터베이스에서 데이터를 복사하여 여러 백엔드에 샤딩한 다음, 새 데이터베이스를 통해 읽기 및 쓰기 트래픽을 다시 라우팅하는 과정임.
할 수 있는 쿼리 엔진
- 수평 샤딩 지원을 위한 백엔드 스택의 재설계: 처음에는 어플리케이션 서비스가 연결 풀링 레이어인 PGBouncer와 직접 통신했음. 그러나 수평 샤딩은 훨씬 더 복잡한 쿼리 파싱, 계획 및 실행을 요구함. 이를 지원하기 위해 새로운 golang 서비스인 DBProxy를 구축함. DBProxy는 어플리케이션 계층과 PGBouncer 사이에 위치함. 부하 분산, 향상된 관찰 가능성, 트랜잭션 지원, 데이터베이스 토폴로지 관리 및 경량 쿼리 엔진을 포함한 로직이 있음.
-
쿼리 엔진의 핵심 구성 요소:
- 쿼리 파서: 어플리케이션에서 보낸 SQL을 읽고 이를 추상 구문 트리(AST)로 변환함.
- 논리적 플래너: AST를 파싱하고 쿼리 계획에서 쿼리 유형(삽입, 업데이트 등)과 논리적 샤드 ID를 추출함.
- 물리적 플래너: 쿼리를 논리적 샤드 ID에서 물리적 데이터베이스로 매핑함. 적절한 물리적 샤드에서 실행하기 위해 쿼리를 다시 작성함.
- "scatter-gather" 방식: 데이터베이스 전체에서 숨바꼭질 게임처럼 작동함: 쿼리를 모든 샤드에 보내고(흩뿌림), 각각으로부터 답변을 모음(수집). 재미있지만, 복잡한 쿼리로 과도하게 사용하면, 데이터베이스가 달팽이처럼 느껴질 수 있음.
- 수평 샤딩된 세계에서의 쿼리 구현: 단일 샤드 쿼리는 단일 샤드 키로 필터링됨. 쿼리 엔진은 샤드 키를 추출하고 쿼리를 적절한 물리적 데이터베이스로 라우팅하기만 하면 됨. 쿼리 실행의 복잡성을 Postgres로 "내려보냄". 그러나 쿼리에서 샤딩 키가 누락된 경우, 쿼리 엔진은 더 복잡한 "scatter-gather"를 수행해야 함. 이 경우, 모든 샤드로 쿼리를 팬 아웃해야 하며(흩뿌림 단계), 그런 다음 결과를 집계해야 함(수집 단계).
- SQL 호환성의 간소화: DBProxy 서비스가 전체 SQL 호환성을 지원한다면, Postgres 데이터베이스 쿼리 엔진과 매우 유사해 보였을 것임. API를 단순화하여 DBProxy의 복잡성을 최소화하고, 지원되지 않는 쿼리를 다시 작성해야 하는 어플리케이션 개발자의 작업을 줄이고자 함. 적절한 서브셋을 결정하기 위해, 테이블의 잠재적 샤딩 스키마를 정의할 수 있고 실시간 생산 트래픽 위에서 논리적 계획 단계를 실행할 수 있는 "그림자 계획" 프레임워크를 구축함. 쿼리와 관련된 쿼리 계획을 Snowflake 데이터베이스에 로깅하여 오프라인 분석을 실행할 수 있음. 이 데이터에서, 쿼리 엔진의 최악의 경우 복잡성을 피하면서 가장 일반적인 90%의 쿼리를 지원하는 쿼리 언어를 선택함. 예를 들어, 모든 범위 스캔 및 포인트 쿼리는 허용되지만, 조인은 같은 콜로의 두 테이블 간에 샤딩 키에 대해 수행될 때만 허용됨.
미래의 전망
- 논리적 샤드의 캡슐화: 논리적 샤드를 어떻게 캡슐화할지 결정해야 했음. 별도의 Postgres 데이터베이스 또는 Postgres 스키마를 사용하여 데이터를 분할하는 것을 탐색함. 불행히도, 이는 논리적으로 샤딩할 때 물리적 데이터 변경이 필요했으며, 이는 물리적 샤드 분할만큼 복잡했음.
-
Postgres 뷰를 통한 샤드 표현: 대신, 샤드를 Postgres 뷰로 표현하기로 결정함. 각 테이블마다 여러 뷰를 생성할 수 있으며, 각각은 주어진 샤드의 데이터 부분 집합에 해당함. 이는 다음과 같은 형태를 가짐:
CREATE VIEW table_shard1 AS SELECT * FROM table WHERE hash(shard_key) >= min_shard_range AND hash(shard_key) < max_shard_range)
. 모든 읽기와 쓰기 작업은 이 뷰를 통해 이루어짐. - 기존의 미샤딩 물리적 데이터베이스 위에 샤딩된 뷰 생성: 위험한 물리적 리샤드 작업을 수행하기 전에 논리적으로 샤딩할 수 있음. 각 뷰는 자체 샤딩된 연결 풀러 서비스를 통해 접근됨. 연결 풀러는 여전히 미샤딩 물리적 인스턴스를 가리킴으로써 샤딩된 것처럼 보임. 쿼리 엔진의 기능 플래그를 통해 샤딩된 읽기와 쓰기의 출시를 점진적으로 위험을 줄이고, 주 테이블로 트래픽을 다시 라우팅하여 몇 초 내에 언제든지 롤백할 수 있었음. 첫 번째 리샤드를 실행할 때까지, 샤딩된 토폴로지의 안전성에 대해 확신을 가질 수 있었음.
- 뷰에 의존하는 위험: 뷰는 성능 오버헤드를 추가하며, 경우에 따라 Postgres 쿼리 플래너가 쿼리를 최적화하는 방식을 근본적으로 변경할 수 있음. 이 접근법을 검증하기 위해, 살균된 생산 쿼리의 쿼리 코퍼스를 수집하고, 뷰를 사용하고 사용하지 않는 상태에서 부하 테스트를 실행함. 대부분의 경우에 뷰가 최소한의 성능 오버헤드만 추가하고, 최악의 경우에도 10% 미만임을 확인할 수 있었음. 또한, 모든 실시간 읽기 트래픽을 뷰를 통해 보내고, 뷰가 있는 쿼리와 없는 쿼리의 성능 및 정확성을 비교하는 쉐도우 리드 프레임워크를 구축함. 그 결과, 뷰가 최소한의 성능 영향으로 실행 가능한 해결책임을 확인할 수 있었음.
토폴로지 문제 해결
- 쿼리 라우팅을 위한 DBProxy의 토폴로지 이해: 테이블과 물리적 데이터베이스의 토폴로지 이해 필요. 논리적과 물리적 샤딩 개념의 분리로 인해, 이러한 추상화를 토폴로지 내에 표현할 방법의 필요성.
- 테이블 및 샤드 키 매핑: 'users' 테이블을 'user_id' 샤드 키에 매핑하는 방법, 논리적 샤드 ID('123')를 적절한 논리적 및 물리적 데이터베이스에 매핑하는 방법의 필요성.
- 수직 파티셔닝과 하드코딩된 구성 파일 의존성: 수직 파티셔닝에서는 테이블을 해당 파티션에 매핑하는 간단한 하드코딩된 구성 파일에 의존. 수평 샤딩으로의 전환에서 더 복잡한 시스템 요구.
- 토폴로지의 동적 변경과 DBProxy의 신속한 상태 업데이트 요구: 샤드 분할 중 토폴로지의 동적 변경, 잘못된 데이터베이스로의 요청 라우팅을 피하기 위한 DBProxy의 상태 신속 업데이트의 필요성.
- 토폴로지 변경의 후방 호환성: 모든 토폴로지 변경이 후방 호환 가능, 사이트의 중요 경로에 있는 변경사항의 부재.
- 복잡한 수평 샤딩 메타데이터를 캡슐화하는 데이터베이스 토폴로지 구축: 복잡한 수평 샤딩 메타데이터를 캡슐화하고 1초 미만으로 실시간 업데이트를 제공하는 데이터베이스 토폴로지의 구축.
- 논리적 및 물리적 토폴로지의 분리를 통한 데이터베이스 관리의 단순화: 비생산 환경에서 생산 환경과 동일한 논리적 토폴로지를 유지하면서 물리적 데이터베이스 수를 줄임으로써 비용 절감 및 복잡성 감소.
- 토폴로지 라이브러리를 통한 토폴로지 내 불변성 강제: 모든 샤드 ID가 정확히 하나의 물리적 데이터베이스에 매핑되어야 하는 등의 토폴로지 내 불변성 강제를 통해 수평 샤딩 구축 시 시스템의 정확성 유지.
물리적 샤딩 작업
- 테이블의 샤딩 준비 완료 후 마지막 단계: 미샤딩에서 샤딩된 데이터베이스로의 물리적 장애 전환. 수평 샤딩을 위해 동일한 로직의 많은 부분을 재사용할 수 있었으나, 1대1 데이터베이스에서 1대N으로 이동하는 등 몇 가지 주목할 만한 차이점이 있었음.
- 장애 전환 과정의 내구성 강화 필요성: 샤딩 작업이 데이터베이스의 일부분에서만 성공할 수 있는 새로운 실패 모드에 대비하여 장애 전환 과정을 내구성 있게 만들어야 했음.
- 수직 파티셔닝 동안 이미 위험 요소 대부분 해결: 많은 위험 요소들이 이미 수직 파티셔닝 동안에 위험을 줄였기 때문에, 그렇지 않았다면 가능했을 것보다 훨씬 빠르게 첫 번째 물리적 샤딩 작업으로 나아갈 수 있었음.
수평 샤딩 여정의 현재 위치
- 수평 샤딩의 다년간 투자: Figma의 미래 확장성을 위한 수평 샤딩에 대한 다년간의 투자가 필요하다고 인식후 2023년 9월 첫 수평 샤딩 테이블 출시.
- 성공적인 페일오버 실행: 데이터베이스 프라이머리에 대한 일시적 부분 가용성 10초와 복제본에 대한 가용성 영향 없음으로 성공적인 페일오버 달성. 샤딩 후 지연 시간이나 가용성에서의 회귀 없음.
- 복잡한 샤드 처리: 최고 쓰기율 데이터베이스의 상대적으로 단순한 샤드 처리. 올해는 수십 개의 테이블과 수천 개의 코드 호출 지점을 가진 점점 더 복잡한 데이터베이스 샤딩 예정.
- Figma의 모든 테이블에 대한 수평 샤딩 필요: 마지막 스케일링 한계 제거와 진정한 비상을 위해. 완전히 수평 샤딩된 세계는 향상된 신뢰성, 비용 절감, 개발자 속도 증가 등의 다양한 이점 제공.
-
해결해야 할 문제들:
- 수평 샤딩된 스키마 업데이트 지원
- 수평 샤딩된 기본 키를 위한 전역적으로 고유한 ID 생성
- 비즈니스 핵심 사용 사례를 위한 원자적 샤드 간 트랜잭션
- 분산된 전역적으로 고유한 인덱스(현재 샤딩 키를 포함하는 인덱스에서만 지원)
- 수평 샤딩과 원활하게 호환되는 ORM 모델로 개발자 속도 증가
- 버튼 클릭으로 샤드 분할을 실행할 수 있는 완전 자동화된 리샤드 작업
- 기존 RDS 수평 샤딩 접근 방식의 재평가: 18개월 전 매우 타이트한 타임라인 압박 속에서 시작한 여정. NewSQL 스토어의 지속적인 발전과 성숙. 현재 경로를 유지하는 것과 오픈 소스 또는 관리형 솔루션으로 전환하는 것의 트레이드오프 재평가할 충분한 여유 확보.
- 수평 샤딩 여정에서의 흥미로운 진전: 여전히 해결해야 할 도전이 시작 단계에 있음. 수평 샤딩 스택의 다양한 부분에 대한 더 많은 심층 분석 기대. 이와 같은 프로젝트에 관심이 있는 경우, 연락 요망. 채용 중임.
GN⁺의 의견
- Figma의 데이터베이스 팀은 수평 샤딩을 통해 데이터베이스 확장성의 한계를 극복하고자 했으며, 이는 클라우드 기반 협업 툴의 성장과 성능 유지에 중요한 단계임.
- 수평 샤딩은 데이터 관리와 쿼리 최적화에서 새로운 도전을 제시하며, 이는 데이터베이스 관리자와 개발자에게 새로운 지식과 기술을 요구함.
- 수평 샤딩은 데이터베이스의 확장성을 크게 향상시키지만, 복잡한 쿼리 처리와 데이터 일관성 유지에 대한 새로운 해결책을 필요로 함.
- 이와 유사한 기능을 제공하는 오픈소스 프로젝트로는 CitusDB가 있으며, Postgres 데이터베이스를 수평적으로 확장할 수 있는 기능을 제공함.
- 수평 샤딩 기술을 도입할 때는 데이터 모델의 복잡성, 쿼리 성능, 시스템의 유연성 및 유지 관리 측면을 고려해야 하며, 이는 데이터베이스 확장성과 관리 용이성 사이의 균형을 찾는 것을 의미함.
Hacker News 의견
-
대용량 테이블과 RDS IOPS 한계
- 가장 큰 테이블이 수TB에 달하며, 곧 RDS가 지원하는 최대 IOPS를 초과할 것으로 언급됨.
- PostgreSQL용 RDS는 64TB 볼륨에서 최대 256,000 IOPS에 도달.
- 다중 AZ 설정의 경우, 이는 월 $70K의 비용 발생.
-
샤딩 결과와 비용
- 최종적으로 각 샤드가 약 50,000 IOPS와 12TB 데이터를 지원하는 5-way 샤드를 가정.
- 다중 AZ 설정의 경우, 이는 월 $100K의 비용 발생.
-
샤딩에 소요된 시간과 비용
- 첫 테이블을 샤딩하는 데 9개월이 걸림.
- 애플리케이션 변경도 필요했으므로, 9개월 * 20 근무일/월 * (3명의 DB 엔지니어 + 2명의 앱 엔지니어) = 900 근무일로 계산.
- 엔지니어의 평균 연봉이 $100K라고 가정하면, 총 비용은 약 $400K.
-
YugabyteDB와 비교한 비용
- PostgreSQL 호환 NewSQL인 YugabyteDB는 RDS의 최고 성능을 맞추는 데 월 $15K 비용 예상.
- Figma는 내부적으로 수평 샤딩을 구현하는 데 약 25배($400K/$15K)의 비용을 지출하고 여전히 RDS를 사용 중이며, 이는 약 6배($100K/$15K)의 비용.
-
고객별 데이터베이스 분리 제안
- 각 고객을 별도의 (논리적) 데이터베이스에 넣는 것이 더 쉬울 수 있음을 제안.
- 다른 고객 간의 트랜잭션이 필요하지 않으므로, 실제보다 더 어려운 문제를 해결하고 있는 것으로 보임.
- PostgreSQL의 (논리적) 데이터베이스가 잘 확장될지는 불확실하지만, 원칙적으로 불가능하지는 않음.
-
MySQL의 Vitess와 유사한 PG 버전 구축
- 쿼리 재작성이 흥미로움.
- DB와 애플리케이션 사이에 계층을 두면 다양한 ACL(액세스 제어 목록)도 가능.
-
FoundationDB에 대한 고려
- FoundationDB를 시도하지 않은 이유에 대한 궁금증.
- PostgreSQL의 vacuuming(가비지 컬렉션)에 문제가 있었음.
- 이전 버전에서는 vacuuming을 위해 두 배의 공간이 필요했으나, 최신 버전에서는 변경되었을 수 있음.
-
샤딩을 해킹처럼 다루는 접근
- 저수준 I/O 버퍼링/캐싱을 직접 처리하지 않고 OS API에 의존하는 것이 좋음.
- DB 샤딩을 위한 유사한 기술/인프라가 여전히 부족하다는 인식.
-
Citus 확장 미사용에 대한 의문
- Citus, 이미 성숙한 Postgres 확장인데, 기사에서 언급되지 않음.
- Citus를 모르거나 특정 이유로 무시했을 수 있음.
-
Aurora Limitless 사용 가능성
- Amazon Aurora Limitless를 사용할 수 있는지에 대한 질문.
-
NoSQL 데이터베이스에 대한 이해
- NoSQL은 복잡한 관계형 모델이 필요하지 않은 무정형 데이터를 수용하는 백엔드에 적합.
- Postgres는 jsonb 데이터 타입으로 이를 지원하지만, 이미 좋은 데이터 모델을 가지고 있어서 많이 사용할 필요가 없음.
-
샤딩의 성숙도와 NewSQL 솔루션 고려
- 샤딩이 성숙해진 상황에서, 자동 샤딩과 다중 지역 지원을 위해 NewSQL 솔루션을 고려할 가치가 있는지에 대한 질문.
- NewSQL 데이터베이스를 운영하는 방법을 배우는 것도 추가 비용.
-
Google의 Spanner 기술과 Figma의 평가
- Google에서는 Spanner가 무한 수평 샤딩과 트랜잭션을 지원하는 마법 같은 기술로, 거의 모든 프로젝트가 Spanner로 이동 중.
- Figma가 Cloud Spanner를 어떻게 평가했는지, 그리고 그들의 수평적 Postgres 스키마로 실제 트랜잭션 지원을 포기했는지(임시적으로라도) 궁금증을 표함.