0

Series Docker & Database cho Laravel #2: Ép xung Cỗ máy InnoDB và Chiến lược Backup Không Downtime

1. Nỗi đau "Too many connections" và "Disk I/O Bottleneck"

Kịch bản kinh điển: Vào lúc 7h sáng, lượng khách đi tàu tăng vọt. Nginx và PHP-FPM của bạn đã được scale rất tốt để nhận hàng ngàn request. Nhưng khi PHP cố gắng mở kết nối xuống MySQL, lỗi SQLSTATE[HY000] [1040] Too many connections xuất hiện đỏ rực trên Kibana.

Nguyên nhân là do MySQL mặc định chỉ cho phép 151 kết nối đồng thời.

Hơn nữa, MySQL mặc định chỉ cấp cho bộ đệm InnoDB (nơi chứa dữ liệu và index trên RAM) khoảng 128MB. Nếu bảng transactions của bạn phình to đến 2GB, MySQL sẽ phải liên tục đọc/ghi dữ liệu tạm xuống ổ cứng vật lý, tạo ra nút thắt cổ chai cực kỳ khủng khiếp.

2. Can thiệp cấu hình Lõi (my.cnf)

Để giải quyết, chúng ta không thể chui vào container gõ lệnh (vì sẽ mất khi restart). Nhớ lại ở Bài 1, chúng ta đã mount một file cấu hình:

  • ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf

Bây giờ, hãy mở file ./docker/mysql/my.cnf trong source code của bạn và bơm "Steroid" cho nó:

[mysqld]
# 1. ÉP XUNG KẾT NỐI (Connection Tuning)
# Tăng số lượng kết nối tối đa lên để chịu tải giờ cao điểm
max_connections = 1000
# Giảm thời gian chờ để giải phóng nhanh các kết nối rác (mặc định 28800s - 8 tiếng)
wait_timeout = 600
interactive_timeout = 600

# 2. TỐI ƯU HÓA BỘ NHỚ RAM (InnoDB Tuning)
# QUAN TRỌNG NHẤT: Cấp 60-70% tổng RAM của Server cho InnoDB Buffer Pool. 
# Giả sử Server của bạn có 4GB RAM -> Set khoảng 2.5GB (2560M). 
# Toàn bộ dữ liệu quẹt thẻ sẽ được xử lý trên RAM với tốc độ ánh sáng!
innodb_buffer_pool_size = 2G

# Tăng kích thước log file để giảm tần suất ghi xuống ổ cứng vật lý (giảm Disk I/O)
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M

# 3. TỐI ƯU HÓA QUERY (Query Tuning)
# Giới hạn kích thước gói tin gửi đi (Hữu ích khi lưu/truy xuất file JSON lớn)
max_allowed_packet = 64M

# 4. CHỐNG MẤT DỮ LIỆU TỐI ĐA (Durability)
# Đảm bảo mỗi giao dịch commit đều được ghi xuống đĩa ngay lập tức
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

Chỉ cần khởi động lại container bằng lệnh docker-compose restart db, MySQL sẽ nạp file cấu hình này và biến thành một con quái vật thực sự, sẵn sàng nuốt trọn hàng ngàn giao dịch quẹt thẻ mỗi giây mà CPU vẫn không bị nghẽn.

3. Vấn đề Connection Pooling ở tầng Laravel

Cấu hình Database xịn là chưa đủ nếu Laravel dùng sai cách.

Mặc định, mỗi lần có một Request API bay vào, Laravel sẽ mở một kết nối (TCP Handshake) tới MySQL, thực hiện câu lệnh SQL, rồi đóng kết nối đó lại. Hành động Mở/Đóng này mất từ 10-20ms. Nếu có 1000 request, bạn lãng phí một lượng thời gian khổng lồ.

Chuẩn Senior: Hãy bật tính năng Persistent Connections (Kết nối bền vững) của PDO trong Laravel.

Mở file config/database.php, tìm đến mảng mysql và thêm option này:

'mysql' => [
            'driver' => 'mysql',
            // ...
            'options' => [
                // Yêu cầu PHP-FPM giữ lại kết nối MySQL sau khi dùng xong để dùng cho Request tiếp theo!
                PDO::ATTR_PERSISTENT => env('DB_PERSISTENT', true), 
            ],
        ],

Nhờ PDO::ATTR_PERSISTENT, các worker của PHP-FPM sẽ tái sử dụng lại đường ống TCP đã kết nối với MySQL, giảm thiểu tối đa độ trễ mạng và CPU cho quá trình Handshake.

4. Chiến lược Backup "Live" (Không gián đoạn)

Dữ liệu là mạng sống. Bạn phải có chiến lược sao lưu (Backup) tự động hàng ngày. Tuy nhiên, việc dừng Container MySQL lại để copy thư mục /var/lib/mysql là điều cấm kỵ vì nó gây gián đoạn hệ thống (Downtime).

Cách an toàn nhất là sử dụng lệnh mysqldump ngay bên trong container đang chạy, xuất ra file SQL, và đưa nó ra ngoài ổ cứng của Server gốc.

Bạn có thể tạo một bash script backup.sh chạy bằng Cronjob trên Server vật lý:

#!/bin/bash

# Khai báo biến
CONTAINER_NAME="afc_mysql"
DB_USER="root"
DB_PASSWORD="SecretMetro@2026"
DB_NAME="afc_metro_db"
BACKUP_DIR="/opt/backups/mysql"
DATE=$(date +'%Y-%m-%d_%H-%M-%S')

# Tạo thư mục nếu chưa có
mkdir -p $BACKUP_DIR

# Thực thi lệnh mysqldump xuyên thẳng vào trong Container mà KHÔNG CẦN DỪNG Container
# Cờ --single-transaction cực kỳ quan trọng: Nó đảm bảo dữ liệu toàn vẹn mà không khóa (lock) các bảng. 
docker exec $CONTAINER_NAME /usr/bin/mysqldump -u $DB_USER -p$DB_PASSWORD --single-transaction --quick --lock-tables=false $DB_NAME | gzip > "$BACKUP_DIR/afc_backup_$DATE.sql.gz"

# Tự động xóa các file backup cũ hơn 7 ngày để tránh đầy ổ cứng
find $BACKUP_DIR -type f -name "*.sql.gz" -mtime +7 -exec rm {} \;

echo "Backup Database thành công lúc $DATE"

Khi cronjob này chạy vào lúc 2h sáng mỗi ngày, nó sẽ âm thầm vắt toàn bộ dữ liệu ra một file nén cực nhẹ mà người dùng đang thao tác quẹt vé ban đêm hoàn toàn không cảm nhận được bất kỳ sự gián đoạn nào.

Bằng việc làm chủ các thông số cốt lõi của InnoDB và thiết lập cơ chế Backup Live, hệ thống Database của bạn giờ đây đã đạt tiêu chuẩn Vận hành 24/7 của một hệ thống Metro.

Để kết thúc chuỗi kiến trúc Database này ở Bài 3, bạn muốn chúng ta đi sâu vào việc cấu hình Replication (Mô hình Master-Slave) để chia tách việc đọc/ghi (Read/Write Splitting) ở nhiều Container, hay muốn tìm hiểu cách tự động hóa quá trình chạy Migration & Seeding thẳng vào Docker bằng CI/CD Pipeline?


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.