做了个下载器 FluxDown ,Rust 写引擎,Flutter 做界面。不打算在这儿铺功能列表,讲一个我自己制造又自己修掉的 bug ,觉得挺有意思。
多线程下载收尾的时候会有落后者问题:快的连接下完闲着,整体时间由最慢那条决定。常规解法是把剩余最多的那段从中点再切一刀给空闲连接。但切分有成本(一次 HTTP 往返),所以要设下限,我设的是 2MB ,按实时吞吐往下调到最低 512KB 。
然后发现一个漏网场景:1GB 文件 16 段,最后一段剩 1.5MB ,门槛 2MB 切不动,15 个 worker 干等 1 个。于是加了个「尾部微拆分」——常规切不动时降到 64KB 再试一次。
上线之后 99% 那里掉得更狠了。
排查出来是这样:50MB 的下载收尾时 48 个 worker ,每段都只剩约 66KB 。66KB > 64KB ,所以每段都「可以」被微拆。于是 worker 们互相把对方手里的段切成 33KB 碎片,切完发现再也找不到 ≥64KB 的段可切,集体退休,活跃 worker 从 48 直接掉到 16 。我为了修「最后 1% 慢」,造出了一个更严重的「最后 1% 慢」。
修法是加一道落后者判据:只有当最大剩余的活跃段 ≥ 128KB ( 2 倍微拆粒度)时才允许微拆。语义上就是「只有存在真实不均衡时才抢救」——所有段都一样小不叫落后者,那是带宽问题,切碎解决不了。加上之后级联自然停止:下完 33KB 的 worker 发现最大剩余才 66KB ,优雅退休而不是继续切碎同伴。
事后想想这个坑挺普适的。Spark 的推测执行判据也必须是「显著慢于其他 task 」而不是「没跑完」; CI 测试分片再切一刀的收益也要减去容器启动成本。重新切分的收益必须大于切分本身的固定成本,而且只对显著不均衡动手。
顺带说个相关的:最大连接数不是越大越快。有些服务器对多连接做惩罚性限速,我实测见过 59MB/s 掉到 2MB/s 。所以引擎是从 2 条连接起步,每 2 秒用真实吞吐投票——涨超 5% 继续翻倍,跌破一半判定崩塌立刻回滚,并且把这个域名学到的连接上限缓存 24 小时。基本是把 AIMD 搬到应用层。
项目情况:AGPL-3.0 ,Windows / macOS / Linux / Android 都有包,NAS 有 Docker 和群晖 QNAP 的原生包,headless 那份带 aria2 兼容 RPC ( AriaNg 可以直连)。无广告无追踪不要账号。
代码在 native/engine/src/segment_coordinator.rs,上面这段护栏的注释比代码长,因为记录的是一次真实事故。
有做过并行分片调度的老哥,你们是怎么处理尾部落后者的?想听听别的思路。