Vấn đề thực tế: Cài MySQL 8 trên CentOS Stream 9 mà toàn gặp rắc rối
Hồi CentOS 8 bị EOL năm 2021, mình phải migrate gấp 5 server sang Rocky Linux trong 1 tuần — và một trong những bài học đau nhất là chuyện database. Mấy server đó dùng MariaDB từ repo mặc định, nhưng ứng dụng lại viết đặc thù cho MySQL 8. Kết quả là tốn thêm 2 ngày debug chỉ vì syntax stored procedure khác nhau giữa hai engine, JSON path expression hoạt động lạ, và một đống edge case khác mà không ai nghĩ tới trước khi migrate.
Từ đó mình rút ra rule đơn giản: setup MySQL 8 trên CentOS Stream 9 thì phải làm đúng từ đầu. Cài từ repo chính thức Oracle, giữ SELinux chạy thay vì tắt phứt đi, kiểm soát firewalld, tune InnoDB theo workload thực. Bài này ghi lại đúng cái workflow đó.
Phân tích: Tại sao dnf install mysql-server là sai lầm?
Chạy dnf install mysql-server trên CentOS Stream 9 sẽ không cài MySQL — nó cài MariaDB (fork của MySQL). Hai engine này khác nhau ở nhiều điểm quan trọng:
- JSON support: MySQL 8 có native JSON với indexing qua generated column, MariaDB implement khác
- Window functions: MySQL 8 support đầy đủ SQL:2003, MariaDB có một số điểm lệch chuẩn
- Authentication mặc định: MySQL 8 dùng
caching_sha2_passwordthay vìmysql_native_password. Client cũ như Connector/J 5.1 hay libmysqlclient < 8.0 sẽ báo lỗi kết nối ngay — phải config explicit hoặc upgrade driver - InnoDB engine: MySQL 8 có nhiều cải tiến về parallel read-ahead, adaptive hash index mà MariaDB chưa có
Nếu ứng dụng viết đặc thù cho MySQL, dùng MariaDB sẽ tạo ra những bug khó trace — đặc biệt với triggers phức tạp và JSON operations.
Các cách cài MySQL 8 trên CentOS Stream 9
Cách 1: MySQL Community Repository (Khuyến nghị)
Cách chính thức từ Oracle — repo luôn có phiên bản mới nhất, GPG key tự động verify, updates qua dnf update bình thường.
Cách 2: Tải RPM package thủ công
Phù hợp với môi trường air-gapped không có internet, nhưng phải tự quản lý dependencies và version — rất phiền khi có security patch.
Cách 3: Compile từ source
Dành cho trường hợp cần custom build flags đặc biệt (TLS library custom, non-standard path…). Trừ khi có lý do kỹ thuật cực kỳ cụ thể, đừng làm thế trên production.
Cách tốt nhất: Cài từ MySQL Official Repository
Bước 1: Thêm MySQL repository
Tải package RPM từ trang MySQL — package này tự thêm GPG key và cấu hình tất cả các channels (community, tools, connectors):
# Tải MySQL repo package cho EL9 (CentOS Stream 9 / RHEL 9)
sudo rpm -Uvh https://dev.mysql.com/get/mysql84-community-release-el9-1.noarch.rpm
# Kiểm tra repository đã active chưa
dnf repolist | grep mysql
Nếu muốn cài MySQL 8.0 thay vì 8.4, cần switch channel:
# Tắt 8.4, bật 8.0
sudo dnf config-manager --disable mysql-8.4-lts-community
sudo dnf config-manager --enable mysql80-community
Bước 2: Cài đặt MySQL Server
# Cài MySQL Community Server
sudo dnf install mysql-community-server -y
# Khởi động và enable service
sudo systemctl start mysqld
sudo systemctl enable mysqld
# Kiểm tra status
sudo systemctl status mysqld
Bước 3: Lấy temporary password và chạy secure script
MySQL 8 tự sinh password ngẫu nhiên khi khởi động lần đầu — tìm trong log:
sudo grep 'temporary password' /var/log/mysqld.log
Output dạng: A temporary password is generated for root@localhost: Xk9!mNpQ2#rL
Chạy security script — với production, mình không bỏ qua bước nào:
sudo mysql_secure_installation
- Validate password component: YES (chọn STRONG level cho production)
- Remove anonymous users: YES
- Disallow root login remotely: YES — quan trọng, không được bỏ qua
- Remove test database: YES
- Reload privilege tables: YES
Cấu hình SELinux cho MySQL 8
Phần này nhiều admin hay bỏ qua nhất, rồi xử lý nhanh bằng setenforce 0. Sai lầm kinh điển. SELinux không phải kẻ thù — nó chỉ cần được config đúng context, mất chừng 5 phút.
Kiểm tra SELinux context
# MySQL process phải chạy đúng context
ps auxZ | grep mysqld
# Expect: system_u:system_r:mysqld_t:s0
# Data directory phải có context đúng
ls -lZ /var/lib/mysql/
# Expect: system_u:object_r:mysqld_db_t:s0
Nếu dùng data directory tùy chỉnh
Trường hợp mount data sang disk riêng trên production (rất phổ biến khi volume OS và data tách biệt), cần set SELinux context trước khi MySQL khởi động:
# Tạo data directory mới
sudo mkdir -p /data/mysql
sudo chown -R mysql:mysql /data/mysql
# Set SELinux context
sudo semanage fcontext -a -t mysqld_db_t "/data/mysql(/.*)?"
sudo restorecon -Rv /data/mysql
# Thêm vào my.cnf
sudo bash -c 'echo "datadir=/data/mysql" >> /etc/my.cnf'
# Khởi tạo data directory mới
sudo mysqld --initialize --user=mysql
Cho phép MySQL port qua SELinux
# Verify port 3306 đã được allow
sudo semanage port -l | grep mysqld
# mysqld_port_t tcp 1186, 3306, 63132-63164
# Nếu dùng port khác (VD: 3307)
sudo semanage port -a -t mysqld_port_t -p tcp 3307
Cấu hình firewalld cho MySQL
Không bao giờ mở port 3306 cho 0.0.0.0 trên production — mình đã thấy một server bị scan brute-force liên tục chỉ vì admin mở MySQL ra internet để debug tạm thời rồi quên đóng lại.
Chỉ cho phép IP cụ thể kết nối
# Tạo zone riêng cho database traffic (clean hơn là add rule vào default zone)
sudo firewall-cmd --new-zone=dbzone --permanent
# Cho phép IP app server (thay 192.168.1.100 bằng IP thực)
sudo firewall-cmd --zone=dbzone --add-source=192.168.1.100/32 --permanent
sudo firewall-cmd --zone=dbzone --add-port=3306/tcp --permanent
# Reload và verify
sudo firewall-cmd --reload
sudo firewall-cmd --zone=dbzone --list-all
Debug kết nối MySQL nếu bị chặn
# MySQL đang bind port nào?
sudo ss -tlnp | grep mysqld
# Test từ app server
mysql -h <db_ip> -u appuser -p -e "SELECT 1;"
# Check firewalld có chặn packet không
sudo journalctl -u firewalld -f
Tối ưu hiệu suất MySQL 8 trên production
MySQL 8 out-of-the-box được calibrate cho máy ~1GB RAM. Server production 8GB hay 16GB mà dùng default config là phung phí tài nguyên. Mấy thông số dưới đây mình chỉnh đầu tiên trên bất kỳ production server nào:
[mysqld]
# InnoDB Buffer Pool — quan trọng nhất, set 70-80% tổng RAM
# Server 8GB RAM -> 6G, server 16GB -> 12G
innodb_buffer_pool_size = 6G
# Tăng pool instances để giảm mutex contention
# Rule: 1 instance per 1GB buffer pool, max 64
innodb_buffer_pool_instances = 6
# Redo log capacity
# MySQL 8.0.x (trước 8.0.30): dùng innodb_log_file_size = 512M
# MySQL 8.0.30+ / 8.4 LTS: biến cũ bị xóa, dùng tham số này (= log_file_size × 2)
innodb_redo_log_capacity = 1G
# Max connections theo workload (đừng set quá cao — mỗi connection tốn RAM)
max_connections = 200
# Slow query log — luôn bật trên production để phát hiện query chậm
slow_query_log = 1
slow_query_log_file = /var/log/mysql-slow.log
long_query_time = 2
# Binary log cho point-in-time recovery và replication
log_bin = /var/lib/mysql/mysql-bin
binlog_format = ROW
binlog_expire_logs_seconds = 604800
Restart MySQL sau khi chỉnh và verify:
sudo systemctl restart mysqld
# Verify innodb_buffer_pool_size đã apply đúng
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool%';"
Mình thường chạy mysqltuner sau 24 giờ load thực tế — nó phân tích dựa trên traffic thật thay vì estimate:
curl -L https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl -o mysqltuner.pl
perl mysqltuner.pl --user root --pass <password>
Tạo user database theo nguyên tắc least privilege
-- Không bao giờ để ứng dụng kết nối bằng root
CREATE USER 'appuser'@'192.168.1.100' IDENTIFIED BY 'StrongP@ssword123!';
-- Chỉ grant quyền cần thiết trên database cụ thể
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'192.168.1.100';
-- Kiểm tra lại quyền đã set đúng chưa
SHOW GRANTS FOR 'appuser'@'192.168.1.100';
Checklist trước khi đưa lên production
- MySQL service đang start và enable tự động sau reboot
mysql_secure_installationđã chạy xong- Root login remote bị block
- SELinux context của data directory đúng (
mysqld_db_t) - firewalld chỉ mở port 3306 cho IP app server cụ thể
innodb_buffer_pool_sizeset theo RAM thực tế (70-80%)- Slow query log bật
- Binary log bật nếu cần backup point-in-time
- App user tạo với quyền tối thiểu cần thiết

