rusty-cat 分片并发上传完全指南

rusty-cat 分片并发上传完全指南(Rust 小白版)

本文面向 刚接触 Rust、想快速把「大文件分片并发上传」跑起来 的开发者。 每段代码都可以直接复制到你的项目里编译运行,遇到需要替换的地方都写了中文注释。 配套阅读:《rusty-cat 分片并发下载完全指南》


目录

  1. [rusty-cat 是什么,能帮你解决什么问题](#rusty-cat 是什么,能帮你解决什么问题 "#1-rusty-cat-%E6%98%AF%E4%BB%80%E4%B9%88%E8%83%BD%E5%B8%AE%E4%BD%A0%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98")
  2. [第一步:在 Cargo.toml 里加依赖](#第一步:在 Cargo.toml 里加依赖 "#2-%E7%AC%AC%E4%B8%80%E6%AD%A5%E5%9C%A8-cargotoml-%E9%87%8C%E5%8A%A0%E4%BE%9D%E8%B5%96")
  3. [10 分钟跑通第一个上传(完整可运行程序)](#10 分钟跑通第一个上传(完整可运行程序) "#3-10-%E5%88%86%E9%92%9F%E8%B7%91%E9%80%9A%E7%AC%AC%E4%B8%80%E4%B8%AA%E4%B8%8A%E4%BC%A0%E5%AE%8C%E6%95%B4%E5%8F%AF%E8%BF%90%E8%A1%8C%E7%A8%8B%E5%BA%8F")
  4. 核心概念:两个「并发」参数千万别搞混
  5. 开启「单文件分片并发」------with_max_parts_in_flight
  6. 四种云存储上传配方(各有独立实战文章)
  7. [从内存字节上传 / 自建服务器上传](#从内存字节上传 / 自建服务器上传 "#7-%E4%BB%8E%E5%86%85%E5%AD%98%E5%AD%97%E8%8A%82%E4%B8%8A%E4%BC%A0--%E8%87%AA%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E4%BC%A0")
  8. 进度、暂停、恢复、取消
  9. 断点续传:进程重启后接着传
  10. 内存占用与参数怎么选
  11. 错误处理与自动重试
  12. 常见问题排查表(FAQ)
  13. 附录:配置项速查表

1. rusty-cat 是什么,能帮你解决什么问题

rusty-cat 是一个 异步(async)的 Rust 文件传输 SDK,专门解决「大文件的可断点续传上传 / 下载」。它帮你处理这些又琐碎又容易出 bug 的事:

  • 分片(chunk):把一个大文件切成很多小块分别上传;
  • 并发(concurrent) :同一个文件的多个分片可以同时上传,把带宽跑满;
  • 重试 / 退避:网络抖动时自动重试,且用指数退避 + 抖动,不会「重试风暴」;
  • 暂停 / 恢复 / 取消:随时控制任务;
  • 进度回调:实时把进度百分比推给你的 UI;
  • 断点续传:进程被杀掉、崩溃、重启后能接着传,而不是从头再来。

不帮你做 的事(这是刻意的设计,让 SDK 保持小巧、适配任意后端):

  • 不内置数据库(要不要持久化任务记录、用什么库,你自己决定);
  • 不管理你的云密钥 / 权限(你从自己的后端提供)。

支持的上传后端:普通 HTTP、阿里云 OSS(直传 / 预签名)、Azure Blob(直传 / SAS)。

一句话记住它的用法 :创建 MeowClient → 用 Builder 构建一个「上传任务」→ 提交给客户端 → 收进度回调 → 用完 close()


2. 第一步:在 Cargo.toml 里加依赖

rusty-cat 用「feature(特性开关)」来控制引入哪些云厂商的代码,你只需要开启你用到的那一个,包体积和依赖都最小。

toml 复制代码
[dependencies]
# 按需选一个(或多个)云厂商特性:
rusty-cat = { version = "0.2.4", features = ["aliyun-oss-direct"] }
# rusty-cat = { version = "0.2.4", features = ["aliyun-oss-presigned"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-direct"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-sas"] }

# rusty-cat 是 async 库,需要一个异步运行时,这里用 tokio:
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

各个 feature 的含义:

Feature 用途 谁持有密钥
aliyun-oss-direct 阿里云 OSS 直传,客户端直接用 AccessKey 签名 客户端(受信任的环境,如你自己的服务器)
aliyun-oss-presigned 阿里云 OSS 预签名,后端签好短期 URL,客户端只负责传 后端
azure-blob-direct Azure Blob 直传,用账号 Shared Key 签名 客户端(受信任环境)
azure-blob-sas Azure Blob SAS,后端签好短期 SAS URL 后端

怎么选? 如果你的程序跑在不完全受信任 的环境(比如用户的手机、桌面 App、浏览器端 WASM),优先用预签名 / SAS :密钥永远留在后端,客户端只拿到几分钟有效的临时 URL。只有当程序跑在你完全掌控的服务器上时,才考虑直传(direct)。


3. 10 分钟跑通第一个上传(完整可运行程序)

下面是一个完整main.rs,演示把本地文件分片并发上传到阿里云 OSS。把顶部四个常量换成你自己的,cargo run 即可。

需要在 Cargo.toml 里开启 features = ["aliyun-oss-direct"]

rust 复制代码
use std::sync::Arc;
use std::time::Duration;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{FileTransferRecord, MeowClient, MeowConfig, UploadPounceBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // ===== 改成你自己的信息 =====
    let bucket = "my-bucket";                 // OSS Bucket 名
    let access_key_id = "AK_ID";              // AccessKeyId
    let access_key_secret = "AK_SECRET";      // AccessKeySecret
    let region = "cn-beijing";                // 地域,例如 cn-beijing / cn-hangzhou
    let local_file = "./big-object.bin";      // 要上传的本地文件
    let object_url =                          // 上传到 OSS 后的对象完整 URL
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin";
    // ============================

    // 1) 创建配置和客户端
    let config = MeowConfig::builder()
        .max_upload_concurrency(3)            // 同时最多传 3 个「文件」
        .http_timeout(Duration::from_secs(30))
        .build()?;
    let client = MeowClient::new(config);

    // 2) 准备上传协议(阿里云 OSS 直传)
    let protocol =
        AliOssDirectUpload::new(bucket, access_key_id, access_key_secret, region);

    // 3) 构建上传任务
    let task = UploadPounceBuilder::new(
        "big-object.bin",   // 展示用的文件名(回调 / 日志里能看到)
        local_file,         // 本地文件路径
        1024 * 1024,        // 分片大小 = 1 MiB
    )
    .with_url(object_url)
    .with_breakpoint_upload(Arc::new(protocol)) // 协议对象必须放进 Arc
    .with_max_parts_in_flight(4)                // 同一个文件最多 4 片并发上传
    .with_max_chunk_retries(3)                  // 每片失败最多再重试 3 次
    .build()?;

    // 4) 提交任务并等待它跑完(enqueue_and_wait 是最省事的方式)
    let outcome = client
        .enqueue_and_wait(task, |record: FileTransferRecord| {
            println!(
                "上传进度 {:.1}%  状态 {:?}",
                record.progress() * 100.0,
                record.status()
            );
        })
        .await?;

    println!(
        "上传完成!task_id={} 服务端返回={:?}",
        outcome.task_id, outcome.payload
    );

    // 5) 关闭客户端(这一步是必须的!见下方说明)
    client.close().await?;
    Ok(())
}

几个新手最容易忽略的点:

  • enqueue_and_wait(task, progress_cb) 是最简单的提交方式:它提交任务、在每次进度更新时调用你的回调、并在任务到达「终态」时返回。成功返回 TaskOutcome,失败 / 被取消会返回 Err(MeowError),所以你直接用 ? 就能处理错误。
  • 协议对象要放进 ArcArc::new(protocol)):因为并发上传时,同一个协议对象 &self 会被多个异步任务共享。
  • with_max_parts_in_flight(4) 就是「单文件分片并发」开关,第 5 节详细讲。
  • 一定要调用 client.close().await :它会取消在途任务、把缓冲的回调发完、并优雅地关闭后台调度线程。不要依赖 Drop 自动清理------那只是兜底,会丢事件。

进度是个 0.0~1.0 的比例record.progress() 返回 f32,乘 100 就是百分比。没有「已传字节数」的直接读取方法,如需字节数请自己算:(f64::from(record.progress()) * record.total_size() as f64) as u64


4. 核心概念:两个「并发」参数千万别搞混

这是新手 第一大坑 。rusty-cat 有 两个完全独立 的并发旋钮:

旋钮 作用范围 默认值 控制什么
MeowConfig::builder().max_upload_concurrency(k) 整个客户端 2 同时上传 几个文件
UploadPounceBuilder::with_max_parts_in_flight(n) 单个文件内部 1 一个文件的 几个分片 同时传
  • max_upload_concurrency(k):客户端级别,控制多少个文件同时在传。上传和下载各自独立计数、互不占用。
  • with_max_parts_in_flight(n):任务级别,控制同一个文件的多少个分片同时在传(这才是本文的主角)。

两者会相乘 。如果 max_upload_concurrency(3),且每个文件都 with_max_parts_in_flight(4),那么客户端最多会有 3 × 4 = 12 个上传请求同时在飞。没有全局上限帮你兜底,请自己把乘积控制在合理范围。

🖼️ 【配图占位 U2|两个并发旋钮的层级关系】 建议配图:外层一个大框「客户端」,标 max_upload_concurrency(3),里面并排 3 个「文件」框;每个文件框内再画 4 个并行的「分片」块,标 with_max_parts_in_flight(4)。图注:外层控制「几个文件」、内层控制「一个文件几个分片」,两者相乘 = 最多 12 个请求在飞。 临时 ASCII 草图:

scss 复制代码
             ┌─ 文件A ─ 分片1 分片2 分片3 分片4   ← with_max_parts_in_flight(4)
客户端 ──────┼─ 文件B ─ 分片1 分片2 分片3 分片4
             └─ 文件C ─ 分片1 分片2 分片3 分片4
             ↑
   max_upload_concurrency(3):最多 3 个文件同时传

5. 开启「单文件分片并发」------with_max_parts_in_flight

默认值是 1,也就是严格串行 :一片传完再传下一片,和最经典的单流上传逐字节一致。传入大于 1 的值即开启并发。

什么时候该用?

  • 适合 :单个大文件 (几十 MiB 以上)、走一条快网(单流跑不满带宽,比如服务器往对象存储推大对象)。
  • 没必要 :小文件、单流已经跑满带宽、或者你想省内存的场景,保持默认 1 即可。

加速效果和你的文件大小、网络状况强相关,SDK 没有承诺任何固定的倍数。把它当作一个「可以试、要实测」的工具。

三个前置条件(并发闸门)

设了 with_max_parts_in_flight(8) 并不保证 真的并发。执行器只有在 同时满足以下三条 时才走并发路径:

  1. n > 10 会被归一化成 1);
  2. 文件总大小已知 ------ 上传时总大小由本地文件 / 内存字节自动得到,所以上传天然满足这条;
  3. 上传协议是「并发安全」的(下表)。

如果条件 1 或 3 不满足,任务会静默回退到串行:不报错、不告警,照样成功,只是一片一片传。

🖼️ 【配图占位 U3|并发闸门决策流程图】 建议画一张流程图(判断框依次串联,任一为「否」就走串行):n > 1? →否→ 串行;→是→ 总大小已知? →否→ 串行(上传天然满足);→是→ 协议并发安全? →否→ 串行;→是→ 并发。 临时 ASCII 草图:

shell 复制代码
n>1? ──否──▶ 串行
 │是
总大小已知? ──否──▶ 串行
 │是
协议并发安全? ──否──▶ 串行
 │是
 ▼
并发 ✅

哪些上传协议支持并发?

并发要求协议是「乱序安全」的:分片 N 不依赖分片 N-1 完成,且最后的「合并」步骤能按正确顺序拼装。

上传协议 Feature 并发安全? 说明
AliOssDirectUpload aliyun-oss-direct ✅ 是 OSS Multipart,分片号由偏移量推导,重传幂等,Complete 按序合并
PresignedMultipartUpload(阿里云预签名) aliyun-oss-presigned ✅ 是 预签名分片,按偏移记账,完成清单按分片号重排
AzureBlobDirectUpload azure-blob-direct ✅ 是 Block Blob,block id 由分片下标决定,Put Block List 固定提交顺序
PresignedMultipartUpload(Azure SAS) azure-blob-sas ✅ 是 同样的 Block Blob 模型,走 SAS URL
内置默认 HTTP 上传(DefaultStyleUpload ❌ 否 依赖服务端单一 nextByte 游标,必须严格顺序
你自己实现的 BreakpointUpload 自定义 ❌ 否(默认) 除非你显式声明支持,否则默认串行

划重点 :想要并发上传,就 给任务挂上上面四种 provider 上传协议之一 ,并设 n > 1。内置的默认 HTTP 上传永远串行。


6. 四种云存储上传配方(各有独立实战文章)

每种云存储的上传配方(含完整可运行程序、内部数据流讲解、断点续传要点)都拆成了独立的实战文章,方便你按需只看用到的那一个:

实战文章 Feature 谁持密钥 一句话
阿里云 OSS 直传直下 aliyun-oss-direct 客户端 可信服务器用 AccessKey 直连 OSS
阿里云 OSS 预签名 aliyun-oss-presigned 后端 后端签短期 URL,客户端只传字节
Azure Blob 直传直下 azure-blob-direct 客户端 可信服务器用 Shared Key 直连 Blob
Azure Blob SAS azure-blob-sas 后端 后端签短期 SAS URL

四种配方共用同一套「构建协议 → with_breakpoint_upload(Arc::new(...)) → 设 with_max_parts_in_flight(4)enqueue_and_wait」骨架,只有「怎么构建协议」不同。最小上手片段(以 OSS 直传为例):

rust,ignore 复制代码
let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
let task = UploadPounceBuilder::new("big-object.bin", "./big-object.bin", 1024 * 1024)
    .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin")
    .with_breakpoint_upload(Arc::new(protocol))
    .with_max_parts_in_flight(4) // ← 单文件并发开关
    .build()?;
client.enqueue_and_wait(task, |r| println!("{:.1}%", r.progress() * 100.0)).await?;

🖼️ 【配图占位 U1|四种上传配方的密钥模型对比】 建议配图:左右两栏对比。左「direct(直连)」:客户端图标里有钥匙,直接连云;标注「仅限可信服务器」。右「presigned/SAS」:钥匙在后端图标里,客户端只从后端拿短期 URL、再连云;标注「适合手机/浏览器等不可信端」。 临时 ASCII 草图:

css 复制代码
direct          [可信服务器 🔑] ─────────▶ [云存储]
presigned/SAS   [客户端] ─要URL→ [后端 🔑] ─签URL→ [客户端] ─传字节→ [云存储]

并发上传时 SDK 的保证

在并发路径上,没有任何单个分片会去「结束」整个上传 。每个分片只管把自己的字节传上去;Complete / Put Block List 这一步被提升到调度器层,只执行一次,且只在所有分片都落地、拼成连续前缀之后才执行。这就是乱序分片安全的原因------先传完的分片不会触发提前完成。


7. 从内存字节上传 / 自建服务器上传

7.1 上传内存里的字节(而不是本地文件)

如果你的数据在内存里(比如刚生成的一段 buffer),用 from_bytes 代替 new

rust 复制代码
use std::sync::Arc;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{MeowClient, MeowConfig, UploadPounceBuilder};

async fn upload_from_memory(
    data: Vec<u8>, // 你内存里的数据
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());
    let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");

    // from_bytes(展示文件名, 字节, 分片大小)
    let task = UploadPounceBuilder::from_bytes("in-memory.bin", data, 1024 * 1024)
        .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/in-memory.bin")
        .with_breakpoint_upload(Arc::new(protocol))
        .with_max_parts_in_flight(4)
        .build()?;

    client.enqueue_and_wait(task, |_r| {}).await?;
    client.close().await?;
    Ok(())
}

Vec<u8> 会被零拷贝地转成 bytes::Bytes,后续切片和克隆都只是引用计数 +1,不会复制大块内存。

7.2 上传到你自己的 HTTP 服务器

如果不用云厂商、而是传到你自己的服务器,有两种方式:

  1. 用内置默认协议 :不挂任何 provider 协议,直接 build()。它走一套约定好的 multipart/form-data 分片请求格式(默认协议串行,不支持并发)。
  2. 自己实现 BreakpointUpload :完全控制每片的请求方法、URL、Header、Body。下面是一个最小示例,把每片用 PUT 发到某个 URL,并带上 x-offset 头告诉服务器写入位置:
rust 复制代码
use async_trait::async_trait;
use reqwest::header::{HeaderMap, HeaderValue};
use rusty_cat::api::{
    BreakpointUpload, InnerErrorCode, MeowError, UploadChunkCtx, UploadPrepareCtx,
    UploadResumeInfo,
};

#[derive(Clone)]
struct MyBinaryUpload {
    url: String,
}

#[async_trait]
impl BreakpointUpload for MyBinaryUpload {
    // prepare 在上传开始前调用一次,返回「从第几个字节续传」
    async fn prepare(&self, _ctx: UploadPrepareCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
        Ok(UploadResumeInfo {
            completed_file_id: None,
            next_byte: Some(0), // 0 表示从头开始
            provider_upload_id: None,
        })
    }

    // 每个分片调用一次
    async fn upload_chunk(&self, ctx: UploadChunkCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            "x-offset",
            HeaderValue::from_str(&ctx.offset.to_string()).unwrap(),
        );
        let resp = ctx
            .client // SDK 提供的、已按配置构建好的 reqwest::Client
            .put(&self.url)
            .headers(headers)
            .body(reqwest::Body::from(ctx.chunk.clone()))
            .send()
            .await
            .map_err(|e| {
                MeowError::from_source(InnerErrorCode::HttpError, e.to_string(), e)
            })?;
        if !resp.status().is_success() {
            return Err(MeowError::from_code(
                InnerErrorCode::ResponseStatusError,
                format!("上传返回状态 {}", resp.status()),
            ));
        }
        Ok(UploadResumeInfo {
            completed_file_id: None,
            next_byte: Some(ctx.offset + ctx.chunk.len() as u64),
            provider_upload_id: None,
        })
    }
}

然后像 provider 协议一样挂上去(注意自定义协议默认串行):

rust,ignore 复制代码
let task = UploadPounceBuilder::new("http-upload.bin", "./http-upload.bin", 1024 * 1024)
    .with_url("http://127.0.0.1:8080/upload")
    .with_method(reqwest::Method::PUT)
    .with_breakpoint_upload(Arc::new(MyBinaryUpload { url: "http://127.0.0.1:8080/upload".into() }))
    .build()?;

8. 进度、暂停、恢复、取消

8.1 用 try_enqueue 拿到更细的回调(进度 + 完成)

enqueue_and_wait 适合「提交后一直等到结束」。如果你要构建 UI、需要在后台异步跑,用底层的 try_enqueue,它返回 TaskId,并让你分别处理进度回调和完成回调:

rust 复制代码
use std::sync::Arc;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{
    FileTransferRecord, MeowClient, MeowConfig, TaskId, TransferStatus, UploadPounceBuilder,
};

async fn upload_with_callbacks() -> Result<TaskId, Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());
    let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
    let task = UploadPounceBuilder::new("f.bin", "./f.bin", 1024 * 1024)
        .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/f.bin")
        .with_breakpoint_upload(Arc::new(protocol))
        .with_max_parts_in_flight(4)
        .build()?;

    let task_id = client
        .try_enqueue(
            task,
            // 进度回调:每次进度更新都会调用
            move |record: FileTransferRecord| match record.status() {
                TransferStatus::Failed(err) => eprintln!("上传失败:{err}"),
                TransferStatus::Canceled => println!("已取消"),
                _ => println!("进度 {:.1}%", record.progress() * 100.0),
            },
            // 完成回调:任务成功到达 Complete 时调用一次
            move |task_id, payload| {
                println!("任务 {task_id} 完成,服务端返回 {payload:?}");
            },
        )
        .await?;

    // 保存好 task_id,后面用它来 暂停/恢复/取消
    Ok(task_id)
}

