欢迎光临
我们一直在努力

从 0 到 1:KMS 平台大文件上传全链路方案实战

一、引言:当文件上传遇上金融科技

KMS(Knowledge Management System)是公司内部的知识管理平台,承载着各部门的培训视频、合规文档扫描件、产品手册等核心资产。随着业务扩张,用户上传的文件体积从早期的几 MB 变成了动辄 500MB 的培训视频,甚至出现了 2GB 的高清扫描件合集。

问题随之而来。

2025 年 Q2,我们对 KMS 的文件上传模块做了一次全面的数据分析,结果触目惊心:文件体积超过 50MB 后,单文件上传失败率飙升至 18%。这意味着每 5 个上传大文件的用户中,就有近 1 人遭遇失败。更糟糕的是,失败后用户必须从头重传——一个已经传了 80% 的 1GB 文件,因为网络抖动中断,之前传输的全部作废,重试耗时等于首次上传的 100%。

还有一类隐形成本:同一个培训视频被不同部门的同事分别上传了三次,存储空间白白浪费了 3GB,后端去重完全依赖人工沟通。

问题的本质有且只有三个:

  • 大文件:HTTP 单次请求不适合传输 GB 级文件,超时和内存占用是硬伤
  • 断网重传:网络不稳定导致上传中断后,缺乏有效的恢复机制
  • 重复上传:同一文件多次上传,浪费带宽和存储
  • 本文的目标,是给出一套经过 KMS 生产环境验证的端到端解决方案,用一个统一的架构同时解决上述三个问题。完成阅读后,你将获得:

    • 一份可直接落地的分片上传前端代码(React + TypeScript)
    • 一套完整的后端 API 设计(路由、分片合并、状态管理)
    • 五个生产环境真实踩坑案例及其解法
    • 一个可复用的 useChunkedUpload Hook

    我们不只聊前端 slice(),而是从浏览器到 Nginx 到后端存储,覆盖全链路。


    二、全链路架构总览

    2.1 全链路架构图

    在这里插入图片描述

    2.2 上传状态机

    一次完整的上传,从文件选择到合并完成,经历以下状态流转:

    在这里插入图片描述

    2.3 关键技术决策一览表

    决策项可选方案KMS 最终选择决策理由
    分片大小 1MB / 5MB / 10MB / 20MB 5MB 平衡请求数(200 个/GB)与失败重传成本(5MB/次)
    并发数 1 / 3 / 6 / 10 3 规避浏览器同域 6 连接限制,为其他请求留出空间
    哈希算法 MD5 / SHA-256 / 抽样哈希 全量 MD5(SparkMD5) 与 OSS 兼容,计算速度可接受,Web Worker 异步不阻塞
    秒传机制 无 / 仅文件名 / 哈希匹配 哈希匹配 文件名不可靠,哈希才能保证内容唯一性
    重试策略 固定间隔 / 线性退避 / 指数退避 指数退避(1s, 2s, 4s) 避免雪崩,给服务端恢复留出时间
    状态存储 内存 / localStorage / 后端 Redis 后端 Redis + localStorage 兜底 Redis 保证并发安全,localStorage 防止页面刷新丢失 uploadId
    文件存储 本地磁盘 / MinIO / 阿里云 OSS MinIO(生产),本地(开发) MinIO 兼容 S3 协议,运维成本低,私有化部署满足合规要求

    三、前端核心实现

    3.1 文件分片:为什么是 5MB?

    分片大小的选择,是分片上传方案的第一个关键决策。太小,请求数爆炸;太大,重传代价高。在 KMS 项目中,我们做了一个简单的数学建模:

    文件大小1MB 分片(请求数)5MB 分片(请求数)10MB 分片(请求数)20MB 分片(请求数)
    100MB 100 20 10 5
    500MB 500 100 50 25
    2GB 2048 410 205 103

    1MB 分片在 2GB 文件场景下会产生 2048 个 HTTP 请求。每个请求都有 TCP 握手、TLS 协商、HTTP header 等固定开销,累积延迟非常可观。

    20MB 分片虽然请求数少,但单个分片上传失败一次就浪费 20MB 的已传输数据,对移动端弱网用户极不友好。

    5MB 是我们的甜点值:2GB 文件仅产生 410 个分片请求,单次失败重传成本 5MB,响应速度在可接受范围内。

    分片代码如下:

    // utils/createChunks.ts
    export interface Chunk {
    blob: Blob;
    index: number;
    start: number;
    end: number;
    }

    export const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB

    export function createChunks(file: File): Chunk[] {
    const chunks: Chunk[] = [];
    let start = 0;

    while (start < file.size) {
    const end = Math.min(start + CHUNK_SIZE, file.size);
    chunks.push({
    blob: file.slice(start, end),
    index: chunks.length,
    start,
    end,
    });
    start = end;
    }

    return chunks;
    }

    3.2 哈希计算与秒传逻辑

    哈希计算是大文件上传中一个容易被低估的性能陷阱。

    抽样哈希 vs 全量哈希

    业界有一种常见做法——抽样哈希:只读取文件头部、中部、尾部的各一段数据计算哈希,以大幅降低计算量。这个方案在视频网站的转码场景中很常见。

    但 KMS 没有走这条路。原因有二:(1)KMS 知识库存储的是金融合规材料,文件完整性校验不允许任何妥协;(2)抽样哈希存在理论上的碰撞可能——两个不同文件被判定为同一个,导致某个用户的文件被错误地"秒传"到另一个文件上。这是不可接受的风险。

    我们在 Web Worker 中计算全量哈希。配合 SparkMD5 的增量计算能力(append()),Worker 每读取一个分片就将数据追加到哈希计算器中,避免一次性将整个文件加载到内存:

    // workers/hash.worker.ts
    import SparkMD5 from 'spark-md5';

    self.onmessage = (e: MessageEvent<File>) => {
    const file = e.data;
    const spark = new SparkMD5.ArrayBuffer();
    const chunkSize = 5 * 1024 * 1024; // 5MB
    let offset = 0;

    function readNext() {
    const slice = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();
    reader.onload = (ev) => {
    spark.append(ev.target!.result as ArrayBuffer);
    offset += chunkSize;
    if (offset < file.size) {
    self.postMessage({ type: 'progress', progress: offset / file.size });
    readNext();
    } else {
    self.postMessage({ type: 'done', hash: spark.end() });
    }
    };
    reader.readAsArrayBuffer(slice);
    }

    readNext();
    };

    秒传逻辑非常直接:前端拿到文件哈希后,调用 POST /api/upload/init。后端查询 Redis / DB 判断该哈希对应的文件是否已经存在于存储中——如果存在,直接返回已有的文件 URL,整个上传在毫秒级完成。

    // utils/checkSecretUpload.ts
    import axios from 'axios';

    export interface InitResult {
    uploadId: string;
    secret: boolean; // true = 秒传命中
    url?: string; // 秒传时直接返回文件 URL
    uploadedChunks?: number[]; // 断点续传:已上传的分片序号
    }

    export async function initUpload(
    fileHash: string,
    fileName: string,
    fileSize: number,
    totalChunks: number,
    ): Promise<InitResult> {
    const { data } = await axios.post<InitResult>('/api/upload/init', {
    fileHash, fileName, fileSize, totalChunks,
    });
    return data;
    }

    3.3 并发上传与进度监控

    上传的核心是一个有界并发池(bounded concurrency pool)。我们维护一个大小为 3 的"槽位"数组,每个槽位负责一个分片的上传。当一个分片上传完成(无论成功或最终失败),槽位被释放,立即从待上传队列中取出下一个分片填入。

    在这里插入图片描述

    代码实现使用了一个经典的 Promise 竞态模式:

    // utils/concurrentUpload.ts
    import axios, { AxiosProgressEvent } from 'axios';
    import { Chunk } from './createChunks';

    export interface UploadProgress {
    uploaded: number; // 已上传字节数
    total: number; // 总字节数
    percentage: number; // 0-100
    }

    export async function concurrentUpload(
    chunks: Chunk[],
    uploadId: string,
    onProgress: (p: UploadProgress) => void,
    signal: AbortSignal,
    ): Promise<void> {
    const CONCURRENCY = 3;
    let completed = 0;
    const totalBytes = chunks.reduce((s, c) => s + c.blob.size, 0);
    let uploadedBytes = 0;

    async function uploadOne(chunk: Chunk): Promise<void> {
    if (signal.aborted) throw new Error('Upload canceled');
    const formData = new FormData();
    formData.append('file', chunk.blob);
    formData.append('uploadId', uploadId);
    formData.append('chunkIndex', String(chunk.index));

    await axios.post('/api/upload/chunk', formData, { signal });
    uploadedBytes += chunk.blob.size;
    completed += 1;
    onProgress({
    uploaded: uploadedBytes,
    total: totalBytes,
    percentage: Math.round((completed / chunks.length) * 100),
    });
    }

    const pool = new Set<Promise<void>>();
    for (const chunk of chunks) {
    const p = uploadOne(chunk).finally(() => pool.delete(p));
    pool.add(p);
    if (pool.size >= CONCURRENCY) await Promise.race(pool);
    }
    await Promise.all(pool);
    }

    AbortController 被传入每个请求的 signal 参数中。当用户点击"取消"或页面卸载时,调用 controller.abort(),所有进行中的分片上传会被浏览器立即中断,不会在网络层留下悬空连接。

    3.4 断点续传与失败重试

    断点续传的核心思路是:上传中断后(无论是网络问题还是用户主动暂停),重新发起时先向后端查询已有分片列表,跳过已存在的分片,只传输缺失部分。

    // utils/resumeUpload.ts
    import axios from 'axios';

    export async function getUploadedChunks(
    uploadId: string,
    ): Promise<number[]> {
    const { data } = await axios.get<{ chunks: number[] }>(
    '/api/upload/status', { params: { uploadId } },
    );
    return data.chunks;
    }

    getUploadedChunks 返回已上传分片序号的数组(如 [0, 1, 2, 3, 5] 表示第 4 个分片缺失)。前端据此过滤 chunks 数组,只将缺失的分片推入并发池。

    重试策略采用指数退避(exponential backoff)。每次失败后等待时间翻倍,单个分片最多重试 3 次:

    // utils/retryWithBackoff.ts
    export async function retryWithBackoff<T>(
    fn: () => Promise<T>,
    maxRetries = 3,
    baseDelay = 1000,
    ): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
    return await fn();
    } catch (err) {
    if (attempt === maxRetries) throw err;
    const delay = baseDelay * 2 ** attempt; // 1s, 2s, 4s
    await new Promise((r) => setTimeout(r, delay));
    }
    }
    throw new Error('unreachable');
    }

    将 retryWithBackoff 包裹在 uploadOne 函数的外层,就能实现单个分片的自动重试,而不影响其他分片的并发执行。

    3.5 完整的上传 Hook:useChunkedUpload

    将上述所有逻辑封装到一个 React Hook 中,对外暴露简洁的控制面和状态:

    // hooks/useChunkedUpload.ts
    import { useState, useRef, useCallback } from 'react';
    import { Chunk, createChunks } from '@/utils/createChunks';
    import { concurrentUpload, UploadProgress } from '@/utils/concurrentUpload';
    import { initUpload, InitResult } from '@/utils/checkSecretUpload';
    import { getUploadedChunks } from '@/utils/resumeUpload';

    type UploadStatus = 'idle' | 'hashing' | 'checking' | 'uploading'
    | 'merging' | 'paused' | 'done' | 'error' | 'canceled';

    interface UseChunkedUploadReturn {
    start: (file: File) => Promise<void>;
    pause: () => void;
    resume: () => Promise<void>;
    cancel: () => void;
    progress: UploadProgress;
    status: UploadStatus;
    }

    export function useChunkedUpload(
    onHashProgress?: (p: number) => void,
    ): UseChunkedUploadReturn {
    const [progress, setProgress] = useState<UploadProgress>(
    { uploaded: 0, total: 0, percentage: 0 },
    );
    const [status, setStatus] = useState<UploadStatus>('idle');
    const abortRef = useRef<AbortController | null>(null);
    const fileRef = useRef<File | null>(null);
    const uploadIdRef = useRef<string>('');
    const chunksRef = useRef<Chunk[]>([]);
    const uploadedChunksRef = useRef<Set<number>>(new Set());

    const start = useCallback(async (file: File) => {
    fileRef.current = file;
    setStatus('hashing');

    // 1. Web Worker 计算哈希
    const hash = await computeHashInWorker(file, onHashProgress);

    // 2. 初始化上传任务
    const chunks = createChunks(file);
    chunksRef.current = chunks;
    setStatus('checking');

    const result: InitResult = await initUpload(
    hash, file.name, file.size, chunks.length,
    );
    uploadIdRef.current = result.uploadId;
    localStorage.setItem('lastUploadId', result.uploadId);

    if (result.secret) {
    setStatus('done');
    return;
    }

    // 3. 断点续传:跳过已上传分片
    uploadedChunksRef.current = new Set(result.uploadedChunks ?? []);
    const pendingChunks = chunks.filter(
    (c) => !uploadedChunksRef.current.has(c.index),
    );

    // 4. 并发上传
    abortRef.current = new AbortController();
    setStatus('uploading');

    await concurrentUpload(
    pendingChunks,
    result.uploadId,
    setProgress,
    abortRef.current.signal,
    );

    // 5. 请求后端合并
    setStatus('merging');
    await axios.post('/api/upload/merge', { uploadId: result.uploadId });
    setStatus('done');
    }, [onHashProgress]);

    const pause = useCallback(() => {
    abortRef.current?.abort();
    setStatus('paused');
    }, []);

    const resume = useCallback(async () => {
    if (!fileRef.current || !uploadIdRef.current) return;
    // 重新查询已上传分片,继续上传剩余部分
    const uploaded = await getUploadedChunks(uploadIdRef.current);
    uploadedChunksRef.current = new Set(uploaded);
    const pending = chunksRef.current.filter(
    (c) => !uploadedChunksRef.current.has(c.index),
    );
    abortRef.current = new AbortController();
    setStatus('uploading');
    await concurrentUpload(
    pending, uploadIdRef.current, setProgress, abortRef.current.signal,
    );
    setStatus('merging');
    await axios.post('/api/upload/merge', { uploadId: uploadIdRef.current });
    setStatus('done');
    }, []);

    const cancel = useCallback(() => {
    abortRef.current?.abort();
    setStatus('canceled');
    }, []);

    return { start, pause, resume, cancel, progress, status };
    }


    四、后端协同设计

    4.1 API 设计总览

    后端提供了四个核心接口,它们共同构成了一次完整的分片上传生命周期:

    接口方法功能幂等
    /api/upload/init POST 接收文件哈希、名称、大小、总分片数,返回 uploadId。若哈希命中已存储文件,返回 secret: true 和已有 URL
    /api/upload/chunk POST 接收 uploadId + chunkIndex + 分片文件,存入临时目录,Redis 标记该分片已到达
    /api/upload/status GET 根据 uploadId 查询 Redis,返回已上传分片序号列表
    /api/upload/merge POST 校验所有分片已到齐,按序号合并写入最终文件,校验文件哈希,写入元数据库

    4.2 后端分片合并逻辑(伪代码)

    这是整个后端链路中最关键的一步。合并不是简单的 cat 拼接——它前面有校验,后面有验证:

    # services/upload_service.py (伪代码)

    def merge_chunks(upload_id: str) > str:
    # 1. 从 Redis 获取元信息
    meta = redis.hgetall(f"upload:{upload_id}:meta")
    total_chunks = int(meta["total_chunks"])
    expected_hash = meta["file_hash"]
    file_name = meta["file_name"]

    # 2. 校验分片是否全部到达
    uploaded = redis.smembers(f"upload:{upload_id}:chunks")
    if len(uploaded) != total_chunks:
    raise UploadNotComplete(f"{len(uploaded)}/{total_chunks}")

    # 3. 校验每个分片的大小
    tmp_dir = f"/tmp/uploads/{upload_id}"
    for i in range(total_chunks):
    chunk_path = f"{tmp_dir}/{i}"
    actual_size = os.path.getsize(chunk_path)
    if actual_size > CHUNK_SIZE * 1.1: # 允许 10% 冗余
    raise ChunkSizeMismatch(f"chunk {i}: {actual_size}")

    # 4. 按序号合并
    final_path = f"/data/files/{upload_id}_{file_name}"
    with open(final_path, "wb") as out:
    for i in range(total_chunks):
    with open(f"{tmp_dir}/{i}", "rb") as f:
    out.write(f.read())

    # 5. 验证最终文件哈希
    if sha256_file(final_path) != expected_hash:
    os.remove(final_path)
    raise HashMismatch("合并后哈希与预期不一致")

    # 6. 清理临时目录 & 写入文件元数据
    shutil.rmtree(tmp_dir)
    redis.delete(f"upload:{upload_id}:chunks")
    db.insert_file_record(upload_id, file_name, final_path, expected_hash)
    return final_path

    4.3 存储策略

    KMS 在不同环境采用了不同的存储后端:

    • 开发环境:直接写入本地文件系统 /data/files/,简化调试流程,不引入外部依赖
    • 生产环境:使用 MinIO(兼容 S3 协议的对象存储)。分片仍然先写入本地临时目录(避免频繁的网络 IO),合并完成后再通过 MinIO SDK 的 put_object 上传最终文件
    • OSS 预签名 URL 方案(阿里云 OSS 场景的演进方向):后端生成每个分片的预签名上传 URL,前端直接 PUT 到 OSS,完全绕过应用服务器,大幅节省带宽。但出于金融行业的合规考量,当前阶段 KMS 仍采用"应用服务器中转"的方案,确保所有上传流量经过审计层

    4.4 为什么需要一个 uploadId

    你可能会问:直接用文件哈希作为标识不行吗?为什么还要引入一个额外的 uploadId?

    答案是三个场景需要它:

  • 关联同一批次的分片:一个 2GB 文件有 410 个分片,这些分片是分批次、分时段到达后端的。uploadId 将它们关联为一次上传任务,后端据此维护临时目录和 Redis 状态
  • 支持多文件并发上传:如果用户同时上传两个文件,仅靠哈希无法区分"文件 A 的第 3 个分片"和"文件 B 的第 3 个分片"。uploadId 是一个独立命名空间
  • 断点续传的上下文恢复:用户刷新页面后,localStorage 中保存的 uploadId 是恢复上传上下文的唯一凭据

  • 五、生产环境踩坑与优化

    以下五个坑都是在 KMS 生产环境中真实踩过、逐一解决的。每个坑都遵循"现象-根因-解决"的三段式分析。

    5.1 Nginx 默认 body size 只有 1MB

    现象:分片大小设置为 5MB 后,后端始终无法收到分片请求。浏览器控制台显示 413 Request Entity Too Large。

    根因:生产环境的 Nginx 配置中,client_max_body_size 使用了默认值 1m。即使用户拆分成 5MB 的分片,Nginx 在 HTTP 层面就直接拒绝了请求体大于 1MB 的任何 POST。

    解决:在 Nginx 的 server/location 块中添加:

    client_max_body_size 2g;

    值为 2g 而非 5m,一方面考虑了未来可能的更大分片,另一方面也兼容了其他非分片上传的常规文件上传接口。

    5.2 SparkMD5 计算 2GB 文件导致浏览器卡死

    现象:用户选择了一个 2GB 的文件后,整个浏览器选项卡冻结了约 15 秒,期间无法点击任何按钮,DevTools Performance 面板显示主线程被一个巨大的 JavaScript 任务占满。

    根因:最初的实现将 SparkMD5 的 append() 调用直接放在了主线程。即使是增量计算,读取 2GB 文件仍然需要遍历所有字节,这会形成一个长时间运行的主线程任务,阻塞所有 UI 交互。

    解决:将哈希计算完全移入 Web Worker(见 3.2 节代码),主线程只负责发送 postMessage 和接收结果。计算过程中,Worker 每完成一个分片的哈希追加就回传 progress 消息,主线程据此更新进度条。

    5.3 浏览器关闭/刷新导致上传任务丢失

    现象:用户上传了一个 800MB 的文件,已完成 60%。此时不慎关闭了浏览器标签页,重新打开后,上传进度归零,之前传输的 480MB 数据全部作废。

    根因:uploadId 仅保存在组件的 React state 中,页面刷新后 state 自然丢失。用户无法向后端查询之前的 uploadId,因为他不知道这个 ID。

    解决:在 init 成功后立即将 uploadId 持久化到 localStorage。页面重新加载时,useChunkedUpload 检查 localStorage 中是否有未完成的 uploadId,如果有则自动调用 /api/upload/status 查询进度,展示"继续上传"的提示:

    // 页面初始化时检查是否有未完成的上传任务
    useEffect(() => {
    const lastUploadId = localStorage.getItem('lastUploadId');
    if (lastUploadId) {
    getUploadedChunks(lastUploadId).then((chunks) => {
    if (chunks.length > 0) {
    showResumePrompt(lastUploadId, chunks);
    }
    });
    }
    }, []);

    5.4 同一文件被多次上传浪费存储

    现象:某部门的培训视频(1.2GB)被三位同事分别上传了一次,存储中存了三份完全相同的文件,白白浪费了 2.4GB 空间。

    根因:早期的上传接口没有去重逻辑,每次上传都创建新的文件记录。

    解决:实施基于文件哈希的秒传机制(见 3.2 节)。init 接口在创建上传任务前,先查询哈希映射表。如果发现同一哈希的文件已存在,直接返回 secret: true 和已有文件的 URL,前端跳过所有分片上传步骤,直接展示"秒传成功"。

    5.5 并发数太高触发浏览器同域连接限制

    现象:将并发数设置为 8 后,发现前 6 个分片可以同时上传,但第 7、8 个分片一直处于 pending 状态,直到前面的分片完成才发出请求。

    根因:HTTP/1.1 协议下,浏览器对同一域名的并发连接数限制为 6 个(Chrome)。超过这个数目的请求会排队等待。实际上我们浪费了"创建更多连接"的意图——多余的并发请求只能排队。

    解决:将并发池上限从 8 降至 3。这样,(1)分片上传最多占用 3 个连接;(2)剩余 3 个连接留给页面上的其他请求(API 调用、图片加载等);(3)3 个并发的吞吐量对于绝大多数网络环境已经够用(单分片 5MB,3 并发 = 15MB 同时在传)。

    5.6 踩坑优化汇总表

    问题现象根因方案效果
    413 错误 5MB 分片被 Nginx 拒绝 client_max_body_size 默认 1MB 设为 2GB 所有分片正常通过
    浏览器卡死 2GB 文件哈希计算 15 秒无响应 主线程执行 SparkMD5 全量计算 Web Worker 异步计算 UI 始终可交互,进度可展示
    上传任务丢失 页面刷新后进度归零 uploadId 仅在 React state localStorage 持久化 uploadId 刷新后可恢复上传
    重复存储 同一文件 3 个副本 无去重机制 文件哈希秒传 存储占用降至单份
    连接耗尽 并发 8 只有 6 个在传 Chrome 同域 6 连接限制 并发池降为 3 留出连接给其他请求

    六、总结与展望

    6.1 核心思路回顾

    KMS 大文件上传方案可以浓缩为五个关键词:分片、哈希、并发、断点续传、秒传。

    分片解决了"大文件无法单次传输"的问题;哈希提供了文件完整性和唯一性的验证手段;并发池在有限的浏览器连接资源下最大化吞吐量;断点续传让中断不再是灾难;秒传则让重复上传变得毫无意义。

    这五个技术点并不是孤立的——它们通过 uploadId 和 Redis 状态管理串联在一起,形成了一套完整的状态机。

    6.2 适用场景

    这套方案在 KMS 知识库的场景(培训视频、合规扫描件、产品手册)中表现稳定。推而广之,凡是涉及 50MB 以上文件上传 的场景——文档管理系统、网盘、视频平台、设计素材库——都可以直接复用这套架构。

    对于 50MB 以下的小文件,分片上传的额外开销(多次 HTTP 请求、Web Worker 哈希计算、Redis 状态维护)可能得不偿失。KMS 的做法是:前端判断 file.size > 50 * 1024 * 1024 时走分片上传流程,否则走传统单次 multipart/form-data 上传。

    6.3 待探索方向

    KMS 上传方案还有几个值得探索的优化方向:

    • WebAssembly 加速哈希计算:SparkMD5 是纯 JavaScript 实现。WASM 版的 MD5(如 Rust 编译到 WASM)在 2GB 级别文件上的计算速度可以提升 2-4 倍
    • HTTP/3 多路复用:QUIC 协议消除了 TCP 队头阻塞问题,理论上可以将并发数提高而不受连接数限制。这对移动端弱网场景尤其有吸引力
    • 客户端边分片边上传:当前流程是先完整计算哈希再开始上传。如果哈希计算和上传交错执行(已算完哈希的分片立即开始上传),可以进一步缩短端到端耗时

    这些方向目前还在调研阶段,欢迎有实践经验的朋友交流讨论。


    赞(0)
    未经允许不得转载:171主机测评 » 从 0 到 1:KMS 平台大文件上传全链路方案实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址