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

电商秒杀在香港机房部署:Nginx 微缓存、Kafka 异步化与 MySQL 主从延迟治理

发布人:Minchunlin 发布时间:2025-09-24 17:27 阅读量:687


香港葵涌的机房里,我站在 18U 的半柜前,手心全是汗。倒计时 12 分钟,Slack 上的最后一条广播写着:“开关在灰度 50%,风控阈值按方案 B。”我抬头看了眼 ToR 交换机上闪烁的绿灯,脑子里把整条链路又过了一遍:BGP 入口—L4—Nginx 微缓存—应用—Kafka—MySQL 主—只读从—监控告警。这是我们把秒杀活动迁到香港机房后的第一战,成败在此一举。

1. 业务背景与目标

  • 场景:百万 DAU 的电商应用,秒杀活动峰值 40–60 万 QPS 的静态读(商品详情、倒计时、库存只读)+ 3–5 万 QPS 的下单写入,主要来源于内地用户,跨境访问对链路质量极其敏感。
  • 迁移动机:合规与网络路径优化,选择香港机房(直连 CN2/GIA/CMI 混合线路,延迟可控)。
  • 目标:L7 微缓存命中率 ≥ 80%;2) 下单核心路径 P99 ≤ 120ms;3) MySQL 从库延迟 ≤ 300ms 并具备延迟感知路由;4) 全链路 可回滚、可演练、可观测。
  • 操作系统:CentOS 7.9(核心业务内核参数已贴近生产最佳实践)。

2. 机房与网络拓扑

2.1 物理与网络

结构:
   互联网用户 (CN/SEA)
         |
   Anycast/BGP 边界 (2 x ASR1001-X)
         |  双上联 2 x 10G
      [L4 LVS/Keepalived VIP]
         |  ECMP 4 x 25G
     ┌───────────────────────┐
     |         ToR (25G)     |
     └───────────────────────┘
      |        |        |
   [Nginx]  [App]    [Kafka]
      |        |        |
    [Redis]  [MySQL 主]  [监控/日志]
      |               |
   [只读从 x2]       [备份/归档]
  • 入口:BGP Anycast + 两台边界路由器,黑洞与清洗策略对接运营商。
  • L4:LVS/IPVS + Keepalived,VIP 漂移 < 1s,ECMP 到 Nginx 池。
  • 交换:ToR 25G,上行 100G 聚合,MLAG。

2.2 硬件清单(关键节点)

角色 型号/规格 CPU 内存 存储 网卡 数量
Nginx Dell R650 2 x Intel 6348 128GB 2 x 480G SATA 2 x 25G SFP28 6
应用 Dell R650 2 x Intel 6338 256GB 2 x 960G NVMe 2 x 25G SFP28 12
Kafka Broker Dell R740xd 2 x Intel 6348 256GB 8 x 1.92TB NVMe 2 x 25G 5
MySQL 主 Dell R750 2 x Intel 8352Y 512GB 8 x 3.84TB NVMe 2 x 25G 1
MySQL 从 Dell R750 2 x Intel 8352Y 512GB 8 x 3.84TB NVMe 2 x 25G 2
Redis Dell R650 2 x Intel 6338 256GB 4 x 1.92TB NVMe 2 x 25G 3

说明:Kafka 使用 NVMe 本地盘,避免网络存储尾延迟;MySQL 单机 NVMe RAID10,控制器直通。

3. 系统与内核参数(CentOS 7)

网络栈:

# /etc/sysctl.d/99-surge.conf
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_synack_retries = 2
net.ipv4.neigh.default.gc_thresh1 = 4096
net.ipv4.neigh.default.gc_thresh2 = 8192
net.ipv4.neigh.default.gc_thresh3 = 16384
net.nf_conntrack_max = 4194304

文件句柄与中断绑核:

# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576

# irqbalance 关闭,手动绑核 (示例,视 NIC 队列调整)
for i in {0..15}; do echo $i > /proc/irq/$((80+i))/smp_affinity_list; done

4. Nginx 微缓存(Microcaching)

