香港服务器运行 RHEL 9,如何把百万级跨境电商的数据库负载稳住

双十一前一周,凌晨 3:10,香港机房 5 楼 A 走廊的空调像一台巨型呼吸机。我盯着 PMM 的红色告警条——订单库 p95 延迟 760ms、重做日志写满、主从复制延迟 18s。微信群里运营在问:‘还能顶吗?’ 我回了三个字:给我十分钟。
这篇文章是我在 RHEL 9 上为一套跑在香港裸金属上的 MySQL 集群做性能压榨与稳定性加固的完整手记。不管你是第一次接手电商数据库,还是常年在机房“修飞机”的老司机,我尽量把每一步做成可复现的操作,并附上真实踩坑与修复过程。
1. 场景与约束
- 业务画像:跨境电商,用户主要来自中国内地、东南亚;读多写重(商品浏览 + 购物车读)、促销瞬时写峰(下单、库存扣减)。
- 目标:峰值百万级在线用户,订单峰时 68 万 TPS(含读),写入峰 815k TPS,p95 ≤ 50ms。
- 约束:必须在香港机房落地,RHEL 9,尽量不容器化数据库(DB 裸金属),应用侧可容器化。
2. 硬件与网络拓扑(实配清单)
| 角色 | 数量 | CPU | 内存 | 系统盘 | 数据盘 | 网卡 | 备注 |
|---|---|---|---|---|---|---|---|
| DB Primary (订单库) | 2(主备) | AMD EPYC 7543(32C/64T) | 512GB DDR4 | SATA SSD 480GB | 4× NVMe U.2 Samsung PM9A3 3.84TB 组 RAID10 | 2×25GbE(Mellanox CX-5) | 裸金属,RHEL 9.3 |
| DB Replica (只读) | 3 | 同上 | 512GB | 同上 | 同上 | 同上 | 读分流、报表 |
| Redis 集群 | 6 | EPYC 7313 | 256GB | SSD | NVMe | 25GbE | 会话、热点缓存 |
| ProxySQL/HAProxy | 4 | Xeon Gold | 64GB | SSD | - | 2×10GbE | 读写分离、连接池 |
| 备份/归档节点 | 1 | EPYC | 256GB | SSD | 大容量 SATA 盘 | 10GbE | XtraBackup + 对象存储 |
| 边界网络 | - | - | - | - | - | BGP,多线 | 到内地平均 RTT 20~35ms |
拓扑要点
- 主库(A 区)—备库(B 区)同城双机房,跨机房 L2 网络互通;
- 只读副本 3 台,跨机房均衡分布;
- 半同步复制(MySQL 8.0 group semi-sync),GTID 开启;
- 前置 ProxySQL 做读写分离与连接池;
- 应用侧 gRPC/HTTP 超时控制与幂等保障;
- 存储为 NVMe RAID10(mdadm),文件系统 XFS。
3. RHEL 9 操作系统层优化(从“地基”开始)
3.1 基础工具与固件
# 固件与微码
dnf update -y
dnf install -y microcode_ctl nvme-cli tuned chrony irqbalance numactl fio
# 开启 irqbalance
systemctl enable --now irqbalance
# NVMe 固件检查
nvme list
nvme fw-log /dev/nvme0
3.2 tuned 配置:低延迟优先
tuned-adm profile latency-performance
tuned-adm active
3.3 内核参数(/etc/sysctl.d/99-db.conf)
vm.swappiness=1
vm.dirty_ratio=10
vm.dirty_background_ratio=5
vm.max_map_count=1048576
fs.aio-max-nr=1048576
fs.file-max=10000000
net.core.somaxconn=65535
net.core.netdev_max_backlog=250000
net.ipv4.tcp_max_syn_backlog=262144
net.ipv4.ip_local_port_range=10000 65535
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_mtu_probing=1
# BBR(核 5.x)
net.core.default_qdisc=cake
net.ipv4.tcp_congestion_control=bbr
⚠️ 注:cake 在某些内核特性中更友于多队列;若不可用,退回 fq。
sysctl --system
3.4 透明大页与 NUMA
# 透明大页关闭(MySQL 在高写场景下更稳)
grubby --update-kernel=ALL --args="transparent_hugepage=never numa_balancing=disable"
# 验证
cat /sys/kernel/mm/transparent_hugepage/enabled
MySQL 进程绑定为 interleaved 避免单 NUMA 节点吃满:
numactl --interleave=all --cpunodebind=0-1 <mysqld>
3.5 磁盘与文件系统
RAID10(mdadm):4 块 NVMe,chunk=512K
mdadm --create /dev/md0 --level=10 --raid-devices=4 /dev/nvme{0,1,2,3}n1 --chunk=512
mkfs.xfs -f /dev/md0
mkdir -p /data/mysql
echo '/dev/md0 /data/mysql xfs defaults,noatime,nodiratime 0 0' >> /etc/fstab
mount -a
IO 调度:NVMe 使用 none(默认即可),读取前瞻
blockdev --setra 1024 /dev/md0
定期 trim
systemctl enable --now fstrim.timer
3.6 打开文件与进程限制(/etc/security/limits.d/mysql.conf)
mysql soft nofile 2000000
mysql hard nofile 2000000
mysql soft nproc 65535
mysql hard nproc 65535
4. MySQL 8.0:参数与布局(InnoDB 专注)
4.1 目录规划
- 数据目录:/data/mysql/data
- redo/undo:同阵列但独立子卷(便于度量)
- binlog:/data/mysql/binlog(单独子卷)
- 临时目录:/data/mysql/tmp
4.2 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=101
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_format=ROW
log_bin=/data/mysql/binlog/mysql-bin
binlog_expire_logs_seconds=604800 # 7天
sync_binlog=1 # 金融类建议 1;若追求吞吐可 100
innodb_buffer_pool_size=350G # ≈ 70% 内存
innodb_buffer_pool_instances=16
innodb_log_file_size=8G # redo 更大缓冲峰写
innodb_log_files_in_group=2
innodb_flush_log_at_trx_commit=1 # 强一致;如峰值过高可临时 2
innodb_flush_method=O_DIRECT_NO_FSYNC
innodb_io_capacity=40000
innodb_io_capacity_max=80000
innodb_read_io_threads=8
innodb_write_io_threads=8
innodb_page_cleaners=8
innodb_undo_log_truncate=ON
innodb_undo_tablespaces=3
max_connections=8000 # 配合 ProxySQL 连接池
table_open_cache=40000
open_files_limit=1500000
thread_cache_size=2000
# 事务与隔离
transaction_isolation=READ-COMMITTED
lock_wait_timeout=10
# 统计信息
innodb_stats_persistent=ON
histogram_generation_max_mem_size=20000000
# 半同步复制(组半同步)
plugin_load_add='semisync_master.so;semisync_slave.so'
rpl_semi_sync_master_enabled=ON
rpl_semi_sync_master_timeout=5000
rpl_semi_sync_slave_enabled=ON
# 性能/诊断
performance_schema=ON
performance-schema-instrument='stage/%=ON'
performance-schema-consumer-events-waits-current=ON
踩坑复盘:我们一开始 innodb_log_file_size=1G,在促销尖峰时 redo checkpoint 飙升,fsync 压死 IO,p95 直接爆炸。把 redo 提到 8G×2 后,尖峰写被平滑了。
4.3 表结构与主键设计
订单表避免 UUID 主键,采用 雪花 ID(时间 + 机器位 + 序列)。
二级索引尽量覆盖查询路径,减少回表。
分区:订单历史按月 RANGE 分区;库存事务按日期 + 商品维度分区。
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
sku_id BIGINT UNSIGNED NOT NULL,
status TINYINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY(id),
KEY idx_user_created (user_id, created_at),
KEY idx_sku_created (sku_id, created_at),
KEY idx_status_created (status, created_at)
)
PARTITION BY RANGE COLUMNS(created_at) (
PARTITION p2024m11 VALUES LESS THAN ('2024-12-01'),
PARTITION p2024m12 VALUES LESS THAN ('2025-01-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
经验:分区不是银弹。它优化历史归档与范围扫描,但不减少单分区内的索引复杂度。对热点分区的二级索引设计依旧关键。
4.4 统计信息与优化器
ANALYZE TABLE orders UPDATE HISTOGRAM ON user_id, sku_id WITH 1024 BUCKETS;
当出现“错误选择驱动表”时,先尝试直方图与索引隐身(Invisible Index)校正路径,而不是上来就写 Hint。
ALTER TABLE orders ALTER INDEX idx_status_created INVISIBLE;
EXPLAIN SELECT ... ; -- 验证计划
ALTER TABLE orders ALTER INDEX idx_status_created VISIBLE;
5. 读写分离与连接治理(ProxySQL)
5.1 连接池 sizing(经验公式)
MySQL 实际并发:CPU_cores × 2 到 CPU_cores × 4(取决于 IO)
以 32C→并发 128~256 为宜。让 ProxySQL 收纳“短连接风暴”。
5.2 ProxySQL 配置片段
-- 后端
INSERT INTO mysql_servers(hostgroup_id, hostname, port, max_connections)
VALUES (10, '10.0.0.11',3306,2000), -- 主
(20, '10.0.1.12',3306,2000), -- 从1
(20, '10.0.2.13',3306,2000), -- 从2
(20, '10.0.2.14',3306,2000); -- 从3
-- 用户
INSERT INTO mysql_users(username,password,default_hostgroup,transaction_persistent)
VALUES ('app','StrongPass',10,1);
-- 读写分离规则
INSERT INTO mysql_query_rules (rule_id,active,match_pattern,destination_hostgroup,apply)
VALUES
(100,1,'^SELECT.*FOR UPDATE',10,1),
(110,1,'^SELECT',20,1);
LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK;
LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
线上坑:一次因为 SELECT ... FOR UPDATE 写错成 SELECT ... FOR UPDATE NOWAIT 被老版本驱动误识别,ProxySQL 规则没命中,读请求打到只读库,造成“幽灵写”。加上SQL 审计与规则回归测试后解决。
6. 复制与故障切换
- GTID + 半同步,主库提交需至少 1 从确认;
- Orchestrator 监控复制拓扑,自动提升最优从库;
- 异地只读(新加坡)走 binlog 压缩传输(--compress),保证跨境链路的稳定性。
- 切换演练(季度):手动降主,欢迎写入失败 30s 以内恢复,应用侧幂等重试 + 下单 Token 防重。
7. 缓存与库存扣减路径
Redis:会话、购物车、商品详情、库存余量窗口;
库存扣减:先 Redis 预扣(原子脚本 + TTL)→ MySQL 最终落库 → 失败回补;
熔断策略:当从库延迟 > 5s 或主库 p95 > 200ms,降级只返回“基础属性”与延迟加载评价。
8. 安全与合规(在 RHEL 9 上保持 Enforcing)
# 防火墙
firewall-cmd --permanent --add-port=3306/tcp
firewall-cmd --reload
# SELinux 保持 Enforcing,添加策略而非关闭
dnf install -y policycoreutils-python-utils
semanage port -a -t mysqld_port_t -p tcp 3306
数据加密
- MySQL 8.0 InnoDB 表空间加密 + keyring_file(或 KMIP/云 KMS);
- 备份全程 传输层 TLS + 对象存储端加密。
9. 备份与 PITR(可恢复才叫稳定)
9.1 全量 + 增量(Percona XtraBackup)
# 全量
xtrabackup --backup --target-dir=/backup/full-$(date +%F) --parallel=8
# 增量(对上一个全量)
xtrabackup --backup --target-dir=/backup/inc-$(date +%F-%H) \
--incremental-basedir=/backup/full-2025-08-20 --parallel=8
# 准备
xtrabackup --prepare --apply-log-only --target-dir=/backup/full-2025-08-20
xtrabackup --prepare --target-dir=/backup/full-2025-08-20 \
--incremental-dir=/backup/inc-2025-08-21-10
9.2 Binlog 保留与时间点恢复
mysqlbinlog --start-datetime="2025-08-21 10:30:00" --stop-datetime="2025-08-21 10:45:00" \
/data/mysql/binlog/mysql-bin.000123 | mysql -uroot -p
10. 压测与成效(sysbench)
10.1 准备
dnf install -y sysbench
sysbench oltp_read_write --mysql-host=127.0.0.1 --mysql-user=bench --mysql-password=*** \
--mysql-db=shop --tables=32 --table-size=2000000 prepare
10.2 压测命令
sysbench oltp_read_write --threads=256 --time=300 \
--report-interval=10 --db-driver=mysql \
--mysql-host=127.0.0.1 --mysql-user=bench --mysql-password=*** \
--mysql-db=shop run
10.3 前后对比(节选)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 总 TPS(混合) | 18.5k | 34.2k |
| p95 延迟(ms) | 210 | 42 |
| 主从复制延迟 | 8~18s | ≤ 1.5s |
| fsync/s 高峰 | 12k | 7k(更平滑) |
| CPU sys% | 28% | 14% |
| 磁盘队列深度 | 长期 > 64 | 稳定在 16~32 |
11. 真实踩坑与“十分钟救火”的细节
redo 日志过小 → 尖峰卡死
现象:InnoDB: page_cleaner 警告、checkpoint age 快速接近上限;
处理:把 innodb_log_file_size 从 1G → 8G,O_DIRECT_NO_FSYNC;峰值写被缓冲。
NUMA 失衡 → 单节点内存打满
现象:节点 0 内存 95%,节点 1 45%,P95 波动;
处理:numactl --interleave=all + mysqld 重启窗口 + 观察巨页碎片回收。
ProxySQL 规则漏匹配 → 读请求打到从库
现象:SELECT ... FOR UPDATE 变体未命中规则,产生“幽灵写”;
处理:补充正则、引入SQL 回归测试与灰度发布。
binlog I/O 饱和
现象:binlog fsync 延迟,当天新开活动模块写放大;
处理:binlog 独立子卷 + sync_binlog=1 保持一致性(非金融峰可临时 100),并开启 binlog_group_commit 观测(MySQL8 默认有效)。
XFS 元数据峰值
现象:创建/删除临时表过多;
处理:应用侧减少临时表,DB 侧 tmpdir 独立子卷 + 足量 inode;定期 fstrim。
复制延迟异常
现象:从库 SQL_Thread 间歇性阻塞;
处理:把从库也调到 READ-COMMITTED,避免 GAP 锁;热点表增补覆盖索引,消除回表热点。
那天的“十分钟”,实际上做了三件事:切只读、扩 redo、强制规则命中。延迟从 700ms 直接落回 70ms,运营那边回我一个“👍”。
12. 运维日常与监控告警
PMM(Percona Monitoring & Management):采集 MySQL、OS、Disk 指标;
自定义告警:
- p95 > 80ms 连续 5 分钟;
- checkpoint age / max age > 0.8;
- 复制延迟 > 3s;
- Innodb_row_lock_time/s 异常;
- 只读副本线程池队列长度 > 100。
BPFtrace 针对 IO 热点、fsync 延迟做临时剖析;
演练:季度故障切换、月度备份恢复演练。
13. 部署清单(一键交付便利包)
- RHEL 9 基础更新 & 微码
- tuned latency-performance
- sysctl & limits 下发(Ansible)
- NVMe RAID10 + XFS + trim
- MySQL 8.0 安装与 my.cnf 下发
- ProxySQL 规则与健康检查
- Orchestrator 拓扑接管
- XtraBackup 任务 & 对象存储上传
- PMM 监控仪表盘 & 告警
- 故障演练剧本
Ansible 片段示例(下发 sysctl 与 limits):
- hosts: db
become: yes
tasks:
- name: push sysctl
copy:
dest: /etc/sysctl.d/99-db.conf
content: |
vm.swappiness=1
vm.max_map_count=1048576
fs.aio-max-nr=1048576
fs.file-max=10000000
net.core.somaxconn=65535
net.core.default_qdisc=cake
net.ipv4.tcp_congestion_control=bbr
- name: sysctl reload
command: sysctl --system
- name: push limits
copy:
dest: /etc/security/limits.d/mysql.conf
content: |
mysql soft nofile 2000000
mysql hard nofile 2000000
mysql soft nproc 65535
mysql hard nproc 65535
14. FAQ:几个常见抉择
Q1:innodb_flush_log_at_trx_commit 到底 1 还是 2?
金融强一致或下单强一致:1。
极端峰值、可接受秒级丢失:活动窗口可临时 2,结束回到 1。
Q2:表空间加密会不会掉速?
NVMe 下影响可控(5~10%),换取合规与数据安全,值得。
Q3:分库分表要不要上?
单实例在 NVMe + 正确 schema 下可扛到千万级行 / 库与万级 TPS。先把索引与缓存打满价值,再评估逻辑分片(按 user_id % N)与中间件成本。
15. 尾声:灯又亮起来
「凌晨 3:28,图表从红回绿。机房的监控屏上,TPS 像海浪退去。运营在群里发了句‘辛苦了’,我回了一个笑脸。第二天上午我把复盘贴到了 Wiki:redo 扩容、规则校验、NUMA 调整、分区清理。有人说,‘这篇看得出是你写的,因为连汗点都写出来了。’
我心想:数据库这行,没有神话,只有一次次把细节做对。」
附录:一键校验脚本(节选)
#!/usr/bin/env bash
set -euo pipefail
echo "[1/6] Kernel & tuned"
uname -a
tuned-adm active
echo "[2/6] THP & NUMA"
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /proc/sys/kernel/numa_balancing
echo "[3/6] Sysctl hot set"
sysctl vm.swappiness fs.aio-max-nr fs.file-max | cat
sysctl net.core.somaxconn net.ipv4.tcp_congestion_control | cat
echo "[4/6] NVMe & FS"
nvme list
lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT | grep -E 'md0|nvme'
xfs_info /data/mysql || true
echo "[5/6] MySQL key params"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_log_file_size';"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';"
echo "[6/6] Replication & semi-sync"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'rpl_semi_sync%';"
mysql -uroot -p -e "SHOW SLAVE STATUS\G" || true