Tối ưu hiệu năng Database: Hướng dẫn thực tế về Read Replicas trong PostgreSQL và MySQL

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Cuộc gọi lúc 2:15 sáng: Khi Database chạm ngưỡng giới hạn

PagerDuty của tôi đổ chuông vào lúc 2:15 sáng thứ Ba. Tôi mở laptop và phát hiện instance PostgreSQL chính của chúng tôi—một node AWS db.m5.2xlarge—đang bị treo ở mức 99% CPU. Thời gian phản hồi của ứng dụng đã vọt lên từ 150ms lên đến 20 giây đáng kinh ngạc. Chúng tôi không chỉ bị chậm; chúng tôi gần như đã ngừng hoạt động hoàn toàn.

Một email marketing vừa được gửi tới 50.000 khách hàng. Hàng ngàn người dùng truy cập dashboard cùng lúc, kích hoạt các truy vấn SELECT phức tạp để xem lịch sử đơn hàng và nhật ký hoạt động. Đồng thời, các lượt đăng ký mới cũng đổ dồn vào cùng một database với các thao tác INSERT. Một node duy nhất không thể chịu đựng nổi áp lực kết hợp từ lượng ghi lớn và khối lượng đọc khổng lồ.

Hầu hết các database quan hệ đều có một điểm gãy chung. Một node đơn lẻ có chu kỳ CPU và băng thông I/O hữu hạn. Đêm đó, rõ ràng là mở rộng theo chiều dọc (vertical scaling) không còn đủ nữa. Chúng tôi cần tách biệt lưu lượng truy cập bằng chiến lược Read Replica trước khi đợt cao điểm sáng hôm sau ập đến.

Bẫy tranh chấp tài nguyên

Trong một thiết lập instance đơn lẻ cơ bản, mọi truy vấn đều cạnh tranh cho cùng một bộ nhớ và tài nguyên đĩa I/O. Các engine hiện đại như PostgreSQL và MySQL rất hiệu quả, nhưng chúng vẫn phải đối mặt với xung đột cơ bản giữa khóa ghi (write locks) và hiệu năng đọc.

Các truy vấn phân tích chạy lâu thường giữ các khóa chia sẻ (shared locks) hoặc tiêu tốn một lượng lớn RAM để sắp xếp. Khi một thao tác ghi tần suất cao xuất hiện, nó phải đợi trong hàng đợi, gây ra hiện tượng thắt nút cổ chai lan tỏa ra toàn bộ ứng dụng. Bạn có thể nâng cấp lên một server lớn hơn, nhưng cuối cùng bạn sẽ chạm đến điểm mà việc tăng gấp đôi hóa đơn cloud hàng tháng từ 800 USD lên 1.600 USD chỉ mang lại 10% hiệu năng. Đây chính là ngõ cụt của việc mở rộng theo chiều dọc.

Lựa chọn phương án mở rộng

Chúng tôi đã đánh giá ba cách chính để xử lý tải:

  • Vertical Scaling: Tăng RAM và CPU. Đây là cách sửa lỗi dễ nhất nhưng chi phí cao và yêu cầu downtime trong quá trình thay đổi kích thước.
  • Sharding: Phân tán dữ liệu trên nhiều node chính. Dù mạnh mẽ nhưng nó tạo ra sự phức tạp cực độ về kiến trúc mà hầu hết các đội ngũ không cần tới cho đến khi đạt quy mô dữ liệu hàng petabyte.
  • Read Replicas: Tạo các bản sao chỉ đọc (read-only) đồng bộ với node chính. Cách này giúp giảm khoảng 80% lưu lượng web thông thường cho node chính và cung cấp một mục tiêu dự phòng (failover) để đảm bảo tính sẵn sàng cao.

Triển khai Read Replicas trong PostgreSQL

PostgreSQL sử dụng Streaming Replication. Server chính truyền các bản ghi Write-Ahead Log (WAL) tới replica, sau đó replica sẽ áp dụng chúng trong thời gian thực. Đây là cấu hình chúng tôi đã sử dụng để ổn định môi trường.

1. Cấu hình Node chính (Primary)

Đầu tiên, chỉnh sửa tệp postgresql.conf để cho phép các kết nối replication và xác định việc lưu trữ dữ liệu:

# Kích hoạt các tính năng replication
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on

Tiếp theo, cập nhật pg_hba.conf để cho phép địa chỉ IP cụ thể của replica kết nối:

