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

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

发布人:Minchunlin 发布时间:2025-08-29 09:54 阅读量:674


双十一前一周,凌晨 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
目录结构
全文