跨境电商的双11:香港机房多活与峰值削峰,BGP Anycast + CDN 预热 + Redis 热点隔离

去年双11凌晨 00:03 分,我们的购物车接口被一波「秒杀+黄牛」打穿,Redis 热点键像一颗微缩的恒星,疯狂坍缩把线程都拽进去;北向链路的 BGP 会话因为健康检查迟缓,Anycast 没及时撤宣,部分用户绕了半个地球。凌晨机房的冷风从走线孔里灌进来,我拎着手电,盯着机柜门上的冷凝水珠,耳机里轮番响的是 Slack 的 P1 告警和清洗厂商的回铃。
今年我不想再赌。于是有了这篇实战稿:一套我亲手在香港机房落地的多活架构与峰值削峰方案,包含硬件配置、网络拓扑、参数、风控与演练清单,也包括那些只会写在值班手册边角的「坑与解法」。
目标与指标
- 峰值 QPS(入口层):1.2M QPS(含静态/动静混合),目标 95% 命中 CDN。
- 应用层(核心写接口)稳态 85k QPS,峰值 150k QPS,P99 < 120 ms。
- 交易失败率 < 0.15%;风控拦截准确率 > 97%(人工复核样本)。
- 弹性回退:3 分钟内切出降级策略,30 秒内完成 BGP 撤宣与灰度恢复。
- 变更窗口:T-7 至 T 日 00:00 冻结高风险变更;T 日只允许白名单开关。
1. 拓扑鸟瞰(香港双可用区 + 广州冷备)
拓扑:
┌──────────────── CDN/Edge (多厂商) ────────────────┐
公网用户 → Anycast VIP (203.0.113.0/24) → 边缘 POP → 原站回源 (东亚最优)
└──────────────────────────────────────────────────┘
│ BGP Anycast(多运营商)
┌──────┴───────────┐
│ │
HK-A 可用区 HK-B 可用区
(新田/荃湾) (葵涌/将军澳)
┌──────────┐ ┌──────────┐
入口LB (Envoy/Nginx) 接入交换 入口LB (Envoy/Nginx) 接入交换
+ eBPF/XDP抗SYN TOR 100G + eBPF/XDP抗SYN TOR 100G
└──────────┘ └──────────┘
│ L3/L4/L7 负载均衡 (一致性哈希/熔断/限流)
▼
┌───────────────────────────────────┐
│ 应用层(无状态)K8s/裸金属混部 │
│ - checkout / cart / sku / auth │
└───────────────────────────────────┘
│
▼
┌───────────────┐ ┌──────────────────┐
│ Redis 集群(冷/热池) │ ←→ 消息队列(Kafka) │ ←→ 异步削峰
└───────────────┘ └──────────────────┘
│
▼
MySQL 主从 + Binlog CDC → ClickHouse 实时分析
│
▼
广州冷备站(跨域读库/对象存储)- 断路器级别切换
2. 硬件与机房落地清单(我真买了这些)
2.1 机柜与供配电
| 项 | 规格/参数 | 备注 |
|---|---|---|
| 机柜 | 42U,双路 32A C13/C19 | 两可用区各 3 柜 |
| 冷通道 | 封闭冷通道,地板下走线 | 热点机柜前置空调风墙 |
| 带宽 | HKIX/多运营商混接,2×100G 上联 | 各区独立上联,BFD |
2.2 网络设备
| 角色 | 型号/配置 | 用途 |
|---|---|---|
| 边界路由 | Juniper MX204(2×100G) | 运行 FRR/原生 BGP,Anycast 宣告 |
| TOR 交换 | Arista 7050X/Juniper QFX5120(48×25G + 6×100G) | 接入与汇聚 |
| 防火墙 | FortiGate 2000E(HA) | 东西向 ACL,DDoS 信令协同 |
2.3 服务器(CentOS 7.9,统一装 ELRepo kernel-ml 5.4.x 便于 eBPF)
| 角色 | 型号 | CPU/RAM/NIC | 存储 |
|---|---|---|---|
| 入口 LB | Dell R650 | 2×Xeon 8358,256G,2×100G | 2×480G SSD (RAID1) |
| 应用 | Dell R7525 | 2×EPYC 7543,512G,2×25G | 2×960G SSD (RAID1) |
| Redis 冷池 | Dell R650 | 1×Xeon 6348,256G,2×25G | 4×3.2TB NVMe |
| Redis 热池 | Dell R750 | 2×Xeon 8368,768G,2×100G | 6×3.84TB NVMe |
| MySQL | Dell R750 | 2×EPYC 7543,512G,2×25G | 8×3.84TB NVMe(RAID10) |
| Kafka | Dell R650 | 1×Xeon 6338,256G,2×25G | 6×3.84TB NVMe(JBOD) |
3. BGP Anycast:宣告、检测与撤宣
3.1 地址与策略
- Anycast 前缀:203.0.113.0/24(仅示例),各区宣告同一 /24,入口 VIP:203.0.113.10(HTTP/HTTPS)。
- 每区两家运营商(如 HKT、PCCW),MED/LocalPref 控制就近。
- BFD 300ms × 3,health-check + ip route replace 降级撤宣。
3.2 FRR bgpd.conf 片段(边界路由)
router bgp 65001
bgp router-id 203.0.113.2
neighbor 10.10.10.1 remote-as 100 # HKT
neighbor 10.10.20.1 remote-as 200 # PCCW
neighbor 10.10.10.1 timers 5 15
neighbor 10.10.20.1 timers 5 15
neighbor 10.10.10.1 bfd
neighbor 10.10.20.1 bfd
address-family ipv4 unicast
network 203.0.113.0/24
neighbor 10.10.10.1 route-map OUT out
neighbor 10.10.20.1 route-map OUT out
exit-address-family
route-map OUT permit 10
set community 65001:100 additive
set local-preference 200
3.3 健康检查联动撤宣(systemd + 脚本)
逻辑:当入口 LB 健康探测失败 **≥3 次/**30s 或上游清洗厂商回调 attack=severe,执行 vtysh -c 'conf t' -c 'router bgp 65001' -c 'no network 203.0.113.0/24',并在对侧 AZ 提高 LocalPref。
#!/bin/bash
FAIL=$(curl -s -m2 http://127.0.0.1:9000/health | jq -r .pass)
DDoS=$(curl -s -m2 http://127.0.0.1:9100/ddos_state | jq -r .level)
if [[ "$FAIL" != "true" || "$DDoS" == "severe" ]]; then
logger -t anycast "withdrawing /24 due to health:$FAIL ddos:$DDoS"
vtysh -c 'conf t' -c 'router bgp 65001' -c 'no network 203.0.113.0/24'
fi
4. CDN 预热与源站策略(命中率就是生命线)
4.1 预热清单(T-3 小时启动,T-30 分钟复核)
| 类目 | 路径/Pattern | TTL | 备注 |
|---|---|---|---|
| 静态资源 | /assets/*.{js,css} |
7d | 强缓存 + 版本号 |
| 商品图 | /img/sku/** |
3d | 大图+缩略图变体 |
| 活动页 | /promo/2025/1111/* |
1h | 多语言变体 |
| API GET | /api/v1/sku?id=* |
60s | 可缓存+签名忽略 |
4.2 预热脚本(多厂商 API 抽象)
# urls.txt 预生成(TopN SKU、活动页、静态清单等)
xargs -I{} -P16 bash -c '
curl -s -XPOST "https://edgeA.example.com/prefetch" -d "url={}" >/dev/null
curl -s -XPOST "https://edgeB.example.com/prefetch" -d "url={}" >/dev/null
' < urls.txt
4.3 源站 Cache 与签名回源
Nginx 原站:
proxy_cache_path /var/cache/nginx keys_zone=STATIC:10g max_size=100g inactive=1h;
map $request_uri $cache_key { default $request_uri; }
server {
listen 443 ssl http2;
location /assets/ {
proxy_cache STATIC;
proxy_ignore_headers Set-Cookie;
add_header X-Cache $upstream_cache_status;
proxy_pass http://app_pool;
}
# 对 GET 接口允许缓存(鉴权签名从 Vary 中剥离)
location /api/v1/sku {
proxy_cache STATIC;
proxy_cache_valid 200 60s;
proxy_hide_header Set-Cookie;
proxy_ignore_headers Cache-Control Expires;
proxy_pass http://app_pool;
}
}
5. 入口层削峰(令牌桶 + 排队 + 断路器)
Envoy 限流(关键接口如 POST /checkout):
rate_limit:
domain: "checkout"
descriptors:
- entries:
- key: remote_address
timeout: 0.02s
token_bucket:
max_tokens: 2000
tokens_per_fill: 2000
fill_interval: 1s
同时在 Nginx 层为长尾接口开 漏斗队列(超时丢弃):
limit_req_zone $binary_remote_addr zone=one:10m rate=50r/s;
location /cart/update {
limit_req zone=one burst=200 nodelay;
proxy_connect_timeout 1s;
proxy_read_timeout 2s;
}
6. Redis:热点识别与隔离(冷池/热池双轨)
6.1 集群拓扑
- 冷池(主业务缓存):Redis 7 Cluster,6 主 6 从,每主 64G Maxmemory,volatile-lru。
- 热池(热点隔离):独立 Redis 7 实例 4 台主(无从),maxmemory 256G/台,allkeys-lfu,100G NIC,CPU 绑核。
- 访问层:应用侧用 一致性哈希 + 本地 sidecar LRU(1G)。
6.2 热点识别(TopK + 采样)
- 采样器在网关侧以 1‰ 比例上报 <key, hit> 到 Kafka hotkey-topic。
- 消费器基于 Count-Min Sketch + TopK(redis-bloom 或内存实现),滚动窗口 30s。
- 阈值:同一 key 30s 命中 > 100k 且 miss 率 < 1% → 加入热池白名单。
简化版 Go 伪代码:
if cm.Estimate(key) > 100000 && missRate(key) < 0.01 {
hotPool.Add(key)
}
6.3 热点旁路(Lua 原子搬运 + TTL 重写)
当 key 进入热池:应用先读热池,不命中则回冷池,命中后把 TTL 重写为短 TTL(例如 30~60s),防止雪崩。
Lua(在应用侧 eval):
-- KEYS[1]: key, ARGV[1]: ttlSeconds
local v = redis.call('GET', KEYS[1])
if v then
redis.call('PEXPIRE', KEYS[1], ARGV[1]*1000)
return {1, v}
else
return {0, nil}
end
6.4 击穿/穿透/雪崩策略
击穿:互斥锁(setnx 锁 300ms)+ 后台刷新。
穿透:布隆过滤(skuId 不存在直接 404,本地布隆 1e7,fpp=0.01)。
雪崩:TTL 抖动(±20%),多层 cache(sidecar→热池→冷池)。
6.5 Redis 关键参数(热/冷池)
| 参数 | 冷池 | 热池 |
|---|---|---|
| maxmemory-policy | volatile-lru | allkeys-lfu |
| hz | 10 | 50 |
| tcp-keepalive | 60 | 30 |
| cluster-node-timeout | 15000 | N/A |
| activedefrag | yes | yes |
| latency-monitor-threshold | 10 | 1 |
7. 异步削峰(Kafka + 事务外化)
- 交易链路仅做必要校验和预占库存,重任务(发券、积分、消息)走 Kafka。
- Kafka 主题:checkout-log(3×集群副本;分区数 48),xfs + noatime,num.network.threads=8,num.io.threads=16。
- 消费端开启批量确认与幂等写。
- 超时策略:入口层超过 100ms 排队直接 429 + 降级提示(可配置开关)。
8. 数据库层(只写必须的东西)
- MySQL 8.0 主从,Semi-sync 打开,binlog_row_image=FULL(配合 CDC)。
- 核心表:order(主键雪花+时间分区)、stock(行级乐观锁 + version 字段)。
- 典型 SQL 限流:在 DAO 层为 SELECT ... FOR UPDATE 增加热点分片键,避免整段锁。
9. 系统与内核调优(CentOS 7.9 + kernel 5.4)
/etc/sysctl.d/99-tuning.conf:
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 10
net.core.netdev_max_backlog = 250000
fs.file-max = 10485760
vm.swappiness = 1
vm.max_map_count = 262144
绑核与 IRQ:
# 入口LB绑核:接收队列与工作线程分 NUMA 节点
for q in /sys/class/net/eth0/queues/rx-*; do
i=${q##*-}; echo $((i%16)) > $q/rps_cpus
done
10. 监控与红线(告警要「少而准」)
| 层级 | 指标 | 红线/动作 |
|---|---|---|
| Anycast | BFD flap > 3/5min | 自动降 Pref + Pager P1 |
| CDN | 命中率 < 90%(静态)、<70%(API) | 自动触发二次预热 |
| 入口LB | SYN 队列利用率 > 80% | 开启 XDP 抛弃+提升 backlog |
| 应用 | P99 > 200ms 或 5xx > 0.5% | 打开降级开关(读缓存/禁推荐) |
| Redis 热池 | GET avg > 1ms 或 hit < 95% | 扩容热池/扩大白名单 |
| MySQL | InnoDB row lock time > 50ms | 部分接口转异步/限流 |
11. 风控(把「羊毛」挡在门口)
- 设备画像:UA + IP 段 + TLS JA3 + 指纹(Canvas/字体/传感器),落盘至风控网关。
- 信誉分:基于近期行为滑窗(5min/24h),对低分用户限频/验证码。
- 清洗联动:WAF 规则自动同步至清洗厂商,命中阈值自动切高级防护。
- 关键接口:/login、/checkout 开强校验;灰名单仅走冷池缓存,拒绝热池提速。
Nginx 简化规则:
map $ja3_hash $rbl {
default 0;
"7721d..." 1; # 黑样本
}
server {
if ($rbl) { return 403; }
location /checkout {
limit_req zone=vip burst=100 nodelay;
# 验证签名/时间戳/nonce...
}
}
12. 演练清单(实战前的实战)
T-7 天(全链路压测 + 灰度)
- 合成流量 60% 峰值,逐层开缓存与限流。
- Anycast 人为降优;验证撤宣与回宣路径稳定。
- Redis 热点注入(Top100 SKU),验证热池命中。
- MySQL 主从延迟注入 200ms,验证事务外化。
T-1 天(静态预热 + 规则冻结)
- CDN 全量预热校验(抽样 2% 命中)。
- WAF/风控策略冻结并导出快照。
- 回滚点创建(镜像 + 配置版本)。
- 值班席位表、联络矩阵更新。
T 日(战术动作)
- 00:00 前 30 分钟降低探测阈值,开启更激进限流。
- 监控面板只保留 9 块关键大盘(其他静音)。
- 每 10 分钟节奏报(CDN 命中、P99、交易失败率)。
- 发生拥塞先开降级,再排查原因(纪律!)。
13. 坑与现场修复(血泪账本)
- Anycast 黑洞:健康检查只测 200 OK,未覆盖上游 TLS 握手超时 → 改为 合成交易探测(下单沙箱),并将失败计入撤宣阈值。
- Redis 热池抖动:TopK 窗口太短,热点在 1 分钟内频繁进出 → 改 30s 滑窗 + min_stay=120s。
- CDN 预热误伤:API 带签名参数被当不同 URL,缓存碎片化 → Vary 精简 + 参数白名单。
- Kafka 背压:磁盘写入放大导致 IO 等待高 → 改为 直接 IO + 批量大小调小,log.flush.interval.messages 调整配合。
- 内核队列满:netdev_max_backlog 不足导致丢包 → 提升至 250k,并配合 RSS 队列绑核。
- 数据库行锁:stock 表热行锁冲突 → 将 sku_id 做热分片(hash 路由),把锁扩散为多行。
14. 复盘模板(我怎么判断赢了)
| 维度 | 目标 | 实际 | 结论/改进 |
|---|---|---|---|
| CDN 命中 | 静态≥90%,API≥70% | ||
| 入口 P99 | ≤80ms | ||
| 交易失败率 | ≤0.15% | ||
| 风控准确率 | ≥97% | ||
| Redis 热池命中 | ≥95% | ||
| Anycast 撤宣 | ≤30s |
15. 附:落地指令与示例
15.1 一键调整系统参数(Ansible 片段)
- hosts: all
tasks:
- copy: src=99-tuning.conf dest=/etc/sysctl.d/99-tuning.conf
- command: sysctl --system
- lineinfile:
path: /etc/security/limits.conf
line: "* soft nofile 1048576"
- service: name=irqbalance state=stopped enabled=no
15.2 Redis 热池白名单推送
# hotlist.txt: 每行一个 key
xargs -I{} -P32 redis-cli -h HOTPOOL1 -a "$PASS" SET {} "$(date +%s)" EX 60 < hotlist.txt
15.3 eBPF/XDP 快速丢 SYN(cloudflare/katran 思路,示意)
# bpftool 加载 XDP 程序(示意,不展开)
ip link set dev eth0 xdp obj xdp_syn.o sec xdp
今年的 00:00,我会照例站在机柜前。监控面板的曲线像精心修剪的花园,Anycast 的路由几乎没有抖,Redis 热池的命中率稳得像块大理石。
00:07,风控提示有一波羊毛党加速入场,我们的限流轻轻合上闸门;00:15,物流回传开始涌入,Kafka 的水位像练过台阶舞,一步不差。
我摘下耳机,听见冷通道风机的低鸣——这声音去年听来像风暴,今年更像稳稳的呼吸。
走出机房的时候,天刚要亮。
真实的系统不是完美的,而是「在最差情况下还能优雅地坏」。
这就是我在香港机房做多活与削峰的全部:不追求神话,追求可控、可预期、可复原。
明年我们还会继续把这套打法磨下去,把每一次实践经验铆在架构上。