4.1 策略设计

  • 缓存对象:商品详情 GET、SKU 价格/库存只读接口(非下单所用库存),倒计时与页面切片。
  • TTL:1–3 秒,可按接口分级。核心详情页 2s,价格接口 1s。
  • Key:`$scheme$host$request_uri$is_args$args|VARY:lang,device`(移除登录态 Cookie;多语言/端区分)。
  • 防击穿:`proxy_cache_lock on` + `lock_timeout 5s`;`stale-while-revalidate` 风格回源。
  • 一致性:库存写路径不读缓存;重要读(如“我的订单”)强制走主库。

4.2 配置片段(OpenResty/Nginx 1.20+)

proxy_cache_path /data/nginx/micro levels=1:2 keys_zone=MICRO:512m \
    max_size=40g inactive=3s use_temp_path=off;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_background_update on; # 后台刷新

map $http_cookie $bypass_login {
    default 0;
    ~*(session|token)= 1; # 登录态绕过缓存
}

map $http_accept_language $v_lang { default "zh-CN"; }
map $http_user_agent $v_device { default "mobile"; ~*"Windows|Mac" "pc"; }

server {
  listen 80 reuseport;
  server_name seckill.example.hk;

  set $cache_key "$scheme$host$request_uri$is_args$args|$v_lang|$v_device";

  location /api/v1/item/detail {
    proxy_cache MICRO;
    proxy_cache_key $cache_key;
    proxy_cache_valid 200 206 2s;
    proxy_cache_bypass $bypass_login $arg_nocache;
    proxy_no_cache    $bypass_login $arg_nocache;
    proxy_ignore_headers Set-Cookie;
    add_header X-Cache-Status $upstream_cache_status;

    proxy_next_upstream error timeout http_502 http_504;
    proxy_connect_timeout 1s;
    proxy_read_timeout 0.8s;
    proxy_send_timeout 0.8s;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://app_backend_pool;
  }

  # 防刷:
  limit_req_zone $binary_remote_addr zone=req_100:20m rate=100r/s;
  limit_req_status 429;
  location / {
    limit_req zone=req_100 burst=200 nodelay;
  }
}

upstream app_backend_pool {
  server 10.0.1.11:8080 max_fails=2 fail_timeout=3s;
  server 10.0.1.12:8080 max_fails=2 fail_timeout=3s;
  keepalive 512;
}

4.3 效果与指标

指标 活动前(无微缓存) 活动后(开启微缓存)
详情页平均 QPS(单节点) 15k 85k
Nginx CPU 占用(单核等效) 70% 42%
回源比例 92% 18%
详情页 P99 180ms 48ms

坑 1:初期忽略了 `Set-Cookie` 的回源打标,导致命中率被登录态污染。解决:`proxy_ignore_headers Set-Cookie` 并基于 Cookie 映射 cache\_bypass。

坑 2:瞬时击穿导致上游连接耗尽。解决:`proxy_cache_lock` + 合理 `burst`,并对 SKU 热点做静态化预热(后台更新)。

5. Kafka 异步化

5.1 主题与分区

* `order.created`(48 分区,RF=3)
* `order.paid`(24 分区,RF=3)
* `inventory.decr`(96 分区,RF=3,按 SKU Key 保序)

5.2 Broker/Topic 关键参数

参数 说明
num.partitions 48/96 按峰值与消费并行数计算
replication.factor 3 三副本抗单点
min.insync.replicas 2 acks=all 配合
log.segment.bytes 1GB 减少段切换
log.retention.hours 72 活动三天回溯窗口
unclean.leader.election.enable false 保证数据一致性

5.3 Producer(Java)

Properties p = new Properties();
p.put("bootstrap.servers", "kafka1:9092,kafka2:9092,kafka3:9092");
p.put("acks", "all");
p.put("enable.idempotence", true);
p.put("max.in.flight.requests.per.connection", 5);
p.put("retries", Integer.MAX_VALUE);
p.put("compression.type", "lz4");
p.put("linger.ms", 5);
p.put("batch.size", 262144); // 256KB
p.put("delivery.timeout.ms", 120000);

KafkaProducer<String, byte[]> producer = new KafkaProducer<>(p);
producer.send(new ProducerRecord<>("order.created", userId, payload));

5.4 Consumer(Go,按 SKU 分区保序)

cfg := sarama.NewConfig()
cfg.Version = sarama.V2_8_0_0
cfg.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
cfg.Consumer.Fetch.Default = 2 * 1024 * 1024
cfg.Net.MaxOpenRequests = 5

