如题,多线程分段下载文件时,为什么不下载到同一个大文件中?而是要分别下载到单独的文件然后再合并。我自己写了个 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(()) }
简要描述:单文件下载,如果出错,那么出错的代价太大了;而分文件下载,可以减少这个代价。
大部分程序员,可能对网络和文件系统接口都不熟。现代操作系统提供的网络基本上都不会出现传输错误,现代操作系统也都会提供 seek+随机写的功能。对于 preallocate 会有各种实现方式,不过提供的结果基本一致,也就是实现了随机写。另外,即使不依赖 seek ,也可以有 mmap 这样的方式实现随机写。
再说从网络下载到磁盘,大部分操作系统也会提供 zero-copy 或者类似 pipe 的功能,即使没有,IO 操作时基本上也是使用一个固定 buffer ,比如 64k ,不会把全部内容放到内存再操作。再说一下状态恢复,类似数据库一样,分片的下载状态一般是单独存放,比如 aria 、flashget 或者迅雷,只有分片下载完成后再去更新状态,这样即使程序崩溃,也可以下次启动从恢复点续传。
一般程序员在软件设计和实现的时候,都会倾向于使用资源需求最小并且吞吐量高的方案。所以当小文件没有明显优势,而且特殊场景下资源开销和性能都有明显损耗的情况下,选择大文件是一个比较自然的方案。
类似的需求还有比如数据库,就像如果网络客户端发送了一些 INSERT ,你是把他们写入小文件然后再合并呢,还是直接放到大文件中。这里的就会有和网络下载类似的权衡,当然也有不一样的地方。