Series Docker & Database cho Laravel #3: Kiến trúc Master-Slave và Nghệ thuật Phân tách Đọc/Ghi (Trạm cuối)
1. Tư duy Phân mảnh: Ai làm việc nấy
Mô hình Master-Slave (hoặc Primary-Replica) giải quyết bài toán hiệu năng bằng cách chia nhỏ trách nhiệm:
- Master (Máy chủ Chủ): Sẽ là nơi DUY NHẤT nhận các lệnh làm thay đổi dữ liệu (INSERT, UPDATE, DELETE).
- Slave (Máy chủ Tớ): Sẽ là nơi chuyên phục vụ các lệnh truy xuất dữ liệu (SELECT).
Mọi thay đổi xảy ra trên Master sẽ được ghi vào một file nhật ký (Binary Log). Slave sẽ liên tục "nhìn" vào file này và sao chép (Replicate) y hệt dữ liệu sang bên mình với độ trễ tính bằng mili-giây.
2. Dựng cụm Cluster bằng Docker Compose
Trong thực tế vận hành DevOps, người ta thường dùng các Image được cấu hình sẵn cho việc Replication (như của Bitnami). Cấu trúc docker-compose.yml lúc này sẽ chứa 2 Container độc lập:
version: '3.8'
services:
# 1. MÁY CHỦ MASTER (Chuyên Ghi)
db_master:
image: bitnami/mysql:8.0
container_name: afc_mysql_master
environment:
- MYSQL_REPLICATION_MODE=master
- MYSQL_REPLICATION_USER=repl_user
- MYSQL_REPLICATION_PASSWORD=ReplSecret@2026
- MYSQL_ROOT_PASSWORD=RootSecret@2026
- MYSQL_DATABASE=afc_metro_db
- MYSQL_USER=afc_app_user
- MYSQL_PASSWORD=AppSecret@2026
ports:
- "3306:3306"
volumes:
- master_data:/bitnami/mysql/data
networks:
- afc_network
# 2. MÁY CHỦ SLAVE (Chuyên Đọc)
db_slave:
image: bitnami/mysql:8.0
container_name: afc_mysql_slave
depends_on:
- db_master
environment:
- MYSQL_REPLICATION_MODE=slave
- MYSQL_MASTER_HOST=db_master
- MYSQL_MASTER_USER=repl_user
- MYSQL_MASTER_PASSWORD=ReplSecret@2026
- MYSQL_MASTER_ROOT_PASSWORD=RootSecret@2026
ports:
- "3307:3306" # Mở port khác để Dev dễ test
volumes:
- slave_data:/bitnami/mysql/data
networks:
- afc_network
volumes:
master_data:
slave_data:
networks:
afc_network:
driver: bridge
Chỉ với file cấu hình này, khi cụm Docker khởi động, máy db_slave sẽ tự động bám đuôi máy db_master. Nếu có 10.000 user đang xuất báo cáo Excel, toàn bộ CPU của máy Slave sẽ gánh chịu, trong khi máy Master vẫn thảnh thơi mở cổng cho hành khách quẹt vé.
3. Laravel Read/Write Splitting: Phép màu của Framework
Cụm Database đã sẵn sàng, nhưng làm sao để code Laravel biết khi nào thì nên chọc vào Master, khi nào thì chọc vào Slave?
Rất may, Laravel hỗ trợ kiến trúc này tận răng. Bạn không cần phải sửa bất kỳ dòng code logic hay Eloquent nào cả. Tất cả chỉ nằm ở file config/database.php.
Mở cấu hình mysql của bạn lên và biến đổi nó như sau:
'mysql' => [
'driver' => 'mysql',
// Khai báo cụm kết nối ĐỌC (Slave)
'read' => [
'host' => [env('DB_SLAVE_HOST', '127.0.0.1')], // Có thể truyền 1 mảng nhiều IP nếu có nhiều Slave
],
// Khai báo cụm kết nối GHI (Master)
'write' => [
'host' => [env('DB_MASTER_HOST', '127.0.0.1')],
],
// Các thông số dùng chung cho cả Master và Slave
'sticky' => true, // RẤT QUAN TRỌNG (Sẽ giải thích bên dưới)
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
// ...
],
Sau khi cấu hình, phép màu sẽ tự động xảy ra:
- Khi gọi Transaction::where('status', 'success')->get(); -> Laravel tự động kết nối vào DB_SLAVE_HOST.
- Khi gọi Transaction::create([...]); -> Laravel tự động kết nối vào DB_MASTER_HOST.
4. Thuốc giải cho "Replication Lag" (Độ trễ đồng bộ)
Khi triển khai kiến trúc này, 99% các kỹ sư chưa đủ kinh nghiệm sẽ dính một lỗi chí mạng: Replication Lag.
Kịch bản lỗi: Hành khách quẹt thẻ nạp 500.000đ vào tài khoản. Laravel đẩy lệnh UPDATE vào Master. Ngay mili-giây tiếp theo, màn hình ứng dụng gọi lệnh SELECT để hiển thị số dư mới. Laravel ngoan ngoãn chọc vào Slave để lấy số dư. Tuy nhiên, do đường truyền vật lý, Slave cần khoảng 50ms để đồng bộ số dư 500.000đ từ Master sang. Hành khách nhìn thấy số dư cũ, tưởng nạp lỗi, tiếp tục bấm nạp thêm lần nữa -> Lỗi hệ thống nghiêm trọng.
Giải pháp Chuẩn Senior: Đó chính là dòng 'sticky' => true mà chúng ta đã cấu hình ở bước 3.
Khi bật cờ sticky (dính), Laravel sẽ ghi nhớ hành vi trong một Vòng đời Request (Request Lifecycle). Nếu nó phát hiện ra bạn vừa thực hiện một lệnh Ghi (Write) vào Database, nó sẽ lập tức "dán nhãn" Request đó lại. Mọi câu lệnh Đọc (Read) diễn ra ngay sau đó trong cùng một chu kỳ sẽ bị Laravel ép buộc phải đọc thẳng từ Master, bỏ qua Slave.
Nhờ vậy, hành khách luôn nhìn thấy dữ liệu mới nhất do chính họ vừa tạo ra, trong khi các tác vụ đọc dữ liệu của người khác (như admin xem báo cáo) vẫn được phân bổ đều đặn xuống Slave.
Lời kết Series Docker & Database
Chúng ta đã chính thức hoàn thiện bộ khung xương vững chắc nhất cho trái tim lưu trữ của hệ thống:
- Bảo vệ dữ liệu không bị bốc hơi nhờ Volumes và cấp quyền tự động (Bài 1).
- Ép xung tối đa sức mạnh của InnoDB và chiến lược Backup không gián đoạn (Bài 2).
- Vượt qua giới hạn vật lý bằng kiến trúc Master-Slave và nghệ thuật phân tách luồng Đọc/Ghi với tuỳ chọn sticky sắc sảo (Bài 3).
Sự kết hợp giữa Nginx (Web Server) chặn đứng dội bom, Redis (Cache/Queue) xử lý bất đồng bộ, ELK (Log) giám sát 24/7, và một cụm Database phân tán (Replication) đã tạo nên một kiến trúc Backend hoàn hảo. Hệ thống của bạn giờ đây đã sẵn sàng đứng vững trước mọi luồng truy cập khổng lồ, xử lý dữ liệu chuẩn xác và mang lại trải nghiệm không độ trễ!
All rights reserved