// 每分区一个 worker,单分区内串行,避免库存扣减的乱序

5.5 实战要点

  • 回压:消费者端限速 + Dead Letter Topic(失败重试 3 次入 `*.dlq`)。
  • 幂等:订单号为业务幂等键,MySQL `UNIQUE(order_no)` 避免重复入库。
  • 再均衡抖动:活动期间固定消费组并关闭自动伸缩,避免频繁 rebalance。

> 坑 3:批量太大(512KB)导致单批延迟上升。调整到 256KB + 5ms `linger`,吞吐/延迟平衡更佳。

6. MySQL 主从延迟治理

6.1 版本与参数

版本:MySQL 8.0.30(GTID + 并行复制显著优于 5.7)。

复制:半同步 + 并行复制

# /etc/my.cnf
server_id = 1001
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
binlog_row_image = FULL
sync_binlog = 1
rpl_semi_sync_master_enabled = ON
rpl_semi_sync_master_timeout = 1000
rpl_semi_sync_slave_enabled = ON

# InnoDB
innodb_buffer_pool_size = 360G
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT

# 并行复制
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 16
relay_log_recovery = ON

# 连接池
max_connections = 40000

6.2 延迟感知与读写路由

策略:

  1. 读优先路由到从库,但当从库延迟 > 300ms 或活跃事务数 > 阈值时自动切回主库;
  2. 秒杀核心读(库存、下单页)活动窗口内强制读主;
  3. Cache 旁路:热点 SKUs 使用 Redis 只读缓存,不影响下单路径。

实现:ProxySQL + 心跳表 + `Seconds_Behind_Master` 与 `gtid_executed` 差值双指标判定。

-- 心跳
CREATE TABLE cluster_heartbeat (ts TIMESTAMP NOT NULL, id TINYINT NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB;
-- 应用侧每 100ms 读取 ts 差值估算复制延迟

6.3 库存扣减模型(强一致)

写路径:应用发起下单 → MySQL 主悲观锁扣减库存(或 `UPDATE ... WHERE stock > 0`)→ 成功后写 `order.created` → 异步后置处理(发券、消息、物流)。

热点冲突:对超级热点 SKU 使用Redis + Lua 预扣(库存阈值-100 的安全水位),回写 MySQL 主。一旦 Redis 与主不一致,优先以主为准并短期关闭该 SKU 的预扣。

-- Redis 预扣示例
local k = KEYS[1]
local decr = tonumber(ARGV[1])
local stock = tonumber(redis.call('GET', k) or '0')
if stock >= decr then
  redis.call('DECRBY', k, decr)
  return 1
else
  return 0
end

> 坑 4:从库并行复制 `slave_parallel_workers` 过高会出现锁竞争和回放抖动。最佳点在 16(我们的数据/事务形态下),高于 24 反而抖动增加。

> 坑 5:仅凭 `Seconds_Behind_Master` 不可靠,在 I/O 线程卡顿时会“看起来很好”。必须结合 GTID 差值与心跳表。

7. 风控与限流

7.1 体系

  • 设备指纹 + IP 信誉 + UA 规则 + 行为评分(点击速率、页面序、Referer 连贯性)
  • 多级开关:灰度、阈值、白名单、黑名单、验证码、排队页

7.2 Nginx 层限速/连接

# IP 限速
limit_req_zone $binary_remote_addr zone=ip_50:20m rate=50r/s;
# 并发连接限制
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
  location /api/ {
    limit_req zone=ip_50 burst=100 nodelay;
    limit_conn addr 50;
  }
}

7.3 行为风控(应用侧)

  • 超阈值直接丢到排队页(静态资源,CDN/微缓存友好)。
  • 风控事件入 `risk.events` 主题,实时侧写到 ClickHouse 做回溯。

8. 演练与上线清单

8.1 演练项目

  • 缓存命中回归:逐接口压测,命中率≥80%
  • 回源保护:关闭后端 50% 实例,验证 `proxy_cache_lock` 有效
  • Broker 故障:下线 1 台 Kafka,验证 ISR 收敛与消费无感
  • 主从延迟注入:从库人为延迟 1s,验证路由回切主库
  • 入口黑洞:模拟 DDoS,运营商清洗回注流
  • 回滚:一键切换到老链路(CDN → 老机房),DNS/路由 TTL 验证