# Cho phép IP của replica (ví dụ: 10.0.0.5) kết nối
host replication replication_user 10.0.0.5/32 md5

2. Khởi tạo Replica

Trên server phụ, sử dụng công cụ pg_basebackup để sao chép dữ liệu từ node chính. Lệnh này tạo ra một bản cơ sở để replica bắt đầu theo dõi luồng log của node chính:

bash
pg_basebackup -h primary-ip -D /var/lib/postgresql/15/main -U replication_user -P -R

Flag -R là phần quan trọng nhất. Nó tạo ra tệp standby.signal, yêu cầu Postgres khởi động ở chế độ chỉ đọc và theo dõi node chính ngay lập tức.

Triển khai Read Replicas trong MySQL

MySQL thường sử dụng Binary Log (binlog) để replication. Node chính ghi lại mọi thay đổi và replica sẽ lấy các sự kiện này để thực thi lại tại địa phương.

1. Thiết lập Node chính

Thêm các dòng sau vào tệp my.cnf hoặc mysqld.cnf của bạn:

[mysqld]
server-id = 1
log-bin = mysql-bin
# Mỗi node phải có một server-id duy nhất

2. Thiết lập Replica

Gán một ID duy nhất và kích hoạt relay log trên server replica:

[mysqld]
server-id = 2
relay-log = replica-relay-bin
read_only = 1

Cuối cùng, liên kết replica với node chính thông qua SQL:

sql
CHANGE MASTER TO
  MASTER_HOST='primary-ip',
  MASTER_USER='replica_user',
  MASTER_PASSWORD='mat_khau_bao_mat',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=154;
START SLAVE;

Cơ sở hạ tầng mới chỉ là bước đầu tiên

Việc khởi tạo một replica không tự động giải quyết được vấn đề hiệu năng. Nếu bạn không cập nhật mã nguồn ứng dụng, database chính của bạn vẫn sẽ duy trì ở mức 100% CPU trong khi replica thì rảnh rỗi. Bạn phải điều hướng các truy vấn của mình một cách rõ ràng.

Cách tiếp cận đáng tin cậy nhất là định nghĩa hai kết nối database riêng biệt trong backend: một Writer cho các yêu cầu POST/PUT/DELETE và một Reader cho các yêu cầu GET.

Ví dụ logic trong Python (SQLAlchemy)

Thay vì sử dụng một session toàn cục, hãy triển khai một router để chọn engine dựa trên thao tác:

python
class DatabaseRouter:
    def __init__(self, primary, replica):
        self.primary = primary
        self.replica = replica

    def get_engine(self, is_write=False):
        return self.primary if is_write else self.replica

# Cách sử dụng
# Tạo một bản ghi
engine = db_router.get_engine(is_write=True)
# Lấy dữ liệu
engine = db_router.get_engine(is_write=False)

Thực tế về độ trễ sao chép (Replication Lag)

Việc sao chép thường là không đồng bộ (asynchronous). Điều này có nghĩa là có một khoảng trễ nhỏ—thường từ 10ms đến 500ms—giữa lúc dữ liệu được ghi vào node chính và lúc nó xuất hiện trên replica. Chúng ta gọi đây là Replication Lag.

Nếu người dùng cập nhật cài đặt và bạn ngay lập tức chuyển hướng họ đến trang hồ sơ đọc dữ liệu từ replica, họ có thể thấy dữ liệu cũ. Điều này dẫn đến các yêu cầu hỗ trợ gây nhầm lẫn. Để ngăn chặn điều này, chúng tôi điều hướng các truy vấn “Read-Your-Own-Writes” về node chính trong khoảng thời gian 15 giây sau bất kỳ thao tác cập nhật nào.

Kết quả

Chuyển sang kiến trúc Read Replica đã thay đổi mọi thứ cho cơ sở hạ tầng của chúng tôi. Bằng cách chuyển lưu lượng SELECT nặng nề sang hai replica nhỏ hơn, CPU của database chính đã giảm từ 99% xuống mức ổn định 15%. Độ trễ truy vấn cho người dùng giảm từ vài giây xuống dưới 100ms.

Nếu database của bạn đang gặp khó khăn, hãy xem xét tỷ lệ giữa đọc và ghi. Hầu hết các ứng dụng web có 80-90% là thao tác đọc. Giảm tải khối lượng đó là cách bền vững nhất để phát triển. Bạn sẽ tiết kiệm được tiền phần cứng và quan trọng hơn, cuối cùng bạn sẽ có thể ngủ ngon suốt đêm.

Share: