Herding elephants: Lessons learned from sharding Postgres at Notion

Bài tóm tắt Notion 2021 2. Tăng trưởng Độ khó: Trung bình

Notion · 2021 · 2. Tăng trưởng · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Một Postgres monolith (RDS) đã chạy 5 năm qua 4 bậc độ lớn tăng trưởng. Đến giữa 2020 rõ ràng nó sẽ không theo kịp.
  • Điểm gãy: VACUUM liên tục bị kẹt, không thu hồi được chỗ của dead tuple. Đáng sợ hơn là nguy cơ TXID wraparound: Postgres sẽ ngừng mọi lệnh ghi để tránh ghi đè dữ liệu. Theo Notion, đó là "mối đe doạ sống còn".
  • Theo Notion, scale dọc bằng nút "Resize Instance" không bền: hiệu năng và các tiến trình bảo trì xuống cấp trước khi chạm trần phần cứng.

Thay đổi chính

  • Shard ở tầng ứng dụng, không dùng Citus hay Vitess, vì muốn kiểm soát cách phân bố dữ liệu và không muốn logic cluster "mờ". Họ cũng đã cân nhắc và loại DynamoDB (quá rủi ro) và Postgres trên NVMe bare-metal (tốn công backup và replication).
  • Shard gì: bảng block cùng mọi bảng nối tới block qua khoá ngoại (space, discussion, comment...). Lý do: nếu block và comment nằm khác DB thì xoá block không còn nằm trong một transaction.
  • Khoá shard: workspace ID. Mỗi block thuộc đúng một workspace, và user thường chỉ truy vấn trong một workspace, nên tránh được phần lớn join xuyên shard.
  • Dung lượng (dự phóng 2 năm): cần ít nhất 60K IOPS; mỗi bảng tối đa 500 GB, mỗi DB vật lý tối đa 10 TB. Họ chọn 480 shard logic trên 32 DB vật lý (mỗi shard là một Postgres schema, 15 schema mỗi DB). 480 chia hết cho rất nhiều số, nên có thể đi 32 → 40 → 48 host thay vì phải gấp đôi như 512.
  • Bảng riêng cho mỗi schema thay vì partitioned table, để chỉ có một nguồn sự thật cho định tuyến workspace → shard, nằm trong ứng dụng.
  • Migration:
    1. Ghi kép qua audit log: ghi mọi thay đổi vào một bảng audit, một script catch-up áp sang shard. Logical replication không theo kịp lượng ghi vào block ở bước snapshot ban đầu. Họ chuẩn bị sẵn audit log ngược để quay về monolith.
    2. Backfill: khoảng 3 ngày trên máy 96 CPU, so version bản ghi để không ghi đè dữ liệu mới hơn.
    3. Kiểm chứng: script so ngẫu nhiên các dải UUID; thêm dark read (đọc cả hai, so sánh, vứt bản shard) dù tăng latency API. Người viết logic migration và người viết logic kiểm chứng là hai người khác nhau.
    4. Chuyển: bảo trì 5 phút có thông báo.

Bài học (Notion tự nêu)

  • Shard sớm hơn: chờ tới khi DB quá tải nên mọi thao tác migration phải rất tiết kiệm. Không backfill được cột workspace ID vào DB cũ (sẽ thêm tải), không dùng được logical replication.
  • Nhắm tới không downtime: nút cổ chai là throughput của script catch-up. Thêm một tuần tối ưu để catch-up dưới 30 giây thì đã có thể chuyển ở load balancer mà không downtime.
  • Gộp khoá: bảng shard giờ dùng khoá kép (id, space_id), nên phải truyền space_id khắp ứng dụng. Đã full scan thì nên gộp thành một khoá mới.
  • Chú thích của Notion: shard quá sớm có thể khoá cứng mô hình sản phẩm, ví dụ shard theo user rồi sản phẩm chuyển sang theo team.

Bài học cho SWE

  • Tín hiệu shard của Postgres thường là vacuum và TXID wraparound, không phải CPU. Figma gặp đúng vấn đề này (Figma 2023).
  • Chọn khoá shard theo đơn vị sở hữu dữ liệu (workspace) và shard trọn cụm bảng liên quan để giữ transaction, giống Slack shard theo workspace (Sharding và tách database).
  • Số shard logic nên có nhiều ước số để thêm host từng bước nhỏ.
  • Khung migration chuẩn: ghi kép → backfill có so version → kiểm chứng (dark read) → chuyển; luôn có đường lui (Migration không downtime).

Liên quan