香港服务器RHEL8用 BGP Anycast + 负载均衡把 SaaS 平台 API 延迟从 180ms 拉到 26ms

我们部署在香港葵涌机房的 SaaS 平台在国内外都有客户,API 挂在香港节点,按理说延迟应该不难看。但那天凌晨 2:17,Grafana 的 P99 曲线又一次顶到了 400ms——白天高峰甚至 700ms。电话那头客户一句“Webhook 频繁超时”,把我从咖啡拉回了现实。
排查一圈:后端没明显慢查询,容器 CPU 不高,网卡队列也还撑得住。真正的问题,在“路”上:入口连接排队、TLS 握手抖动、跨网运营商路径漂移。于是我做了一个决定——在香港把入口彻底重构:BGP Anycast 提前把用户流量拉近,入口用 HAProxy + 内核调优把握手和排队压平。这篇就是那次改造的完整过程与所有坑。
环境与架构总览
业务特征(决定了优化方向)
- 请求模型:短连接为主(REST/JSON),峰值 QPS 3.5k,平均响应体 < 32KB,TLS 必开,少量长轮询。
- SLO:P50 ≤ 50ms,P95 ≤ 120ms,P99 ≤ 250ms;错误率 < 0.1%。
机房与硬件(香港,双上联 + 本地交换)
| 角色 | 规格 | 关键参数 |
|---|---|---|
| 边缘接入(LB/Anycast 节点)×2 | Dell R7525(RHEL 8.9),AMD EPYC 7543×1,内存 128GB,Intel E810 25GbE ×2(LACP) | BIOS 性能模式;NUMA 2;NVMe 1TB(系统 + 核心服务);FRR 9.x;HAProxy 2.8;OpenSSL 3 |
| 应用节点(API)×6 | RHEL 8.9,EPYC 7452,内存 128GB,Intel X710 10GbE | 容器运行时 podman;CNI Calico;内核 4.18(启 BBR) |
| 上联/对等 | Transit A(ASxxxx),Transit B(ASyyyy),本地交换(HKIX) | eBGP;BFD;大社区策略(调入站偏好) |
Anycast VIP:203.0.113.10/32(示例网段),在 香港两个 LB 都对外宣告;同城 ECMP,跨运营商最优路径靠 BGP 选路。
目标拓扑(简图)
[Client] ──↗ Transit A ─┐
↘ HKIX ────┼── [PoP-HK: LB1 (FRR+HAProxy, VIP 203.0.113.10)]
[Client] ──↘ Transit B ─┘ │
└── [API Nodes x6 via L4/L7 LB]
(同城有 LB2,ECMP/Anycast 同宣)
基线数据(优化前)
| 指标 | 峰值前(10:00) | 峰值(14:00) |
|---|---|---|
| API 延迟 P50 | 82ms | 110ms |
| API 延迟 P95 | 220ms | 380ms |
| API 延迟 P99 | 410ms | 700ms |
| TLS 失败率 | 0.05% | 0.6% |
| SYN 丢包(入口) | 0.2% | 1.5% |
| LB CPU(单机) | 28% | 78%(握手占比高) |
步骤一:RHEL8 系统层调优(把“路基”先夯实)
1)性能轮廓
sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y tuned-profiles-compat irqbalance ethtool numactl jq
sudo tuned-adm profile throughput-performance
sudo systemctl enable --now irqbalance
2)内核网络参数(/etc/sysctl.d/99-anycast.conf)
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 536870912
net.core.wmem_max = 536870912
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_fastopen = 3 # client+server
net.ipv4.tcp_tw_reuse = 1
net.netfilter.nf_conntrack_max = 2097152
net.ipv4.neigh.default.gc_thresh1 = 4096
net.ipv4.neigh.default.gc_thresh2 = 8192
net.ipv4.neigh.default.gc_thresh3 = 16384
sudo sysctl --system
3)网卡队列与中断亲和
# 多队列(示例 E810,按 CPU 数量与 NUMA 配比)
sudo ethtool -L ens3f0 combined 32
sudo ethtool -K ens3f0 rxhash on tso on gso on gro on lro off
# 基于队列的 CPU 亲和(仅示例,实际按 lscpu/numactl 绑定)
for i in /proc/irq/*/ens3f0-TxRx-*/smp_affinity_list; do
cpu=$(echo $i | grep -oE 'TxRx-([0-9]+)' | awk -F- '{print $2}')
echo $cpu | sudo tee $i
done
经验:保留 GRO/GSO 可显著降低 CPU/pps;LRO 在 L7 LB 场景一般关闭以免影响上层分片与时延观测。
步骤二:在 RHEL8 上部署 FRR,做 BGP Anycast
1)安装与启用
sudo dnf install -y frr frr-pythontools
sudo systemctl enable --now frr
sudo sed -i 's/bgpd=no/bgpd=yes/' /etc/frr/daemons
sudo sed -i 's/zebra=no/zebra=yes/' /etc/frr/daemons
sudo systemctl restart frr
2)配置 VIP(不依赖“network”宣告的连通路,避免撤销困难)
# 用 dummy 承载 VIP,便于与宣告逻辑分离
sudo ip link add dummy0 type dummy
sudo ip addr add 203.0.113.10/32 dev dummy0
sudo ip link set dummy0 up
3)FRR 主配置(/etc/frr/frr.conf)
思路:只 redistribute static,由“健康脚本”在内核加/删 “黑洞静态路由”,控制对外宣告;真正收包仍走 connected VIP(dummy0),不会影响本机收包。
frr version 9.0
service integrated-vtysh-config
!
hostname hk-lb1
!
log syslog informational
!
router bgp 65001
bgp router-id 1.1.1.1
no bgp default ipv4-unicast
timers bgp 3 9 ! 收敛更快(结合 BFD)
neighbor 203.0.113.254 remote-as 65010 ! Transit A
neighbor 203.0.113.254 timers 3 9
neighbor 203.0.113.254 bfd
neighbor 203.0.113.253 remote-as 65020 ! Transit B
neighbor 203.0.113.253 timers 3 9
neighbor 203.0.113.253 bfd
!
address-family ipv4 unicast
neighbor 203.0.113.254 activate
neighbor 203.0.113.253 activate
redistribute static route-map ONLY-VIP
exit-address-family
!
bfd
peer 203.0.113.254
no shutdown
!
peer 203.0.113.253
no shutdown
!
ip prefix-list VIP permit 203.0.113.10/32
route-map ONLY-VIP permit 10
match ip address prefix-list VIP
set community 65001:100 ! 示例:给上游打标(可配合上游 local-pref)
set metric 50 ! MED 微调(看对端是否采纳)
!
line vty
4)健康宣告脚本(静态路由控制宣告)
逻辑:探测 HAProxy/TLS/后端健康 → 健康则 ip route add blackhole 203.0.113.10/32;不健康则删除之。FRR 只会把静态黑洞宣出去,真正收包仍走 connected VIP(dummy0)。
脚本:/usr/local/bin/vip-health.sh
#!/usr/bin/env bash
VIP="203.0.113.10/32"
CHECK_URL="https://203.0.113.10/healthz"
TIMEOUT=1
ok=0
# 1. 本机 HAProxy 端口通不通
if timeout ${TIMEOUT}s bash -c "</dev/tcp/127.0.0.1/443" 2>/dev/null; then ok=$((ok+1)); fi
# 2. TLS+上游健康(穿透 LB 到后端探活)
if curl -sk --max-time ${TIMEOUT} ${CHECK_URL} | grep -q "ok"; then ok=$((ok+1)); fi
if [ $ok -ge 2 ]; then
# 宣告(存在则替换)
ip route replace blackhole ${VIP}
else
# 撤销
ip route del blackhole ${VIP} 2>/dev/null
fi
定时器:/etc/systemd/system/vip-health.service
[Unit]
Description=Anycast VIP health announce
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vip-health.sh
/etc/systemd/system/vip-health.timer
[Unit]
Description=Anycast VIP health check timer
[Timer]
OnBootSec=10s
OnUnitActiveSec=2s
AccuracySec=1s
Unit=vip-health.service
[Install]
WantedBy=timers.target
sudo chmod +x /usr/local/bin/vip-health.sh
sudo systemctl daemon-reload
sudo systemctl enable --now vip-health.timer
经验:2s 周期足够快;BFD 负责邻居失效,脚本负责业务层健康撤告,组合后 收敛从分钟级降到秒级。
步骤三:HAProxy 作为 L7 入口(TLS、连接、队列全压平)
1)安装
sudo dnf install -y haproxy
sudo setsebool -P haproxy_connect_any=1
sudo semanage port -a -t http_port_t -p tcp 8443 || true
2)配置(/etc/haproxy/haproxy.cfg)
关键点:多线程 + 绑核、TLS1.3、会话复用、TFO、OCSP Stapling、连接池、Maglev/一致性哈希,后端健康检查 httpchk。
global
log /dev/log local0
log /dev/log local1 notice
master-worker
nbthread 16
cpu-map auto:1/1-16 0-15
tune.ssl.default-dh-param 2048
tune.ssl.cachesize 200000
tune.bufsize 32768
ssl-server-verify none
defaults
log global
mode http
option httplog
option http-keep-alive
option splice-auto
timeout connect 2s
timeout client 30s
timeout server 30s
timeout http-request 5s
timeout http-keep-alive 10s
retries 2
frontend fe_api
bind 203.0.113.10:443 tfo ssl crt /etc/haproxy/certs/api.pem alpn h2,http/1.1
http-reuse safe
http-response set-header Strict-Transport-Security "max-age=31536000"
default_backend be_api
backend be_api
balance uri whole map-based # 一致性(或:hash-type consistent)
hash-type consistent
http-reuse always
option httpchk GET /healthz
http-check expect status 200
server-template api 6 api-%[srv_id].svc.local:8443 check resolvers dns resolve-prefer ipv4 init-addr none
resolvers dns
nameserver dns1 127.0.0.1:53
hold valid 10s
如果你偏 TCP 四层,mode tcp + balance source 或 balance consistent-hash 即可;有状态会话建议 JWT/无状态化,否则跨 PoP 粘滞要更精细(比如 Maglev 一致性)。
3)TLS 细节
证书用 ECDSA + RSA 双证书;开启 1.3,1.2 仅作回退。
会话票据开启、票据旋转;如需多 LB 共享可放 Redis/文件同步(本例单 PoP 两节点,不共享也可)。
OCSP Stapling 降首包延迟。
步骤四:后端(API)与集群面的小优化
podman/K8s 节点开启 BBR、somaxconn 同步到容器 runtime。
gRPC/HTTP2 场景下,务必打开 连接复用,增大 MAX_CONCURRENT_STREAMS。
应用内置 超时与重试:短超时(connect 200ms,read 1s),幂等接口允许 1 次重试,带随机抖动。
指标埋点:入口暴露 /healthz(综合探活),业务指标 P50/P95/P99 上报 Prometheus。
步骤五:观测与压测
黑盒探测
# ICMP + TCP + TLS
smokeping / blackbox_exporter (module: http_2xx, tls_handshake_duration_seconds)
压测(示例)
# 短连接 HTTP/1.1
wrk -t16 -c512 -d60s --timeout 2s https://203.0.113.10/api/v1/ping
# HTTP/2
h2load -n 500000 -c 200 -m 50 https://203.0.113.10/api/v1/ping
结果:优化后数据(Same Day +1)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| API 延迟 P50 | 82ms | 26ms |
| API 延迟 P95 | 220ms | 74ms |
| API 延迟 P99 | 410–700ms | 138ms |
| TLS 失败率 | 0.6% | 0.03% |
| SYN 丢包(入口) | 1.5% | 0.12% |
| LB CPU(峰值) | 78% | 41% |
主要收益来自:路径缩短(Anycast)+ 握手开销下降(TLS/内核)+ 排队压平(队列/亲和)。
踩坑复盘(真 · 现场问题与解决)
BGP 对端用 Loopback 对等却没提醒
现象:会话起不来,ttl 1 被丢。
处理:neighbor x.x.x.x ebgp-multihop 2,并在对端授权我方源地址;同时启 GTSM(neighbor ... ttl-security hops 1)提升安全。
MTU 抖动导致 TLS 首包重传
现象:某运营商路径实际 1472,H2 首包较大时偶发碎片/重传。
处理:入口链路保留 1500,启 tcp_mtu_probing=1,同时在后端对跨网接口做 MSS Clamping(nftables)。
conntrack 打爆
现象:峰值时 nf_conntrack 警告,短连接高并发下条目陡增。
处理:把 nf_conntrack_max 拉到 2M,并调大哈希槽;HAProxy 限制入站最大并发和队列,应用缩短 TIME_WAIT。
OCSP 反查卡顿
现象:偶发握手阻塞。
处理:改为 Stapling 定时预取;实测握手 P50 下降 6–10ms。
任何“撤告=删 VIP”都是坑
经验:很多人会“健康失败就把 VIP 从 lo 删除”,这会影响本地绑定与探活。正确做法是保持 VIP connected,仅通过静态黑洞路由控制宣告。
运营商与路由小技巧
与上游约定社区值,实现 分运营商优先(例如本地运营商本地优先,海外走更优 Transit),必要时轻度 AS-PATH prepend 做细调。
能接入 本地交换(HKIX) 就一定接,对本地与周边地区 P95 降幅非常明显。
BFD + 快速 keepalive 保证故障秒级收敛;但别把定时器调得过激,避免误触。
完整变更清单(Checklist)
- RHEL8 tuned throughput-performance + irqbalance
- sysctl 网络栈参数(somaxconn、backlog、BBR、TFO、conntrack)
- 网卡多队列 + 中断亲和(E810/X710)
- dummy0 绑定 VIP(203.0.113.10/32)
- FRR BGP:只 redistribute static + BFD + route-map
- 健康脚本:黑洞静态路由加/删 + systemd timer(2s)
- HAProxy:多线程/绑核、TLS1.3、OCSP Stapling、连接复用、一致性哈希
- 后端:BBR、keep-alive、幂等重试
- 观测:黑盒探测、P50/P95/P99、TLS/握手指标
- 回归压测:wrk/h2load
常见问答(把坑踩在你的前面)
Q:SaaS 有状态怎么办?
A:优先无状态化。若必须粘滞:入口 balance source/一致性哈希(用户 ID / 会话 ID),或使用 全局会话存储(Redis/Memcached)+ 短 TTL,必要时跨 PoP 同步。
Q:只有一个上游能 Anycast 吗?
A:可以,但意义有限。至少两家 Transit + 一个本地交换效果更稳。
Q:FRR 和 ExaBGP 选哪个?
A:本方案用 FRR 统一;如果你要更灵活的应用层宣告,ExaBGP 控制静态路由同样好用。
凌晨 3:40 的直线
改造完成那晚,我把 Health 定时器调到 2s,看着 BFD 会话跳了几次又稳了下来。Grafana 的 P99 从山峰变成了平线,像是机房外港湾的水面。手机里客户的监控也安静了。
Anycast 不是什么银弹,但它把路缩短了;负载均衡和内核调优不是魔法,却把排队摁下去了。真正的价值,是这些技术 在你的业务里变成了“稳定的体验”。
如果你也在香港承载面向多网用户的 SaaS 接口,希望这篇复盘能让你少熬几杯咖啡。