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

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

发布人:Minchunlin 发布时间:2025-09-25 09:55 阅读量:694


我们部署在香港葵涌机房的 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)

  1.  RHEL8 tuned throughput-performance + irqbalance
  2.  sysctl 网络栈参数(somaxconn、backlog、BBR、TFO、conntrack)
  3.  网卡多队列 + 中断亲和(E810/X710)
  4.  dummy0 绑定 VIP(203.0.113.10/32)
  5.  FRR BGP:只 redistribute static + BFD + route-map
  6.  健康脚本:黑洞静态路由加/删 + systemd timer(2s)
  7.  HAProxy:多线程/绑核、TLS1.3、OCSP Stapling、连接复用、一致性哈希
  8.  后端:BBR、keep-alive、幂等重试
  9.  观测:黑盒探测、P50/P95/P99、TLS/握手指标
  10.  回归压测: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 接口,希望这篇复盘能让你少熬几杯咖啡。

目录结构
全文