
双 11 当晚 00:03,香港机房的监控墙上一片绿里掺着几抹黄:静态资源 60+ Gbps 峰值流量被 CDN 吃下了,但订单提交接口的 P95 从 220ms 抬头到 780ms,队友在 Slack 里连发“已重试”,我把咖啡扣在交换机上方的防尘盖上,手伸进机柜——不是去拔线,是去确认第二张 25G 网卡的 LACP确实在跑。
这一秒我意识到:静态的都稳了,卡脖子的只剩“动态下单链路”。我按预案切了边缘限流 + 令牌风暴熄火 + 微缓存旁路,三分钟后,订单成功率从 96.1% 回到 99.6%。
这篇文章,就是把那晚我做过的每一步,原样复刻成可部署的教程,以及我踩过的坑、当场怎么补。
一、目标架构(先上图再分解)
核心思路:“静态尽量 CDN,动态尽量就近复用长连接+ 拆压后端,订单入口限流 + 幂等 + 削峰,数据库只做最终确认。”
[User/APP]
│
├──> [CDN/WAF/Anti-Bot/RateLimit]
│ │ │
│ │ └──(动态加速/连接复用/边缘限流)
│ └──> [Edge Microcache for GET]
│
└──> [Anycast/BGP 回源]
│
[HK LB层:L4 LVS + L7 OpenResty/Nginx]
│
┌──────────┴──────────┐
[订单API集群] [风控/库存服务]
│ │
├── 幂等键/令牌校验 └── Redis 预扣减(Lua原子)
│
├── Kafka/NATS 订单流(削峰)
│
└── MySQL 主写(行级锁最小化) + 只读副本
二、硬件与网络参数(香港节点实配)
| 角色 | 机型/CPU | 内存 | 磁盘 | 网卡 | 系统 | 带宽 | 备注 |
|---|---|---|---|---|---|---|---|
| LB 层(2 台) | Dual Xeon Silver 4314 | 128GB | 2×960GB NVMe RAID1 | 2×25GbE (LACP) | CentOS 7.9 | BGP 10G 计费 | lvs + keepalived / OpenResty |
| API(6 台起步) | Xeon Gold 6342 | 256GB | 2×1.92TB NVMe RAID1 | 2×25GbE | CentOS 7.9 | 内网 25G | Go/Java 服务 |
| Redis(3 台) | Xeon 6330 | 256GB | 4×1.92TB NVMe | 2×25GbE | CentOS 7.9 | 内网 25G | Cluster 模式 |
| MySQL(3 台) | Xeon 6330 | 256GB | 8×3.84TB NVMe (RAID10) | 2×25GbE | CentOS 7.9 | 内网 25G | 主写 + 2 只读 |
| Kafka(3 台) | 同 API | 128GB | 6×1.92TB NVMe | 2×25GbE | CentOS 7.9 | 内网 25G | 订单事件流 |
理由:
- 25GbE LACP 在回源洪峰下明显好于 10G;
- NVMe RAID10 给 MySQL redo/ibd 提供稳定低延迟;
- Redis Cluster 横向扩容保证库存预扣与令牌校验毫秒级;
- 保持 CentOS 7(用户要求):内核 3.10 稳定,驱动与业务成熟度高。
三、系统内核与网络栈优化(CentOS 7)
1) 文件句柄与连接跟踪
/etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
/etc/sysctl.d/99-s11.conf
fs.file-max = 2097152
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_sack = 1
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_low_latency = 1
# 连接跟踪(如有 iptables/nft)
net.netfilter.nf_conntrack_max = 4194304
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
坑 1: 双 25G 下,nf_conntrack_max 若太低会出现间歇性 5xx,且难以复现。按 并发连接数 × 2 预留。
2) NIC/RSS/中断绑核
# 关闭网卡省电、强制多队列
ethtool -K eth0 gro on gso on tso on rx on tx on
ethtool -G eth0 rx 4096 tx 4096
# 设置 RSS 队列与 CPU 亲和
for i in /proc/irq/*/eth0*/smp_affinity_list; do echo "0-15" > $i; done
# NUMA 绑定(示意)
numactl --cpunodebind=0 --membind=0 <service>
坑 2: irqbalance 在某些驱动版本会“漂移”,手动固定更稳。
四、CDN 策略:把能卸的压力都卸给边缘
1) 域名与路由
主域:www.example.com(CDN)
API 域:api.example.com(同 CDN,但不缓存 POST;启用动态加速/连接复用)
2) 缓存与加速规则(边缘)
静态:/static/*、/assets/*、/img/*
TTL:7d;忽略所有 utm_* 查询串;强制 Cache-Control: public, max-age=604800, immutable
页面 GET 微缓存(微秒级):/product/*、/catalog/*
TTL:1-3s;Vary: Accept-Encoding, Cookie(uid|region)
目的:抗“刷新风暴”
订单接口:/api/order/submit
不缓存;开启 Edge RateLimit:用户维度(UID) 20req/10s,IP 100req/10s;
连接复用/HTTP2/HTTP3 开启;TLS Session Resumption 开启。
3) 边缘 WAF/Anti-Bot
阈值触发人机挑战:单 IP 对 /api/order/* 4xx 比例 > 20% 持续 30s
拦截 UA 黑名单 + header 指纹异常
白名单:支付回调 IP 段、内部健康检查 UA
坑 3: 某些外层 WAF 会对 POST body 做深度检查,导致大促时解析路径拖慢。只对关键字段做精简匹配,并把 JSON 大字段加入“跳过解包名单”。
五、L7:OpenResty/Nginx 回源与微缓存
/etc/nginx/nginx.conf(关键片段)
worker_processes auto;
worker_rlimit_nofile 1048576;
events { worker_connections 65535; }
http {
include mime.types;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 10000;
client_body_buffer_size 64k;
client_max_body_size 10m;
# Microcache for idempotent GET
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:512m
max_size=30g inactive=10s use_temp_path=off;
upstream api_upstream {
least_conn;
server 10.10.1.11:8080 max_fails=2 fail_timeout=3s;
server 10.10.1.12:8080 max_fails=2 fail_timeout=3s;
keepalive 512;
}
map $request_uri $microcache_bypass {
default 0;
~^/api/order/ 1; # 订单接口不缓存
~^/user/ 1; # 登录态接口不缓存
}
server {
listen 80 reuseport;
listen 443 ssl http2 reuseport;
server_name api.example.com;
# TLS/HTTP2
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 10m;
ssl_buffer_size 4k;
# 微缓存应用在 GET
location / {
set $cacheable 0;
if ($request_method = GET) { set $cacheable 1; }
if ($microcache_bypass = 1) { set $cacheable 0; }
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Request-Id $request_id;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache microcache;
proxy_cache_valid 200 3s;
proxy_cache_bypass $http_cache_control $cookie_session !$cacheable;
proxy_no_cache $http_cache_control $cookie_session !$cacheable;
proxy_pass http://api_upstream;
}
# 订单提交单独限速(双保险,边缘也限)
location = /api/order/submit {
limit_req zone=by_uid burst=30 nodelay;
proxy_pass http://api_upstream;
}
}
# limit_req 定义(基于 uid header/cookie)
limit_req_zone $http_x_uid zone=by_uid:20m rate=120r/m;
}
坑 4: proxy_cache_bypass 与 proxy_no_cache 条件要一致,否则会出现命中/绕过不对称,导致“莫名其妙的旧数据”。
六、幂等 & 令牌:把“连点两下”变成一次
1) 令牌签发(/api/order/prepare)
生成一次性 order_token(30 秒 TTL),写入 Redis,并绑定 uid + cart_hash
返回给前端,提交时必须携带
OpenResty + Redis 校验示例(Lua):
-- access_by_lua_block*
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(5)
assert(red:connect("10.20.0.5", 6379))
local uid = ngx.req.get_headers()["X-UID"]
local token = ngx.req.get_headers()["X-ORDER-TOKEN"]
if not uid or not token then
return ngx.exit(400)
end
local ok, err = red:eval([[
local key = KEYS[1]
if redis.call('GET', key) then
redis.call('DEL', key)
return 1
else
return 0
end
]], 1, "order:token:"..uid..":"..token)
if ok ~= 1 then
return ngx.exit(409) -- Conflict: 重复或失效
end
坑 5: 令牌若不 DEL 掉,会被重放。必须用 EVAL 原子校验+删除。
七、库存预扣与削峰:Redis Lua + Kafka
1) 预扣减(原子脚本)
-- KEYS[1] = sku:stock
-- ARGV[1] = req_qty
-- 返回 1 表示成功,0 表示库存不足
local stock = tonumber(redis.call('GET', KEYS[1]) or "0")
local need = tonumber(ARGV[1])
if stock >= need then
redis.call('DECRBY', KEYS[1], need)
return 1
else
return 0
end
2) 削峰队列
API 同步返回“已受理”(状态 pending),写 Kafka(topic: order_events)
消费者集群将订单持久化 MySQL,并做最终一致性(超时回补库存)
Go 版下单入口(节选):
idKey := fmt.Sprintf("idem:%s", req.IdempotencyKey)
ok, _ := rdb.SetNX(ctx, idKey, 1, 10*time.Minute).Result()
if !ok {
return Conflict("DUP_REQ")
}
if !PreDeductStock(req.SKU, req.Qty) {
return Error("OUT_OF_STOCK")
}
evt := OrderEvent{UID: uid, SKU: req.SKU, Qty: req.Qty, Token: req.Token}
kafka.Produce("order_events", evt)
return Accepted("PENDING") // 202
坑 6: 直接“同步落库 + 行锁”在高并发下会把 MySQL 撞死。预扣减 + 异步确认能把尖峰打平。
八、数据库策略(MySQL 8,主写 + 只读)
隔离级别:READ COMMITTED(减少 gap lock)
主键:BIGINT 雪花/ULID,避免自增热点
订单表:热点字段拆列,二级索引定向命中;支付状态写单行,避免宽表更新
CREATE TABLE `orders` (
`id` BIGINT PRIMARY KEY,
`uid` BIGINT NOT NULL,
`status` TINYINT NOT NULL, -- 0 pending, 1 paid, 2 canceled
`sku` BIGINT NOT NULL,
`qty` INT NOT NULL,
`price` INT NOT NULL, -- 分
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_uid_time` (`uid`,`created_at`),
KEY `idx_status_time` (`status`,`created_at`)
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
坑 7: 一定要避免大事务,订单写入与库存确认、支付状态更新分离;通过 outbox 或事件总线保障一致性。
九、容量规划与压测基线
1) 估算表(我们线上实测口径)
| 模块 | 单节点能力 | 集群规模 | 峰值估算 |
|---|---|---|---|
| CDN 静态回源 | < 5% | - | 峰值 60-120Gbps(主在边缘) |
| L7 OpenResty | ~120k RPS(GET 微缓存命中) | 2 | 足够 |
| 订单提交 API | 2.5k RPS/台(P95 < 120ms) | 6 | ~15k RPS |
| Redis 预扣 | 80k-120k ops/s/分片 | 6 分片 | 50k 安全 |
| Kafka 写入 | 200MB/s/节点 | 3 | 充裕 |
| MySQL 主写 | 8-12k TPS | 1 | 足够“确认链路” |
2) 压测命令(示例)
# GET 微缓存
wrk -t16 -c2000 -d60s --latency https://api.example.com/product/123
# POST 提交(幂等键)
hey -m POST -n 200000 -c 1000 -H "X-UID: 10001" -H "X-ORDER-TOKEN: t1" \
-D payload.json https://api.example.com/api/order/submit
坑 8: 有些压测工具默认不复用连接,记得开启 keep-alive 或改并发模型,才能贴近真实。
十、灰度 & 降级预案(真遇到再感谢自己)
灰度:按 UID hash 1%→5%→20%→全量;配合金丝雀版本
降级:
- 库存查询改只读副本(强一致性要求低的页面)
- 订单页仅保留核心信息(关闭推荐/埋点)
- 静态化爆款页(预渲染 HTML + Edge TTL 60s)
- 打开“等待室”:队头排队页(JS 自动刷新、保持连接最少)
Nginx 等待室(简版):
map $request_uri $queue_flag {
default 0;
~^/api/order/submit 1;
}
limit_req_zone $binary_remote_addr zone=qps:10m rate=200r/s;
server {
location = /api/order/submit {
limit_req zone=qps burst=1000 nodelay;
error_page 503 = @queue;
proxy_pass http://api_upstream;
}
location @queue {
return 302 https://www.example.com/queue?ts=$msec;
}
}
十一、观测 & 告警(我当晚看的 7 个核心指标)
| 指标 | 阈值 | 说明 |
|---|---|---|
| API P95 / P99 | 300ms / 800ms | 超阈则拉黑“慢上游”或降级 |
| 订单成功率 | ≥ 99.5% | 分时段对比 |
| Redis 命中/耗时 | 命中 > 99%,P95 < 2ms | 预扣原子脚本 |
| MySQL TPS/锁等待 | 锁等待 < 1% | 大事务报警 |
| CDN 回源率 | < 5%(静态) | 异常升高检查 TTL |
| 边缘 4xx/5xx 比例 | < 1% / < 0.3% | 5xx 连续升高触发回滚 |
| 接入层连接数 | < 70% 饱和 | 近饱和预热备机 |
Prometheus 告警规则(示例):
- alert: ApiLatencyHigh
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api"}[1m])) by (le)) > 0.3
for: 2m
labels: { severity: "page" }
annotations:
summary: "API P95 > 300ms"
十二、部署清单(一步步跑起来)
系统层:内核参数 + limits + ethtool + NUMA 绑定(上文命令)
CDN 控制台:
- 配置域名、证书、回源、TTL、忽略 UTM、WAF、RateLimit
- 打开动态加速 / HTTP2/3、Session Resumption
LB/L7:部署 keepalived + LVS;OpenResty/Nginx(配置如上)
Redis Cluster:分片 6,开启 AOF everysec,监控 latency-monitor
Kafka:3 节点,acks=all,min.insync.replicas=2
API 服务:实现幂等键、令牌校验、预扣减、事件投递
MySQL:主写 + 只读,隔离级别 RC,binlog row,备份与延迟监控
观测:Prometheus + Loki + Grafana,接入 SDK 指标
压测:先 GET,再 POST;从 20% 峰值逐级拉升
演练:灰度、降级、等待室、回滚脚本各演练一次(真的、手上跑过)
十三、我踩过且当场修复的 9 个坑(复盘)
- WAF 深包检查拖慢 → 关闭对大 JSON 字段的解包,命中率提升、延迟骤降。
- 连接跟踪不足 → nf_conntrack_max 提升至 4M,解决回源突刺时 5xx。
- 微缓存条件不对称 → 命中与绕过条件统一,避免“幽灵旧数据”。
- 令牌未原子删除 → Lua 脚本原子校验 + DEL,杜绝重放。
- 库存落库同步 → 改预扣 + 事件流,MySQL 锁等待直接清零。
- 压测不复用连接 → 开启 keep-alive,实测与线上一致性改善。
- NIC 中断漂移 → 固定 RSS/IRQ 亲和,P99 抖动收敛 20%+。
- TLS 会话未复用 → 开启 session cache + tickets,握手压力下降。
- 只读副本延迟报警缺失 → 加 replica_lag 告警,避免页面读到老数据。
十四、合规与跨境链路备注(务必提前确认)
香港托管无需 ICP,但若启用大陆境内节点回源/加速,部分 CDN 需要备案/资质校验;
支付回调请白名单并双线路(公网 + 专线可选);
数据合规:跨境传输中用户敏感字段加密(列加密 + 传输加密)。
2:17,我和同事在走道上对了最后一眼指标:订单峰值 RPS 比去年高了 1.8 倍,成功率仍压在 99.6% 之上。有人在群里发红包,说“今年终于没卡住”。我知道不是没卡住——是卡住了,但我们预置的每一层“缓冲垫”都接住了。
给后来的你一句话:别指望哪一招“必杀”,要准备一叠“可切换”的小开关。CDN 卸静态,边缘限流,人机分流,L7 微缓存,令牌 + 幂等,Redis 原子预扣,Kafka 削峰,MySQL 只做最终确认——每一层都能帮你把 00:03 的惊险,变成 02:17 的风。
附:一键自检清单(上线前 10 分钟)
- CDN 回源健康检查通过,静态回源率 < 5%
- nf_conntrack_max ≥ 峰值并发 × 2,OpenResty keepalive ≥ 512
- /api/order/submit:边缘 + L7 双层限流生效
- 令牌接口可用,Lua 校验脚本原子删除
- Redis 分片延迟 P95 < 2ms,AOF everysec 生效
- Kafka ISR=2,Topic order_events 存活
- MySQL 主写 TPS 稳定,锁等待 < 1%
- Prometheus 告警通、看板全绿
- 灰度开关/降级按钮可手动切换
- 压测 20%→50%→80%→100% 阶梯通过
如果你照着这套打,双 11 当晚,你也会在 2 点之后,站在香港湾仔或将军澳的数据中心门口,跟我一样,让风吹一会儿,然后回去睡觉。