IT

64. MySQL 샤딩을 미들웨어에 맡기다. (2·완) — Vitess 아키텍처와 도입 판단 기준

Hans L 2026. 8. 9. 15:33

원문: Scaling YouTube's Backend: The Vitess Trade-offs — @Scale 2014 (Sugu Sougoumarane)
이 글은 위 발표를 재구성한 2부작 중 2편(해법편) 입니다.


1편 요약: 우리는 여기까지 왔다

1편에서는 단일 MySQL이 무너지는 과정을 따라갔습니다. 배송 시스템(TMS)과 물류센터 입고 시스템(WMS Inbound)은 서로 완전히 분리된 별개 시스템이지만, 각자의 작업 로그 테이블이 독립적으로 똑같은 벽에 부딪혔습니다. 수십억 행·수백 GB에 도달한 테이블에서는 ALTER TABLE 한 번이 수 시간짜리 작업이 되고, 그때 발생한 I/O 부하가 현장 준장애로 이어지기도 했습니다.

그리고 그 벽 앞에서 선택지는 전부 대가를 요구했습니다.

  • 레플리카를 붙이면 → 데이터 최신성을 잃는다
  • 파티셔닝·Prime Cache로 버티면 → 시간만 벌 뿐 근본 해결이 아니다
  • 샤딩을 하면 → 크로스 샤드 트랜잭션과 자유로운 JOIN을 잃는다

그리고 마지막 두 개의 문제가 남습니다.

  1. 샤딩의 복잡도를 애플리케이션이 떠안게 되면, 기능 개발자의 실수 한 번이 장애가 됩니다.
  2. 시스템이 분리되어 있으면 그 복잡도를 각 팀이 따로 감당하게 됩니다. TMS와 WMS가 각자 라우팅 로직, 크로스 샤드 조회, 리샤딩 절차, 운영 도구를 만들게 되는 것입니다.

Vitess는 이 복잡도를 애플리케이션에서 빼앗아 미들웨어로 옮기는 프로젝트입니다. 그리고 그 미들웨어는 여러 시스템이 공유할 수 있습니다.


Vitess의 전체 구조

핵심은 애플리케이션이 여전히 "하나의 큰 MySQL"에 접속하는 것처럼 보인다는 점입니다.

VTGate — 앱이 보는 단일 진입점

애플리케이션은 그냥 MySQL 프로토콜로 VTGate에 접속합니다. 어느 샤드에 데이터가 있는지, 여러 샤드에 흩어진 결과를 어떻게 합칠지는 VTGate가 처리합니다.

1편에서 봤던 지저분한 코드가 이렇게 돌아옵니다.

// Before — TMS 애플리케이션이 직접 샤딩을 감당 (1편 예시)
class DeliveryWorkLogRepository(
    private val shards: List<DataSource>,
    private val primary: DataSource,
) {
    fun findByDriver(driverId: Long, requireFresh: Boolean): List<DeliveryWorkLog> {
        val shardId = (driverId % shards.size).toInt()          // 샤드 라우팅
        val ds = if (requireFresh) primary else shards[shardId] // 신선도 판단
        return ds.query("SELECT * FROM delivery_work_log WHERE driver_id = ?", driverId)
    }
}

// After — 평범한 JDBC 코드로 복귀
class DeliveryWorkLogRepository(private val vtgate: DataSource) {
    fun findByDriver(driverId: Long): List<DeliveryWorkLog> =
        vtgate.query("SELECT * FROM delivery_work_log WHERE driver_id = ?", driverId)
    // 샤드 라우팅: VSchema 설정으로 VTGate가 처리
    // 신선도 정책: 접속 대상(primary/replica)을 커넥션 레벨에서 분리
}

 

샤딩 정책이 코드에서 설정으로 옮겨간 것이 가장 큰 변화입니다. 샤드 수를 4개에서 16개로 늘려도 리포지토리 코드는 그대로입니다.

🚚 여기서 두 시스템이 분리되어 있다는 점이 오히려 유리해집니다. 1편에서 TMS는 driver_id, WMS는 center_id 축으로 샤드 키가 갈린다고 했는데, Vitess에서는 이게 keyspace별 VSchema 설정의 차이일 뿐입니다. 시스템별로 다른 샤딩 전략을 가져가면서도, 그것을 굴리는 인프라와 운영 도구는 공유할 수 있습니다.

VTTablet — MySQL 앞의 경호원

각 MySQL 인스턴스 앞에 붙는 사이드카입니다. 단순 프록시가 아니라 MySQL을 보호하는 장치에 가깝습니다.

