Anycast 让一个地址拥有多个家。多个站点宣告同一个前缀,每个网络按照路由策略选出一条可用路径。本文关注互联网 BGP;本地 Anycast 也可以使用内部路由协议。客户端通常无需先经过全球负载均衡就能到达拓扑上较近的副本。某个站点撤回宣告后,控制面可以收敛到另一个家。这种设计把“就近”和“切换”交给网络完成,但也把策略错误和收敛窗口带到了应用面前。
一个地址,多点宣告
Anycast 把同一个单播地址或前缀配置给相互独立的实例,每个边缘路由器用 BGP 宣告可达性。邻居比较路径,在自己的路由信息库中安装最佳路由,数据包随后沿普通转发路径走向选定的下一跳。应用看到的仍是一个目的地址,实际上却有许多站点可以回答。客户端不需要知道副本列表,运维也可以独立扩容某个站点。
“最近”指路由策略和拓扑意义上的最近,并不保证地理距离最短。本地偏好较高的路径可以胜过物理上更短的路径;本地偏好相同时,通常再偏向 AS path 更短的路径,之后还有厂商和策略相关的决胜条件。运营者可以调整这些属性、做 AS path prepend 或撤回前缀来引流。真正的契约是可达性,而不是承诺每个客户端永远选择同一站点。不同 ISP 看到的路径不同,所以同一时刻两个用户落在不同城市并不矛盾。
路由完成故障切换
站点失去服务或上游时,应当由健康联动的会话撤回路由。上游传播撤回消息,选择仍在宣告的副本。转发路径无需等待新的 DNS 应答,因此整个边缘站点消失后地址仍然有效。BGP 收敛不是瞬时完成的:切换期间可能丢包,不同网络的收敛时间也不同,客户端仍要设置重试和合理的连接超时。把“最终会切过去”写进设计,并不等于承诺零丢包。
Anycast 特别适合无状态或容易重建的协议,例如 DNS、基于 UDP 的发现服务和 CDN 边缘。它也能把攻击面分散到多个站点,但容量和过滤策略必须协同。进程已经死了却仍在宣告的路由,比没有路由更糟,因为流量会被送进黑洞。应把宣告绑定到真实的就绪信号,同时监控控制面可达性和应用响应。撤回机制要有明确的延迟上限,不能只依赖人工登录路由器。
有状态会话会介怀
路由变化后,已经建立的 TCP 连接可能被送往另一个站点。新实例没有旧套接字状态,可能重置连接或丢弃报文。共享后端能保留应用状态,却不能把原来的 TCP 连接自动搬过去,因此客户端仍要接受重连并重建状态;幂等键可以防止重连后重发请求产生重复副作用。一次成功的跟踪并不等于具有粘性;拓扑变化不仅会改变下一条流的选择,也可能让当前流的后续报文到达另一个副本。
可观测性还应记录服务站点、路由年龄和撤回原因。一个全局成功率会掩盖某个区域的黑洞,因为其他区域仍然健康。应从多个网络探测,把 Anycast 地址和站点身份分开记录,并在“站点仍在宣告但服务检查失败”时告警。日志中保留连接建立时的站点,才能解释切换前后的重试和状态差异。
坏掉的样子
路由泄漏或误设的高本地偏好,会把流量吸引到很远的站点;只撤回一部分邻居,则会让一些网络停留在旧路径,表现为不对称丢包而不是干净切换。副本之间的 MTU、防火墙和 ACL 差异,也会制造看似随机的路由故障。服务带本地状态时,用户可能只在控制面已经收敛后才看到连接重置。把一个站点标记为健康,却没有检查回程路径,也会制造只进不出的黑洞。
站点的进程、依赖和回程路径都准备好之前,不要宣告前缀。用专门的演练地址测试撤回,从多个观测点测量收敛,并保留一条非 Anycast 的管理路径。检查路由对象、应用探针和撤回日志的时间线,确认它们描述的是同一次事件。路由是选择机制,只有显式绑定健康检查时才具备应用层健康含义。
快速验证
下面的本地 Go 模型只保留演示所需的两个 BGP 策略字段:本地偏好高者优先,其次 AS path 短者优先。它测试路径决胜、输入顺序无关、策略覆盖、撤回、空候选集和备用站点恢复;没有数据包或 BGP 会话离开进程。
package main
import "fmt"
type route struct {
name string
localPref int
asPath int
advertised bool
}
// This fixture models only local preference and AS-path length for one prefix.
func best(routes []route) (route, bool) {
var chosen route
for _, candidate := range routes {
if !candidate.advertised {
continue
}
if chosen.name == "" || candidate.localPref > chosen.localPref ||
(candidate.localPref == chosen.localPref && candidate.asPath < chosen.asPath) {
chosen = candidate
}
}
return chosen, chosen.name != ""
}
func want(ok bool, message string) {
if !ok {
panic(message)
}
}
func main() {
routes := []route{
{name: "tokyo", localPref: 100, asPath: 2, advertised: true},
{name: "singapore", localPref: 100, asPath: 3, advertised: true},
}
chosen, ok := best(routes)
want(ok && chosen.name == "tokyo", "shorter AS path should win the tie")
chosen, ok = best([]route{routes[1], routes[0]})
want(ok && chosen.name == "tokyo", "input order must not beat path length")
routes[1].localPref = 200
chosen, ok = best(routes)
want(ok && chosen.name == "singapore", "local policy should override path length")
routes[1].advertised = false
chosen, ok = best(routes)
want(ok && chosen.name == "tokyo", "withdrawal should select the surviving site")
routes[0].advertised = false
_, ok = best(routes)
want(!ok, "with every announcement withdrawn there is no route")
_, ok = best(nil)
want(!ok, "an empty candidate set must not invent a route")
fallback := route{name: "sydney", localPref: 50, asPath: 4, advertised: true}
chosen, ok = best(append(routes, fallback))
want(ok && chosen.name == "sydney", "a backup announcement should recover reachability")
fmt.Println("PASS: path length, policy priority, withdrawal, empty set, and backup")
}
将程序保存为 anycast_model.go,运行 go run anycast_model.go,并用 gofmt -d anycast_model.go、go run -race anycast_model.go 与 go vet anycast_model.go 检查格式、竞争和静态问题。预期输出是 PASS: path length, policy priority, withdrawal, empty set, and backup。若两个建模字段都相同,模型保留先遇到的候选,因此输入顺序无关只适用于本例的非完全平局路径。模型故意省略后续决胜条件、MED、eBGP/iBGP 规则、community、真实拓扑和收敛计时;它验证的是给定候选路径时的选择,不是互联网在某个时刻已经完成收敛的证明。
延伸阅读
来源
- RFC 4271:边界网关协议 4,路由选择和路径属性,已核验 2026-09-09。
- RFC 4786:Anycast 服务的运行,部署、路由与故障考量,已核验 2026-09-09。