Skip to content

第 8 章:选块与请求调度 —— Rarest-first 只是排序规则之一

“优先下载最稀有的 piece”是 BitTorrent 的经典口号,但生产实现还要处理文件优先级、快完成 piece、顺序下载、Peer 差异以及超时后的再分配。Transmission 把这些规则集中在 Wishlist

一、三个坐标系

理解选块前,先分清:

  • file:用户看到和选择的文件;
  • piece:metainfo 中有独立 hash 的完整性单位;
  • block:Peer Wire REQUEST/PIECE 的传输单位,默认固定大小,最后一块可能更短。

一个 file 可跨多个 piece,一个边界 piece 也可跨两个 file;一个 piece 又包含多个 block。用户说“不要下载文件 B”,最终必须转换成 piece wanted 和 block request 决策。

二、tr_file_piece_map 负责边界事实

它预计算:

  • 每个文件的 byte span 与 piece span;
  • 每个 piece 覆盖的 file span;
  • 哪些 piece 是文件边界 piece;
  • 全局 byte offset 到 (file index, file offset) 的映射。

文件优先级映射到 piece 时,边界 piece 会综合其覆盖文件;wanted 状态也不能简单按 piece index 切割。这个值对象让选块器不直接理解路径和文件系统。

三、Wishlist 的候选对象

每个尚需下载的 piece 对应 Candidate

text
piece                 piece 编号
block_span            可参与普通请求的 block 范围
raw_block_span        原始完整范围
unrequested           尚未被任何 Peer 请求的 blocks
replication           当前连接中拥有此 piece 的 Peer 数
priority              由文件优先级折算
salt                  随机或顺序下载排序键
is_sequential         当前是否顺序下载

unrequested 用降序 set 保存,使取较小 block 时可以从容器末尾移除,避免中间移动。

四、真正的候选排序

非顺序模式下,Candidate::operator<=> 依次比较:

  1. 剩余未请求 block 更少:优先把接近完成的 piece 收尾;
  2. 文件优先级更高
  3. replication 更低:rarest-first;
  4. salt:打散完全相同候选,避免所有客户端形成相同热点。

为什么“快完成”排在 rarest 前面?完成一个 piece 后才能 hash 验证并对外 HAVE;散着下很多 piece 的一半,既不能贡献给 swarm,也增加失败重传面。

五、顺序下载不是把 rarest-first 关掉就结束

顺序模式通过重新计算 salt,让 piece 按逻辑顺序排列;sequential_download_from_piece 支持从某个 piece 起循环:例如从 3 开始时,顺序可变成 3,4,5,1,2

候选仍会尊重文件 priority 和 Peer 是否拥有 piece。顺序只是强改变 tie-break/位置偏好,不会请求不存在或 unwanted 的数据。

顺序下载适合边下边预览,但会降低 swarm 健康:热门开头 piece 更拥挤、稀有尾部 piece 复制不足。因此它是用户选择,不是默认优化。

六、Wishlist 是增量维护的缓存

每次选块都扫描所有 piece × 所有 Peer 会很贵。WishlistController 把 Torrent/Swarm 的信号接入 Wishlist:

  • files wanted/priority 变化 → 重建或更新 candidates;
  • Peer bitfield/have/have_all → 调整 replication;
  • sent request → 从 unrequested 移除;
  • reject/cancel/timeout/disconnect → 放回 block;
  • got block → 删除并重排 piece;
  • piece complete → 移除 candidate;
  • sequential 配置变化 → 重算 salt。

这是典型的读优化派生索引:真实状态仍在 Torrent/Peer bitfield,Wishlist 缓存为高频 next() 查询服务。

七、为某个 Peer 选择下一批请求

Wishlist::next(n, peer_has_piece) 遍历已排序 candidates,只从对端拥有的 piece 中取 unrequested blocks,并尽量合并连续 block 为 tr_block_span_t

返回 span 而非单个 index 可以减少容器与协议层的循环开销。发送事件随后立刻把这些 block 标成 requested,防止另一个 Peer 在普通阶段拿到同一块。

八、active requests 存在哪里

系统需要两种视角:

  • 每个 Peer 的 active_requests:这个连接正在负责哪些 block;
  • Swarm/Wishlist 的 unrequested 集合:全局还有哪些 block 未分配。

Peer disconnect 或 choke 时,其 active bitfield 会一次性交回 Wishlist;收到 reject/cancel/timeout 也把单块放回。这样“分配出去但永远不回来”的 block 不会永久丢失。

九、当前版本如何处理下载尾部

经典 BitTorrent 客户端常用 Endgame:下载尾声向多个 Peer 请求同一 block,以少量重复流量换取更低的尾延迟。但当前快照的 Wishlist::next() 只选择 unrequested blocks,active_requests 的源码注释也明确把“避免重复请求”列为职责之一。因此,不能把经典 Endgame 直接当成这个版本的已实现行为。

这个版本采用更保守的路径:请求超时、被拒绝、连接断开或 choke 后,先取消原 active request,再把 block 交回 Wishlist,由后续调度重新分配。即便没有主动发出重复请求,旧 Peer 仍可能在取消后迟到地发送 PIECE,所以 Torrent 接收路径仍必须幂等地处理重复或迟到数据。

十、请求数量由 Peer 能力和速度共同决定

Wishlist 只回答“哪些 block 最值得要”,不决定“一次要多少”。tr_peerMsgsImpl 根据当前速度、目标 pipeline、block size、对端 reqq 与 active count 计算缺口。

分层后职责变成:

text
PeerMsgs:我现在还能并行请求 N 个
Wishlist:对这个 Peer,最合适的 N 个是这些
Bandwidth:这些请求返回时,每个周期最多读多少字节

三个策略互不替代。只调大 request window 不会突破带宽限制;只做 rarest-first 也不能解决慢 Peer。

十一、坏 piece 如何反馈到调度

piece hash 失败后:

  1. Torrent 清除 piece 完成状态并增加 corrupt bytes;
  2. Wishlist 把 piece 的 blocks 重新加入候选;
  3. Peer Manager 根据每个 Peer 对该 piece 的 blame 记录增加 strike;
  4. 达到条件的 Peer 可被 ban/disconnect。

由于一个 piece 的 blocks 可能来自多个 Peer,不能简单断言最后一个发送者就是坏节点。blame 是统计性归因,而不是密码学证明。

十二、设计取舍

选择收益代价
增量 candidates高频选块快需要正确处理 15 类以上事件
快完成优先更早得到可验证/可上传 piece不是纯粹 rarest-first
random salt减少 swarm 同步热点测试需可控随机性
block spans批处理更高效边界与合并逻辑更复杂
超时后取消并重新分配避免长期卡在慢 Peer,且不主动制造重复请求尾部延迟可能高于经典 Endgame

源码锚点

文档采用 CC BY-SA 4.0;源码片段保留 Transmission 上游许可。