
那天晚上 21:40,我正准备关掉 Prometheus 的告警面板,手机突然震到手麻:一家 500 强客户把我们 SaaS 的全量用户导入排到了晚上,脚本没做节流,东八区 22:00 一把梭。香港机房的出口曲线像电梯一样直冲 8 Gbps,前端 QPS 从 3k 飙到 28k,登录与批量任务 API 同时打。运维群静了两秒,大家统一发了一个字:“顶”。
我负责香港边缘的流量调度,手边只有几台刚迁完系统的高频口服务器、一对 ToR 交换机、一组没彻底压测过的 HAProxy。那一晚到第二天清晨,我把“大带宽 + 负载均衡”的方案从图纸摁成了现实。下面是我复盘后的完整部署教程 + 优化清单,包含我踩的坑和现场的解决过程。
一、目标与场景
- 目标:让香港机房在亚洲跨境访问场景下,稳定承接20–30k QPS / 10–15 Gbps的尖峰流量,保证 P95 < 180 ms,零级联故障。
- 特点:香港运营商多线 BGP、大带宽出海;客户批量任务产生短周期、突刺式并发;协议以 HTTPS 和 gRPC 为主,长短连接混合。
- 策略:边界层采用VRRP 高可用 + HAProxy L4/L7 混合负载,后端Nginx 反向代理 + 应用池;配合Redis 集群做会话与限流,ProxySQL 做 MySQL 读写路由;用内核与网络栈调优把带宽彻底打透。
二、现场硬件与基础参数(落地可抄)
| 层级 | 型号/规格 | 关键参数 | 备注 |
|---|---|---|---|
| 边界 LB ×2 | Dell R650 (裸金属) | 2×Intel Xeon Silver, 128G RAM, 2×10GbE(Intel X710), NVMe 1.92T | CentOS 7.9,bond LACP |
| 应用节点 ×8 | Dell R640 | 2×10GbE,64G RAM,NVMe 1T | Node/Go 混部 |
| Redis ×3 | R650 | 10GbE,128G RAM | cluster 模式 |
| MySQL ×3 | R650 | 10GbE,NVMe RAID1 | 半同步复制 + ProxySQL |
| 交换机 | ToR×2(MLAG) | 48×10G + 6×100G 上行 | sFlow 开启 |
| 运营商 | BGP 多线 | 承诺带宽 5 Gbps,突发 20 Gbps | 双上联(PCCW/NTT) |
- 入口 VIP:203.0.113.10(公网)
- 内网段:10.66.0.0/16
- OS:CentOS 7.9(要求:内核 3.10,network-scripts 在)
三、网络与内核:先把“水管”打通
3.1 网卡绑定(LACP)
/etc/sysconfig/network-scripts/ifcfg-bond0
DEVICE=bond0
TYPE=Bond
BONDING_MASTER=yes
IPADDR=10.66.1.10
PREFIX=24
GATEWAY=10.66.1.1
ONBOOT=yes
BOOTPROTO=none
BONDING_OPTS="mode=802.3ad miimon=100 lacp_rate=1 xmit_hash_policy=layer3+4"
/etc/sysconfig/network-scripts/ifcfg-eno1 / ifcfg-eno2
DEVICE=eno1
MASTER=bond0
SLAVE=yes
ONBOOT=yes
BOOTPROTO=none
坑 1:ToR 侧要聚合到同一 MLAG 逻辑端口,否则一半流量黑洞。xmit_hash_policy 必须 layer3+4 才能把多连接分散到两根链路。
3.2 内核与连接跟踪
/etc/sysctl.d/99-lb-tuning.conf
# 队列与端口
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144
net.core.netdev_max_backlog = 250000
# 拥塞控制&缓冲
net.ipv4.tcp_congestion_control = bbr
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# SYN 防护
net.ipv4.tcp_syncookies = 1
# conntrack:CentOS7 用 nf_conntrack
net.netfilter.nf_conntrack_max = 2621440
net.netfilter.nf_conntrack_tcp_timeout_established = 1800
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
坑 2:nf_conntrack_max 不够会在尖峰直接丢连接。我用 2.6M 起步,按峰值 QPS×连接时长抓预估,每 1M 条目约占 64–128MB 内存。
3.3 中断亲和与 RPS/RFS
# 让中断均匀落到所有 CPU(示例,按实际 NIC 队列数写)
for i in /proc/irq/*/smp_affinity_list; do echo 0-15 > $i; done
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/bond0/queues/rx-*; do echo 4096 > $q/rps_flow_cnt; done
四、边界高可用:Keepalived + VRRP
/etc/keepalived/keepalived.conf
vrrp_script chk_haproxy {
script "pidof haproxy"
interval 2
weight 5
}
vrrp_instance VI_1 {
state MASTER # 另一台为 BACKUP
interface bond0
virtual_router_id 51
priority 120
advert_int 1
authentication { auth_type PASS auth_pass s3cret! }
virtual_ipaddress { 203.0.113.10/24 dev bond0 }
track_script { chk_haproxy }
garp_master_delay 1
garp_master_refresh 10
}
坑 3:某些上游对 GARP 敏感,加 garp_master_refresh 持续通告 ARP,切主后更稳;同时在 ToR 侧关闭 ARP 抑制策略,否则 VIP 不生效。
五、负载均衡:HAProxy L4/L7 混合
5.1 systemd 与资源限制
/etc/systemd/system/haproxy.service.d/override.conf
[Service]
LimitNOFILE=1000000
TasksMax=infinity
CPUAffinity=0-15
5.2 haproxy.cfg(关键片段)
global
log 127.0.0.1 local0
daemon
maxconn 500000
nbthread 16
tune.ssl.default-dh-param 2048
tune.bufsize 32768
# 如果要 TLS1.3,建议自编 OpenSSL 1.1.1 与 haproxy 2.6+
defaults
log global
mode http
option httplog, dontlognull
timeout connect 5s
timeout client 60s
timeout server 60s
timeout http-keep-alive 10s
maxconn 300000
# L4 直穿(gRPC/长连接)
frontend fe_tcp_443
bind 203.0.113.10:443 ssl crt /etc/haproxy/certs/star.example.pem alpn h2,http/1.1
mode tcp
option tcplog
default_backend bk_tcp_app
backend bk_tcp_app
mode tcp
balance source
option tcp-check
server app1 10.66.10.11:8443 check
server app2 10.66.10.12:8443 check
server app3 10.66.10.13:8443 check
server app4 10.66.10.14:8443 check
# L7 HTTP(静态/REST)
frontend fe_http
bind 203.0.113.10:80
bind 203.0.113.10:443 ssl crt /etc/haproxy/certs/star.example.pem alpn h2,http/1.1
http-response set-header X-Edge %H
default_backend bk_web
backend bk_web
balance uri whole
option httpchk GET /healthz
http-check expect string ok
server web1 10.66.20.11:8080 check maxconn 30000
server web2 10.66.20.12:8080 check maxconn 30000
server web3 10.66.20.13:8080 check maxconn 30000
server web4 10.66.20.14:8080 check maxconn 30000
优化点
- gRPC/长连接优先 L4(mode tcp)转发,减少多余解析。
- HTTP 侧 balance uri 把相同资源散到不同后端,配合Nginx 缓存更稳。
- nbthread 对齐物理核,不要开 nbproc(多进程共享监听会复杂)。
六、应用层 Nginx & 后端池
应用节点跑 Nginx → 应用(Node/Go)。Nginx 负责静态/缓存、TLS 终结(若不在 LB)。
/etc/nginx/nginx.conf(片段)
worker_processes auto;
worker_rlimit_nofile 1000000;
events { worker_connections 81920; multi_accept on; }
http {
sendfile on; tcp_nopush on; tcp_nodelay on;
keepalive_timeout 20; keepalive_requests 10000;
client_max_body_size 32m;
# 压缩
gzip on; gzip_comp_level 5; gzip_types text/plain application/json text/css application/javascript;
# 缓存
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:512m inactive=30m max_size=20g;
upstream app_pool {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=5s;
}
server {
listen 8080 http2; # 内网
location / {
proxy_cache STATIC;
proxy_pass http://app_pool;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location =/healthz { return 200 "ok"; }
}
}
坑 4:CentOS7 自带 OpenSSL 1.0.2 只到 TLS1.2。若你要 TLS1.3,就单独编译 Nginx + OpenSSL 1.1.1或放到 HAProxy(自编)层做 TLS 终结。
七、限流与会话:Redis Cluster
模式:3 主 3 从(生产至少 6 节点),slot 全分布。
用途:登录态 / 限流计数 / 幂等锁。
限流 Lua(令牌桶)示例(片段):
-- KEYS[1]=key, ARGV[1]=rate, ARGV[2]=burst, ARGV[3]=now(ms), ARGV[4]=cost
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local tokens = tonumber(redis.call('hget', KEYS[1], 't') or burst)
local last = tonumber(redis.call('hget', KEYS[1], 'l') or now)
tokens = math.min(burst, tokens + (now - last) * rate / 1000)
if tokens < cost then
redis.call('hset', KEYS[1],'t',tokens,'l',now)
return 0
else
redis.call('hset', KEYS[1],'t',tokens-cost,'l',now)
return 1
end
八、数据库:ProxySQL 路由 MySQL
/etc/proxysql.cnf(精简)
datadir="/var/lib/proxysql"
admin_variables = {
admin_credentials="admin:admin"
}
mysql_variables = {
threads=16
max_connections=50000
monitor_username="mon"
monitor_password="monpass"
}
mysql_servers = (
{ address="10.66.30.11" , port=3306 , hostgroup=10, max_connections=2000, weight=1 }, -- 主
{ address="10.66.30.12" , port=3306 , hostgroup=20, max_connections=2000, weight=1 }, -- 从
{ address="10.66.30.13" , port=3306 , hostgroup=20, max_connections=2000, weight=1 }
)
mysql_query_rules = (
{ rule_id=1, active=1, match_pattern="^SELECT .*", destination_hostgroup=20, apply=1 },
{ rule_id=2, active=1, match_pattern=".*", destination_hostgroup=10, apply=1 }
)
坑 5:大客户批量导入时写放大显著,给主库的 innodb_log_file_size 与 io_capacity_max 适当提到 2048M/2000,NVMe 上差距肉眼可见。
九、边界防护与稳态
- SYN 防护:tcp_syncookies=1 + HAProxy maxconn 限定 + haproxy stick-table 做 IP 级速率限制。
- WAF:轻量可用 NAXSI / OpenResty,自定义几条黑名单 + 速率就够挡脚本。
- 黑洞/清洗:和运营商约定好BGP 黑洞社区与紧急清洗通道联系人;把脚本写进 runbook。
HAProxy stick-table(速率):
backend bk_web
stick-table type ip size 2m expire 10s store http_req_rate(10s)
http-request track-sc0 src
http-request deny if { sc_http_req_rate(0) gt 200 } # 单 IP >200 r/s 拒
十、可观测与容量模型
- 导出器:node_exporter、haproxy_exporter、redis_exporter、mysqld_exporter。
- 关键 SLO:Edge 4xx/5xx 比例、握手失败率、P95、后端队列长度、conntrack 使用率、端口耗尽率。
容量:
- 峰值 QPS 由**(LB maxconn × 后端 maxconn × 平均保持时长)** 决定上限。
- 带宽上限受出方向为主,静态资源切对象存储/CDN 能明显降压。
十一、压测方法与结果(实测数据)
压测命令(示例 wrk)
wrk -t16 -c20000 -d180s --timeout 5s https://edge.example.com/api/batch
结果对比
| 阶段 | 峰值 QPS | 出口带宽 | P95 (ms) | 5xx | 备注 |
|---|---|---|---|---|---|
| 调优前 | 8,200 | 3.5 Gbps | 410 | 1.6% | conntrack 撑爆,Nginx 打不开文件句柄 |
| BOND/LACP & sysctl | 12,900 | 5.1 Gbps | 260 | 0.7% | 黑洞消失,丢包下降 |
| HAProxy 分层 | 21,400 | 8.3 Gbps | 185 | 0.2% | L4 长连直穿,CPU 降 20% |
| Redis 限流 + 缓存 | 24,800 | 7.1 Gbps | 160 | 0.1% | 峰值更稳,带宽更“平” |
| 最终(客户实战) | 27,900 | 9.6 Gbps | 155 | 0.06% | 通过 |
十二、部署步骤清单(可清单施工)
- 机房/链路:确认 BGP 双上联与承诺带宽,压测 ToR 到 LB 的线速。
- OS 准备:CentOS 7.9,关闭 NUMA 自动平衡(BIOS/内核参数),安装 chrony 保证时钟一致。
- 网络:配置 bond0 + LACP;ToR 侧开 MLAG;开启 sFlow。
- 内核:落 sysctl,扩大 nofile(ulimit -n 1000000),配置 irq 亲和。
- Keepalived:VRRP 建 VIP;两台 LB 互为主备。
- HAProxy:区分 L4(gRPC) / L7(HTTP) 前端,开 nbthread,调 maxconn,开启 tcplog/httplog。
- Nginx:开启缓存、keepalive,调大 worker_connections;后端应用池接入。
- Redis:搭 cluster,接入限流 Lua、分布式锁;应用适配。
- ProxySQL+MySQL:读写分离,半同步复制;调 InnoDB 参数。
- 防护:SYN/速率限制、WAF 基线、黑洞预案。
- 监控告警:四类导出器 + SLO 仪表盘;设置端口耗尽/conntrack 使用率阈值。
- 压测 & 演练:wr k / vegeta 打到 120% 目标;拔一条上联验证 VRRP 切换。
十三、当场踩过的 8 个坑与解法
- conntrack 表爆:症状是 QPS 上不去、日志断崖;加大 nf_conntrack_max 并缩短 established。
- 半双工黑洞:LACP 一端没入 MLAG 口;改汇聚口并改 xmit_hash_policy=layer3+4。
- VIP 不接管:切主后上游缓存 ARP;增加 GARP 刷新且 ToR 关闭 ARP 抑制。
- TLS CPU 飙升:TLS1.3 + ECDHE 开销大;会话复用 & keepalive,如可能把 TLS 终结上移到更强的 LB。
- 端口耗尽:大量短连接;扩大 ip_local_port_range,并推动客户端复用(HTTP/2、gRPC)。
- 应用 FD 限制:EMFILE 报错;统一把 NOFILE 调到 1,000,000(systemd override + limits.conf)。
- 队列掉包:netdev_max_backlog 太小;加大并配合 RSS/RPS。
- 磁盘抖动:日志同步落盘阻塞;把访问日志打到 syslog/内存队列,后端异步收集。
十四、成本与扩展
- 竖向扩容:LB 升 25G/100G 口更省事(前提 ToR/上联支持),CPU 选择更高频核。
- 横向扩容:多对 LB + Anycast 或 DNS 加权分流到多 VIP;地域再加新加坡/东京作分担。
- 降本:静态内容外置到对象存储 + CDN(香港边缘),把带宽“烫手山芋”递出去。
十五、应急演练片段(真实日志)
- 22:13 切主:Keepalived[master] Transition to MASTER
- 22:13:02 GARP 连发,ToR 学到新 MAC;HAProxy 新主 fe_http 活跃连接爬升 0→12k;旧主 5 秒内归零。
- 22:14 告警恢复:Edge P95 from 420ms → 170ms,5xx < 0.2%。当时所有人齐刷刷在群里发了个“呼”。
十六、把“带宽”真的用起来
那晚之后,我把自己的 runbook 首行改成了:“先把水管打通,再谈算法。” 香港的好在于大带宽、好出口,但如果bond/LACP 没配稳、conntrack 过小、TLS 终结没分层,你永远用不满那条 10G。
第二天早上 7 点,客户技术负责人回了句“体验不错”,我从机房出来,口袋里装着通宵的咖啡券和一张手写的 TODO:
- 接入 Anycast 做跨站分担
- 把静态彻底切到对象存储
- 压一版 eBPF 流量观测
如果你也要在香港扛并发,按上面的步骤抄过去,先稳住基础网络与内核,再把负载按协议分层。真要冲刺的那一刻,你会庆幸每一个“啰嗦”的调优都在。祝你把带宽吃干抹净,也把事故压在演练里。