record.status() 返回的是 &TransferStatus,可以直接 matchTransferStatus 的取值有:Pending(排队)、Transmission(传输中)、Paused(暂停)、Complete(完成)、Failed(err)(失败)、Canceled(取消)。

8.2 暂停 / 恢复 / 取消

拿到 TaskId 后就能控制任务:

rust,ignore 复制代码
client.pause(task_id).await?;   // 暂停
client.resume(task_id).await?;  // 从断点继续(TaskId 不变)
client.cancel(task_id).await?;  // 取消(会跑 provider 的清理,比如 OSS 的 AbortMultipartUpload)
  • 取消上传 会触发协议的 abort_upload(例如 OSS 的 AbortMultipartUpload),让未提交的分片停止占用存储、不再计费。
  • 取消是尽力而为的、且不可逆------取消后想再传,就重新建一个任务。

8.3 并发下的进度行为(重要且反直觉)

即使分片是乱序完成的,SDK 只上报「最长连续前缀」的进度(业界叫 watermark,水位线):

  • 进度是单调递增的,绝不回退;
  • 如果靠后的分片先传完、但前面还有「空洞」,进度会原地不动 ,直到空洞被填上------这是正常现象,不是卡死
  • 每次水位线推进,你只会收到一次合并后的进度事件(不是每片一个),终态只有一次。

