[开源] 我在自己写的下载器里造了个 bug:为了修「最后 1% 变慢」,活跃连接反而从 48 掉到了 16

clow 2026-07-27 09:19 1

做了个下载器 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,上面这段护栏的注释比代码长,因为记录的是一次真实事故。



  • https://github.com/zerx-lab/FluxDown

  • https://fluxdown.zerx.dev


有做过并行分片调度的老哥,你们是怎么处理尾部落后者的?想听听别的思路。

最新回复 (2)
  • clow 楼主 07-27 09:32
    1
    补充一个同源的坑,也是这套并发模型里最反直觉的一处:64 个 worker 各持一个指向同一文件的 fd ,如果各自周期性 fdatasync ,等于每个周期把整个文件的脏页重复刷 N 遍,磁盘直接被打满。

    后来做了个全局合并闸:整文件级别每 2 秒至多一次 fdatasync ,并且只有当这次 fsync 的 [起始时刻] 晚于某个段自己 flush 的时刻,才允许把该段的进度偏移写进 DB 。维持「 DB 里记录的偏移 <= 已经被 fsync 覆盖的字节」这个不变式,崩溃恢复才不会在文件里拼出一个洞。

    所以内存里明明下到 1.2G ,DB 里只敢写 1.19G ,差的那点是还没落盘的。这条不变式一开始没有,表现是断电重启后续传的文件 md5 偶尔对不上,非常难查。
  • studyingss 07-27 10:17
    2
    唉,ai 犯的错,用 ai 排查,再用 ai 写总结,复制来发帖……
* 帖子来源V2EX
返回