Vì Sao Replication Lag Khiến Tôi Mất Ngủ
Sáu tháng trước, một sự cố production đã giáng một đòn mạnh vào nhóm của tôi. Người dùng đọc phải dữ liệu cũ — đơn hàng hiển thị đang chờ xử lý dù thanh toán đã được xác nhận. Nguyên nhân gốc rễ? Replication lag trên các MySQL read replica đã âm thầm tăng lên hơn 40 giây trong một đợt traffic tăng đột biến. Chúng tôi không có cảnh báo, không có khả năng theo dõi, và không có runbook nào cả.
Tôi đã làm việc với MySQL, PostgreSQL và MongoDB qua hàng chục dự án. Mỗi hệ thống có điểm mạnh riêng. Nhưng replication lag không quan tâm bạn chọn database nào — đây là kiểu lỗi xuất hiện vào lúc bạn ít mong đợi nhất, thường là trong đợt traffic tăng đột biến lúc 11 giờ đêm. Hướng dẫn này là thứ tôi ước mình đã có trước sự cố đó.
Bước Đầu Tiên: Kiểm Tra Độ Trễ Ngay Bây Giờ
Trước bất cứ điều gì khác, hãy mở terminal và chạy một trong các lệnh dưới đây. Chỉ mất năm phút. Đừng bỏ qua bước này.
MySQL: Kiểm Tra Trạng Thái Replica
SSH vào replica server và chạy:
SHOW REPLICA STATUS\G
-- hoặc trên các phiên bản MySQL cũ hơn:
SHOW SLAVE STATUS\G
Hai con số quan trọng nhất:
- Seconds_Behind_Source (hoặc
Seconds_Behind_Master) — số giây replica đang chậm hơn primary. Bất kỳ giá trị nào trên 5 giây trong production đều cần được chú ý. - Replica_SQL_Running — phải là
Yes. Nếu làNo, replication đã dừng hoàn toàn.
PostgreSQL: Kiểm Tra Replication Lag
Trên server primary, truy vấn view replication tích hợp sẵn:
SELECT
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
(sent_lsn - replay_lsn) AS replication_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
Trên chính replica, cách kiểm tra đơn giản hơn:
SELECT
now() - pg_last_xact_replay_timestamp() AS replication_delay;
Kết quả NULL có nghĩa replica chưa replay bất kỳ transaction nào. Trên hệ thống bận rộn, đây là dấu hiệu cần điều tra ngay lập tức.
Nguyên Nhân Thực Sự Gây Ra Replication Lag
Áp dụng sai biện pháp khắc phục sẽ tốn hàng giờ. Sau khi truy tìm vấn đề này trên nhiều hệ thống, tôi nhận thấy lag xuất phát từ ba nguyên nhân riêng biệt:
1. Đợt Tăng Đột Biến Write Throughput
Primary nhận writes nhanh hơn khả năng replica có thể áp dụng. Các tác vụ bulk import và batch job chạy trong giờ làm việc thường là thủ phạm. Mặc định, SQL thread của replica áp dụng các transaction theo tuần tự từng cái một — chính nút thắt cổ chai tuần tự đó mới là nguyên nhân gây ra sự cố dưới tải nặng.
2. Transaction Chạy Lâu Trên Replica
MySQL: một câu SELECT chạy lâu trên replica chiếm metadata lock, chặn SQL thread không áp dụng được các bản cập nhật. PostgreSQL: hot_standby_feedback = on khiến primary giữ lại các phiên bản hàng cũ, trong khi các truy vấn chạy lâu trên standby tạo ra backlog riêng. Cả hai trường hợp đều trông giống lag nhưng cần biện pháp khắc phục khác nhau.
3. Tắc Nghẽn Mạng và I/O
Disk I/O chậm trên replica gây ra write lag. Đường mạng bão hòa khiến binary log hoặc WAL bị ùn tắc ở giai đoạn gửi. PostgreSQL giúp xác định dễ dàng — các cột write_lag, flush_lag và replay_lag trong pg_stat_replication cho bạn biết chính xác thời gian đang bị mất ở đâu.
Xác Định Giai Đoạn Nào Đang Chậm (PostgreSQL)
-- So sánh từng giai đoạn lag riêng biệt
SELECT
application_name,
write_lag, -- thời gian ghi WAL vào OS buffer của replica
flush_lag, -- thời gian flush WAL xuống disk trên replica
replay_lag -- thời gian áp dụng các thay đổi
FROM pg_stat_replication;
write_lag cao? Vấn đề mạng. flush_lag cao? Disk I/O trên replica. replay_lag cao? Có thể do áp lực CPU hoặc các truy vấn chạy lâu đang chặn việc áp dụng WAL.
Giám Sát Tự Động: Vì Kiểm Tra Thủ Công Lúc 3 Giờ Sáng Không Khả Thi
Bạn không thể SSH vào replica mỗi giờ. Đây là cách để hệ thống tự gửi cảnh báo cho bạn.
Prometheus + postgres_exporter
Cài postgres_exporter trên mỗi replica. Công cụ này đã tích hợp sẵn metric pg_replication_lag.
docker run -d \
--name postgres-exporter \
-e DATA_SOURCE_NAME="postgresql://monitor_user:password@localhost:5432/postgres?sslmode=disable" \
-p 9187:9187 \
quay.io/prometheuscommunity/postgres-exporter
Sau đó thêm alert rule trong Prometheus:
groups:
- name: postgres_replication
rules:
- alert: PostgresReplicationLagHigh
expr: pg_replication_lag > 30
for: 2m
labels:
severity: warning
annotations:
summary: "Replication lag đang ở mức {{ $value }}s trên {{ $labels.instance }}"
Script Giám Sát Replication Lag cho MySQL
Một script Python bạn có thể chạy như cron job hoặc tích hợp vào hệ thống cảnh báo hiện có:
import pymysql
import sys
REPLICA_DSN = {
"host": "replica-host",
"user": "monitor",
"password": "secret",
"database": "information_schema",
}
LAG_THRESHOLD_SECONDS = 10
def check_replica_lag():
conn = pymysql.connect(**REPLICA_DSN)
with conn.cursor(pymysql.cursors.DictCursor) as cur:
cur.execute("SHOW REPLICA STATUS")
row = cur.fetchone()
if not row:
print("LỖI: Không có trạng thái replica — đây có thực sự là replica không?")
sys.exit(2)
sql_running = row.get("Replica_SQL_Running") or row.get("Slave_SQL_Running")
lag = row.get("Seconds_Behind_Source") or row.get("Seconds_Behind_Master") or 0
if sql_running != "Yes":
print(f"NGHIÊM TRỌNG: SQL thread không chạy")
sys.exit(2)
if lag > LAG_THRESHOLD_SECONDS:
print(f"CẢNH BÁO: Replication lag là {lag}s (ngưỡng: {LAG_THRESHOLD_SECONDS}s)")
sys.exit(1)
print(f"OK: Replication lag là {lag}s")
sys.exit(0)
if __name__ == "__main__":
check_replica_lag()
Bật Parallel Replication trong MySQL
Write throughput cao? Chuyển sang multi-threaded replication. Một thay đổi duy nhất này đã giúp chúng tôi giảm lag 80% trong các đợt batch import ban đêm — đây là tinh chỉnh có tác động lớn nhất có thể thực hiện trên replica ghi nặng:
-- Trên replica (cần STOP REPLICA trước)
STOP REPLICA;
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL replica_preserve_commit_order = ON;
START REPLICA;
PostgreSQL: Tinh Chỉnh max_wal_senders và wal_keep_size
Replica tụt hậu quá xa có thể mất đi các WAL segment cần thiết để bắt kịp — lúc đó replication bị đứt và cách duy nhất là đồng bộ lại toàn bộ. Hãy thiết lập một vùng đệm để ngăn điều này xảy ra:
# postgresql.conf trên primary
wal_keep_size = 1024 # giữ lại 1GB WAL, điều chỉnh theo tốc độ ghi của bạn
max_wal_senders = 5
wal_level = replica
Replication slot cung cấp đảm bảo mạnh hơn, nhưng kèm theo đánh đổi thực sự: nếu replica offline vài ngày, primary sẽ giữ WAL vô thời hạn và disk sẽ đầy. Hãy giám sát WAL được giữ lại trước khi triển khai slot trên production:
-- Tạo physical replication slot trên primary
SELECT pg_create_physical_replication_slot('replica_slot_1');
-- Giám sát WAL được giữ lại bởi slot
SELECT slot_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
Bài Học Thực Tế Sau 6 Tháng Trên Production
1. Không Bao Giờ Đọc Dữ Liệu Nhạy Cảm Từ Replica Mà Không Kiểm Tra Lag
Trạng thái thanh toán, số lượng tồn kho, quyền người dùng — những dữ liệu này phải lấy từ primary, hoặc phải qua kiểm tra ngưỡng lag trước:
def get_replication_delay(conn):
with conn.cursor() as cur:
cur.execute("SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))")
return cur.fetchone()[0] or 0
if get_replication_delay(replica_conn) < 5:
# an toàn để đọc từ replica
result = replica_conn.execute(query)
else:
# fallback về primary
result = primary_conn.execute(query)
2. Lên Lịch Các Tác Vụ Batch Nặng Ngoài Giờ Cao Điểm
Các tác vụ bulk insert và data migration nên chạy trong khung giờ ít traffic — 2–4 giờ sáng, không phải 2 giờ chiều. Với MySQL, hãy chia nhỏ các tác vụ LOAD DATA INFILE lớn thành các batch nhỏ hơn với một khoảng SLEEP(0.1) giữa chúng. Điều này cho replica không gian để thở thay vì ngày càng tụt hậu thêm.
3. Theo Dõi Các Truy Vấn Chạy Lâu Trên Replica
Các truy vấn phân tích trên PostgreSQL standby có thể chặn WAL replay trong nhiều phút. Hãy chuyển hướng các báo cáo nặng sang một replica chuyên dụng, hoặc đặt giới hạn cứng cho role analytics:
ALTER ROLE analytics_user SET statement_timeout = '30s';
4. Viết Runbook, Không Chỉ Đặt Cảnh Báo
Cảnh báo không có runbook chỉ là tiếng ồn lúc 3 giờ sáng. Người nhận cảnh báo đó cần biết chạy những lệnh nào trước và quy trình leo thang ra sao. Sau sự cố của chúng tôi, chúng tôi đã viết REPLICATION_LAG_RUNBOOK.md và đính kèm trực tiếp vào nội dung cảnh báo PagerDuty. Không ai phải đoán mò nữa.
5. Thực Hành Replica Failover Trước Khi Cần Dùng
Replication chỉ hữu ích nếu bạn có thể promote replica dưới áp lực. Hãy thực hiện failover drill trong môi trường staging — pg_ctl promote cho PostgreSQL, STOP REPLICA; RESET REPLICA ALL; cho MySQL. Đảm bảo toàn bộ đội on-call đã thực hành ít nhất một lần. Lần đầu tiên không nên xảy ra trong lúc có sự cố thật.
Cần Làm Gì Trước Đợt Tăng Đột Biến Tiếp Theo
Replication lag ẩn náu cho đến khi bùng phát. Nó vô hình với chúng tôi suốt nhiều tuần trước sự cố 40 giây đánh thức nửa đội lúc nửa đêm. Cả PostgreSQL và MySQL đều cung cấp đủ các metric nội bộ để phát hiện sớm — nhưng chỉ khi có người thực sự theo dõi chúng.
Bắt đầu với các lệnh kiểm tra nhanh ở trên. Thiết lập giám sát trong tuần này, không phải sprint sau. Viết runbook đó trước khi bạn cần đến nó. Bản thân bạn lúc 3 giờ sáng sau này sẽ biết ơn điều đó.

