上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-26 10:18 阅读量:642


那天凌晨 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 告警路由按团队维护
目录结构
全文