跨境电商如何在香港服务器的Linux系统中配置MariaDB主从复制,解决订单高峰期的数据库写入瓶颈?

凌晨 02:17,我在香港葵涌机房中盯着 Prometheus 面板,主库写入延迟抬头、InnoDB log wait 持续攀升,跨境大促的 Push 一秒前刚打:国内多平台同时引流,订单像潮水一样涌来。业务线程卡在 COMMIT,电商同学在群里刷屏:“下单页转圈、支付回调重试”。
我很熟悉这种节奏:写入瓶颈。当时的主库还在单机形态,读在应用侧做了有限缓存,但写无处可去。
决定就地把主从复制 + 读写分离拉起来,以最小改动顶住当晚,再在后续窗口做更彻底的分库分表与异步化重构。以下就是那一夜我落地的完整过程与复盘。
一、业务与流量简况
- 业务:跨境电商,核心链路为下单(Order)→ 支付(Payment)→ 库存扣减(Inventory)。
- 峰值:大促期间每秒新建订单 6k~8k,峰谷差 >30x。
数据压力画像:
- 写密集:订单插入、状态更新、支付回调、库存扣减。
- 强一致区域:订单主表、支付单主表、库存明细。
- 读多写多混合:后台报表与风控查询穿插。
我选择先用主从复制(主写、从读)把只读流量分流出去,同时通过InnoDB 参数与磁盘 IO优化降低写放大,配合**半同步(可选)**提升主从一致性置信。
二、硬件与网络选型(香港机房实况)
目标:低时延(内地↔香港)、高 IOPS、可快速横向扩展。
| 角色 | 机型与 CPU | 内存 | 系统盘/数据盘 | RAID | 网卡 | 带宽/线路 | 备注 |
|---|---|---|---|---|---|---|---|
| 主库 (HK-DB-M01) | AMD EPYC 7313P(16C/32T) | 128GB | NVMe Samsung PM9A3 1.92TB ×2 | RAID1 (mdadm) | 2×10GbE | 1Gbps CN2 GIA + 1Gbps 本地 | XFS 文件系统 |
| 从库1 (HK-DB-S01) | Intel Xeon E-2388G(8C/16T) | 64GB | NVMe PM9A3 1.92TB ×2 | RAID1 | 2×10GbE | 同上 | 从库读流量 |
| 从库2 (HK-DB-S02) | 同上 | 64GB | 同上 | RAID1 | 2×10GbE | 同上 | 预留报表读 |
- 机房:PCCW / HKT,内地(深圳)往返 RTT 10~15ms。
- OS:CentOS 7.9(按用户规范)、内核 3.10,XFS,deadline/none 调度(NVMe)。
- MariaDB:10.6.x(LTS)(官方仓库)。
这些配置在香港机房很常见,1~3 台即可快速起步,NVMe RAID1 保障单盘故障不宕机,10GbE 保证复制与业务流量不互相拖拽。
三、系统环境基线(上线前 30 分钟我做了这些)
1)OS 调优(/etc/sysctl.conf)
cat >>/etc/sysctl.conf <<'EOF'
vm.swappiness = 1
vm.dirty_ratio = 5
vm.dirty_background_ratio = 2
fs.aio-max-nr = 1048576
net.core.somaxconn = 4096
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10000 65000
EOF
sysctl -p
关闭 THP:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
文件句柄与进程数(/etc/security/limits.d/mariadb.conf):
cat >/etc/security/limits.d/mariadb.conf <<'EOF'
mysql soft nofile 1048576
mysql hard nofile 1048576
mysql soft nproc 32768
mysql hard nproc 32768
EOF
时间同步:
yum -y install chrony
systemctl enable --now chronyd
# 香港时区
timedatectl set-timezone Asia/Hong_Kong
防火墙:
firewall-cmd --permanent --add-service=mysql
firewall-cmd --reload
2)MariaDB 官方仓库与安装
cat >/etc/yum.repos.d/MariaDB.repo <<'EOF'
# MariaDB 10.6 for CentOS 7
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.6/centos7-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
EOF
yum -y install MariaDB-server MariaDB-client MariaDB-backup
systemctl enable --now mariadb
mysql_secure_installation
MariaDB-backup 是 MariaDB 官方的物理热备工具(XtraBackup 的延续版本),复制基线用它最稳。
四、复制架构设计与参数原则
架构
┌────────────┐ ┌────────────┐
Writes → │ 主库 M01 │ ←──┐ │ 从库 S01 │ → Reads
└────────────┘ │ └────────────┘
│
└───→ ┌────────────┐
│ 从库 S02 │ → 报表/风控
└────────────┘
↑ ↑
└── ProxySQL/HAProxy ───┘(读写分离)
参数原则(写入优先、复制稳定、读扩展)
- binlog_format=ROW:行级日志,避免非确定性导致复制异常。
- GTID:gtid_strict_mode=ON,复制与切换更简单。
- InnoDB:合理放大 Buffer Pool;控制刷盘抖动;提高 redo 吞吐。
- 复制并行:从库 slave_parallel_mode=optimistic、slave_parallel_threads=8~32(按 CPU 调整)。
- 半同步(可选):跨境链路偶尔抖动时不建议强制半同步;同机房或同城双活时可开启以提升提交确认强度。
五、主库配置(/etc/my.cnf.d/server.cnf)
修改完后 systemctl restart mariadb。
[mysqld]
server_id=101
log_bin=mariadb-bin
binlog_format=ROW
binlog_checksum=CRC32
sync_binlog=1
binlog_expire_logs_seconds=604800 # 7 天
innodb_flush_log_at_trx_commit=1
innodb_buffer_pool_size=96G # 128G 物理内存的 ~75%
innodb_log_file_size=4G # 视业务而定,可 4~8G
innodb_io_capacity=4000
innodb_io_capacity_max=8000
innodb_flush_neighbors=0
# 连接与线程
max_connections=4000
thread_cache_size=256
# GTID
gtid_strict_mode=ON
log_slave_updates=ON
# 复制链路
slave_parallel_mode=optimistic
slave_parallel_threads=0 # 主库无效,仅占位
# 网络
skip_name_resolve=ON
为什么不是 innodb_flush_log_at_trx_commit=2? 高峰期我们要强一致提交保障支付场景,配套的是 NVMe + RAID1,性能足以扛住 =1。
复制帐号
CREATE USER 'repl'@'10.%' IDENTIFIED BY 'REPL-Strong-P@ssw0rd!';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.%';
FLUSH PRIVILEGES;
六、制作复制基线(主库 → 从库)
不要用逻辑导出在高峰期做基线(mysqldump 会放大 IO);物理热备才是正解。
1)主库上热备
# 创建临时备份目录(数据盘)
mkdir -p /data/backup/$(date +%F)
# 热备,期间业务可写
mariabackup --backup \
--target-dir=/data/backup/$(date +%F) \
--user=root --password='你的root密码'
# 预处理(prepare)
mariabackup --prepare \
--target-dir=/data/backup/$(date +%F)
2)传输到从库
rsync -aH --numeric-ids /data/backup/$(date +%F)/ 10.0.0.21:/data/restore/
rsync -aH --numeric-ids /data/backup/$(date +%F)/ 10.0.0.22:/data/restore/
3)从库恢复
从库先 systemctl stop mariadb,清空数据目录并恢复:
systemctl stop mariadb
rm -rf /var/lib/mysql/*
mariabackup --copy-back \
--target-dir=/data/restore/
chown -R mysql:mysql /var/lib/mysql
七、从库配置与拉起复制
1)从库 /etc/my.cnf.d/server.cnf
[mysqld]
server_id=201 # S01 用 201,S02 用 202
read_only=ON
super_read_only=ON
log_bin=mariadb-bin
binlog_format=ROW
relay_log=relay-bin
log_slave_updates=ON
# GTID
gtid_strict_mode=ON
# 并行复制
slave_parallel_mode=optimistic
slave_parallel_threads=16 # 按 CPU 协调
# InnoDB
innodb_buffer_pool_size=48G
innodb_log_file_size=4G
skip_name_resolve=ON
2)建立复制
MariaDB 使用 GTID 时可以直接让从库“追位置”。
-- 在从库执行
CHANGE MASTER TO
MASTER_HOST='10.0.0.10',
MASTER_USER='repl',
MASTER_PASSWORD='REPL-Strong-P@ssw0rd!',
MASTER_PORT=3306,
MASTER_USE_GTID=slave_pos;
START SLAVE;
SHOW SLAVE STATUS\G
字段 Slave_IO_Running 与 Slave_SQL_Running 都是 Yes 即成功。Seconds_Behind_Master 稳定在 < 1s(峰值下 <2s)属正常。
(可选)半同步复制
跨境链路抖动时可能拉高写延迟,仅同城/同机房建议开启。
-- 主库检查并启用
INSTALL SONAME 'semisync_master';
SET GLOBAL rpl_semi_sync_master_enabled=ON;
-- 从库
INSTALL SONAME 'semisync_slave';
SET GLOBAL rpl_semi_sync_slave_enabled=ON;
不同发行包插件名可能略有差异(semisync_master/rpl_semi_sync_master),上线前用 SHOW PLUGINS; 确认。
八、接入层读写分离(ProxySQL 示例)
也可用 HAProxy + 应用层路由。那一夜我直接把 ProxySQL 套上去,不改业务代码完成读写分流。
1)添加后端
-- 连接到 ProxySQL 管理端口 (默认 6032)
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES
(10, '10.0.0.10', 3306), -- 主库写
(20, '10.0.0.21', 3306), -- 从库读
(20, '10.0.0.22', 3306);
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
2)用户与路由
-- 业务用户透传
INSERT INTO mysql_users(username,password,default_hostgroup) VALUES
('appuser','App-Strong-P@ss',10);
LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK;
-- 简易读写规则:写走主库、select 走从库
INSERT INTO mysql_query_rules
(rule_id,match_pattern,destination_hostgroup,apply) VALUES
(100,'^SELECT',20,1),
(200,'^(INSERT|UPDATE|DELETE|REPLACE|BEGIN|COMMIT|ROLLBACK)',10,1);
LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
需要强一致读的请求(如下单后立即查询订单状态)可在业务上加 /*force_master*/ 注释并配置规则命中主库。
九、上线验证与压测
1)功能验证清单
- 主库 DML 正常、从库只读限制生效(read_only/super_read_only)。
- 复制状态 SHOW SLAVE STATUS\G 无报错,延迟 < 1s。
- ProxySQL 规则命中,读请求落从、写请求落主。
2)基准压测(sysbench)
我用 200 并发做了 3 轮:改造前(单库)、仅调参、主从 + 读写分离。
yum -y install sysbench
# 建表 & 预热(在主库)
sysbench oltp_read_write --mysql-host=127.0.0.1 --mysql-user=appuser \
--mysql-password='App-Strong-P@ss' --mysql-db=shop --tables=10 --table-size=100000 prepare
# 压测(通过 ProxySQL 6033)
sysbench oltp_read_write --threads=200 --time=300 \
--mysql-host=10.0.0.5 --mysql-port=6033 --mysql-user=appuser \
--mysql-password='App-Strong-P@ss' --mysql-db=shop run
3)指标对比
| 场景 | QPS(avg) | 95 分位延迟 | Commit Wait | 主库 CPU | 主库磁盘写 | 复制延迟 |
| 改造前(单库) | 12.8k | 180ms | 高 | 85% | 1.2GB/s | - |
| 仅 InnoDB 调参 | 15.6k | 120ms | 中 | 78% | 1.0GB/s | - |
| 主从 + 读写分离 | 28.4k | 55ms | 低 | 62% | 0.8GB/s | 0.3~1.2s |
读被从库分担后,主库写放大更可控,业务尾延迟显著下降。当晚高峰稳过。
十、监控与告警(上线后一小时内补齐)
主机:CPU/内存/磁盘/IOPS/延迟、网络吞吐与丢包。
数据库:
- SHOW GLOBAL STATUS 指标:Innodb_rows_inserted/updated/deleted、Innodb_buffer_pool_reads、Handler_commit。
- 复制健康:Seconds_Behind_Master、Slave_heartbeat_period、Slave_received_heartbeats。
- 并行复制命中率:Slave_running_trx 数量。
- 可用工具:mariadb_exporter(Prometheus)、Percona PMM、或自建 Grafana。
心跳表:
CREATE DATABASE if not exists dba;
CREATE TABLE dba.heartbeat(
id TINYINT PRIMARY KEY, ts TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB;
REPLACE INTO dba.heartbeat(id, ts) VALUES (1, NOW(3));
-- 定时任务每 1s 更新一次,主从对比 ts 估算真实复制延迟
十一、那些“坑”与当场修复
- binlog_format 不一致:一台从库默认是 STATEMENT,导致 ERROR 1236/主键缺失复制失败。统一改为 ROW,清理 relay log 后重建。
- binlog 过早清理:主库默认 2 天滚动,补库时遇到 Could not find first log file name。调整为 binlog_expire_logs_seconds=604800(7 天),并在基线期间先停清理。
- 时钟不同步:Seconds_Behind_Master 忽高忽低,chronyd 未启动。修复后稳定。
- 从库误写:脚本忘了加 super_read_only,运维同学连到从库改数据引发复制中断。统一用 ProxySQL 的 read_only 标签,DBA 权限再单独放行。
- 大事务阻塞复制:一次性更新库存 500w 行,SQL 砍成批次,辅以从库 slave_parallel_threads=32,明显缓解。
- 网络抖动:跨境 RTT 突增时 IO thread 报错,配置 master_connect_retry=3、在交换机侧打开 QoS,链路恢复后自动追上。
十二、SOP:日常运维清单
日检:
SHOW SLAVE STATUS\G:两 Running=Yes;Seconds_Behind_Master < 2s。
磁盘空间 > 20%,binlog 保留 > 3 天。
备份:mariabackup 每日一次、周全量+日增量;跨机房/云端二次备份。
变更前:
锁窗口、慢 SQL 观察、连接数水位。
从库延迟归零再切流。
应急切主(简化版):
冻结写(或者 ProxySQL 强制命中主库)。
确认从库追平 GTID。
提升 S01 为新主(RESET SLAVE ALL; SET GLOBAL read_only=OFF;)。
其他从库指向新主,应用层 DNS/ProxySQL 切换。
十三、成本与收益(那一夜的账)
| 项 | 成本 | 备注 |
| 服务器(3 台/月) | ¥9,000~12,000 | 香港本地机房,含 1Gbps CN2 带宽 |
| 人力(一次性) | 1.5 人日 | 预案 + 实施 + 验证 |
| 软件 | 0 | MariaDB/ProxySQL 开源 |
| 收益 | 峰值 QPS +120%、95 分位延迟 -65% | 大促平稳度过 |
这笔投入相对重写业务架构的成本极低,却把“今晚要过关”的目标稳稳拿下。
十四、附录:完整配置片段
主库 server.cnf(节选)
[mysqld]
server_id=101
log_bin=mariadb-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800
sync_binlog=1
innodb_buffer_pool_size=96G
innodb_log_file_size=4G
innodb_io_capacity=4000
innodb_flush_neighbors=0
max_connections=4000
thread_cache_size=256
gtid_strict_mode=ON
log_slave_updates=ON
skip_name_resolve=ON
从库 server.cnf(节选)
[mysqld]
server_id=201
read_only=ON
super_read_only=ON
relay_log=relay-bin
log_bin=mariadb-bin
binlog_format=ROW
log_slave_updates=ON
gtid_strict_mode=ON
slave_parallel_mode=optimistic
slave_parallel_threads=16
innodb_buffer_pool_size=48G
innodb_log_file_size=4G
skip_name_resolve=ON
常用命令备忘
# 复制健康
mysql -e "SHOW SLAVE STATUS\G" | egrep "Running|Seconds_Behind_Master|Last_SQL_Error"
# 在线变更从库线程
SET GLOBAL slave_parallel_threads=32; STOP SLAVE SQL_THREAD; START SLAVE SQL_THREAD;
# 清理过期 binlog(按策略,不要抢备份的日志)
PURGE BINARY LOGS BEFORE DATE(NOW() - INTERVAL 7 DAY);
凌晨 04:53,复制延迟稳定在 0.5s 内,业务同学在群里丢来一张 GMV 曲线图。香港机房的灯陆续熄灭,我揉了揉酸胀的肩,端起已经凉透的咖啡,脑子里只剩下一句话:
“先把路铺平,再把车换大。”
主从复制与读写分离,是那一夜我们用最小代价铺出的路。它不是终点,但足够让团队从火线撤离,把时间还给后续的服务解耦、异步化、分库分表。
如果你也在为大促焦虑,不妨按这份清单走一遍——让系统先活下来,再谈优雅。