MySQL低延迟复制:香港机房落地 GTID + 半同步 + 可视化延迟告警与 SLO 的完整部署与优化

那天凌晨 3 点,香港荃湾机房监控面板上,复制延迟(replication lag)像心电图一样忽高忽低,内地业务只要一到“整点促销”,延迟就会从 100ms 抬升到几秒,随后又神秘恢复。电商小哥在电话里说:“下单页偶发超时,但主库压力不高,像有只手在背后拽着。”
我盯着屏幕想:我们缺的不是吞吐,而是“低延迟、可观测、可收敛”的复制链路。于是我把方案拉回白板:GTID 保证复制一致性,半同步把主库提交和副本确认绑到同一个命运里,结合心跳表 + Prometheus + Grafana做真实延迟可视化,再用SLO(目标:p99 延迟 < 150ms)和多窗口燃尽率告警把“幽灵延迟”盯死。下面,是这次落地的完整过程与踩坑记录。
环境与基线
硬件与网络(香港机房,单可用区 + 同城双机房)
| 角色 | 机型 | CPU | 内存 | 存储 | 网卡 | 系统 | 机架/网络 |
|---|---|---|---|---|---|---|---|
主库 hk-mysql-primary |
1U 定制 | 2 × Intel Xeon Silver 4314 | 128GB DDR4 | 2 × 1.92TB NVMe(RAID1,用于数据)+ 1 × 960GB SATA SSD(binlog) | 2 × 10GbE | CentOS 7.9 (3.10 内核) | 同机房 TOR 交换机,东西向 10GbE |
副本 hk-mysql-replica-a |
同上 | 同上 | 同上 | 同上 | 同上 | 同上 | 同机房,同机架相邻 |
副本 sz-mysql-replica-b |
同上 | 同上 | 同上 | 同上 | 1 × 10GbE | 同上 | 跨城专线(HK ↔ SZ) |
- 磁盘策略:InnoDB 数据与 redo/undo 在 NVMe;binlog 单独 SSD(减少写放大);文件系统 XFS;挂载 noatime,nodiratime。
- 时钟:全部节点对同一 NTP 池。
- MySQL 版本:8.0.x(稳定小版本,启用 GTID 与并行复制)。
延迟基线(部署前)
| 链路 | ICMP RTT (p50) | TCP RTT 3306 (p50) | 峰值业务时秒杀窗口(p99) |
|---|---|---|---|
| 同机架(主→副 A) | 0.18 ms | 0.22 ms | 0.45 ms |
| 同城跨机房(主→副 B) | 0.85 ms | 1.1 ms | 2.4 ms |
| 港深专线(主→深) | 7.6 ms | 8.2 ms | 14.7 ms |
目标 SLO:复制真实延迟(以心跳差值计)p99 < 150ms,每月错误预算 0.1%。
架构与策略
- 复制拓扑:1 主(HK)+ 2 副(HK、SZ)。
- 模式:GTID 全局事务标识;半同步仅对本地低延迟副本 A 强制(确保“至少 1 个确认”);对跨城副本 B 保持异步(避免远端 RTT 拖慢提交)。
- 并行复制:副本启用 writeset 并行(后述参数),缓解大事务/热点写场景下的应用端延迟。
- 可观测:Seconds_Behind_Source 仅作参考;以心跳表为准衡量真实滞后,接入 Prometheus/Grafana/Alertmanager,落地 SLO 告警(多窗口燃尽率)。
操作系统与内核调优(主/副一致)
系统参数(/etc/sysctl.d/99-mysql.conf)
net.core.somaxconn = 20480
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_mtu_probing = 1
fs.aio-max-nr = 1048576
vm.swappiness = 1
vm.dirty_background_ratio = 5
vm.dirty_ratio = 15
sysctl --system
ulimit
echo '* soft nofile 1048576' >> /etc/security/limits.conf
echo '* hard nofile 1048576' >> /etc/security/limits.conf
文件系统与挂载
XFS,挂载 noatime,nodiratime,discard(NVMe 支持 TRIM)
NVMe 用于 /data/mysql;SSD 用于 /binlog
MySQL 安装与初始化(CentOS 7 / RPM)
以下命令在主库与副本均执行,版本号按你仓库实际为准。
# 官方 repo(示例)
rpm -Uvh https://repo.mysql.com/mysql80-community-release-el7-7.noarch.rpm
yum-config-manager --enable mysql80-community
yum install -y mysql-community-server mysql-shell
# 目录准备
mkdir -p /data/mysql/{data,log,tmp} /binlog
chown -R mysql:mysql /data/mysql /binlog
核心 my.cnf(主库)
/etc/my.cnf 关键段落如下(其余保持默认或按容量微调):
[mysqld]
user = mysql
datadir = /data/mysql/data
tmpdir = /data/mysql/tmp
socket = /var/lib/mysql/mysql.sock
pid-file = /var/run/mysqld/mysqld.pid
# 基础
server_id = 1001
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
skip_name_resolve = 1
max_connections = 3000
# InnoDB
innodb_buffer_pool_size = 80G
innodb_log_file_size = 4G
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_io_capacity = 4000
innodb_io_capacity_max = 8000
# 并发控制
innodb_thread_concurrency = 0
# Binlog
log_bin = /binlog/mysql-bin
binlog_format = ROW
sync_binlog = 1
binlog_row_image = minimal
binlog_expire_logs_seconds = 604800
binlog_order_commits = 1
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
log_slave_updates = ON
# 半同步(8.0 新名/旧名兼容:按版本择一;都配上也兼容)
plugin_load_add = semisync_master.so
plugin_load_add = semisync_slave.so
# 或(新命名)
# plugin_load_add = semisync_source.so
# plugin_load_add = semisync_replica.so
rpl_semi_sync_master_enabled = ON
rpl_semi_sync_master_timeout = 100 # ms,等待副本确认超时
rpl_semi_sync_master_wait_no_slave = ON
rpl_semi_sync_master_wait_for_slave_count = 1
# 等待点(更安全的 AFTER_SYNC;如追求更低提交等待可评估 AFTER_COMMIT)
# rpl_semi_sync_master_wait_point = AFTER_SYNC
# 加密(机房内也建议启用)
require_secure_transport = ON
ssl_ca = /etc/mysql/ssl/ca.pem
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_key = /etc/mysql/ssl/server-key.pem
# 监控
performance_schema = ON
副本将 server_id 改为 1002/1003,其余对称;并在副本端开启并行复制(见后文)。
初始化并启动:
systemctl enable mysqld
systemctl start mysqld
mysql -uroot -p'临时密码' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Str0ng!Passw0rd';"
创建复制账户(强制 SSL)
在主库执行:
CREATE USER 'repl'@'10.%' IDENTIFIED BY 'Repl!Passw0rd' REQUIRE SSL;
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.%';
FLUSH PRIVILEGES;
启用半同步插件(兼容命名)
有的版本使用 rpl_semi_sync_source/replica,有的使用 semisync_master/slave。两个都写在 my.cnf 后,重启一次能自动加载;也可在线安装:
-- 若未加载,可执行:
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
-- 或
-- INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
-- INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
基于物理热备搭建副本(建议 XtraBackup)
大库不建议全量 mysqldump。这里以 Percona XtraBackup 为例(副本先停 MySQL,清空数据目录,再恢复)。
在主库:
yum install -y percona-xtrabackup-80
innobackupex --user=root --password='Str0ng!Passw0rd' \
--stream=xbstream --parallel=8 /tmp \
| ssh replica-a "xbstream -x -C /data/mysql/data"
在副本:
yum install -y percona-xtrabackup-80
innobackupex --prepare --use-memory=8G /data/mysql/data
chown -R mysql:mysql /data/mysql/data
如果必须逻辑备份(小库):mysqldump --set-gtid-purged=ON --single-transaction --master-data=2
配置副本(GTID + 半同步)
并行复制 & 写集依赖(副本 my.cnf)
# 并行复制(8.0 新旧参数兼容)
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK
# 8.0+ 推荐:
replica_parallel_workers = 8
replica_parallel_type = LOGICAL_CLOCK
# 写集依赖跟踪,提升并行度
binlog_transaction_dependency_tracking = WRITESET
transaction_write_set_extraction = XXHASH64
重启副本后,在副本执行(8.0 新语法;老版把 SOURCE 替换为 MASTER):
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='10.0.0.10',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl!Passw0rd',
SOURCE_SSL=1,
SOURCE_AUTO_POSITION=1;
START REPLICA; -- 老版:START SLAVE;
仅副本 A(同机房)保留半同步确认;**副本 B(跨城)**建议保持异步,避免拉长主库提交耗时。可在副 B 上关闭半同步接收(或不安装 semisync 插件):
SET GLOBAL rpl_semi_sync_slave_enabled = OFF; -- 或不加载插件
验证状态:
-- 主库:是否处于半同步
SHOW STATUS LIKE 'Rpl_semi_sync_master_status'; -- ON 表示至少一个副本在确认
SHOW STATUS LIKE 'Rpl_semi_sync_master_clients';
-- 副本:
SHOW REPLICA STATUS\G -- 关注 Replica_IO_Running/Replica_SQL_Running、Seconds_Behind_Source
“真实”复制延迟:心跳表 + mysqld_exporter
Seconds_Behind_Source 在副本 SQL 进程停顿、切换或大事务时可能不准。我们用心跳表严谨量化。
1)心跳表(主库)
CREATE DATABASE IF NOT EXISTS meta;
CREATE TABLE meta.heartbeat (
id TINYINT PRIMARY KEY,
ts TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB;
INSERT INTO meta.heartbeat (id, ts) VALUES (1, NOW(3))
ON DUPLICATE KEY UPDATE ts=VALUES(ts);
-- 每 100ms 更新一次(根据负载酌情)
DELIMITER //
CREATE EVENT IF NOT EXISTS meta.evt_heartbeat
ON SCHEDULE EVERY 0.1 SECOND
DO
INSERT INTO meta.heartbeat (id, ts) VALUES (1, NOW(3))
ON DUPLICATE KEY UPDATE ts=VALUES(ts);
//
DELIMITER ;
SET GLOBAL event_scheduler = ON;
2)副本端视角的“真实延迟”视图(副本)
CREATE OR REPLACE VIEW meta.v_heartbeat_lag_ms AS
SELECT
1000 * TIMESTAMPDIFF(MICROSECOND, ts, NOW(3)) / 1000 AS lag_ms
FROM meta.heartbeat WHERE id=1;
3)mysqld_exporter 收集心跳延迟
安装:
# 二进制安装示例
useradd -r -s /sbin/nologin mysqld_exporter
mkdir -p /etc/mysqld_exporter
创建采集账户(在副本):
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'Expo!Passw0rd' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
systemd 单元(副本 /etc/systemd/system/mysqld_exporter.service):
[Unit]
Description=mysqld_exporter
After=network.target
[Service]
User=mysqld_exporter
ExecStart=/usr/local/bin/mysqld_exporter \
--config.my-cnf=/etc/mysqld_exporter/.my.cnf \
--collect.info_schema.innodb_metrics \
--collect.engine_innodb_status \
--collect.perf_schema.eventsstatements \
--collect.perf_schema.tablelocks \
--collect.heartbeat \
--heartbeat.compatibility-mode \
--heartbeat.database=meta \
--heartbeat.table=heartbeat \
--heartbeat.utc
Restart=always
[Install]
WantedBy=multi-user.target
配置凭据 /etc/mysqld_exporter/.my.cnf:
[client]
user=exporter
password=Expo!Passw0rd
socket=/var/lib/mysql/mysql.sock
--collect.heartbeat 会导出 mysql_heartbeat_lag_seconds(按心跳表推算),比 Seconds_Behind_Source 更可信。
Prometheus / Grafana / Alertmanager:SLO 可视化与告警
Prometheus 抓取(prometheus.yml 片段)
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets:
- 'hk-replica-a:9104'
- 'sz-replica-b:9104'
- 'hk-primary:9104'
关键指标(PromQL)
真实复制延迟(ms):
mysql_heartbeat_lag_seconds * 1000
半同步是否有效(主库):
mysql_global_status_rpl_semi_sync_master_status
副本 SQL 线程状态:
mysql_slave_status_slave_sql_running
or
mysql_replica_status_replica_sql_running
SLO:p99 < 150ms(记录规则)
# recording-rules.yml
groups:
- name: mysql-lag-slo
rules:
- record: job:mysql:lag_ms
expr: mysql_heartbeat_lag_seconds * 1000
# 计算 5m/30m 窗口内超过阈值的错误比例(>150ms 记为 1)
- record: job:mysql:lag_error_ratio_5m
expr: sum(rate((job:mysql:lag_ms > 150)[5m])) by (instance) / sum(rate((job:mysql:lag_ms >= 0)[5m])) by (instance)
- record: job:mysql:lag_error_ratio_30m
expr: sum(rate((job:mysql:lag_ms > 150)[30m])) by (instance) / sum(rate((job:mysql:lag_ms >= 0)[30m])) by (instance)
多窗口燃尽率告警(99.9% 月度 SLO,错误预算 0.1%)
# alerts.yml
groups:
- name: mysql-lag-slo-alerts
rules:
# 快速燃尽(2%/1h,短窗口)
- alert: MySQLReplicationLagBudgetBurnFast
expr: job:mysql:lag_error_ratio_5m > (0.1 * 14.4) # 0.1%/30d 的 14.4x
for: 10m
labels:
severity: critical
annotations:
summary: "MySQL 复制延迟快速燃尽({{ $labels.instance }})"
description: "5m 窗口错误比率 {{ $value | humanizePercentage }},超过月度 SLO 错误预算燃尽速率。"
# 慢速燃尽(1%/6h,长窗口)
- alert: MySQLReplicationLagBudgetBurnSlow
expr: job:mysql:lag_error_ratio_30m > (0.1 * 6) # 0.1%/30d 的 6x
for: 1h
labels:
severity: warning
annotations:
summary: "MySQL 复制延迟慢速燃尽({{ $labels.instance }})"
description: "30m 窗口错误比率 {{ $value | humanizePercentage }},注意持续性退化。"
Grafana 面板:一个展示 job:mysql:lag_ms 的时间序列;一个单值 p99(用 PromQL histogram_quantile 或在 Grafana 转换器里取 p99);另加半同步状态与 SQL 线程状态灯。
复制提速与提交端到端优化
1)半同步取舍与超时
rpl_semi_sync_master_timeout = 100ms:等待本地副本 A确认。如果 A 暂时掉线,超时后自动退化为异步,保障可用性。
rpl_semi_sync_master_wait_for_slave_count = 1:只需1 个副本确认(本地 A),降低等待成本。
不要对跨城副本 B 强制半同步,否则主库提交被远端 RTT 牵制。
2)并行复制与写集(副本)
replica_parallel_workers = 8 + binlog_transaction_dependency_tracking = WRITESET
让副本能并行应用互不冲突的事务。热点表写入冲突多时,适当增加到 16/32,观测 CPU 与 Apply Queue。
3)binlog 组提交策略
sync_binlog = 1(严格持久化)+ NVMe:低延迟且安全。
为追求吞吐可评估 binlog_group_commit_sync_delay = 200(微秒)+ binlog_group_commit_sync_no_delay_count = 10,但会增加提交延迟,我的实测 p95 多出 ~0.2–0.4ms,于是线上保持 0。
4)InnoDB
innodb_flush_log_at_trx_commit = 1(一致性优先)
innodb_log_file_size = 4G(避免频繁切换)
保持 O_DIRECT,减少页缓存抖动;buffer_pool_size ≈ 物理内存 60–70%(我这里 80G)
5)网络与 TLS
机房内也启用 SSL/TLS:AES-NI 下 CPU 开销可接受(< 2%),换来链路安全。在 exporter 中也可走本地 socket,避免额外负载。
验收:SLO 达标与数据
变更前后(以副本 A 为例)
| 指标 | 变更前 p99 | 变更后 p99 | 说明 |
|---|---|---|---|
| 真实复制延迟(ms, 心跳) | 420 ms | 82 ms | 半同步只等本地副本 + 并行复制 |
| 提交端到端耗时增量(ms) | 1.3 ms | 0.9 ms | NVMe + sync_binlog=1 下仍可控 |
| 峰值促销时长尾(p99.9) | 2.8 s | 220 ms | 大事务拆分、writeset 并行生效 |
| 月度 SLO(p99<150ms)可用性 | 98.3% | 99.97% | 错误预算充足 |
现场踩坑与处理
GTID 不一致:历史脚本里有 CREATE TABLE ... SELECT 等非 GTID 安全语句,启动 enforce_gtid_consistency=ON 后报错。
处理:回滚到影子库跑一遍,改写为显式 INSERT … SELECT 或分两步建表。
Seconds_Behind_Source 不可信:副本 SQL 线程重启后该指标 0,但应用端依然延迟。
处理:以心跳表为准接入监控,告警基于 mysql_heartbeat_lag_seconds。
半同步卡顿:副本 A IO 线程短暂阻塞,主库事务提交卡 100ms 超时。
处理:rpl_semi_sync_master_timeout 从 300ms 降到 100ms,并设置 wait_no_slave=ON 允许快速退化;同时对副本 A 打开 replica_parallel_workers=16。
大事务导致 Apply 堵塞:历史批处理一次性更新数十万行。
处理:把批处理改成分批 5k/次,并在业务侧使用幂等与重试;开启 writeset 并行显著改善。
插件重启未加载:线上重启后发现半同步未启用。
处理:把 plugin_load_add=semisync_* 写入 my.cnf,纳入重启检查清单;在启动探针里校验 Rpl_semi_sync_master_status=ON。
运维清单(Runbook 摘要)
检查半同步状态
SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
查看真实延迟(SQL)
SELECT * FROM meta.v_heartbeat_lag_ms;
观测并行复制效率
SHOW REPLICA STATUS\G
-- 关注 Replica_SQL_Running_State、Worker 数、Coordinator 队列
临时退化为异步(应急)
SET GLOBAL rpl_semi_sync_master_enabled = OFF;
灯熄了,但不是运气好
第二天清晨,促销流量再起,面板上的延迟曲线像被熨斗抚平——p99 稳在 80–100ms。业务群里没再有人问:“为啥订单晚到几秒?”我在机房把外套拉上拉链,想起那句老话:低延迟不是堆配置,是系统性工程。
这次我们不是“赌运气去顶住流量”,而是用 GTID 保证一致性,用半同步界定提交语义,用心跳与 SLO“度量真相”,用并行复制与工程实践去收敛延迟。当你能看见延迟、解释延迟、并约束延迟,它就不再是幽灵。
附:完整命令清单(可直接改名套用)
1)主库 / 副本共用的关键 my.cnf 模板(根据角色微调)
[mysqld]
user = mysql
datadir = /data/mysql/data
tmpdir = /data/mysql/tmp
socket = /var/lib/mysql/mysql.sock
pid-file = /var/run/mysqld/mysqld.pid
server_id = ${UNIQUE_ID}
skip_name_resolve = 1
max_connections = 3000
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
# InnoDB
innodb_buffer_pool_size = 80G
innodb_log_file_size = 4G
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
# Binlog
log_bin = /binlog/mysql-bin
binlog_format = ROW
sync_binlog = 1
binlog_row_image = minimal
binlog_expire_logs_seconds = 604800
# GTID
gtid_mode = ON
enforce_gtid_consistency = ON
log_slave_updates = ON
# 半同步插件(按版本)
plugin_load_add = semisync_master.so
plugin_load_add = semisync_slave.so
# plugin_load_add = semisync_source.so
# plugin_load_add = semisync_replica.so
rpl_semi_sync_master_enabled = ON
rpl_semi_sync_master_timeout = 100
rpl_semi_sync_master_wait_no_slave = ON
rpl_semi_sync_master_wait_for_slave_count = 1
# rpl_semi_sync_master_wait_point = AFTER_SYNC
# 并行复制(仅副本)
# slave_parallel_workers = 8
# slave_parallel_type = LOGICAL_CLOCK
# replica_parallel_workers = 8
# replica_parallel_type = LOGICAL_CLOCK
# binlog_transaction_dependency_tracking = WRITESET
# transaction_write_set_extraction = XXHASH64
# 加密
require_secure_transport = ON
ssl_ca = /etc/mysql/ssl/ca.pem
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_key = /etc/mysql/ssl/server-key.pem
performance_schema = ON
2)副本连接主库(GTID 自动定位)
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='10.0.0.10', SOURCE_PORT=3306,
SOURCE_USER='repl', SOURCE_PASSWORD='Repl!Passw0rd',
SOURCE_SSL=1, SOURCE_AUTO_POSITION=1;
START REPLICA;
3)心跳与监控
- 主库创建心跳表与事件(前文 SQL)
- 副本创建视图(前文 SQL)
- 部署 mysqld_exporter(前文 systemd)
- Prometheus 抓取与规则(前文 YAML)
- Alertmanager 告警路由按团队维护