• 多线程分段下载文件时为什么不下载到同一个大文件中?
  • 发布于 2个月前
  • 126 热度
    6 评论

如题,多线程分段下载文件时,为什么不下载到同一个大文件中?而是要分别下载到单独的文件然后再合并。我自己写了个 demo ,假设开 50 个线程下载,每个线程下载时间为 10s 钟,可以看到最后写入文件的时间才 1s 不到。所以下载文件时,瓶颈是网络 IO 吧,操作文件 IO 的时间可以忽略不计了。所以是不是下载到一个文件中更优?

我的Demo代码如下:

Cargo.toml

[package]
name = "reqwest_download"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "~1.34.0", features = ["full"] } # Event-driven, non-blocking I/O platform.
anyhow="1.0"
reqwest = { version = "0.12", features = ["stream"] }
futures = "0.3"
openssl = { version = "0.10", features = ["vendored"] }
futures-util = "0.3.30"
main.rs
use std::fmt::format;
use std::time::Duration;

use anyhow::ensure;
use futures_util::StreamExt;
use reqwest::Client;
use tokio::fs::File;
use tokio::io::SeekFrom;
use tokio::io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::task::JoinSet;
use tokio::time::{sleep, Instant};
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let now = Instant::now();
    test1().await;
    println!("{}ms", now.elapsed().as_millis());
    Ok(())
}
async fn test1() -> Result<(), anyhow::Error> {
    let mut set = JoinSet::new();
    let byte_array: Vec<u8> = (0..=1024 * 1024).map(|x| x as u8).collect();

    for start in 0..50 {
        let cloned_bytes = byte_array.clone();
        set.spawn(async move {
            download_file1(start, cloned_bytes).await;
        });
    }
    while let Some(res) = set.join_next().await {
        let out = res?;
    }
    Ok(())
}
async fn test2() -> Result<(), anyhow::Error> {
    let mut set = JoinSet::new();
    let byte_array: Vec<u8> = (0..=1024 * 1024).map(|x| x as u8).collect();

    for start in 0..50 {
        let cloned_bytes = byte_array.clone();
        set.spawn(async move {
            download_file2(start, cloned_bytes).await;
        });
    }
    while let Some(res) = set.join_next().await {
        let out = res?;
    }
    Ok(())
}
async fn download_file1(start: i32, byte_array: Vec<u8>) -> Result<(), anyhow::Error> {
    let mut file = tokio::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .open("test.bin")
        .await?;
    let current = start * byte_array.len() as i32;
    sleep(Duration::from_secs(10)).await;
    file.seek(SeekFrom::Start(current as u64)).await;
    file.write_all(&byte_array).await;
    Ok(())
}
async fn download_file2(start: i32, byte_array: Vec<u8>) -> Result<(), anyhow::Error> {
    let file_name = format!("{}.bin", start);
    let mut file = tokio::fs::OpenOptions::new()
        .create(true)
        .write(true)
        .open(file_name)
        .await?;
    let current = start * byte_array.len() as i32;
    sleep(Duration::from_secs(10)).await;
    file.write_all(&byte_array).await;
    Ok(())
}

用户评论
  • 一杯忘情
  • 我觉得应该是数据一致性的问题:如果在下载过程中出现错误(例如网络中断),那么已经下载的部分可能会被破坏。如果所有数据都写入同一个文件,那么就需要重新下载整个文件。而如果数据被写入不同的文件,那么就只需要重新下载出现错误的那部分。
    简要描述:单文件下载,如果出错,那么出错的代价太大了;而分文件下载,可以减少这个代价。
  • 2024/5/12 12:20:00 [ 0 ] [ 0 ] 回复
  • 共老河山
  • 大部分程序员,可能对网络和文件系统接口都不熟。现代操作系统提供的网络基本上都不会出现传输错误,现代操作系统也都会提供 seek+随机写的功能。对于 preallocate 会有各种实现方式,不过提供的结果基本一致,也就是实现了随机写。另外,即使不依赖 seek ,也可以有 mmap 这样的方式实现随机写。


    再说从网络下载到磁盘,大部分操作系统也会提供 zero-copy 或者类似 pipe 的功能,即使没有,IO 操作时基本上也是使用一个固定 buffer ,比如 64k ,不会把全部内容放到内存再操作。再说一下状态恢复,类似数据库一样,分片的下载状态一般是单独存放,比如 aria 、flashget 或者迅雷,只有分片下载完成后再去更新状态,这样即使程序崩溃,也可以下次启动从恢复点续传。


    最后补充一个极端的例子,BT 下载每次传输单元是 16KB ,难不成要创建一堆 16KB 的文件,然后完成之后再合并?从文件系统的效率角度来说,从 socket 读并且单独写入一个大文件,这里可以只需要频繁调用 read(socket) + write(fd)。 但是如果是一堆小文件,你需要频繁创建和关闭文件,这两个操作在大部分操作系统,尤其是 HDD 上的文件系统,开销会非常大,你的顺序写操作会变成随机写,同时如果你的操作系统安装了杀毒软件,杀软也会在文件关闭时进行扫描,结果就是更慢了。

    一般程序员在软件设计和实现的时候,都会倾向于使用资源需求最小并且吞吐量高的方案。所以当小文件没有明显优势,而且特殊场景下资源开销和性能都有明显损耗的情况下,选择大文件是一个比较自然的方案。

    类似的需求还有比如数据库,就像如果网络客户端发送了一些 INSERT ,你是把他们写入小文件然后再合并呢,还是直接放到大文件中。这里的就会有和网络下载类似的权衡,当然也有不一样的地方。
  • 2024/5/12 12:12:00 [ 0 ] [ 0 ] 回复
  • 遥忘而立
  • 一般都是写到一个大文件吧,下载到单独文件,相当于 IO 两次,除了某些场景下,现在的 OS 对于这个没有特殊差别。但是对于真的大文件,比如 10GB 以上的,你这种写两遍的操作会占用两倍的磁盘空间,而且对于 HDD ,一边读一边写会巨慢。所以,这么做,单纯的是菜(不会 seek )。
  • 2024/5/12 12:04:00 [ 0 ] [ 0 ] 回复
  • 亦東風
  • 文件分开可以解决更多普遍的问题,而且文件名是天生的索引信息,支持的功能更广泛。单一文件在在特定情况下确实很好,比如知道文件总大小 等等。个人感觉好像单一文件的优势不是特别多。结合一下也是一种方案, 用较小的缓冲区不断下载, 同时完成的数据合并入大文件中。实现方案各有优缺点。
  • 2024/5/12 11:53:00 [ 0 ] [ 0 ] 回复
  • 深山夕照
  • 首先,迅雷、FDM ,都是下载到同一个大文件里的。这种方式,首先需要申请这个文件的空间,也就是你刚开始下载 10g 的文件,立刻就要在硬盘里创建 10g 的文件。然后,写数据是一个持续的过程,多线程需要自己调度好文件占用的问题。你 demo 是等 10 秒后一次性写入。分段需要处理临时文件,各有优劣,看自己更熟悉哪种方式了。
  • 2024/5/12 11:48:00 [ 0 ] [ 0 ] 回复
  • 旧街浪人
  • 感觉是利用网速吧。很多时候 1 个文件下载就 100KB/s 那么要下载好久。分多个文件,每个文件 100KB/s 不是更快了?为什么 1 个文件下载就 100KB/s 不清楚呀。带宽几百 M 下载一个文件我也没有跑满。
  • 2024/5/12 11:11:00 [ 0 ] [ 0 ] 回复