- 1. Khi nào Kiến trúc Mở rộng Dọc (Scale-Up) Chạm Trần Vật lý trên Hệ thống MySQL Chịu tải Cao?
- 2. Kiến trúc Tầng Trung gian ProxySQL v2.7+: Connection Multiplexing và Phân tách Đọc/Ghi (Read/Write Splitting)
- 2.1. Cơ chế Connection Multiplexing và Điều kiện Bắt buộc để Không Bị Khóa Ghép Kênh
- 2.2. Cấu hình Khai báo /etc/proxysql.cnf và Luật Định tuyến Read/Write Splitting Chuẩn Production
- 3. Triệt tiêu Độ trễ Sao chép trên MySQL 8.4 LTS (WRITESET Parallel Replication) và Đọc Nhất quán Nhân quả (GTID)
- 3.1. Tinh chỉnh Sao chép Đa luồng Dựa trên Tập Ghi (WRITESET) trên MySQL 8.4 LTS
- 3.2. Đảm bảo Đọc Nhất quán Sau Khi Ghi (Read-Your-Own-Writes) bằng WAIT_FOR_EXECUTED_GTID_SET
- 4. Chiến lược Phân vùng Bảng (Declarative Partitioning) và Phân mảnh Ngang (Horizontal Sharding) với UUIDv7
- 4.1. Cấp độ 1: Phân vùng Bảng theo Thời gian (PARTITION BY RANGE) trên Cùng Một Máy chủ
- 4.2. Cấp độ 2: Phân mảnh Ngang Đa Máy chủ (Horizontal Sharding với Vitess / Application-Level Routing)
- 5. Chẩn đoán Trực tiếp Sự cố Tranh chấp Khóa (data_locks), Deadlock và Nghẽn Truy vấn Chậm trên Production
- 5.1. Truy vấn Bảng performance_schema.data_locks và data_lock_waits trên MySQL 8.4 LTS
- 5.2. Kịch bản Kiểm định Hiệu quả Ghép kênh ProxySQL và Phân tích Slow Query bằng pt-query-digest
- 6. Tài liệu Tham khảo & Nguồn Đối chiếu Chính thống (Primary References)
1. Khi nào Kiến trúc Mở rộng Dọc (Scale-Up) Chạm Trần Vật lý trên Hệ thống MySQL Chịu tải Cao?
Sau khi đội ngũ Kỹ sư Cơ sở dữ liệu (DBA) đã hoàn tất việc tinh chỉnh bộ nhớ đệm trang và thiết kế chỉ mục trên một máy chủ đơn lẻ theo hướng dẫn tại bài tối ưu hiệu năng MySQL 8.0 và InnoDB Buffer Pool, hệ thống sẽ đạt tới giới hạn vật lý của kiến trúc Mở rộng Dọc (Vertical Scale-Up) khi quy mô lưu lượng vượt ngưỡng 10,000+ kết nối TCP đồng thời hoặc dung lượng bảng giao dịch vượt quá 2–5 TB. Ở quy mô này, ngay cả máy chủ sở hữu 128 vCPU và 512 GB RAM trên nền triển khai cụm Kubernetes High Availability trên hạ tầng Bare Metal cũng sẽ đối mặt với bốn điểm nghẽn cổ chai cấp hệ thống phân tán:
- Bão Chuyển Ngữ cảnh Luồng Kết nối (Connection Thread Thrashing): Mô hình xử lý mặc định một luồng trên mỗi kết nối (
one-thread-per-connection) của nhânmysqldkhiến hàng nghìn kết nối nhàn rỗi (Sleep) từ hàng trăm Pod ứng dụng trong kiến trúc Microservices với Docker và Kubernetes thực chiến tiêu tốn hàng chục Gigabyte bộ nhớ ngăn xếp luồng (Thread Stack) và gây tranh chấp quyết liệt trên các khóa nội bộ của nhân (Kernel Futex / Mutex contention). - Nghẽn Băng thông Ghi và Đọc Chéo trên Nút Primary Duy nhất: Trong hầu hết các ứng dụng thương mại điện tử và tài chính số, tỷ lệ truy vấn đọc (
SELECT) chiếm từ80% đến 95%tổng lưu lượng, trong khi truy vấn ghi (INSERT,UPDATE,DELETE) chỉ chiếm5% đến 20%. Nếu dồn toàn bộ truy vấn đọc báo cáo và tìm kiếm vào chung nút Primary đang xử lý giao dịch ghi ACID, hàng đợi I/O và khóa dòng (Row-level Locks) sẽ làm tê liệt luồng thanh toán. - Độ trễ Sao chép Bất đồng bộ (Replication Lag) và Lỗi Đọc Dữ liệu Cũ (Stale Reads): Khi tách truy vấn đọc sang các máy chủ Replica, nếu một người dùng vừa cập nhật số dư ví trên nút Primary (
Hostgroup 10) rồi lập tức tải lại trang để đọc từ nút Replica (Hostgroup 20) trước khi bản ghi Binary Log (GTID) kịp truyền sang, người dùng sẽ thấy số dư cũ—gây lỗi nghiệp vụ nghiêm trọng. - Hiệu ứng Phình to Bảng Lịch sử Giao dịch (Monolithic Table Bloat): Khi một bảng
ordershoặcledger_entriesvượt ngưỡng hàng tỷ dòng, thời gian chạyALTER TABLE, sao lưu vật lý (Percona XtraBackup) và xây dựng lại chỉ mục tăng theo cấp số nhân, đòi hỏi phải phân vùng bảng (Declarative Partitioning) hoặc phân mảnh ngang qua nhiều cụm máy chủ (Horizontal Sharding với Vitess).
Bài viết chuyên sâu này tập trung hoàn toàn vào Kiến trúc Mở rộng Ngang (Horizontal Scale-Out) và Chống Nghẽn Cổ chai Phân tán trên MySQL 8.4 LTS theo chuẩn Production năm 2026: từ thiết lập tầng trung gian ProxySQL v2.7+ (Connection Multiplexing & Read/Write Splitting), đồng bộ nhất quán nhân quả với GTID (WAIT_FOR_EXECUTED_GTID_SET), tự động chuyển đổi dự phòng bằng InnoDB Group Replication / Orchestrator, cho tới chiến lược Horizontal Sharding với UUIDv7 (RFC 9562) và chẩn đoán tranh chấp khóa trực tiếp qua performance_schema.data_locks.
2. Kiến trúc Tầng Trung gian ProxySQL v2.7+: Connection Multiplexing và Phân tách Đọc/Ghi (Read/Write Splitting)
Thay vì để 200 Pod ứng dụng mở trực tiếp 200 x 50 = 10,000 kết nối TCP vào nút MySQL Primary, việc đặt cụm ProxySQL v2.7+ ở tầng L7 SQL Proxy giúp tách rời hoàn toàn Kết nối Phía trước (Frontend Connections) từ ứng dụng và Kết nối Phía sau (Backend Connections) tới mysqld.
flowchart TB
subgraph AppTier["Tầng Ứng Dụng Microservices (10,000+ Frontend Connections)"]
SVC1["Order Service Pods (Go 1.24+)"]
SVC2["Catalog & Search Service Pods"]
SVC3["Billing & Analytics Workers"]
end
subgraph ProxyTier["Tầng Điều Phối L7 ProxySQL v2.7+ Cluster"]
MUX["Connection Multiplexer & Query Cache<br/>(Tỷ lệ gộp kết nối 100:1 -> Giữ < 150 Backend Conns)"]
ROUTER["Bộ Phân Tích Cú Pháp SQL & Regex/Digest Router<br/>(transaction_persistent = 1)"]
MON["Health & Replication Lag Monitor<br/>(max_replication_lag = 2s)"]
end
subgraph MySQLCluster["Tầng Cụm Cơ Sở Dữ Liệu MySQL 8.4 LTS (GTID + WRITESET MTS)"]
WRITER["Hostgroup 10 (Writer / Primary)<br/>10.20.1.10:3306 (read_only = 0)"]
READER1["Hostgroup 20 (Reader Replica 01)<br/>10.20.1.11:3306 (super_read_only = 1)"]
READER2["Hostgroup 20 (Reader Replica 02)<br/>10.20.1.12:3306 (super_read_only = 1)"]
end
SVC1 -->|"TCP :6033"| MUX
SVC2 -->|"TCP :6033"| MUX
SVC3 -->|"TCP :6033"| MUX
MUX --> ROUTER
ROUTER -->|"BEGIN / INSERT / UPDATE / DELETE<br/>SELECT ... FOR UPDATE"| WRITER
ROUTER -->|"SELECT (Read-Only Queries)"| READER1
ROUTER -->|"SELECT (Read-Only Queries)"| READER2
WRITER -->|"GTID Binary Log Stream (WRITESET Parallel)"| READER1
WRITER -->|"GTID Binary Log Stream (WRITESET Parallel)"| READER2
MON -.->|"Kiểm tra read_only & Seconds_Behind_Source"| MySQLCluster
2.1. Cơ chế Connection Multiplexing và Điều kiện Bắt buộc để Không Bị Khóa Ghép Kênh
Trên ProxySQL v2.7+, một kết nối vật lý tới MySQL Backend có thể phục vụ luân phiên hàng trăm kết nối ứng dụng Frontend theo từng câu lệnh đơn lẻ (Statement-level Multiplexing). Nhờ đó, 10,000 kết nối từ ứng dụng được nén xuống chỉ còn 100–200 kết nối thực sự mở trên mysqld, giải phóng hoàn toàn áp lực cấp phát bộ nhớ luồng của nhân Linux (kết hợp cùng kỹ thuật tinh chỉnh hàng đợi socket trong bài tuning Linux Kernel cho máy chủ Web phục vụ hàng triệu kết nối đồng thời).
Tuy nhiên, ProxySQL sẽ tự động vô hiệu hóa Multiplexing (ghim chặt kết nối Backend vào riêng 1 kết nối Frontend) nếu phát hiện ứng dụng sử dụng các trạng thái gắn liền với phiên (Session-pinned state):
- Đang nằm bên trong một khối giao dịch mở (
BEGIN/START TRANSACTIONchưaCOMMIThoặcROLLBACK). - Sử dụng biến người dùng cấp phiên (
SET @user_var = ...). - Tạo bảng tạm (
CREATE TEMPORARY TABLE) hoặc gọiGET_LOCK(). - Thực thi
FOUND_ROWS()hoặcLAST_INSERT_ID().
Vì vậy, quy tắc vàng khi viết mã ứng dụng đi qua ProxySQL là: không bao giờ dùng biến @var trong truy vấn thông thường và luôn bật transaction_persistent = 1 cho tài khoản ứng dụng để mọi câu lệnh SELECT nằm bên trong cùng một khối BEGIN ... COMMIT luôn được định tuyến cố định vào Hostgroup 10 (Writer), tuyệt đối không bị xé lẻ sang Hostgroup 20 (Reader).
2.2. Cấu hình Khai báo /etc/proxysql.cnf và Luật Định tuyến Read/Write Splitting Chuẩn Production
Tệp cấu hình /etc/proxysql.cnf dưới đây thiết lập đầy đủ hai nhóm máy chủ (Hostgroup 10 cho Primary Writer và Hostgroup 20 cho Reader Replicas), tự động cô lập nút Replica nếu độ trễ sao chép vượt quá 2 giây (max_replication_lag = 2), và phân luồng chính xác các truy vấn đọc/ghi:
# File: /etc/proxysql.cnf
# Cấu hình ProxySQL v2.7+ chuẩn Production cho cụm MySQL 8.4 LTS (1 Primary + 2 Replicas)
datadir = "/var/lib/proxysql"
errorlog = "/var/lib/proxysql/proxysql.log"
admin_variables = {
admin_credentials = "radmin:${PROXYSQL_ADMIN_PASSWORD}"
mysql_ifaces = "127.0.0.1:6032"
cluster_username = "radmin"
cluster_password = "${PROXYSQL_ADMIN_PASSWORD}"
}
mysql_variables = {
threads = 8
max_connections = 15000
default_query_delay = 0
default_query_timeout = 30000
have_compress = true
poll_timeout = 2000
interfaces = "0.0.0.0:6033;/var/run/proxysql/proxysql.sock"
default_schema = "information_schema"
stacksize = 1048576
server_version = "8.4.3-Infoteck-ProxySQL"
connect_timeout_server = 1000
monitor_username = "proxysql_monitor"
monitor_password = "${PROXYSQL_MONITOR_PASSWORD}"
monitor_history = 600000
monitor_connect_interval = 2000
monitor_ping_interval = 1000
monitor_read_only_interval = 1000
monitor_read_only_timeout = 500
ping_interval_server_msec = 10000
ping_timeout_server = 500
commands_stats = true
sessions_sort = true
multiplexing = true
}
# Danh sách máy chủ MySQL 8.4 LTS Backend
mysql_servers = (
# Hostgroup 10: Primary Writer (Vừa nhận ghi, vừa làm dự phòng đọc khi Replicas quá tải)
{
address = "10.20.1.10"
port = 3306
hostgroup = 10
max_connections = 250
max_replication_lag = 0
weight = 1000
comment = "mysql84-primary-01 (Writer)"
},
# Hostgroup 20: Reader Replicas (ự động rút khỏi pool nếu trễ quá 2 giây)
{
address = "10.20.1.11"
port = 3306
hostgroup = 20
max_connections = 400
max_replication_lag = 2
weight = 1000
comment = "mysql84-replica-01 (Reader)"
},
{
address = "10.20.1.12"
port = 3306
hostgroup = 20
max_connections = 400
max_replication_lag = 2
weight = 1000
comment = "mysql84-replica-02 (Reader)"
}
)
# Cấu hình tài khoản ứng dụng: Mặc định vào Hostgroup 10 (Writer) và giữ nguyên Hostgroup khi mở Transaction
mysql_users = (
{
username = "app_order_svc"
password = "${APP_MYSQL_PASSWORD}"
default_hostgroup = 10
transaction_persistent = 1
fast_forward = 0
active = 1
max_connections = 8000
}
)
# Bộ quy tắc phân luồng truy vấn (Đánh giá theo thứ tự rule_id từ nhỏ đến lớn)
mysql_query_rules = (
# Rule 10: Bắt buộc giữ mọi câu lệnh SELECT ... FOR UPDATE / LOCK IN SHARE MODE ở Hostgroup 10 (Writer)
{
rule_id = 10
active = 1
match_digest = "^SELECT .* FOR UPDATE"
destination_hostgroup = 10
apply = 1
comment = "Route locking reads to Writer Hostgroup 10"
},
# Rule 15: Các câu truy vấn có gợi ý /*+ HOSTGROUP=10 */ (Đọc nhất quán tức thời sau khi ghi) đi vào Writer
{
rule_id = 15
active = 1
match_pattern = "/*+ HOSTGROUP=10 */"
destination_hostgroup = 10
apply = 1
comment = "Explicit hint for strongly consistent reads on Writer"
},
# Rule 20: Tất cả các câu lệnh SELECT thông thường còn lại được đẩy sang Hostgroup 20 (Reader Replicas)
{
rule_id = 20
active = 1
match_digest = "^SELECT "
destination_hostgroup = 20
apply = 1
comment = "Offload read-only SELECT queries to Reader Hostgroup 20"
}
)
# Tự động quan sát biến read_only của từng máy chủ để chuyển đổi Hostgroup 10 <-> 20 khi Failover
mysql_replication_hostgroups = (
{
writer_hostgroup = 10
reader_hostgroup = 20
check_type = "innodb_read_only"
comment = "Auto-detect Primary vs Replica via super_read_only / innodb_read_only"
}
)
3. Triệt tiêu Độ trễ Sao chép trên MySQL 8.4 LTS (WRITESET Parallel Replication) và Đọc Nhất quán Nhân quả (GTID)
3.1. Tinh chỉnh Sao chép Đa luồng Dựa trên Tập Ghi (WRITESET) trên MySQL 8.4 LTS
Trong các phiên bản MySQL cũ, tiến trình SQL Thread trên máy chủ Replica áp dụng lại sự kiện từ Relay Log một cách tuần tự đơn luồng, khiến Replica luôn bị tụt lại phía sau Primary mỗi khi có giao dịch ghi hàng loạt. Trên MySQL 8.4 LTS (phiên bản Long-Term Support mới nhất năm 2026, trong đó các thuật ngữ và biến hệ thống cũ đã được chuẩn hóa sang source/replica và bật mặc định theo dõi phụ thuộc bằng WRITESET), máy chủ Primary tính toán mã băm của từng khóa dòng bị thay đổi. Nhờ vậy, bất kỳ hai giao dịch nào không chạm vào cùng một dòng dữ liệu đều được thực thi song song hoàn toàn trên 16–32 luồng replica_parallel_workers ở phía Replica, ngay cả khi chúng không chạy đồng thời trên Primary:
# File: /etc/mysql/conf.d/90-replication-scaleout-84.cnf
# Cấu hình MySQL 8.4 LTS tối ưu hóa GTID Replication & WRITESET Parallel Applier
[mysqld]
server_id = 102
gtid_mode = ON
enforce_gtid_consistency = ON
# Bật khóa bảo vệ tuyệt đối trên các nút Replica để ngăn ghi nhầm dữ liệu
read_only = ON
super_read_only = ON
# Tối ưu hóa tốc độ áp dụng Relay Log đa luồng trên MySQL 8.4 LTS
replica_parallel_workers = 16
replica_preserve_commit_order = ON
transaction_write_set_extraction = XXHASH64
# Sử dụng Semi-Synchronous Replication để đảm bảo ít nhất 1 Replica đã nhận được sự kiện trước khi trả kết quả COMMIT
plugin_load_add = "semisync_source.so;semisync_replica.so"
rpl_semi_sync_source_enabled = 1
rpl_semi_sync_source_timeout = 1000
rpl_semi_sync_source_wait_for_replica_count = 1
rpl_semi_sync_replica_enabled = 1
3.2. Đảm bảo Đọc Nhất quán Sau Khi Ghi (Read-Your-Own-Writes) bằng WAIT_FOR_EXECUTED_GTID_SET
Khi kết hợp tầng bộ nhớ đệm trong bài chiến lược Caching đa tầng với Redis Cluster và Cloudflare CDN với Read/Write Splitting qua ProxySQL, làm thế nào để một giao dịch vừa COMMIT trên nút Writer (Hostgroup 10) có thể đọc tiếp từ nút Reader (Hostgroup 20) mà không bao giờ gặp dữ liệu cũ (Stale Read) nhưng cũng không cần dồn mọi truy vấn đọc về Writer?
Giải pháp chuẩn kiến trúc MySQL 8.4 LTS là sử dụng hàm nội tại WAIT_FOR_EXECUTED_GTID_SET(gtid_set, timeout):
- Sau khi thực thi lệnh ghi trên Writer, ứng dụng đọc mã
session_track_gtids = OWN_GTIDtrả về ngay trong gói tinOK_Packetcủa giao thức MySQL (ví dụ:3e11fa47-71ca-11e1-9e33-c80aa9429562:10492). - Khi thực hiện truy vấn đọc kế tiếp trên Replica, ứng dụng gửi kèm
SELECT WAIT_FOR_EXECUTED_GTID_SET('3e11fa47-...:10492', 0.5). Nút Replica sẽ chờ tối đa vài phần nghìn giây cho tới khi chính giao dịch GTID đó vừa được áp dụng xong rồi mới chạy lệnhSELECT, hoặc nếu quá0.5sthì ứng dụng tự động đọc dự phòng từ Writer.
sequenceDiagram
autonumber
participant App as Order Service (Go 1.24+)
participant Proxy as ProxySQL v2.7+
participant Primary as MySQL 8.4 Writer (HG 10)
participant Replica as MySQL 8.4 Reader (HG 20)
App->>Proxy: INSERT INTO orders (...) [session_track_gtids=OWN_GTID]
Proxy->>Primary: Chuyển tiếp lệnh ghi vào Hostgroup 10
Primary-->>App: OK_Packet kèm GTID = "uuid-A:5012"
Primary->>Replica: Truyền luồng Binlog bất đồng bộ (GTID uuid-A:5012)
App->>Proxy: SELECT * FROM orders WHERE id = ? (Kèm yêu cầu chờ GTID uuid-A:5012)
Proxy->>Replica: Định tuyến sang Hostgroup 20
Replica->>Replica: Thực thi WAIT_FOR_EXECUTED_GTID_SET("uuid-A:5012", 1) -> Trả về 0 (Đã đồng bộ)
Replica-->>App: Trả về bản ghi đơn hàng mới nhất (Đảm bảo 100% Causal Consistency)
4. Chiến lược Phân vùng Bảng (Declarative Partitioning) và Phân mảnh Ngang (Horizontal Sharding) với UUIDv7
Khi một bảng giao dịch vượt quá dung lượng bộ nhớ vật lý của một máy chủ đơn lẻ, kỹ sư kiến trúc cần phân biệt rõ hai cấp độ phân tách dữ liệu:
4.1. Cấp độ 1: Phân vùng Bảng theo Thời gian (PARTITION BY RANGE) trên Cùng Một Máy chủ
Nếu nút thắt cổ chai đến từ việc bảng nhật ký giao dịch (audit_logs, payment_events) chứa dữ liệu của 36 tháng nhưng 95% truy vấn chỉ đọc dữ liệu của 30 ngày gần nhất, việc áp dụng Declarative Range Partitioning giúp trình tối ưu hóa tự động loại bỏ các phân vùng cũ (Partition Pruning) và cho phép xóa hàng trăm triệu dòng cũ trong 0.01 giây bằng lệnh ALTER TABLE ... DROP PARTITION p2025_10 thay vì chạy lệnh DELETE gây khóa bảng hàng giờ liền:
-- File: migrations/20261105_partitioned_ledger_events.sql
-- Thiết kế bảng phân vùng theo tháng trên MySQL 8.4 LTS phục vụ Partition Pruning & Instant Archival
CREATE TABLE IF NOT EXISTS wallet_ledger_entries (
entry_id BINARY(16) NOT NULL COMMENT 'UUIDv7 nhị phân sắp xếp theo thời gian (RFC 9562)',
account_id BIGINT NOT NULL,
amount_vnd BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
created_at DATETIME(6) NOT NULL,
-- Trong MySQL 8.4, mọi cột dùng làm biểu thức phân vùng (created_at) bắt buộc phải nằm trong PRIMARY KEY
PRIMARY KEY (entry_id, created_at),
KEY idx_account_created (account_id, created_at DESC)
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS(created_at) (
PARTITION p2026_09 VALUES LESS THAN ('2026-10-01 00:00:00'),
PARTITION p2026_10 VALUES LESS THAN ('2026-11-01 00:00:00'),
PARTITION p2026_11 VALUES LESS THAN ('2026-12-01 00:00:00'),
PARTITION p2026_12 VALUES LESS THAN ('2027-01-01 00:00:00'),
PARTITION p_future VALUES LESS THAN (MAXVALUE)
);
-- Quy trình xoay vòng phân vùng không gây gián đoạn (Zero-Downtime Partition Rotation):
-- 1. Tách p_future để thêm phân vùng tháng mới trước ngày đầu tháng
ALTER TABLE wallet_ledger_entries REORGANIZE PARTITION p_future INTO (
PARTITION p2027_01 VALUES LESS THAN ('2027-02-01 00:00:00'),
PARTITION p_future VALUES LESS THAN (MAXVALUE)
);
4.2. Cấp độ 2: Phân mảnh Ngang Đa Máy chủ (Horizontal Sharding với Vitess / Application-Level Routing)
Khi cường độ ghi (INSERT/UPDATE) vượt quá khả năng IOPS của một nút Primary duy nhất, dữ liệu phải được chia nhỏ ra $N$ cụm MySQL độc lập (Shards). Sai lầm phổ biến nhất khi thiết kế Sharding là chọn order_id làm khóa phân mảnh (Shard Key) trong khi 90% truy vấn của khách hàng lại tìm theo customer_id (SELECT * FROM orders WHERE customer_id = ?), khiến tầng định tuyến (như Vitess VTGate) phải gửi truy vấn tán xạ tới toàn bộ các Shard (Scatter-Gather Query).
Nguyên tắc thiết kế Shard Key chuẩn Production:
- Chọn Thực thể Gốc của Ngữ cảnh Truy vấn (
customer_idhoặctenant_id) làm Shard Key: Đảm bảo mọi đơn hàng, hóa đơn và lịch sử giao dịch của cùng một khách hàng luôn nằm trên cùng một Shard vật lý (Co-located Tables). - Sử dụng Định danh
UUIDv7(RFC 9562) thay choAUTO_INCREMENTĐơn lẻ: Trong môi trường nhiều Shard, việc dùngAUTO_INCREMENTdễ gây trùng khóa khi gộp dữ liệu, cònUUIDv4ngẫu nhiên làm phân mảnh trang chỉ mục B+Tree. ChuẩnUUIDv7kết hợp48-bitdấu thời gian Unix millisecond ở đầu với74-bitngẫu nhiên mật mã ở sau, vừa đảm bảo tính duy nhất toàn cục không cần máy chủ cấp số tập trung, vừa giữ tốc độ chèn nối tiếp tuần tự trên từng Shard:
// File: internal/sharding/mysql_shard_router.go
// Go 1.24+: Bộ định tuyến Horizontal Sharding nhất quán kết hợp Causal GTID Wait trên MySQL 8.4 LTS
package sharding
import (
"context"
"database/sql"
"fmt"
"hash/crc32"
"time"
)
type ShardCluster struct {
ShardID int
WriterDB *sql.DB // Kết nối qua ProxySQL Hostgroup 10 của Shard tương ứng
ReaderDB *sql.DB // Kết nối qua ProxySQL Hostgroup 20 của Shard tương ứng
}
type ShardedOrderRepository struct {
shards []*ShardCluster
}
func NewShardedOrderRepository(shards []*ShardCluster) *ShardedOrderRepository {
if len(shards) == 0 {
panic("at least one MySQL 8.4 shard is required")
}
return &ShardedOrderRepository{shards: shards}
}
// ResolveShard định tuyến xác định (Deterministic) theo tenantOrCustomerID
func (r *ShardedOrderRepository) ResolveShard(tenantOrCustomerID string) *ShardCluster {
checksum := crc32.ChecksumIEEE([]byte(tenantOrCustomerID))
idx := int(checksum) % len(r.shards)
return r.shards[idx]
}
// ReadConsistentAfterWrite đảm bảo đọc từ Replica nhưng chỉ sau khi Replica đã nhận kịp minGTID
func (r *ShardedOrderRepository) ReadConsistentAfterWrite(
ctx context.Context,
customerID string,
orderID string,
minGTID string,
) (string, error) {
shard := r.ResolveShard(customerID)
targetDB := shard.ReaderDB
if minGTID != "" {
var waitStatus int
// Chờ tối đa 0.3 giây trên Replica cho tới khi GTID vừa ghi được áp dụng xong
err := shard.ReaderDB.QueryRowContext(
ctx,
"SELECT WAIT_FOR_EXECUTED_GTID_SET(?, 0.3)",
minGTID,
).Scan(&waitStatus)
// Nếu Replica đang trễ quá 300ms (waitStatus == 1) hoặc lỗi, tự động chuyển hướng về WriterDB
if err != nil || waitStatus != 0 {
targetDB = shard.WriterDB
}
}
queryCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
var status string
err := targetDB.QueryRowContext(
queryCtx,
"SELECT status FROM orders WHERE customer_id = ? AND order_id = ?",
customerID,
orderID,
).Scan(&status)
if err != nil {
return "", fmt.Errorf("shard %d query failed: %w", shard.ShardID, err)
}
return status, nil
}
5. Chẩn đoán Trực tiếp Sự cố Tranh chấp Khóa (data_locks), Deadlock và Nghẽn Truy vấn Chậm trên Production
Khi hệ thống cảnh báo trong bài giám sát hệ thống toàn diện với Prometheus và Grafana phát hiện số lượng luồng đang chạy (Threads_running) tăng đột biến từ 15 lên 300+, nguyên nhân thường không phải do thiếu RAM mà do Hiệu ứng Đoàn tàu Khóa (Lock Convoy): một giao dịch giữ khóa dòng (X,REC_NOT_GAP) quá lâu khiến hàng trăm giao dịch sau xếp hàng chờ dây chuyền.
5.1. Truy vấn Bảng performance_schema.data_locks và data_lock_waits trên MySQL 8.4 LTS
Trên MySQL 8.0/8.4, các bảng cũ trong information_schema đã được thay thế hoàn toàn bằng performance_schema.data_locks và performance_schema.data_lock_waits. Đoạn truy vấn SQL dưới đây giúp kỹ sư trực chiến chỉ mặt đặt tên chính xác PROCESSLIST_ID, câu lệnh SQL đang gây chặn (Blocking Transaction) và thời gian đã giữ khóa để xử lý tức thời:
-- File: scripts/diagnose_live_lock_convoy_mysql84.sql
-- Truy vấn tìm chính xác Giao dịch Gốc (Blocker) đang làm treo các Giao dịch Khác (Waiters) trên MySQL 8.4 LTS
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread_pid,
TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) AS wait_seconds,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread_pid,
b.trx_query AS blocking_current_query,
t_block.PROCESSLIST_STATE AS blocker_state,
esh_block.SQL_TEXT AS blocker_last_executed_sql,
dl.OBJECT_SCHEMA AS locked_schema,
dl.OBJECT_NAME AS locked_table,
dl.INDEX_NAME AS locked_index,
dl.LOCK_MODE AS lock_mode,
dl.LOCK_DATA AS locked_key_value
FROM performance_schema.data_lock_waits dlw
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = dlw.REQUESTING_ENGINE_TRANSACTION_ID
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = dlw.BLOCKING_ENGINE_TRANSACTION_ID
INNER JOIN performance_schema.data_locks dl
ON dl.ENGINE_LOCK_ID = dlw.BLOCKING_ENGINE_LOCK_ID
LEFT JOIN performance_schema.threads t_block
ON t_block.THREAD_ID = dlw.BLOCKING_THREAD_ID
LEFT JOIN performance_schema.events_statements_current esh_block
ON esh_block.THREAD_ID = dlw.BLOCKING_THREAD_ID
ORDER BY wait_seconds DESC;
[!TIP]
Tại saoblocking_current_querythường hiển thịNULL? Khi một giao dịch trong mã ứng dụng mởBEGIN, chạy lệnhUPDATE inventory SET stock = stock - 1 WHERE sku_id = 99, sau đó lại gọi một HTTP API bên thứ ba mất 15 giây trước khi gọiCOMMIT, tại thời điểm bạn kiểm tra, MySQL đã chạy xong lệnhUPDATEnêntrx_queryhiện tại của Blocker làNULL(trạng tháiSleep). Nhờ kết nối thêm bảngperformance_schema.events_statements_current(esh_block.SQL_TEXT) như trong truy vấn trên, bạn sẽ thấy ngay câu lệnh cuối cùng mà phiênSleepđó vừa thực thi trước khi giữ khóa! Để triệt tiêu tận gốc lỗi này, hãy áp dụng mẫu khóa phân tán và tách rời lời gọi mạng ra ngoài giao dịch theo hướng dẫn tại bài thiết kế kiến trúc Distributed Locking và Idempotency Key với Redis & PostgreSQL.
5.2. Kịch bản Kiểm định Hiệu quả Ghép kênh ProxySQL và Phân tích Slow Query bằng pt-query-digest
#!/usr/bin/env bash
# File: /usr/local/sbin/verify-mysql-scaleout-health.sh
# Mô tả: Kiểm tra tỷ lệ ghép kênh trên ProxySQL Admin, độ trễ GTID Replica và phân tích Top Slow Queries
set -euo pipefail
PROXYSQL_ADMIN_HOST="127.0.0.1"
PROXYSQL_ADMIN_PORT="6032"
PROXYSQL_ADMIN_USER="radmin"
echo "=== [1/3] Kiểm tra trạng thái Backend Hostgroups (10 = Writer, 20 = Reader) và số kết nối thực tế ==="
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" -u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" -t -e "
SELECT
hostgroup,
srv_host,
status,
ConnUsed,
ConnFree,
ConnOK,
ConnERR,
MaxConnUsed,
Queries,
Latency_us
FROM stats_mysql_connection_pool
ORDER BY hostgroup, srv_host;
"
echo "=== [2/3] Kiểm tra Top 10 mẫu truy vấn chiếm tổng thời gian thực thi cao nhất qua ProxySQL Digest ==="
mysql -h "${PROXYSQL_ADMIN_HOST}" -P "${PROXYSQL_ADMIN_PORT}" -u "${PROXYSQL_ADMIN_USER}" -p"${PROXYSQL_ADMIN_PASSWORD}" -t -e "
SELECT
hostgroup,
schemaname,
count_star,
ROUND(sum_time / 1000000, 2) AS total_time_sec,
ROUND(sum_time / count_star / 1000, 2) AS avg_latency_ms,
SUBSTRING(digest_text, 1, 90) AS query_pattern
FROM stats_mysql_query_digest
ORDER BY sum_time DESC
LIMIT 10;
"
echo "=== [3/3] Phân tích tệp nhật ký Slow Query Log bằng công cụ Percona pt-query-digest ==="
if command -v pt-query-digest >/dev/null 2>&1 && [ -f /var/log/mysql/mysql-slow.log ]; then
pt-query-digest --limit=5 /var/log/mysql/mysql-slow.log
fi
6. Tài liệu Tham khảo & Nguồn Đối chiếu Chính thống (Primary References)
- MySQL 8.4 LTS Official Reference Manual — Global Transaction Identifiers (GTID) & WAIT_FOR_EXECUTED_GTID_SET Functions
- MySQL 8.4 LTS Official Reference Manual — Performance Schema data_locks and data_lock_waits Tables
- ProxySQL v2.7+ Official Documentation — Connection Multiplexing & Read/Write Split Configuration
- Vitess CNCF Graduated Project Documentation — Horizontal Sharding & VIndex Design on MySQL
- Percona Toolkit Documentation — pt-query-digest Slow Query Analysis Reference
- IETF RFC 9562 — Universally Unique IDentifiers (UUIDv7 Time-Ordered Shard Key Specification)