第 10 章:带宽树与队列 —— 不只“限速”,而是分层资源治理
Transmission 同时面对全局限速、分组限速、单 torrent 限速、Peer 优先级和上传 choke。
tr_bandwidth用树表达约束继承,Peer Manager 再用周期调度决定连接和服务对象。
一、带宽是一棵树
tr_bandwidth 的头文件直接给出设计:
实际父子关系会根据 group 与 torrent 设置调整,但原则不变:叶子是 Peer I/O,根是 Session。
二、测量与限制共用同一结构
每个节点对上下行分别记录:
- raw bytes:协议、握手和 piece 数据全部计入;
- piece bytes:只统计有效载荷;
- 2 秒历史窗口,250 ms 粒度;
- desired speed、limited、bytes left;
- 是否 honor parent limits。
Peer 消耗字节时,notify_bandwidth_consumed() 沿 parent 递归上报,因此:
- Peer 节点给出单连接速度;
- Torrent 节点自然得到其所有 Peer 之和;
- Session 根得到全局速度。
不需要另建一个统计聚合器。
三、allocate() 与 clamp() 分工
周期开始时根节点调用 allocate(period_ms):
- 深度遍历树,为 limited 节点计算本周期
bytes_left; - 收集所有 Peer I/O,并继承路径上的有效 priority;
- 按 high/normal/low 分组;
- 给可运行 Peer 激活读写;
- Peer 真正 I/O 前调用
clamp(dir, wanted_bytes)。
clamp() 会在当前节点限制 byte count,并在 honor_parent_limits 为真时递归询问父节点。最终允许值是整条祖先路径上最严格的剩余额度。
Peer 想写 64 KiB
→ Peer 节点允许 64 KiB
→ Torrent 限速只剩 20 KiB
→ Group 还剩 50 KiB
→ Session 还剩 12 KiB
最终只能写 12 KiB四、“不限速”为什么有两种含义
通常子节点不限速仍要遵守父限制。但 torrent 的 TR_SPEEDLIMIT_UNLIMITED 语义可能是“忽略 Session 全局限速”。因此节点有 honor_parent_limits 开关,而不是把 desired speed 设成一个巨大数。
这是配置语义与数值模型分离的好例子:无限并非某个魔法带宽值,而是约束继承关系改变。
五、原始流量与 Piece 流量为何分开
用户关心的“下载速度”通常是 piece payload;网络和限速必须考虑握手、消息头、PEX、metadata 等 raw bytes。若只按 piece bytes 限速,协议开销可绕过上限;若 UI 只显示 raw,又会让用户看到超过文件增长速度的数字。
同一节点记录两套 history,分别服务控制与展示。
六、Torrent 队列控制的是“运行名额”
Session 对下载与做种分别维护:是否启用队列、最大同时任务数、stall 判定分钟数。Torrent 的 queue direction 由完成度决定:未完成占下载队列,完成后占上传/做种队列。
start(false) 发现无空槽时只设置 is_queued_。Session 的 1 秒 queue timer 检查:
- 当前 active 数;
- 队列上限;
- 运行任务是否 stalled;
- queue position;
- torrent 是否有错误/验证中。
有空槽时按位置启动等待任务。start_now 则绕过队列,但不绕过文件存在和验证等安全条件。
七、队列、带宽、Peer 上限是三层预算
| 预算 | 控制对象 | 解决的问题 |
|---|---|---|
| Torrent queue | 同时运行的任务数 | 避免大量任务争抢资源 |
| Peer limit | 全局/单 torrent 连接数 | 控制 fd、NAT 与握手成本 |
| Bandwidth tree | 每周期可传字节数 | 控制速率与优先级 |
一个 torrent “已运行”也可能因没有 Peer 或被 choke 而零速;有很多 Peer 也可能被全局带宽 cap 限住。排障不能只看单一开关。
八、上传 rechoke:选择服务谁
Peer Manager 定时执行 rechoke。大意是:
- 对下载中的 torrent,偏好向我们提供数据、表现好的 Peer;
- 对做种 torrent,考虑上传速率、interest 和公平性;
- 保留 optimistic unchoke,周期给未知 Peer 机会;
- Fast Extension 的 allowed-fast 与普通 choke 状态协同;
- 每个 swarm 有自己的 Peer 状态,但调度受全局上限影响。
Choke 不是断开连接,而是暂时不接受对方普通 REQUEST。它控制上传服务面,带宽树控制字节速率,两者不能互相替代。
九、连接维护的多个 Pulse
tr_peerMgr 用多个节拍处理不同成本的工作:
- bandwidth pulse:分配额度、pump Peer;
- rechoke pulse:重算上传 choke;
- reconnect pulse:关闭低价值连接、发起新连接;
- peer info pulse:老化候选与状态。
分开周期比每个网络事件都全局重算便宜,也避免单个复杂“大 tick”造成延迟尖峰。紧急变化可把 rechoke timer 临时调到很短间隔,而不是同步重算。
十、Alt speed 是 Session 级策略切换
“乌龟模式”保存一套上下行替代速度,可手动或按每周日程启用。tr_session_alt_speeds 负责时间窗口和 change reason;状态变化时更新 Session 顶层带宽限制,并通过回调/RPC 让 UI 同步。
它没有给每个 Peer 打补丁,说明把全局策略放在带宽树根的价值:一次切换自然传播到所有后代。
十一、分组限速为何是独立节点
Session 的 getBandwidthGroup(name) 动态创建 group bandwidth。Torrent 可把自己的 bandwidth parent 指向 group,从而实现:
- 工作 torrent 总计 5 MB/s;
- 影音 torrent 总计 20 MB/s;
- 所有 group 再共同服从 Session 50 MB/s。
如果只有“全局 + 单任务”两个数,无法表达共享子预算;树结构正是这个配置需求的自然模型。
十二、设计启示
- 限速是层级约束问题,不是 sleep 问题。在 I/O 前 clamp,比传完后统计超速更可控。
- 统计与控制复用一棵树。字节沿父链上报,天然得到各层聚合值。
- 调度周期要按成本拆分。带宽 100ms 级、队列 1s 级、保存数分钟级,各自容忍度不同。
- 策略输出状态,机制执行额度。queue/rechoke 决定谁活跃,bandwidth 决定能传多少。
源码锚点
libtransmission/bandwidth.h、bandwidth.cc:树、历史窗口、allocate/clamplibtransmission/peer-mgr.cc:bandwidth/rechoke/reconnect pulselibtransmission/torrent-queue.cc:持久化队列位置libtransmission/session-alt-speeds.cc:替代限速日程libtransmission/session.cc:队列 timer 与全局带宽更新libtransmission/torrent.cc:start queue 判定与 queue direction