BGP多线主机跑备份,一条 rsync 流永远只走一条线:FIB 多路径哈希、rt_genid 与 --append 假续传的三层真相
先说一个很多人做了三年运维都没想明白的事:你买了一台标着「BGP 多线」的主机,以为备份数据会自动分摊到各条线路上跑满带宽。不会。一条 rsync 流,从 SYN 到 FIN,永远只从一块网卡出去。
更糟的是另一件事——BGP 会话抖了,上游黑洞了,内核压根不知道,你的 rsync 进程还在那儿 ESTAB 着呢,`ss` 看连接活得好好的,`retrans` 慢慢往上爬,半小时后你去看备份窗口,已经炸了。
这篇不讲概念,讲三层真实机制,以及它们怎么联手把「备份成功」变成一个谎言。
第一层:多线是入向的,不是出向的
BGP 多线主机的「多线」通常指:你有多个上游 AS,向它们都宣告了同一个 /24 或 /23 前缀。这叫 inbound multi-homing。别人访问你,数据从哪条线进来,由上游的 BGP best path 决定——这是「多线」。
但备份是出向流量。你的数据要出去到异地机房 / 对象存储。出向走哪条线,跟 BGP 没半点关系,只看一件事:内核的 FIB 怎么选下一跳。
# 先看默认路由到底有几条
ip route show default
default via 10.0.0.1 dev eth0 proto static metric 100
# 再看你的备份目标实际从哪儿出去
ip route get 203.0.113.10 from 198.51.100.5
203.0.113.10 from 198.51.100.5 via 10.0.0.1 dev eth0 src 198.51.100.5 uid 0
cache
如果 `ip route show default` 只有一行,那你的「BGP 多线主机」在出向就是单线机。备份、拉镜像、调 API,全挤在 eth0 上。这是最常见的配置——BGP 只负责宣告,出口走一条静态默认路由。多线的价值全给了入向。
就算真有 ECMP,也救不了你
有些机房会给你配 ECMP 默认路由,内核里是这样:
ip route show 0.0.0.0/0
default proto bgp metric 20
nexthop via 10.0.0.1 dev eth0 weight 1
nexthop via 10.0.1.1 dev eth1 weight 1
这时候选路由多路径哈希决定。关键在哈希策略:
sysctl net.ipv4.fib_multipath_hash_policy
net.ipv4.fib_multipath_hash_policy = 0
0= L3 哈希(源 IP + 目的 IP + flow label),内核默认值1= L4 哈希(标准五元组)2= L3 + L43= 自定义,需要配fib_multipath_hash_fields(5.12+ 才有)
划重点:无论策略是 0 还是 1,同一对 (src, dst, sport, dport) 算出来的哈希值恒定。一条 rsync 连接就是一个固定的五元组,从建立那一刻起就被钉死在某一条链路上,永远不变。多线对它的吞吐增益是零。
想榨出多线带宽?得让内核看到不同的源 IP。主机上多挂几个 IP,然后多进程 rsync,每个进程绑定不同源地址:
# 先确认这些 IP 真的在本机接口上,否则 SYN 直接被丢
ip -4 addr show eth0 | grep inet
for i in 1 2 3 4; do
rsync -aHAX --numeric-ids --no-inplace --append-verify \
--address=198.51.100.$i \
--partial-dir=.rsync-partial \
/data/ backup@203.0.113.10:/backup/ &
done
wait
四个源 IP → 四种哈希 → 大概率落到不同 nexthop。这是唯一能真正吃掉多线出口的手段。注意:这些 IP 必须事先被上游宣告且真的在接口上,否则 SYN 发出去对端回不来。
第二层:BGP 收敛时,已建立的 TCP 连接不会迁移
这是最要命的一层。很多人的心智模型是「我有两条线,一条断了自动切另一条」。这个模型对新建连接成立,对已建立的连接完全不成立。
内核 IPv4 的 socket 上挂着一个缓存的 dst_entry(sk_dst_cache)。每次发送走 __sk_dst_check(),它会比较缓存的 rt_genid 和当前的 net->ipv4.rt_genid。路由表发生变更(add/del/replace)时 genid 会 bump,socket 就能感知到并重新查表。
但这套机制有个致命前提:内核路由表得变。
而 BGP 多线主机的典型形态是——BGP 只做宣告,出口是运维手写的静态默认路由。上游那条线 BGP 会话断了,物理口还 up,你的内核路由表纹丝不动,rt_genid 不变,socket 检查通过,数据包继续往那个下一跳发,进入一个已经不转发你流量的网络。
这就是「静默黑洞」。诊断起来长这样:
# 连接明明是 ESTAB,但它其实在往虚空里灌数据
ss -tinmo state established dport = :873
ESTAB 0 0 198.51.100.5:51234 203.0.113.10:873
skmem:(r0,rb131072,t0,tb2626560,f0,w0,o0,bl0,d0)
cubic wscale:7,7 rto:120000 rtt:0.9/0.3 ato:40 mss:1448
retrans:42/87 ...
# 全局看丢包和超时
nstat -az | grep -E 'TCPTimeouts|TCPLostRetransmit|TCPRetransFail'
rto:120000 —— RTO 已经退避到内核上限 120 秒。retrans:42/87 —— 当前未确认重传 42 次,累计 87 次。这就是「rsync 卡在 99%」的真相:不是 rsync 慢,是 TCP 在一个谁也到不了的黑洞里指数退避重传。
默认 tcp_retries2 = 15,配合 RTO 退避,内核文档给的上限是大约 13~30 分钟才放弃并返回错误。你的备份窗口就这么被吃干净了。
# 让备份连接快速失败,而不是静默挂死
sysctl -w net.ipv4.tcp_retries2=7
# 7 次大约 100~160 秒就放弃,取决于当时的 RTO 基线
改这个要权衡:丢包率高的跨境线路会更早断连。所以更稳的做法是把备份流绑到一条质量可控的出口上。我们后来把这批备份源机迁到轻云互联的 BGP 多线,他们默认给的是 ECMP + L4 哈希策略,出向路由在面板里能直接看到 nexthop 列表,至少不用再靠猜「现在到底走的哪条线」——这一点在排查这种静默黑洞时省了太多时间。
第三层:rsync 的「续传成功」是假的
假设你做对了前两层,BGP 还是抖了一下,TCP 被 RST 掉了。rsync 会重连继续传。这里藏着最阴的一刀。
--append 和 --append-verify 的区别:
--append:假定目标端已有文件是源文件的一个完整前缀,直接从目标文件的当前长度处开始追加。它不校验任何已有内容。--append-verify:会对目标端已有部分做滚动校验,慢,但正确。
现在把 --inplace 加进来。默认的 rsync 是写临时文件再 rename 的(原子操作,中断了目标文件不受影响)。但 --inplace 直接往目标文件上写,图的是省 IO。
于是灾难链条闭合了:
--inplace让中断的写入直接落在目标文件上- TCP RST 时,对端已经落盘了一部分「看起来接上了但其实是错的」数据块
- 重连后
--append从文件的当前长度续写,跳过校验 - 最终文件大小完全正确,
rsync退出码 0,报告 success - 中间那一段是垃圾,只有 restore 的时候才发现
还有一个细节会骗过所有人:--timeout 拦不住这个场景。rsync 的 timeout 监控的是「IO 静默」,而在 TCP 重传期间,rsync 卡在 write() 系统调用里等返回,从 rsync 视角看 IO 是「忙碌」的,计时器压根不触发。
正确的备份命令长这样:
rsync -aHAX --numeric-ids \
--no-inplace \
--append-verify \
--partial-dir=.rsync-partial \
--timeout=300 \
--address=198.51.100.5 \
--bwlimit=80000 \
/data/ backup@203.0.113.10:/backup/
并且记住 rsync 的退出码语义,别只看 0 和 1:23 = 部分文件未传输,24 = 部分源文件在传输中消失,30 = 超时。0 只代表「进程跑完了」,不代表「内容对了」。
第四层:把备份从「逐文件比对」换成「内容寻址」
rsync 的根本问题在于它的模型是「文件 = 路径 + 字节流」,续传的锚点是文件偏移量,而偏移量在网络中断面前是不可信的。
restic / borg 这类工具换了个模型:内容定义分块(CDC,Buzhash / Rabin 指纹)+ 每块独立哈希。每块在写入前就算好了 hash,落盘后能独立验证,天然幂等。续传的锚点是「哪块还没传」,而不是「传到第几个字节」。
restic -r s3:https://s3.example.com/backup-bucket backup /data \
--pack-size 64 \
--read-concurrency 2 \
--exclude-caches
# 关键:定期做真实数据校验,而不是只查索引
restic check --read-data-subset=5%
restic forget --keep-daily 7 --keep-weekly 4 --prune
check --read-data-subset 会真的把 5% 的 pack 文件拉回来重算哈希。这一步才叫「验证备份」,前面那些「任务成功」都只是「进程退出正常」。
收尾:三个必须加的监控点
- 出口一致性:每次备份前后跑一次
ip route get <backup_target> from <src>,把结果记进日志。路由变了,你要在第一时间知道,而不是从备份时长里猜。 - TCP 层重传:用
nstat -az或/proc/net/snmp抓TcpExtTCPTimeouts/TcpExtTCPLostRetransmit的增量,跑成时序指标。备份窗口内的异常重传是黑洞的唯一早期信号。 - 尾部校验:备份完成后对目标端做一次抽样哈希(或者直接依赖 restic 的
check)。别信退出码,信哈希。
多线主机不是万能的。理解哪一段是 BGP 在管(入向宣告)、哪一段是内核 FIB 在管(出向选路)、哪一段是 TCP 在管(连接生命周期),你才能知道备份这件事上,冗余到底存在不存在。多数情况下,答案是:不存在,你得自己造。