Zero-downtime Postgres migrations - the hard parts

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

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

Bối cảnh

  • GoCardless là công ty thanh toán: API ngừng nhận request thì merchant mất tiền. DB không được ngừng trả lời quá 1–2 giây.
  • Các quy tắc đã biết từ trước: không đổi tên cột/bảng đang dùng (copy rồi xoá sau); không rewrite bảng khi giữ khoá độc quyền (ví dụ thêm cột có DEFAULT ... NOT NULL); không làm việc đắt khi giữ khoá độc quyền (tạo index phải có CONCURRENTLY).
  • Dù vậy, một migration "vô hại" vẫn gây ~15 giây downtime API: đổi tên các bảng mới (đang rỗng, chưa có code dùng), rồi xoá và tạo lại foreign key.

Nguyên nhân

  • Thêm foreign key khiến Postgres lấy khoá AccessExclusive trên cả bảng được tham chiếu (dù có NOT VALID), và bảng được tham chiếu lại là một trong những bảng nóng nhất.
  • Một query đọc chạy lâu đang giữ AccessShare trên bảng đó, nên lệnh ALTER TABLE phải xếp hàng chờ. Mọi query khác trên bảng (kể cả SELECT thường) xếp sau lệnh đang chờ, vì xung đột với AccessExclusive. Bản thân lệnh ALTER rất nhanh; chính việc chờ khoá mới gây downtime.
  • Chạy lại migration trên bản backup thì chỉ mất vài trăm ms, vì không có query dài nào chạy cùng lúc.

Thay đổi chính (khuyến nghị của GoCardless)

  • Loại bỏ query và transaction chạy lâu khỏi app; phân tích thì chạy trên replica bất đồng bộ. Bật log_min_duration_statement và log_lock_waits để phát hiện sớm.
  • Đặt lock_timeout trong script migration ở mức app chịu được: thà huỷ deploy còn hơn làm sập app.
  • Chia nhỏ thay đổi schema: dễ chẩn đoán hơn, transaction quanh DDL ngắn hơn.
  • Cập nhật Postgres (khoá được cải thiện qua mỗi bản; từ 9.4, VALIDATE CONSTRAINT dùng khoá nhẹ hơn).
  • Cảnh báo: một developer mở BEGIN; SELECT ... trên console production rồi đi pha trà cũng có thể gây downtime khi có người deploy schema.

Bài học cho SWE

  • Thứ gây sự cố là việc xếp hàng chờ khoá, không phải bản thân câu lệnh. Hàng đợi khoá biến một lệnh nhanh thành downtime.
  • "Bảng rỗng nên an toàn" là giả định sai khi ràng buộc chạm tới một bảng nóng khác.
  • Schema migration là một dạng deploy, cũng cần giới hạn thiệt hại (lock_timeout). Xem Deploy và release an toàn.
  • Bổ sung cho bài sự cố của chính GoCardless (GoCardless 2017).

Liên quan