Upgrading a 56TB database that's growing at 250GB per day with (nearly) no downtime
Bài tóm tắt Trigger.dev 2026 3. Quy mô lớn Độ khó: Nâng cao
Trigger.dev · 2026 · 3. Quy mô lớn · Độ khó: Nâng cao — bài gốc
Bối cảnh
- Một cụm Aurora Postgres khoảng 56 TB: bảng run 29,4 TB, các bảng liên quan tới run 25,4 TB, dữ liệu control plane (tổ chức, project, billing, deploy) chỉ 1,23 TB. Tăng 250 GB/ngày.
- Không có giờ thấp điểm (chạy background job của người khác, nhiều múi giờ); một run có thể kéo dài nhiều tháng; khách mất doanh thu nếu downtime.
- Bắt buộc nâng cấp Postgres 14.15 → 14.22 trước 31/7. Bản nhỏ nhưng phải reboot, mà mọi thứ nằm trong một cụm, nên reboot là sập cả ứng dụng.
- Chép live cả 56 TB thì nguy cơ hàng đợi ghi không bao giờ cạn. Theo Trigger.dev, nhân dịp này họ chuyển sang PlanetScale Postgres Metal: NVMe cục bộ (vacuum không tranh I/O với production như trên ổ mạng của Aurora), đổi tham số không cần reboot cả cụm, nhà cung cấp lo nâng cấp. Nhưng đĩa lớn nhất lúc đó là 60 TB, nên bắt buộc phải chia dữ liệu.
Thay đổi chính
- Bỏ lại dữ liệu nguội: 96,8% run đã xong, và 99% run xong trong 8,4 ngày. Run cũ ở lại Aurora; run mới tạo ở DB mới; chỉ chép 1,23 TB control plane. Kết quả là ba DB: Aurora (run cũ), PlanetScale control plane, PlanetScale RunOps (run mới).
- Định tuyến theo ID: run mới có định dạng ID mới (24 ký tự + ký tự region + ký tự version), nên ứng dụng biết run nằm ở DB nào trước khi query; chừa chỗ cho định tuyến về sau.
- Chuẩn bị:
- 22/6 mọi truy cập run đi qua một "run store" (vẫn trỏ Aurora);
- 4/7 chạy 358 test, không có lỗi mới;
- để bắt đường code bị quên vẫn ghi vào Aurora, họ đổi credential Aurora sang tài khoản không có quyền ghi rồi chạy lại toàn bộ test;
- 14/7 tách xong; chuyển dần từng tổ chức trong giờ thấp điểm của họ; 24/7 PlanetScale thành mặc định cho run mới.
- Công tắc là PgBouncer (transaction mode, tự chạy vì Aurora không có pooler managed trỏ được sang PlanetScale): tạm dừng kết nối, trỏ lại backend, thả ra.
- Chép control plane bằng pgcopydb: chép xong trong 2 giờ 11 phút nhưng CDC kẹt 15 giờ ở 0 MB/s. Buffer chỉ flush cuối transaction hoặc khi đạt 512 MB, và libpq chép lại cả buffer liên tục (99% CPU vào
memmove). Họ vá C để flush mỗi 1 MB; PlanetScale vá thêm bộ lọc bảng không cần chép. - Công cụ cutover chỉ 35 dòng, kèm khoảng 17.500 dòng để làm nó an toàn: giả lập mọi phụ thuộc, 124 kiểm tra preflight, chế độ diễn tập, runbook. Khối
finallybảo đảm thất bại thì PgBouncer trỏ về Aurora rồi mới thả kết nối. - Cutover lần 1 (26/7) thất bại: 105 giây lỗi rồi tự rollback nhờ
finally. Nguyên nhân: kết nối cũ trong PgBouncer giữ username cũ (pool theo cặp DB và username), PlanetScale từ chối. Diễn tập không bắt được vì diễn tập không đổi username, và lần test duy nhất có đổi thì gõ nhầm.- Rollback lại sinh lỗi phụ: ID kiểu dữ liệu tuỳ biến được cache từ PlanetScale không có nghĩa trên Aurora.
- Ghi trong lúc cutover nằm ở PlanetScale mà không có ở Aurora; vì đó chỉ là session nên họ xoá sạch PlanetScale và chép lại (cách duy nhất đã được kiểm chứng).
- Cutover lần 2 (cùng tối) xong trong vài giây. 30/7 nâng cấp Aurora: 90 giây request vào run cũ lỗi, nhưng run vẫn chạy tiếp nhờ retry.
Bài học (theo Trigger.dev)
- Diễn tập phải giữ nguyên kết nối đang mở qua lần chuyển: PgBouncer không trong suốt khi DB bên dưới đổi.
- Tiếp theo: tách DB cho run đang chạy và run đã xong, partition theo ngày để có thời hạn lưu.
Bài học cho SWE
- Tách nóng/lạnh để khỏi phải dời dữ liệu lạnh: dữ liệu mới vào DB mới, dữ liệu cũ ở lại, định tuyến bằng ID. Cùng ý với Etsy (vindex lai, không dời dữ liệu) và Twilio (tách theo vòng đời) (Sharding và tách database).
- Mẫu cutover với pooler tạm dừng kết nối +
finallyquay về là cách làm chuẩn, như GoCardless và Vitess (Migration không downtime). - Thu quyền ghi của DB cũ là cách rẻ để tìm đường ghi bị quên.
- Lý do đổi nhà cung cấp DB ở đây là I/O và vận hành (vacuum tranh I/O trên ổ mạng, reboot khi đổi tham số), không phải tính năng (Chọn database theo workload).
Liên quan
Được nhắc tới trong
Công ty (1)
Bài tóm tắt (3)