🖼️ 【配图占位 U4|水位线(watermark)进度模型】 建议配图:一条按分片切格的进度条。分片 1、2、4 已完成(涂实),分片 3 还是空洞。用一条竖线标出「水位线」停在分片 2 结尾处------尽管分片 4 已完成,进度仍只算到连续前缀的末尾。图注:进度 = 连续前缀 / 总大小,单调递增;空洞填上后水位线才前跳。 临时 ASCII 草图:

ini 复制代码
分片:  [#1✅][#2✅][#3 ⬜空洞][#4✅]
                  ▲
                水位线(进度只算到这里,即使 #4 已完成)

9. 断点续传:进程重启后接着传

rusty-cat 刻意不内置数据库,续传要靠你在自己的库里持久化足够的信息,重启后重建同一个逻辑任务再提交。要点:

  1. 建一张自己的传输表,字段至少包含:业务文件 ID、本地路径、远端 URL / 对象 key、方向(上传/下载)、分片大小、provider 类型、状态、进度等;
  2. 在进度回调里把 FileTransferRecord 落库(不要在回调里做慢速 DB 写,最好丢给你自己的 worker 队列批量写);
  3. 进程重启后,查出未完成的行,重建等价的 PounceTask
  4. 再次调用 try_enqueue(...),provider 协议会从本地 / 远端的断点信息继续。

各 provider 的续传细节:

  • 阿里云 OSS 预签名 :用 PresignedMultipartUpload::with_resumed_parts(...) 把上次持久化的分片重新注入,prepare 会跳过已验证的最长连续前缀、重传其余部分。
  • 阿里云 OSS 直传 :续传时 SDK 通过 ListMultipartUploads 接管已有的 multipart 会话。建议持久化 provider 的 UploadIdAliOssDirectUpload::current_upload_id()),并在 Bucket 上配一条 AbortMultipartUpload 生命周期规则,让废弃会话自动清理、不再计费。
  • Azure 直传 / SAS:没有 upload id,续传靠恢复服务端的「未提交 block 列表」。未提交的 block 会计费到 Azure 垃圾回收(约 7 天),所以也要配生命周期规则。

还有一个「先导入为暂停态、再让用户选择性开始」的高级用法:用 try_enqueue_paused(...) 把任务导入成 Paused 状态,不发任何网络 / 文件 IO ,之后对用户选中的任务调 resume(task_id) 即可。适合「重启后恢复一堆任务,但只启动用户勾选的那几个」。


10. 内存占用与参数怎么选

每个在途分片都需要一个缓冲区,所以:

单个文件的峰值传输内存 ≈ n × 分片大小n = max_parts_in_flight

放到整个客户端,内存要对同时在传的文件求和:

markdown 复制代码
客户端峰值内存  ≈  max_upload_concurrency   × 上传 n × 上传分片大小
              +  max_download_concurrency × 下载 n × 下载分片大小

SDK 没有全局内存上限n 无限大内存就无限大,请务必控制。实用建议:

  • 分片大小 保持在 1--8 MiB(对象存储常见区间)。太小则请求开销大;太大则每次重试要重发更多数据、且抬高内存下限。
  • n 从 4 开始试,大文件 + 快网时用。再往上调,等带宽跑满后收益很小,内存却翻倍。
  • 记得乘以文件级并发:4 个文件 × n=8 × 4 MiB = 最多 128 MiB 缓冲。

11. 错误处理与自动重试

大多数 SDK 调用返回 Result<_, MeowError>MeowError 上有两个关键方法:

  • MeowError::code():稳定的数字错误码(i32,适合结构化日志 / FFI);
  • MeowError::msg():给人看的错误描述。

分支判断请用 code(),不要去匹配错误文本。常见错误码:

名称 什么时候发生
102 ParameterEmpty 入队时必填项(URL、文件名、非零大小)为空
103 DuplicateTaskError 同一个文件 / 任务已经在排队、运行或暂停
107 ClientClosed 客户端已关闭,请新建一个 MeowClient
111 CommandSendFailed 命令队列满了(快速失败),退避重试或调大 command_queue_capacity
120 TaskCanceled 任务在完成前被取消
121 DiskFull 写下载文件时磁盘满
122 LocalFileRemoved 传输过程中本地源 / 目标文件被删了

自动重试是怎么工作的

两个旋钮控制重试次数:

  • with_max_chunk_retries(n):单个分片首次失败后额外重试的次数(默认 30 关闭);
  • with_max_upload_prepare_retries(n):上传 prepare 阶段首次失败后额外重试的次数(默认 3)。

在这个预算内,SDK 只重试「瞬时(transient)传输错误」 ------连接被重置、超时、消息不完整、以及 HTTP 408/429/5xx------并在两次尝试之间用指数退避(基准 200ms、上限 5000ms)+ ±20% 抖动 等待,避免重试风暴。非瞬时错误(如 403、格式错误的响应、无效 Range)快速失败,不消耗重试预算,因为重试也没用。


12. 常见问题排查表(FAQ)

现象 可能原因 解决
设了 n > 1 但还是串行 三个前置条件没满足 确认 n > 1、协议并发安全、且总大小已知(上传天然满足总大小)
上传没并发 用的是内置默认 HTTP 上传 / 自定义协议 挂上四种 provider 上传协议之一(direct / presigned / SAS)
进度卡住不动然后突然跳 靠后的分片先传完、前面有空洞 正常现象,进度按连续前缀单调推进,不是卡死
入队大量任务时报 CommandSendFailed 命令队列满了(try_enqueue 快速失败) 调大 command_queue_capacity,或自己退避重试
DuplicateTaskError 同一个文件 / URL 的任务已经存在 别重复提交;或先取消旧任务
关闭后再提交报 ClientClosed 客户端 close() 之后不可复用 新建一个 MeowClient
忘了调 close(),事件丢失 / 线程不退 依赖了 Drop 兜底 关机流程里显式 client.close().await

13. 附录:配置项速查表

MeowConfig(客户端级配置)

MeowConfig::default() 拿安全默认值,或用 MeowConfig::builder() 定制后 build()?。配置在客户端创建后不可变。

参数 默认 约束 说明
max_upload_concurrency 2 >= 1 同时上传的文件数
max_download_concurrency 2 >= 1 同时下载的文件数
http_timeout 5s 正数 内部构建 HTTP 客户端的单请求超时
tcp_keepalive 30s 正数 TCP keepalive
command_queue_capacity 128 >= 1 入队 / 暂停 / 恢复 / 取消 等命令队列容量
worker_event_queue_capacity 256 >= 1 进度 / 状态事件队列容量
http_client reqwest::Client 可注入自定义 HTTP 客户端(代理、TLS、默认头、埋点等)

UploadPounceBuilder(上传任务)

方法 必填? 说明
new(file_name, file_path, chunk_size) 文件来源的上传任务,chunk_size == 0 归一化为默认值
from_bytes(file_name, bytes, chunk_size) 二选一 内存字节来源
with_url(url) 通常要 上传目标 URL
with_breakpoint_upload(Arc::new(...)) 云上传要 挂上 provider / 自定义上传协议
with_max_parts_in_flight(n) 可选 单文件分片并发数,默认 1;仅对并发安全协议生效
with_max_chunk_retries(n) 可选 单片重试次数,默认 3
with_max_upload_prepare_retries(n) 可选 prepare 阶段重试次数,默认 3
with_method(method) 可选 默认 / 自定义上传请求的 HTTP 方法(默认 POST
with_headers(headers) 可选 替换基础请求头
build() 读文件元数据并返回 PounceTask,可能返回 std::io::Error

MeowClient(客户端)常用方法

方法 用途
MeowClient::new(config) 创建客户端(执行器懒启动)
enqueue_and_wait(task, progress_cb).await 提交并等待到终态,返回 TaskOutcome
try_enqueue(task, progress_cb, complete_cb).await 提交任务,返回 TaskId(快速失败,不等完成)
try_enqueue_paused(task, ..).await 以暂停态导入,不做任何 IO,之后 resume 才启动
pause / resume / cancel (task_id).await 暂停 / 恢复 / 取消
register_global_progress_listener(cb) 注册观测「所有任务」进度的全局监听器
snapshot().await 查看排队 / 活跃的任务组数量
close().await 关机必调:优雅关闭

相关阅读

本文基于 rusty-cat 0.2.4 编写,所有代码片段均经过 cargo check 编译验证。

相关推荐
独孤留白3 小时前
从C到Rust:mut 可变性,与恶魔战斗的利剑
rust
Lazy_zheng3 小时前
TDD 实战:Claude Code Superpowers 保姆级教程14 个 Skills 全解析 + 实战开发
前端·claude·vibecoding
Rockbean3 小时前
10分钟-AI × Solana:2.MCP服务器——AI代理接入Solana的标准化通道
rust·web3·智能合约
一次旅行17 小时前
【AI工具】Rust-Based CLI:用 xargs 和并行加速你的 Linux 日常
linux·开发语言·rust
object not found19 小时前
从 PRD 到可运行应用:我用 GPT、Stitch、Figma 和 Codex 跑通了一套 AI 协同开发流程
ai编程·vibecoding
梦醒沉醉1 天前
6、Rust程序设计语言——包、crates与模块
rust
花褪残红青杏小1 天前
Rust图像处理第16节-曼德博集合 Mandelbrot:用一行公式画出无限细节
rust·webassembly·图形学
Arman37681 天前
rusty-cat × 阿里云 OSS 直传直下实战
rust·ai编程
临风细雨1 天前
从 Bun 的 Rust 重写,看 C# 如何重建 AI 基础设施层
人工智能·rust·c#