8.2 上线变更单(节选)

步骤 动作 验收点
1 开启 Nginx 微缓存 20% 灰度 命中率≥50%,5 分钟稳定
2 Kafka Producer 改为 acks=all 无报错,端到端延迟 < 30ms
3 ProxySQL 延迟阈值从 800ms→300ms 读主比例短时上升,错误率不增
4 打开风控方案 B(验证码动态开) 5xx 不升、429 轻微上升可接受

9. 压测结果(节选)

场景 QPS P50 P90 P99 错误率
详情页(微缓存 ON) 500k 9ms 18ms 48ms 0.02%
下单(读主+Redis 预扣) 35k 22ms 55ms 118ms 0.15%
Kafka 端到端(order.created→消费者) 100k 消息/s 7ms 14ms 26ms -
MySQL 主从延迟 - - - ≤ 220ms -

压测工具:wrk/ghz + 自研流量回放;生产流量样本脱敏后重放。

10. 监控与告警

  • Nginx:`$upstream_cache_status`、`accepted/s`、`active connections`、`499/502/504` 比例、后端回源率
  • Kafka:ISR 变化、`RequestQueueTimeMs`、`Purgatory` 长度、`UnderReplicatedPartitions`
  • MySQL:`Seconds_Behind_Master`、GTID 差、`Innodb_row_lock_time`、`Threads_running`
  • 系统:NIC 丢包、软中断占比、上下文切换、磁盘写放大

11. 现场坑点与快速修复

  1. TIME\_WAIT 暴涨:Nginx 后端未启用 HTTP/1.1 keepalive,修复 `proxy_http_version 1.1` + `Connection: ""`。
  2. conntrack 满:入口 L4 未调 `nf_conntrack_max`,峰值被打爆,扩容到 4M 并分配足够哈希桶。
  3. Kafka Rebalance 风暴:监控探针误把消费者进程重启为“修复”,导致一小时内 7 次再均衡。修复:活动窗口内禁止自动滚动升级。
  4. NVMe 温度告警:机柜散热不足,改风道、加盲板并限速后台批处理 IO。
  5. MySQL 自增热点:订单号自增热点页锁,改为随机前缀雪花 ID,B+Tree 分散。

12. 回滚与故障预案

  • 灰度闸门:所有变更可在 30 秒内回到“老路径”(CDN → 老机房集群)。
  • 数据一致性:订单写主—Kafka—异步消费者,若需回滚,确保消费者幂等并可从时间点重放(基于时间戳/偏移量)。
  • 只读降级:详情页降级到纯静态,库存展示延迟提示“约 1–2 秒”。

13. 凌晨 2:17 的那通电话

活动开始 8 分钟后,监控台一片绿色,命中率攀到 83%,下单 P99 稳在 110ms。Kafka 的 ISR 纹丝不动,MySQL 从库延迟 180–220ms 波动。我靠着机柜坐下,给同城的同事打了个电话:“可以把灰度拉到 100% 了。”

他笑,说:“刚才风控把一个 Selenium 军团拦了 70%。”

我看着屏幕上那条细细的延迟曲线,像维港夜里一条平静的航线。第二天清晨,我们把关键参数、演练记录和异常处置全写进了 Runbook。不是因为这次毫无波折,而是因为每一个被修掉的坑,都是下一次的护城河。

这篇记录给未来的我:记住微缓存只是第一层,真正的稳定来自可回滚的架构、可验证的演练,以及对现场细节的偏执。

14. 附:参数速查表

Nginx

  • proxy_cache_valid 200 206 2s
  • proxy_cache_lock on` / `proxy_cache_background_update on
  • limit_req rate=100r/s burst=200
  •  keepalive 512` / `reuseport

Kafka

  • replication.factor=3` / `min.insync.replicas=2
  • enable.idempotence=true` / `acks=all
  • linger.ms=5` / `batch.size=262144` / `compression.type=lz4

MySQL 8.0

  • slave_parallel_workers=16
  • binlog_format=ROW` / `gtid_mode=ON
  • innodb_flush_log_at_trx_commit=1
  • sync_binlog=1

如果你正在准备把秒杀迁到新的机房,愿这些一手坑与参数,能让你少走两步弯路。

目录结构
全文