Pushing Postgres beyond storage: scaling performance at Optiver

Bài tóm tắt Optiver 2026 3. Quy mô lớn Độ khó: Khó

Optiver · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bối cảnh

  • Một số database của Optiver chứa bảng nhỏ đổi rất thường xuyên, như cấu hình và dữ liệu giá, nằm thẳng trên đường đi của các hệ tự động; có thể có hàng nghìn process cần đồng bộ với chúng.
  • Các lựa chọn bị loại (theo Optiver): polling gây trễ hoặc tải thừa; đặt một service trước DB ép mọi ghi qua một đường và đổi cách mọi người dùng dữ liệu; thêm một hệ streaming bên cạnh mang theo một hệ nữa với đảm bảo riêng và khoảng hở giữa lúc ghi và lúc thấy.

Thay đổi chính

  • Giữ đường ghi đơn giản: client ghi thẳng vào Postgres bằng transaction thường, không lớp điều phối. Postgres đã có log đầy đủ mọi thay đổi (WAL), nên bài toán thành "biến WAL thành thứ dùng được".
  • Logical decoding + output plugin tự viết: WAL thô mô tả byte nào trên page nào đổi; logical decoding giải mã thành insert/update/delete. Postgres đã giải mã theo ngữ cảnh transaction, đưa ranh giới begin/commit cho plugin và bỏ phần bị rollback, nên plugin chỉ việc dịch thay đổi đã commit sang giao thức riêng.
  • PG feed: hàng nghìn consumer thì không thể mỗi người một replication slot (mỗi slot giữ trạng thái riêng). Một thành phần nội bộ đọc qua một đường decoding và chuyển tiếp cho client subscribe theo bảng; lọc và phân phối là phần lõi. Thiết kế hẹp có chủ đích: không nằm trên đường ghi, không giữ bản sao DB, chỉ theo bảng (không join, không view), vì cái gì không có trong WAL thì với feed là không tồn tại.
  • Bài toán snapshot + stream: client lấy trạng thái hiện tại rồi nhận cập nhật từ đó, nhưng hai bước chồng nhau về thời gian; chuyển ngay thì client thấy sai thứ tự, trì hoãn thiếu ngữ cảnh thì mất hoặc trùng. PG feed đọc bảng trong transaction Repeatable Read và lấy pg_current_snapshot() cùng lúc, rồi với mỗi thay đổi từ luồng replication quyết định nó đã nằm trong snapshot hay phải gửi sau. Snapshot và luồng được khâu lại ở tầng ứng dụng.

Vì sao

  • Lợi ích chính ở phía ghi: không cần lớp điều phối hay phát event riêng, các hệ khác vẫn phản ứng với latency thấp.
  • Giới hạn (theo Optiver): bảng lớn làm snapshot ban đầu đắt và chậm cho subscriber mới; throughput bị chặn bởi đường logical decoding và việc mỗi slot là một luồng có thứ tự (chia nhiều decoder theo nhóm bảng thì đỡ nhưng vẫn có trần); chỉ có dữ liệu trong WAL.

Bài học cho SWE

  • CDC từ WAL cho phép phát thay đổi mà không đổi cách ghi, tránh dual-write.
  • Fan-out tới hàng nghìn subscriber cần một tầng phân phối riêng thay vì mỗi consumer một slot.
  • "Snapshot rồi stream" là chỗ khó nhất; dùng thông tin hiển thị của transaction (snapshot xmin/xmax) để khâu chính xác.
  • Biết rõ hình dạng dữ liệu mà thiết kế hợp (bảng nhỏ, đổi nhiều, nhiều người đọc) và dừng lại ở đó.

Liên quan