第 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:
piece piece 编号
block_span 可参与普通请求的 block 范围
raw_block_span 原始完整范围
unrequested 尚未被任何 Peer 请求的 blocks
replication 当前连接中拥有此 piece 的 Peer 数
priority 由文件优先级折算
salt 随机或顺序下载排序键
is_sequential 当前是否顺序下载unrequested 用降序 set 保存,使取较小 block 时可以从容器末尾移除,避免中间移动。
四、真正的候选排序
非顺序模式下,Candidate::operator<=> 依次比较:
- 剩余未请求 block 更少:优先把接近完成的 piece 收尾;
- 文件优先级更高;
- replication 更低:rarest-first;
- 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 计算缺口。
分层后职责变成:
PeerMsgs:我现在还能并行请求 N 个
Wishlist:对这个 Peer,最合适的 N 个是这些
Bandwidth:这些请求返回时,每个周期最多读多少字节三个策略互不替代。只调大 request window 不会突破带宽限制;只做 rarest-first 也不能解决慢 Peer。
十一、坏 piece 如何反馈到调度
piece hash 失败后:
- Torrent 清除 piece 完成状态并增加 corrupt bytes;
- Wishlist 把 piece 的 blocks 重新加入候选;
- Peer Manager 根据每个 Peer 对该 piece 的
blame记录增加 strike; - 达到条件的 Peer 可被 ban/disconnect。
由于一个 piece 的 blocks 可能来自多个 Peer,不能简单断言最后一个发送者就是坏节点。blame 是统计性归因,而不是密码学证明。
十二、设计取舍
| 选择 | 收益 | 代价 |
|---|---|---|
| 增量 candidates | 高频选块快 | 需要正确处理 15 类以上事件 |
| 快完成优先 | 更早得到可验证/可上传 piece | 不是纯粹 rarest-first |
| random salt | 减少 swarm 同步热点 | 测试需可控随机性 |
| block spans | 批处理更高效 | 边界与合并逻辑更复杂 |
| 超时后取消并重新分配 | 避免长期卡在慢 Peer,且不主动制造重复请求 | 尾部延迟可能高于经典 Endgame |
源码锚点
libtransmission/peer-mgr-wishlist.h:Candidate 排序与增量事件libtransmission/peer-mgr-wishlist.cc:next()、salt 与候选维护libtransmission/peer-mgr.cc:WishlistController 与请求协调libtransmission/file-piece-map.cc:file/piece 映射libtransmission/block-info.h:byte/piece/block 坐标libtransmission/peer-msgs.cc:动态请求窗口与 timeout