주요 기능을 정리하면 이렇습니다.

  • 커넥션 풀링: MySQL 커넥션 하나하나가 메모리를 먹습니다. 애플리케이션 서버 수천 대가 직접 붙으면 MySQL이 메모리로 죽습니다. VTTablet이 제한된 풀로 흡수합니다.
  • 쿼리 안전장치: LIMIT 없는 위험한 쿼리를 차단하거나 한도를 강제로 붙입니다. 문제 쿼리 블랙리스트도 가능합니다.
  • 타임아웃 / 쿼리 킬러: 오래 도는 쿼리를 자동으로 죽입니다.
  • 트랜잭션 수 제한: 동시 트랜잭션 상한을 걸어 MySQL이 무너지는 것을 막습니다.
  • 중복 쿼리 결과 재사용: 동일한 쿼리가 동시에 여러 개 들어오면 첫 번째가 끝날 때까지 나머지를 대기시켰다가 결과를 공유합니다. 캐시 스탬피드 방지와 같은 원리입니다.

💡 발표에서 가장 인상적인 관점: "개발자는 실수한다는 전제로 시스템을 설계한다."
선의를 가진 엔지니어도 LIMIT 빠뜨린 쿼리를 배포합니다. 코드 리뷰로 100% 막으려 하는 대신, 인프라 계층에서 사고의 폭발 반경을 제한한 것입니다. 조직이 커질수록 "규율"보다 "구조"가 이깁니다.

🚚 물류 시스템에서도 같습니다. 정산 배치가 delivery_work_log를 조건 없이 훑어 운영 DB를 흔드는 일은, 리뷰 프로세스보다 쿼리 레벨 가드레일로 막는 편이 훨씬 확실합니다. 게다가 이 가드레일은 TMS와 WMS가 각자 만들 필요 없이 같은 계층에서 얻어집니다.

포기한 것: MySQL 100% 호환

Vitess는 VTGate와 VTTablet에서 자체 SQL 파서를 씁니다. 쿼리를 이해해야 라우팅하고 검사할 수 있기 때문입니다. 그 대가로 MySQL의 모든 엣지 케이스 문법을 지원하지는 않습니다.

일반적인 애플리케이션 쿼리는 대부분 커버되지만, "MySQL이니까 다 되겠지"는 성립하지 않습니다. 도입 전 POC에서 기존 쿼리 전수 검증이 필수 작업이 됩니다. 특히 레거시 배치의 복잡한 서브쿼리나 벤더 특화 문법이 지뢰입니다.


운영 자동화 — 사람이 못 버티는 규모

인스턴스가 수천 대가 되면, 5분짜리 수동 작업도 재앙이 됩니다.

Reparenting — 프라이머리 승격

프라이머리가 죽었을 때 사람이 해야 하는 일을 생각해 봅시다.

 

Vitess는 topology(락 서버)와 워크플로우 컴포넌트로 이 과정을 자동화합니다. 계획된 작업은 PlannedReparentShard, 장애 상황은 EmergencyReparentShard로 처리하며, 현재는 VTOrc가 감지와 복구를 담당합니다.

온라인 리샤딩 — 다운타임 없이 샤드 쪼개기

발표 당시 기준으로도 상당히 자동화되어 있던 부분입니다.

 

핵심 설계는 다운타임을 최소화하고, 사람이 판단해야 하는 지점(④)만 남긴다는 것입니다. 백업도 tablet이 스스로 수행하므로 서비스를 내릴 필요가 없습니다.

🚚 이 부분이 실무에서 가장 큰 차이를 만듭니다. 직접 샤딩을 구현하면 "샤드를 늘리는 작업"이 매번 대형 프로젝트가 됩니다. 무중단 리샤딩을 직접 만드는 비용을 생각하면, Vitess의 학습 비용이 상대적으로 싸 보이는 지점이 옵니다.


2014년 발표 이후, 지금의 Vitess는

