游戏出海中转:Anycast + GeoDNS + 智能回源在香港服务器上的一体化L4/L7限流

凌晨 3:17,游戏群里炸锅。东南亚玩家延迟飙到 400ms、登录排队、交易接口被刷得喘不过气。报警把我从机房躺椅上拽醒,我端起昨晚没喝完的凉咖啡,一边把 BMC 打开看 CPU 温度,一边在交换机上敲命令确认上联没抖。那一刻我意识到:靠单点出口 + 传统 DNS 轮询,已经扛不住节日活动的把子火了。
接下来的两周,我把香港的这套“出海中转”重做了一遍:边缘 Anycast 承载接入、GeoDNS 精准首跳、智能回源跨区择优、配合 L4/L7 双栈限流与风控。这篇稿子是我从零到一落地的完整笔记,里面有我踩过的坑和当天切流的每一个参数。
1. 目标与整体设计
目标:
面向东亚/东南亚玩家的低时延接入(HK 作为中转 POP)。
多活(任一边缘节点下线不影响业务)。
可观测(全链路 RTT/Jitter/P99 可追)。
抗滥用(L4/L7 限流 + 行为风控)。
可演练(SOP + 自动回退)。
核心技术选型:
Anycast:边缘节点用 BGP 对外公告同一个服务 IP 段,让玩家就近接入。
GeoDNS:权威 DNS 按地理/运营商优先级指向最近/最稳的 Anycast POP。
智能回源:边缘对多地源站(Tokyo/Singapore/Frankfurt)做实时测量与加权一致性哈希,按用户维度稳定分流。
L4/L7 限流:L4 用 nftables + tc 做速率/并发护栏;L7(登录/支付/资产接口)用 NGINX+Lua+Redis 做令牌桶,配风控评分。
2. 机房与硬件清单(HK 单 POP 规模示例)
| 角色 | 数量 | 规格 | 关键点 |
|---|---|---|---|
| 边缘转发/代理节点(Edge) | 4 | AMD EPYC 7313P(16C/32T)/128GB RAM/2×NVMe 1.92TB(RAID1)/Mellanox ConnectX-5 2×25G | 支持 XDP/DPDK,足够的 IRQ/RSS 队列 |
| 权威 DNS(GeoDNS) | 2 | Intel E-2278G / 32GB / 2×10G | PowerDNS + GeoIP2,主主 |
| Redis 限流存储 | 3 | E-2278G / 64GB / NVMe / 10G | Cluster 模式,副本因子 1 |
| 监控/日志 | 2 | 任意 x86 / 64GB / 大盘 | Prometheus + VictoriaMetrics + Loki |
| ToR 交换机 | 2 | 48×25G + 6×100G | MLAG/VSX,互联 2×100G |
| 上联 | 2–3 | HKT/PCCW/NTT(举例) | 与各家 eBGP,收默认+区域前缀 |
操作系统:CentOS 7.9(内核建议换 ELRepo 5.4 LTS,XDP/封包能力稳)
时钟:Chrony 对齐(本地 PPS/上游 NTP 池),MaxSkew < 5ms。
盘用途:边缘本地 NVMe 只放 cache/落盘日志;配置与状态完全可重建。
3. 网络拓扑(文字版)
玩家 → 互联网 → [HK POP - Anycast VIP]
│ eBGP 多家上联
┌─────────┴─────────┐
[Edge-1] [Edge-2] [Edge-3] [Edge-4] ← 4 台无状态边缘
│ │ │ │
(ToR MLAG 25G 接入)
│
上联骨干(HKT/PCCW/NTT)
│
智能回源:Tokyo / Singapore / Frankfurt 源站池
域名路径:玩家解析 game.example.com → GeoDNS 返回离玩家最近/质量最好的 Anycast VIP (A/AAAA) → BGP 把流量收敛到 HK POP 的就近 Edge → Edge 智能转发到最优源站。
4. Anycast:FRR + BGP 实战
前提:拿到可公告的 /24(或更大),对外分发同一 VIP(/32)由多台 Edge 同时公告。
/etc/frr/bgpd.conf(Edge 节点节选):
router bgp 65010
bgp router-id 10.10.10.1
no bgp default ipv4-unicast
neighbor HKT peer-group
neighbor HKT remote-as 9304
neighbor HKT ebgp-multihop 2
neighbor HKT timers 10 30
neighbor 203.0.113.1 peer-group HKT
!
address-family ipv4 unicast
network 203.0.113.100/32 route-map ANYCAST-ATTR
neighbor HKT activate
maximum-paths 4
exit-address-family
!
route-map ANYCAST-ATTR permit 10
set community 65010:100 additive
set local-preference 200
set metric 0
!
经验参数:
timers 10 30(抖动期可降到 3 9,稳定后回升)。
多上联开 ECMP,并控制 BGP community(对大陆回程可选 CN2/直连策略)。
有计划下线时打 Graceful Shutdown community(如 65535:0),避免黑洞。
坑 1: 某次 HKT 路由器升级导致我们被对端收得只剩默认路由,ECMP 失衡,PCCW 单边流量暴涨。
解法: 在上联 peer-group 上加 inbound prefix-list 与 as-path 过滤,确保前缀质量,同时 PromQL 对各 BGP 邻居的 bgp_routes_received 做差分告警。
5. GeoDNS:PowerDNS + GeoIP2
PowerDNS(Authoritative)配置要点:
GeoIP2 City/ASN 库,按 国家/AS 维度返回不同 Anycast VIP(多 POP 时用)。
HK 单 POP 场景也用它做运营商兜底与灰度。
示例(Lua Record):
-- /etc/powerdns/lua/geo.lua
function preresolve ( remoteip, domain, qtype )
if domain ~= "game.example.com" or qtype ~= pdns.A then return false end
local cc = geoip_lookup_country_code(remoteip)
local asn = geoip_lookup_asn(remoteip)
-- 中国大陆优先返回 HK Anycast VIP(CN2 回程更稳)
if cc == "CN" then
pdnslog("CN from " .. remoteip .. " ASN " .. asn)
pdns.replace({{qtype=pdns.A, content="203.0.113.100", ttl=30}})
return true
end
-- 东南亚 => 仍指向 HK(示意),多 POP 时可返回 SG/HK 混合
if cc == "SG" or cc == "MY" or cc == "TH" or cc == "PH" or cc=="VN" then
pdns.replace({{qtype=pdns.A, content="203.0.113.100", ttl=30}})
return true
end
-- 兜底
pdns.replace({{qtype=pdns.A, content="203.0.113.100", ttl=30}})
return true
end
TTL 建议:活动期 A 记录 TTL=30s,平时 300–600s。
坑 2: MaxMind GeoIP 数据延迟导致新兴 ASN 识别错误。解法:关键国家/AS 做手工白名单优先级覆盖;并在日志侧抽样审计(DNS Query → 实际源 IP RTT)。
6. 智能回源:IPVS(UDP/TCP) + Envoy(HTTP/QUIC) 一致性哈希
我们的业务包含 游戏长连接(TCP/UDP) 和 HTTP 登录/支付 两类。
6.1 L4(TCP/UDP)用 IPVS
UDP/TCP 都可代理,开 sh(source-hash) 保持会话亲和;对源站定义权重由实时质量(RTT/Jitter/丢包)驱动。
健康检查用 keepalived 或自写 agent 打点。
示例:
# 建立虚拟服务(监听 Anycast VIP 上的 7001 端口,TCP)
ipvsadm -A -t 203.0.113.100:7001 -s sh
ipvsadm -a -t 203.0.113.100:7001 -r 198.51.100.10:7001 -g -w 5 # Tokyo
ipvsadm -a -t 203.0.113.100:7001 -r 203.0.113.10:7001 -g -w 3 # Singapore
# UDP 服(战斗服)
ipvsadm -A -u 203.0.113.100:9000 -s sh
ipvsadm -a -u 203.0.113.100:9000 -r 198.51.100.20:9000 -g -w 4
ipvsadm -a -u 203.0.113.100:9000 -r 203.0.113.20:9000 -g -w 2
-g 为 Direct Routing(DR);若跨网段用 -m(NAT)。我们 HK→Tokyo/SG 走公网,实际采用 NAT(-m)并在边缘开 SNAT 池避免端口耗尽。
权重更新:通过一个侧车 agent 每 5s 采集到各源站的 RTT/Jitter(TCP SYN/UDP echo),把分数写进本地 /run/ipvs-weights.json,由守护进程增量 ipvsadm -e。
坑 3: UDP NAT 端口耗尽。解法:/proc/sys/net/ipv4/ip_local_port_range 扩到 1024 65535,多 SNAT 源地址池,配合 conntrack 参数(见第 8 节)。
6.2 L7(HTTP/QUIC)用 Envoy Ring Hash
登录/充值类接口我们走 Envoy,拿 Header(X-UID)/Cookie 做 Key,算法用 maglev。
Envoy 路由片段:
load_balancing_policy:
policies:
- ring_hash:
minimum_ring_size: 4096
hash_function: MAGLEV
consistent_hashing_lb_config:
use_hostname_for_hashing: false
回源候选:Tokyo / Singapore / Frankfurt,按实时质量调整 endpoints.weight。
坑 4: 源站证书 OCSP 阻塞导致 TLS 建链尾部长尾。解法:Envoy 开 OCSP stapling 与证书缓存,并在边缘就地启用 session resumption。
7. L4 限流与防护:nftables + tc 护栏
目标:防 SYN 洪水/UDP 洪水、控制单 IP/前缀突刺、保护 conntrack。
/etc/nftables.conf(节选):
table inet filter {
set trusted_asn { type inet_service; flags interval; }
chain input {
type filter hook input priority 0;
ct state invalid drop
# SYN proxy for new connections on TCP ports
tcp flags syn tcp option maxseg size 1-65535 ct state new \
meter synprotect { ip saddr limit rate over 200/second burst 400 packets } \
accept
# 每 IP 基础并发护栏(示意:TCP 7001)
tcp dport 7001 ct state new \
meter per_ip_7001 { ip saddr limit rate over 50/second burst 100 packets } \
drop
# UDP 每 /24 限速
udp dport 9000 meter u9000 {/24 prefix: ip saddr limit rate over 2k/second burst 5k packets} drop
}
}
tc 警戒线(根队列 + policer):
tc qdisc add dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:10 htb rate 20gbit ceil 20gbit
# 攻击期对目的端口 9000 增加保守 policer
tc filter add dev eth0 protocol ip parent 1: prio 1 u32 \
match ip dport 9000 0xffff \
police rate 10gbit burst 5m drop flowid :1
坑 5: nftables set 规模过大(热键/恶 IP)导致评估退化。解法:热键放 ebpf/xdp 快速丢弃;次热键仍由 nft 管。
8. L7 限流 + 风控:NGINX + Lua + Redis
策略:按 用户 ID / 设备指纹 / IP 组合做令牌桶;风控评分(UA/JA3/失败率/时段/ASN)动态调节 burst。
NGINX 片段:
lua_shared_dict limits 100m;
init_by_lua_block {
local redis = require "resty.redis"
-- 连接池等初始化
}
server {
listen 443 ssl http2;
server_name api.example.com;
set $uid $http_x_uid;
if ($uid = "") { set $uid $remote_addr; } # 兜底
access_by_lua_block {
local limiter = require "limit_token_bucket"
local key = ngx.var.uid
local allowed, wait = limiter.consume(key, 10, 30) -- rate=10/s, burst=30
if not allowed then
ngx.status = 429
ngx.say('{"code":429,"msg":"Too Many Requests"}')
return ngx.exit(429)
end
}
location /login {
proxy_pass http://login_pool;
}
}
Redis(Cluster)建议:
3 主 3 从,maxmemory-policy allkeys-lru,慢查询阈值 5ms。
限流 key 过期 60–120s,避免冷启动“爆发”。
坑 6: Redis p99 抖高导致“误伤”。解法:令牌桶逻辑本地 Lua shared dict 预扣,再异步补库存;Redis 仅做跨进程一致性校准。高风险接口做两级限流(本地 429、网关 302 到静态降级页)。
9. 内核与网卡调优(关键参数表)
| 参数 | 建议值 | 说明 |
|---|---|---|
net.core.somaxconn |
16384+ | listen 队列 |
net.ipv4.tcp_max_syn_backlog |
262144 | SYN 队列 |
net.ipv4.tcp_syncookies |
1 | 开 |
net.netfilter.nf_conntrack_max |
8,000,000(按内存评估) | 连接跟踪 |
net.ipv4.ip_local_port_range |
1024 65535 |
NAT 端口池 |
net.core.rmem_max/wmem_max |
134217728 | 套接字缓冲 |
fs.file-max |
10,000,000 | fd 上限 |
| NIC RX/TX ring | 4096/4096 | ethtool -G |
| IRQ 绑核 | irqbalance --ban + 手动 pin |
保证 NUMA 亲和 |
| GRO/LRO | 关 LRO,GRO 视流量 | UDP 场景谨慎开 |
脚本要点:
开机根据 lspci 将 mlx5 队列均分到独立核,RPS/XPS 与队列一致。
sysctl -p 后跑 ss -s、nstat、ethtool -S 验证。
10. 部署步骤(我当时的顺序)
拉业务影子流量到新 POP(5%),灰度 48 小时。
部署 FRR,与三家上联建邻,观察路由收敛与回程 AS Path。
上线 GeoDNS(PDNS),TTL=300,抽样验证客户端 IP→POP 命中率。
Edge 上线 IPVS/Envoy,只接登录只读接口,验证一致性哈希与粘性。
开 L4/L7 限流 在可观测模式(仅打标不拦截),收集一周画像。
限流切到强制模式(逐接口),并配 风控白名单。
演练(见第 12 节),通过后放量到 100%。
11. 观测与告警(关键指标)
| 模块 | 指标 | 阈值/说明 |
|---|---|---|
| BGP | Established 保持时长、flaps |
任意邻居 flap>2/5m 告警 |
| DNS | QPS、解析成功率、地区命中率 | 命中率低于 90% 告 |
| Edge | NIC rx_no_buffer, rx_dropped |
连续 1min > 0 告 |
| L4 | conntrack count / max |
>80% 告,>90% 触发丢弃策略 |
| L7 | 2xx/4xx/5xx 比例、429 比例 |
429 > 1% 检查策略 |
| 回源 | 源站 RTT/Jitter/P95 | 触发权重调整 |
| Redis | p99、连接数、rejected_connections |
p99>5ms 告 |
| 用户 | 登录 P95、战斗服抖动 | 目标 HK→Tokyo < 60ms |
12. 风控与演练清单(SOP)
攻防策略:
高速口子(UDP 9000)只对合法握手流量开放,随机端口 + 首包签名校验(无签名直接丢)。
登录口按 ASN/国家 设不同 burst,对“黑产 ASN”强限。
JA3/UA 指纹异常 + 失败率拉高 → 临时降权甚至黑洞(短时)。
演练(季度至少 1 次):
单上联 BGP 断链:验证 Anycast 回收、DNS 命中率无抖。
某源站不可达:IPVS/Envoy 10 秒内权重调零;会话复用不炸。
Redis 半残:限流退化到本地 shared dict;成功率下降 <1%。
Conntrack 溢出:触发自动采样丢弃 & per-/24 限速。
日志盘打满:Loki/FluentD 退化,边缘仅保最近 2h。
整机下电:BGP 优雅下线 + 接口抢占互斥有效。
DNS 被劫持(演练环境):客户端回落到 Anycast 备用域名。
演练记录表(摘录):
| 演练项 | 触发方式 | 预期行为 | 实际结果 | 回归修复 |
|---|---|---|---|---|
| 上联断链 | ip link set/BGP neighbor shutdown |
30s 内路由收敛 | 28s | 通过 |
| 源站失效 | 防火墙丢弃 Healthcheck | 10s 内权重=0 | 9s | 通过 |
| Redis 降级 | tc 注入 20ms 延迟 |
本地限流接管 | 正常 | 调整 p99 告警阈值 |
13. 线上“坑”与现场解决
ECMP 粘性被打散:某上联修改哈希算法,长连接掉线。→ 在我们侧 ipvs sh + 源地址打标固定,BGP 不再开 maximum-paths 对该邻居。
MTU 陷阱:回源路径 1500/1492 混杂,QUIC 丢包明显。→ 全链路 PMTUD + 业务端 max_udp_payload_size=1200。
iptables 锁竞争:早期脚本仍动 iptables,高 QPS 下加规则阻塞。→ 全面迁移 nftables。
Redis 热点 Key:活动开场 login:uid:* 热点,p999 突刺。→ 二级 Key + 本地预扣 + 限流 Key 分片。
MaxMind 识别错误:新 AS 走了远路。→ 手工白名单 + 每周自动对账(解析结果 vs RTT)。
14. 关键配置清单(可直接套)
sysctl:
cat >/etc/sysctl.d/99-tuning.conf <<'EOF'
fs.file-max = 10000000
net.core.somaxconn = 16384
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_syncookies = 1
net.ipv4.ip_local_port_range = 1024 65535
net.netfilter.nf_conntrack_max = 8000000
net.ipv4.tcp_tw_reuse = 1
EOF
sysctl --system
FRR(bgpd):见第 4 节。
PowerDNS(Lua):见第 5 节。
IPVS:见第 6.1 节。
Envoy:见第 6.2 节。
nftables/tc:见第 7 节。
NGINX + Lua:见第 8 节。
15. 成本与容量规划(经验值)
| 项 | QPS/连接规模 | 机器数 | 备注 |
|---|---|---|---|
| L4(UDP 9000) | 300–500 万 pps | 4 | 单台 25G NIC,irq 绑核后可稳 120–150 万 pps |
| L4(TCP 7001) | 25–40 万并发 | 4 | conntrack 需 8M 级 |
| L7(登录/支付) | 5–8 万 QPS | 2–3 | Envoy+NGINX,命中缓存比例高 |
| Redis 限流 | 30–50 万 ops | 3 主 3 从 | p99 < 3–5ms |
16.活动夜的 0 点到 3 点
大促当天 00:00 切主,会场流量像开闸。bgp_neighbors_established 一直稳在 3,Edge 的 rx_dropped 为 0;Tokyo 源站 RTT P95 42ms,Singapore 做备。凌晨 1 点开始有一波 UDP 污点流量,nft 的 per-/24 限速把高峰削平,429 的比例始终 <0.3%。
2:10 Redis p99 抬头,我们自动切回“本地预扣”模式,成功率没掉。3:05 运营在群里发了句“这次很安静”,我把凉咖啡扔了,去楼下买了碗云吞面。
技术人的成就感就是这样:把复杂的系统揉到玩家感觉不到的顺滑里。