[开源] 我在自己写的下载器里造了个 bug:为了修「最后 1% 变慢」,活跃连接反而从 48 掉到了 16
clow:
我做了个下载器 FluxDown,Rust 写的引擎,Flutter 做的界面。不打算在这儿详细列功能,想讲一个我自己制造又自己修掉的 bug,觉得挺有意思。
多线程下载尾部时会出现落后者的问题:快速的连接下载完成后会闲置,整体时间取决于最慢的那条。常规解决办法是从中点将剩余最多的一段再切一下给闲置的连接。但切分是有成本的(需要一次 HTTP 往返),所以我设了一个下限,定为 2MB,并根据实时吞吐量调整到最低 512KB。
后来发现有一个场景没考虑到:当下载一个 1GB 文件分成 16 段时,最后一段剩下 1.5MB,门槛 2MB 切不动,15 个 worker 只能干等一个。因此我加入了一个「尾部微拆分」的措施——在常规切不动的情况下降到 64KB 再试一次。
上线后发现 99% 的下载速度变得更慢了。
经过排查发现是这样:在下载一个 50MB 的文件尾部时,48 个 worker 的每段都只剩约 66KB。66KB > 64KB,因此每段都「可以」被微拆分。结果是 worker 们互相把对方的段切成 33KB 的碎片,切完后发现再也找不到 ≥64KB 的段可切,最后活跃的 worker 从 48 直接掉到了 16。为了修「最后 1% 变慢」的情况,我却产生了一个更严重的「最后 1% 变慢」。
修法是增加了一道落后者判据:只有当 **最大剩余的活跃段 ≥ 128KB(即微拆粒度的两倍)**时才允许进行微拆分。意思是「只有在存在真实不均衡时才采取抢救」,如果所有段都一样小,那都是带宽的问题,切碎问题并不能解决。加这个判据后,整个过程自然停止:下完 33KB 的 worker 发现最大剩余只有 66KB,优雅退休而不是继续切割同伴。
事后想想这个坑其实相当普适。比如 Spark 的推测执行判据也必须是「显著慢于其他任务」而不是「没有完成」;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](http://segment_coordinator.rs),上面这段护栏的注释比代码长,因为记录的是一次真实事故。
- [FluxDown GitHub](https://github.com/zerx-lab/FluxDown)
- [FluxDown 官网](https://fluxdown.zerx.dev/)
有做过并行分片调度的朋友,你们是怎么处理尾部落后者的?想听听其他的思路。
---
原文链接:[点击查看](https://www.v2ex.com/t/1230009)
我做了个下载器 FluxDown,Rust 写的引擎,Flutter 做的界面。不打算在这儿详细列功能,想讲一个我自己制造又自己修掉的 bug,觉得挺有意思。
多线程下载尾部时会出现落后者的问题:快速的连接下载完成后会闲置,整体时间取决于最慢的那条。常规解决办法是从中点将剩余最多的一段再切一下给闲置的连接。但切分是有成本的(需要一次 HTTP 往返),所以我设了一个下限,定为 2MB,并根据实时吞吐量调整到最低 512KB。
后来发现有一个场景没考虑到:当下载一个 1GB 文件分成 16 段时,最后一段剩下 1.5MB,门槛 2MB 切不动,15 个 worker 只能干等一个。因此我加入了一个「尾部微拆分」的措施——在常规切不动的情况下降到 64KB 再试一次。
上线后发现 99% 的下载速度变得更慢了。
经过排查发现是这样:在下载一个 50MB 的文件尾部时,48 个 worker 的每段都只剩约 66KB。66KB > 64KB,因此每段都「可以」被微拆分。结果是 worker 们互相把对方的段切成 33KB 的碎片,切完后发现再也找不到 ≥64KB 的段可切,最后活跃的 worker 从 48 直接掉到了 16。为了修「最后 1% 变慢」的情况,我却产生了一个更严重的「最后 1% 变慢」。
修法是增加了一道落后者判据:只有当 **最大剩余的活跃段 ≥ 128KB(即微拆粒度的两倍)**时才允许进行微拆分。意思是「只有在存在真实不均衡时才采取抢救」,如果所有段都一样小,那都是带宽的问题,切碎问题并不能解决。加这个判据后,整个过程自然停止:下完 33KB 的 worker 发现最大剩余只有 66KB,优雅退休而不是继续切割同伴。
事后想想这个坑其实相当普适。比如 Spark 的推测执行判据也必须是「显著慢于其他任务」而不是「没有完成」;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](http://segment_coordinator.rs),上面这段护栏的注释比代码长,因为记录的是一次真实事故。
- [FluxDown GitHub](https://github.com/zerx-lab/FluxDown)
- [FluxDown 官网](https://fluxdown.zerx.dev/)
有做过并行分片调度的朋友,你们是怎么处理尾部落后者的?想听听其他的思路。
---
原文链接:[点击查看](https://www.v2ex.com/t/1230009)
后来我加了个全局合并闸,整个文件级别每 2 秒最多刷一次 fdatasync。并且只有当这次 fsync 的起始时刻晚于某个段自己 flush 的时刻,才允许把进度偏移写进 DB,维持“DB里记录的偏移 <= 已经被 fsync 覆盖的字节”这个不变式。不然崩溃恢复的时候文件里就会拼出一个洞,断电重启后续传的文件 md5 经常对不上,巨难查。
· 0 个赞
· 0 个赞