이 발표는 10년이 넘었습니다. 지금 Vitess를 검토한다면 달라진 점을 알아야 합니다.

  • rowcache(memcached 기반 행 캐시)는 제거되었습니다. 발표에서 비중 있게 다뤄지지만 Vitess 2.0에서 deprecated 후 삭제됐습니다. 지금은 MySQL 버퍼 풀과 애플리케이션 레벨 캐시를 쓰는 것이 정석입니다.
  • VReplication이 도입되면서 리샤딩, 테이블 이동(MoveTables), 외부 DB에서의 마이그레이션이 하나의 엔진 위에서 동작합니다.
  • VTOrc가 장애 감지와 복구를 담당합니다.
  • Online DDL을 지원합니다. 1편에서 다룬 "수 시간짜리 ALTER TABLE이 현장 준장애를 부른 사건" 에 대한 직접적인 답입니다. 다만 오해하면 안 되는 지점이 있습니다. Vitess의 Online DDL이 마법을 부리는 게 아니라, 샤딩으로 각 인스턴스가 감당할 데이터가 작아진 덕분입니다. 수백 GB 테이블 하나를 16개 샤드로 나누면 각 샤드의 스키마 변경은 수십 GB짜리 작업이 되고, 그마저도 샤드별로 순차 진행하며 트래픽을 뺀 상태에서 돌릴 수 있습니다. 즉 "큰 작업을 빠르게" 하는 게 아니라 "작은 작업 여러 번"으로 바꾸는 것입니다.
  • CNCF Graduated 프로젝트이며, PlanetScale이 관리형 서비스로 제공합니다. Slack, Square 등이 프로덕션에서 사용합니다.

정리하면, 발표의 문제 정의와 트레이드오프 사고방식은 그대로 유효하지만 구체적 구현은 상당 부분 바뀌었습니다. 발표 영상만 보고 아키텍처를 결정하면 안 되는 이유입니다.


그래서 우리 팀은 Vitess를 써야 할까?

솔직하게 말하면, 대부분의 국내 서비스는 아직 필요 없습니다.

🚚 여러 시스템이 같은 벽에 부딪혔다면, 판단이 조금 달라집니다.
1편에서 본 것처럼 TMS와 WMS Inbound는 별개 시스템이지만 각자 같은 한계에 도달했습니다. 이럴 때 계산이 바뀝니다.

  • 시스템이 하나뿐이라면: Vitess의 학습·운영 비용을 한 시스템이 온전히 부담합니다. 그만한 가치가 있는지 회의적으로 볼 만합니다.
  • 여러 시스템이 같은 문제를 겪는다면: 인프라와 운영 노하우를 공유하면서 비용을 나눠 부담할 수 있습니다. 반대로 각자 샤딩을 구현하면 같은 고민과 같은 실수를 두 번 반복하게 됩니다.

다만 조직 구조가 변수입니다. 두 시스템의 소유 팀이 다르면 "이 공용 인프라는 누가 운영하는가" 가 기술 문제보다 먼저 풀려야 하는 문제가 됩니다. 플랫폼 조직이 없는 상태에서 공용 미들웨어를 도입하면, 결국 어느 한 팀이 떠안고 나머지는 사용자가 되는 구도가 되기 쉽습니다.

 

대안도 반드시 함께 보세요

선택지 강점 약점
Aurora 스케일업 + 리드 레플리카 운영 부담 최소 쓰기·데이터 크기 한계는 그대로
MySQL 파티셔닝 + 아카이빙 도입 비용 낮음 단일 서버 한계
Vitess MySQL 생태계 유지 + 무제한 확장 컴포넌트 학습·운영 비용
TiDB / CockroachDB 분산 트랜잭션 기본 제공 마이그레이션 비용, 생태계 차이
ShardingSphere JDBC 레벨로 가볍게 시작 운영 자동화는 직접

 

Vitess의 진짜 비용은 라이선스나 서버가 아니라 운영 역량입니다. VTGate, VTTablet, topology, VTOrc를 이해하고 새벽 장애에 대응할 사람이 팀에 있어야 합니다. 그 사람이 없다면 관리형 서비스를 쓰거나, 도입 시점을 미루는 편이 정직한 판단입니다.


마치며

이 발표가 10년 넘게 회자되는 이유는 Vitess라는 제품 때문이 아니라, "확장은 문제를 없애는 게 아니라 옮기는 것" 이라는 사실을 정직하게 보여주기 때문이라고 생각합니다.

레플리카를 붙이면 일관성 문제가 생기고, 샤딩을 하면 트랜잭션을 잃고, 미들웨어를 넣으면 운영 부담이 생깁니다. 좋은 아키텍처란 문제가 없는 설계가 아니라, 어떤 문제를 감당할지 의식적으로 고른 설계입니다.

수년째 쌓여만 가는 그 테이블들 앞에서 우리가 해야 할 질문도 같습니다. "무엇을 도입할까"가 아니라 "무엇을 포기할 수 있는가" 입니다.

TMS와 WMS Inbound는 서로 다른 시스템이지만, 결국 같은 질문 앞에 섰습니다. 스키마 변경 한 번이 현장을 멈출 수 있는 상태를 계속 안고 갈 것인가, 아니면 트랜잭션과 JOIN의 자유를 내주고 다음 3년을 살 것인가. YouTube의 답이 우리 답일 필요는 없지만, 답을 고르는 방식은 배울 만합니다.


참고 자료

Thanks

Hans

반응형