香港、新加坡、广州三地多活实战:Anycast+BGP、智能DNS与负载均衡优化跨境网络延迟

那天夜里,机房的空调送风和交换机风扇像合唱团。我盯着广州用户的 p95 时延——186ms,抖得像心电图。白天业务量一上来,偶发超时、重试成风暴,订单回写延迟飙升。我们已经把单点优化做满了(内核、Netplan、队列、BBR),但跨境距离和路径选择还在拖后腿。
我决定上“三板斧”:Anycast 兜底、智能 DNS 精确导流、L4/L7 负载均衡细粒度调度,把跨境路径和会话稳定性一起拿下。
1. 目标与基线
SLO(以华南用户为主)
- p95 时延:≤120ms(静态内容 ≤80ms,动态 API ≤120ms)
- 丢包率:≤0.2%
- 故障切走:≤30s(优先 Anycast / BFD / BGP 收敛;DNS 作为兜底)
基线(优化前)——合成探测(广州、深圳、东莞、福州节点)、真实用户监控(RUM)与黑盒探针(blackbox_exporter)混合:
| 区域来路 | 去向机房 | p50 | p95 | 丢包 | 备注 |
|---|---|---|---|---|---|
| 华南(电信) | 香港 | 98ms | 186ms | 0.9% | 路由绕转、拥塞时跳到非 CN2 |
| 华南(联通) | 新加坡 | 162ms | 228ms | 1.4% | 晚高峰波动明显 |
| 华东(移动) | 香港 | 132ms | 190ms | 0.7% | 途经多跳,回程不一致 |
| 华南(混合) | 广州 | 38ms | 72ms | 0.2% | 本地直达好,但容量有限,故障回切慢 |
2. 拓扑、硬件与角色分工
数据中心与角色
- 香港(HK):主外服流量入口之一;对接国际出口+CN2/精品网,向北回程相对更稳。
- 新加坡(SG):东南亚与备份北向入口;承担海外加速与容灾。
- 广州(GZ):面向华南的计算/缓存与内陆直达节点;对外发布谨慎(合规与路径稳定考虑),主要承接近源读写与边缘逻辑。
硬件(每地节选)
| 角色 | 规格 |
|---|---|
| 边界路由(2 台) | x86_64(Intel Xeon Silver)、128G 内存、NVMe 1.92T、双口 25GbE(Mellanox CX4-Lx),跑 FRR 8.x |
| L4/L7 网关(2 台) | 32C/64G、NVMe、2×25GbE,跑 HAProxy 2.8+ / Nginx、Keepalived |
| 应用/缓存集群 | k8s/裸机混部,节点 10~30 台/点位 |
| ToR & 汇聚 | 25G 接入、100G 上联(ECMP),BFD 开启 |
| 时钟 | PTP(机柜内)、NTP(外部 2 源+内部 1 源) |
3. Anycast:用 BGP 把“最近”送到用户手边
3.1 策略
申请到一段专用公网前缀(示例用保留网段演示):203.0.113.0/24 作为Anycast 服务段。
香港与新加坡向上游发布 /24;广州不对公网发布 /24(合规/稳定性考虑),但参与东西向互联与容灾。
健康驱动撤布:当本地 L7 网关异常,撤销本地 BGP 宣告,让流量自然飘向其它点位。
路径意图:让华南电信/联通更倾向走香港,海外/东南亚更多落在新加坡。通过 AS-Path Prepend、MED、社区标记 做软引导,不做“硬钉死”。
3.2 FRR(bgpd)核心配置示例(香港边界路由)
说明:下方配置以两家上游 ISP为例,使用 BFD 加速失连感知;通过条件广告与健康脚本控制发布。
# /etc/frr/daemons
bgpd=yes
bfdd=yes
# /etc/frr/bgpd.conf (HK)
router bgp 65010
bgp router-id 10.0.0.11
no bgp default ipv4-unicast
neighbor uplink1 peer-group
neighbor uplink2 peer-group
neighbor 203.0.113.254 remote-as 64512
neighbor 203.0.113.254 peer-group uplink1
neighbor 198.51.100.254 remote-as 64513
neighbor 198.51.100.254 peer-group uplink2
! BFD 绑定
neighbor uplink1 bfd
neighbor uplink2 bfd
! 出口策略(软引导北向更偏好HK)
route-map OUT-HK permit 10
set community 65010:100 additive
set metric 50
address-family ipv4 unicast
network 203.0.113.0/24 route-map ANNOUNCE-WHEN-HEALTHY
neighbor uplink1 activate
neighbor uplink1 route-map OUT-HK out
neighbor uplink2 activate
neighbor uplink2 route-map OUT-HK out
exit-address-family
!
ip prefix-list ANYCAST-SIGNAL seq 5 permit 10.255.0.1/32
route-map ANNOUNCE-WHEN-HEALTHY permit 10
match ip address prefix-list ANYCAST-SIGNAL
健康脚本(当本地 L7 正常时,保持信号路由;异常时删除,从而停止宣告):
# /usr/local/bin/anycast-health.sh
#!/usr/bin/env bash
VIP_CHECK_URL="http://127.0.0.1:8404/ready" # HAProxy / Nginx 本地健康端点
SIGNAL_PREFIX="10.255.0.1/32"
if curl -s --max-time 1 "$VIP_CHECK_URL" | grep -q OK; then
ip route show $SIGNAL_PREFIX >/dev/null 2>&1 || ip route add $SIGNAL_PREFIX dev lo
else
ip route del $SIGNAL_PREFIX >/dev/null 2>&1
fi
Crontab/系统定时器每秒探测一次:
* * * * * root /usr/local/bin/anycast-health.sh >/dev/null 2>&1
* * * * * root sleep 1; /usr/local/bin/anycast-health.sh >/dev/null 2>&1
* * * * * root sleep 2; /usr/local/bin/anycast-health.sh >/dev/null 2>&1
...
新加坡侧可做AS-Path Prepend增加 2~3 次,以便华南更偏向香港:
# /etc/frr/bgpd.conf (SG) 片段
route-map OUT-SG permit 10
set as-path prepend 65020 65020 65020
set community 65020:200 additive
set metric 150
实战体会:不要频繁撤布。有一次香港 L7 切滚动升级把健康脚本设置为每 100ms 执行,导致 BGP 一直抖,上游触发了flap dampening,恢复慢得想哭。后来我们把探测颗粒度放到 1s,并引入抖动抑制窗口(3 连续失败才撤布)。
4. 智能 DNS(GSLB):给长连接和非 Anycast 业务兜底
为什么还要 DNS?
Anycast对短连接/幂等非常友好,但对长连接(如 WebSocket、部分 gRPC 流)在单点故障的“连接寿命”里难以立即迁移。
DNS 可以按地理/网络/实时延迟下发更适合的Unicast VIP,并配合短 TTL 做分钟级收敛。
我用了一套自管 CoreDNS(边缘也可以上云厂商 GSLB),做了:GeoIP 分流 → 延迟探测打分 → 健康检查 → 权重回收。
CoreDNS 配置(简化示例)
# /etc/coredns/Corefile
example.com:53 {
log
errors
health
cache 30
# 来自探针的权重文件(外部生成)
rewrite name weight.hk.example.com hk-lb.example.com
rewrite name weight.sg.example.com sg-lb.example.com
rewrite name weight.gz.example.com gz-lb.example.com
# GeoIP 分流(按大洲/国家/AS 号粗分)
geoip /etc/coredns/GeoLite2-City.mmdb {
# 中国南方 → 广州优先,其次香港
1.0.0.0/8 gz-lb.example.com
CN hk-lb.example.com
SG sg-lb.example.com
fallback hk-lb.example.com
}
forward . 8.8.8.8 1.1.1.1
}
权重/健康来源:
我跑了黑盒探针(广州/深圳/东莞/福州/厦门/汕头)每 10s 打点 tcp_connect 与 http_2xx,把移动/联通/电信分开统计,然后写到一个权重文件,被 CoreDNS 的 rewrite 读。
TTL 控制在 20~30s。一次教训:TTL 设 5s 看起来很敏捷,但上游递归解析器会合并/重试,实际未必更快,反而抖动大。
5. L4/L7 负载均衡:连接亲和与退避逻辑
5.1 Keepalived(同城 2 节点 VRRP)
# /etc/keepalived/keepalived.conf (HK)
vrrp_instance VI_1 {
state MASTER
interface eno1
virtual_router_id 51
priority 110
advert_int 1
virtual_ipaddress {
203.0.113.10/24 dev eno1 label eno1:vip
}
track_script {
chk_haproxy
}
}
vrrp_script chk_haproxy {
script "/usr/local/bin/check_haproxy.sh"
interval 1
fall 3
rise 2
}
check_haproxy.sh
#!/usr/bin/env bash
curl -s --max-time 1 http://127.0.0.1:8404/ready | grep -q OK
5.2 HAProxy(L4/L7 混部关键片段)
连接亲和:对会话/用户保持源地址一致性,降低跨数据中心“来回漂移”。
重试与退避:对长尾超时做指数回退,避免雪崩。
# /etc/haproxy/haproxy.cfg
global
nbthread 8
tune.ssl.default-dh-param 2048
log 127.0.0.1 local0
defaults
mode http
option httplog
option http-keep-alive
timeout connect 2s
timeout client 60s
timeout server 60s
retries 2
frontend fe_web
bind 203.0.113.10:80
bind 203.0.113.10:443 ssl crt /etc/ssl/acme/example.com/fullchain.pem
http-response set-header X-DC hk
default_backend be_api
backend be_api
balance hdr(X-User-Id) # 也可 sticky on src,一致性更强用 consistent-hash
hash-type consistent
option httpchk GET /health
http-check expect status 200
server gz-1 10.10.20.11:8080 check inter 1000 rise 2 fall 3
server gz-2 10.10.20.12:8080 check inter 1000 rise 2 fall 3
server hk-1 10.10.10.11:8080 check backup
server sg-1 10.10.30.11:8080 check backup
backend be_static
balance uri
server hk-cdn 10.10.10.21:80 check
server sg-cdn 10.10.30.21:80 check backup
经验:consistent-hash 对“社交/交易类”会话非常关键;而backup 节点能在单城抖动时少量兜底,不至于全城黑暗。
6. 东西向专线/隧道:把三地粘成一块
WireGuard 做控制面与数据面的加密隧道(跨公网/专线混合),MTU 1420 起步,逐步探测升至 1420~1440。
ECMP 两条上行(例:运营商 A/B),BFD 300ms 间隔快速探活。
广州→香港/新加坡走私网回源;香港/新加坡回写广州的强一致操作走消息队列+双写日志,避免直写抖动。
WireGuard(HK 到 GZ)
# /etc/wireguard/wg0.conf (HK)
[Interface]
Address = 172.16.0.1/30
ListenPort = 51820
PrivateKey = <HK_PRIVATE>
MTU = 1420
[Peer]
PublicKey = <GZ_PUBLIC>
AllowedIPs = 172.16.0.2/32, 10.10.20.0/24
Endpoint = gz-edge.example.com:51820
PersistentKeepalive = 15
策略路由(对回源/数据复制打 Tag)
ip rule add from 10.10.10.0/24 table 100
ip route add default dev wg0 table 100
7. 观测与验收:把“感觉快”变成数字
PromQL(p95 延迟)
histogram_quantile(0.95, sum by (le,region,isp) (rate(http_request_duration_seconds_bucket{job="edge",status=~"2.."}[5m])))
收敛时间:
- BFD 失连探测:~0.3s
- BGP 邻居撤布到上游收敛:5~12s(视供应商)
- DNS TTL:20~30s(递归差异会拖长到 40~60s)
上线后一周汇总(节选):
| 来路 | Anycast 命中 | GSLB 命中 | p50 | p95 | 丢包 |
|---|---|---|---|---|---|
| 华南(电信)→HK | 78% | 22% | 62ms | 108ms | 0.18% |
| 华南(联通)→HK | 71% | 29% | 68ms | 115ms | 0.21% |
| 华南(混合)→GZ | 12% | 88% | 34ms | 66ms | 0.12% |
| 东南亚→SG | 92% | 8% | 38ms | 72ms | 0.09% |
关键结果:华南主业务 p95 从 186ms → 108~115ms,稳定在 SLO 之内;广州本地读取/缓存命中场景 p95 66ms。
8. 故障演练回放:拔掉新加坡一根“网线”
场景:SG 边界路由 uplink2 人为 down。
- t+0.3s:BFD 探测失败
- t+1.2s:BGP 邻接撤销,/24 从 uplink2 不再发布
- t+7s:外部路径收敛完成,Anycast 流量回到 uplink1
- t+9s:长连接少量超时,自动重连
- t+30s:DNS 自愈覆盖尾巴群体
用户可感:个别地区 1020s 内出现一次性 200600ms 的尾延迟峰值,无持续报错
9. 我踩过的坑与解法
BGP 抖动被上游拉黑
症状:频繁撤布/发布,路由被“抑制”。
解决:健康脚本增加抖动抑制窗口(3 连续失败才撤)、最小持续时间 30s,且灰度发布(分上游逐个恢复)。
Anycast 与长连接的“背刺”
症状:单点 L7 掉线,存量长连接在超时前不会迁移。
解决:重要长连接走 GSLB Unicast,并在客户端层做心跳+快速重连;服务器侧 idle_timeout 控制在 60s 内。
ECN/DSCP 被清洗
跨公网常被中间路由器清理,无法依赖端到端 QoS。
解决:不把 SLO 押宝在 QoS 标记,更依赖路径选择与容量。
WireGuard MTU 黑洞
一开始贪心设到 1440,部分链路 ICMP 不通。
解决:从 1420 起步,配合 tcp_mtu_probing=1,逐步放大到稳定阈值。
DNS TTL 过短引发缓存行为不可预期
5s TTL 并未带来线性收敛提升,反而使递归合并与突发抖动更频繁。
解决:回到 20~30s,并对关键客户提供备用域名手工回切方案。
监控“看起来都绿”但用户还卡
症状:探针都走 CN2,用户却绕大圈。
解决:多 AS/多省份的探针,RUM 采样扩大;同时对 回程路径做被动采样(tcpinfo RTT/RTTVar)与 mtr 周期巡检。
10. 上线清单(我真的一条条勾过)
- 前缀路由策略与上游对齐(社区、MED、AS Path 预置)
- BFD/BGP 计时调优(抖动抑制阈值)
- Anycast 健康脚本幂等与灰度撤布
- GSLB 探针覆盖(按省/AS/出口)与 TTL 设定
- L4/L7 亲和策略(consistent-hash / sticky-table)
- 东西向链路 MTU & ECMP 验证
- RUM 采样比例 & 黑盒探针上报去重
- 演练:单上游 Down、单 DC L7 Down、DNS 只读、证书过期
- 回滚:单键禁用 Anycast、强制权重到香港/广州/新加坡
- 预案通讯录:上游 NOC、机房值班、内部群
11. 结语:把复杂性“关在机房里”
做完这一切,我最满意的不是 p95 从 186ms 拉到 108ms,而是用户不再需要关心我们在背后做了什么。
Anycast、GSLB、L4/L7、ECMP、BFD、WireGuard……这些名词最后都只剩下一个结果:打开就快、错了能挪、宕了能活。
凌晨三点的演练结束时,指示灯还是那么多,但我知道,复杂性被我们关在了机房里,而不是摊在用户面前。
附:关键系统参数(示例)
# 网络缓冲(边界/LB 节点)
sysctl -w net.core.rmem_max=134217728
sysctl -w net.core.wmem_max=134217728
sysctl -w net.core.netdev_max_backlog=250000
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.ipv4.tcp_mtu_probing=1