
我前阵子帮一家汽车制造企业排查一个线上问题他们的质量管理平台每隔一段时间就要接收来自焊装车间和总装车间的质检录像。这些视频文件单个动辄 1GB 到 5GB是高清摄像头对着关键工位拍的操作过程记录需要长期留存还要保证文件在传输过程中没有被篡改或损坏。他们的 PHP 后端原来用的是最传统的单次上传 接收完成后计算 MD5 做校验结果一到月底集中传输的时候就频繁超时偶尔还会出现上传成功但哈希对不上的情况运维同学被叫去处理了好几次。问题本质其实不复杂PHP 做视频文件接收时把整个文件读入内存再算哈希验证这条路在文件大到 GB 级别之后就很难走通了。后来我用分片上传 哈希分段校验的方案把流程重做了一遍才算把这个问题彻底解决。这篇就把完整的方案拆开讲清楚包括为什么分片上传能解决哈希验证的难题、服务端怎么实现、生产环境还要注意哪些细节给同样被大文件上传折磨的 PHP 开发者一个可以直接落地的参考。1. 汽车制造场景下的视频文件困境单一文件哈希校验为什么很吃力1.1 视频文件的体量与质检规范要求汽车制造企业的视频文件不是普通的监控录像它有很强的业务属性。以我接触的这个项目为例整车厂的焊装车间有几十个工位每个工位上方部署了工业相机拍摄的焊接过程视频要保留至少三个月甚至更久。产线节拍快的时候单条线一天能产生几十 GB 的视频数据。这些视频的用途有三个一是出现质量争议时回溯生产操作二是配合 MES制造执行系统做工艺参数比对三是应对主机厂客户审计时提供证据链。这就导致视频文件有两个鲜明的特征。第一是单个文件特别大一条完整的工位操作视频动辄几 GB时长稍长的甚至能到 10GB 以上。第二是完整性要求极高焊缝外观、装配手法、扭矩拧紧过程这些细节只要视频中间缺了几帧或者某个数据块坏了整段录像的证据价值就打了折扣。传统的做法是把视频文件从产线边缘服务器往平台服务器上传然后计算整个文件的消息摘要算法MD5或者 SHA-256 哈希值跟源端比对一致才算入库完成。1.2 传统上传加校验的三大痛点过去那种“整包上传 整包哈希”的模式放在小文件场景下完全没问题但在汽车制造这种 GB 级视频文件的场景里三个痛点会依次暴露出来。第一个痛点是 PHP 进程的内存上限很容易被打爆。用file_get_contents()或者move_uploaded_file()处理上传时如果配置不当PHP 会把整个请求体读入临时文件或内存。即便 PHP 官方默认走的是 php://input 的流式读取很多开发者图省事仍然会写成一次性读取全部内容再算哈希一个 3GB 的文件把 256MB 的 memory_limit 直接顶穿返回 500 错误这在实际项目中太常见了。第二个痛点是网络中断后整个文件要重传。整车厂的网络环境不像写字楼里面那么干净车间里金属屏蔽严重无线网络信号衰减波动很大有线网络偶尔也会有交换机端口不稳定的时候。一个 2GB 的文件传了三分之一突然网络抖动导致连接断开传统方案下客户端只能重新上传整个文件带宽浪费严重而且重传期间产线质检人员只能在系统里干等。第三个痛点是哈希验证的时候磁盘 I/O 和 CPU 全被拉满。md5_file()或者hash_file()执行时需要把整个文件从头到尾读一遍。GB 级别的文件在机械硬盘上读一圈要几十秒在并发上传多路视频的情况下服务器的 I/O 会成为瓶颈甚至影响同一台机器上其他业务接口的响应速度。1.3 哈希验证的核心目标确保逐字节完整聊优化方案之前得先把哈希验证的本质说清楚。哈希验证的最终目的不是“算出一个字符串”而是确认接收端拿到的每一个字节跟源端发送的字节完全一致。MD5、SHA-1、SHA-256 这类算法的特点是输入数据哪怕只有一个比特的变化输出的摘要就会完全不同。所以比对的逻辑很简单——两边的哈希值相同就说明文件在传输过程中没有发生比特级损坏。但这里有个容易被忽略的工程问题哈希计算必须基于完整的数据集。如果你接收了前 100MB 数据就算一次分片的 SHA-256再把每个分片的哈希拼接起来取哈希跟直接对整个文件取 SHA-256结果是完全不同的。原因在于 SHA-256 算法内部有固定的分块处理逻辑它把输入数据按 64 字节分组每一组的处理都依赖前一组产生的中间状态。分片独立计算哈希相当于打断了这种状态依赖关系。所以分片上传“优化”哈希验证关键点不在于改变算法本身而在于改变数据流经哈希算法的时机和方式。这也是后面整个方案设计的理论起点。2. 分片上传“优化”哈希验证的两种思路边传边算与合并后验证2.1 思路 A每片独立哈希合并后再做最终哈希第一种方案是最直白的也能最大程度复用现有的分片上传逻辑。它的流程是客户端把大视频文件按固定大小切成若干分片比如每片 5MB 或者 10MB。每个分片独立上传到服务端服务端接收后立即计算该分片的 MD5 和 SHA-256。服务端把每个分片的哈希值存进数据库并返回给客户端一个确认。所有分片传完后服务端把所有分片按顺序合并成完整的视频文件。最后对合并后的完整文件重新计算一次哈希跟源端对完整文件计算的哈希做比对。这个方案的优点是实现简单、排查容易。哪个分片传坏了单独重传那个分片就行。缺点是最后合并的步骤仍然需要对整个大文件做一次全量哈希磁盘 I/O 和 CPU 开销并没有减少只是把计算时间挪到了合并且之后。如果视频文件是 5GB这一步仍然会耗时几十秒该卡的还是会卡。2.2 思路 B分片上传过程中流式增量哈希第二种方案更优雅它利用了哈希算法的流式计算特性。PHP 的hash_init()、hash_update()、hash_final()三个函数支持增量计算意思是同一个哈希上下文可以分多次喂入数据最终拿到的结果跟一次性把全部数据喂进去计算的结果完全一致。具体流程是这样的服务端接收到第一个分片时用hash_init(sha256)初始化一个哈希上下文。每接收到一个分片把分片内容通过hash_update()写入这个上下文。最后一个分片接收完成后调用hash_final()拿到完整文件的 SHA-256。整个过程中不需要把大文件完整读一遍做重复计算因为数据已经“流过”哈希算法了。这种做法实际上是把“哈希验证”从上传完成后的单独步骤融合进了分片接收的过程。每个分片数据到达即参与哈希运算最后一步hash_final()几乎是瞬时完成的因为所有数据都已经被消费过了。2.3 两种方案的对比取舍我整理了一个对比表格方便读者直观理解两条技术路线的差异。维度方案 A每片独立哈希 合并后全量哈希方案 B流式增量哈希 最后收尾实现复杂度低分片哈希独立存储逻辑清晰中需要维护哈希上下文状态全量哈希耗时高合并后仍然要整体读一遍文件低数据接收时已流过哈希算法单分片重传支持哪个分片坏了重传哪个支持但重传后要恢复增量哈希的中间状态内存占用低低但进程内要持有上下文适合场景对排查要求高、分片可能频繁失败文件大、对性能敏感、网络较稳定我在实际项目中最终选了方案 B因为汽车制造场景下的视频文件实在太大合并后全量哈希那一步的等待时间在产线高峰期是完全不可接受的。但方案 B 有一个前置隐患需要额外处理——hash_init()返回的上下文对象保存在哪个进程里。如果服务端用了多进程部署或者负载均衡分片请求被分发到不同进程各自的hash_update()调用的就不是同一个上下文最终算出来的哈希全乱了。这个问题的具体解法我会在下一章展开这也是方案 B 在生产环境落地的最大门槛。3. 服务端完整实现分片接收、增量哈希与合并验证的落地代码3.1 数据库与表结构设计开始写代码之前先设计存储结构。分片上传 增量哈希的核心状态必须能够持久化否则服务重启一切都得重来。我建了三张表分别是文件上传任务表、分片信息表、文件归档表。文件上传任务表用于记录一次完整上传任务的全局信息CREATE TABLE upload_task ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, file_id VARCHAR(32) NOT NULL COMMENT 文件唯一标识客户端生成后传入, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名, file_size BIGINT UNSIGNED NOT NULL COMMENT 文件总字节数, chunk_size INT UNSIGNED NOT NULL COMMENT 分片大小字节, chunk_total INT UNSIGNED NOT NULL COMMENT 总分片数, chunk_uploaded INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 已上传分片数, hash_algo VARCHAR(16) NOT NULL DEFAULT sha256 COMMENT 哈希算法, full_file_hash VARCHAR(64) DEFAULT NULL COMMENT 源端提供给完整文件的哈希, computed_hash VARCHAR(64) DEFAULT NULL COMMENT 服务端增量计算的哈希, status TINYINT NOT NULL DEFAULT 0 COMMENT 0初始化 1上传中 2已完成 3失败, hex_context TEXT DEFAULT NULL COMMENT 哈希上下文序列化后的值, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_file_id (file_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;分片信息表记录每个分片的元数据和独立哈希作用有两个一是断点续传时快速确认哪些分片已存在二是排错时能精准定位到具体分片CREATE TABLE upload_chunk ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, file_id VARCHAR(32) NOT NULL, chunk_index INT UNSIGNED NOT NULL COMMENT 分片序号从0开始, chunk_size INT UNSIGNED NOT NULL, chunk_md5 CHAR(32) DEFAULT NULL, chunk_storage_path VARCHAR(255) NOT NULL COMMENT 分片在磁盘上的临时路径, uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_file_chunk (file_id, chunk_index) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节hex_context字段是我特意加的它是增量哈希方案的关键。PHP 的hash_init()返回的HashContext对象可以通过hash_copy()复制但这个对象本身不能直接塞进 MySQL。我用的办法是把上下文对象用serialize()序列化成字符串存入数据库。这个方案的可行性依赖 PHP 版本PHP 7.4 到 8.x 实测下来都能正常工作后面会有专门的代码演示。3.2 初始化上传任务客户端告知文件元信息分片上传流程的第一步是客户端先调用初始化接口把文件的基本信息告诉服务端。我定义了如下的接口契约POST /api/v1/upload/init 请求参数 file_id // 客户端生成的 UUID整次上传保持不变 file_name // 原始文件名 file_size // 文件总字节数 chunk_size // 分片大小建议 5MB 或 10MB chunk_total // 总分片数 full_file_hash // 源端对完整视频文件计算的 SHA-256服务端接收到这个请求后核心逻辑是验证分片参数的合理性并在upload_task表里插入一条初始记录public function initUpload(Request $request): JsonResponse { $fileId $request-input(file_id); $fileSize (int) $request-input(file_size); $chunkSize (int) $request-input(chunk_size); $chunkTotal (int) $request-input(chunk_total); // 分片大小限制在 1MB ~ 50MB 之间防止客户端把分片设得过大或过小 if ($chunkSize 1024 * 1024 || $chunkSize 50 * 1024 * 1024) { return response()-json([code 400, msg 分片大小必须在1MB到50MB之间]); } // 通过文件大小和分片大小反推总分片数如果跟客户端传的不一致直接拒绝 $calculatedTotal (int) ceil($fileSize / $chunkSize); if ($calculatedTotal ! $chunkTotal) { return response()-json([code 400, msg 分片参数不匹配]); } UploadTask::create([ file_id $fileId, file_name $request-input(file_name), file_size $fileSize, chunk_size $chunkSize, chunk_total $chunkTotal, full_file_hash $request-input(full_file_hash), status 0, ]); return response()-json([code 0, msg 初始化成功]); }初始化接口把校验逻辑做了前置避免脏数据进入后续流程。这里的关键校验点是文件大小和总分片数的对应关系用ceil($fileSize / $chunkSize)反推出来必须和客户端传入的chunk_total完全一致否则文件末尾的分片大小不合法合并时容易出问题。3.3 分片上传接口核心的增量哈希处理逻辑分片上传接口是整个方案的心脏。它接收单个分片文件写入临时目录更新分片表同时把分片内容喂给增量哈希上下文。客户端上传分片时通常用表单字段携带元信息文件本体作为文件字段传递。我在代码里用了 Laravel 的请求对象但核心逻辑跟框架无关用原生 PHP 也能实现。public function uploadChunk(Request $request): JsonResponse { $fileId $request-input(file_id); $chunkIndex (int) $request-input(chunk_index); $file $request-file(chunk); // 1. 校验分片序号是否在合法范围内 $task UploadTask::where(file_id, $fileId)-first(); if (!$task || $task-status 2) { return response()-json([code 404, msg 任务不存在或已完成]); } if ($chunkIndex 0 || $chunkIndex $task-chunk_total) { return response()-json([code 400, msg 分片序号越界]); } // 2. 保存分片到磁盘临时目录按 file_id 分层避免文件过多 $tempDir storage_path(app/upload_tmp/ . $fileId); if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $chunkPath $tempDir . / . $chunkIndex . .part; $file-move($tempDir, $chunkIndex . .part); // 3. 计算该分片的 MD5用于排错和断点续传确认 $chunkMd5 md5_file($chunkPath); // 4. 读取分片内容追加到增量哈希上下文 // 注意这里不能使用 file_get_contents 读整个分片要流式读取 $handle fopen($chunkPath, rb); if (!$handle) { return response()-json([code 500, msg 分片读取失败]); } $hexContext $task-hex_context; if ($hexContext) { // 反序列化恢复哈希上下文 $hashContext unserialize($hexContext); } else { // 第一个分片到达时初始化哈希上下文 $hashContext hash_init(sha256); } // 把分片数据写进哈希上下文 // 这里固定读取 1MB 的缓冲区避免一次性把整个分片加载到内存 while (!feof($handle)) { $buffer fread($handle, 1024 * 1024); if ($buffer ! false) { hash_update($hashContext, $buffer); } } fclose($handle); // 5. 更新上传任务表中的分片计数和哈希上下文的持久化状态 $chunkUploaded $task-chunk_uploaded 1; $task-chunk_uploaded $chunkUploaded; $task-hex_context serialize($hashContext); $task-status 1; // 6. 写入分片信息表 UploadChunk::updateOrCreate( [file_id $fileId, chunk_index $chunkIndex], [ chunk_size filesize($chunkPath), chunk_md5 $chunkMd5, chunk_storage_path $chunkPath, ] ); // 7. 判断所有分片是否上传完成 if ($chunkUploaded $task-chunk_total) { $task-computed_hash hash_final($hashContext); $task-hex_context null; // 上下文已经用完清空释放内存 $task-status 2; } $task-save(); return response()-json([ code 0, msg $task-status 2 ? 上传完成 : 分片接收成功, data [chunk_uploaded $task-chunk_uploaded], ]); }这段代码里有几个细节值得展开讲。第一个细节是hash_init()只应该在第一个分片到达时调用后续分片都是从数据库恢复上下文。恢复上下文后$hashContext对象里带着之前所有分片的数据累积状态继续hash_update()就能无缝衔接。第二个细节是hash_final($hashContext)调用之后$hashContext这个对象就被销毁了不能再继续喂数据。所以这个调用必须放在最后一个分片上传完成的判断分支里并且调用之后不能再让新的分片进入hash_update()循环。第三个细节是hash_init()和serialize()的兼容性问题。PHP 7.4 之后HashContext对象可以被serialize()序列化PHP 8.0、8.1、8.2 都验证过没问题。但如果你的项目还在老旧的 PHP 5.6 线上环境这条路走不通需要改用方案 A把每个分片的文件路径和分片 MD5 存下来最后合并时再按顺序统一计算完整哈希。3.4 合并分片与最终完整性比对所有分片上传完成后upload_task表里status已经是 2computed_hash也已经保存了增量计算的完整 SHA-256。但这里还有一个收尾步骤把磁盘上的分片合并成完整的视频文件用于后续的存储、回放和归档。合并操作本身很朴素按分片序号依次把二进制内容写入一个目标文件即可public function mergeChunks(string $fileId): void { $task UploadTask::where(file_id, $fileId)-where(status, 2)-firstOrFail(); $tempDir storage_path(app/upload_tmp/ . $fileId); $finalDir storage_path(app/videos/); if (!is_dir($finalDir)) { mkdir($finalDir, 0755, true); } $finalPath $finalDir . $fileId . .mp4; $outHandle fopen($finalPath, wb); if (!$outHandle) { throw new RuntimeException(无法打开目标文件); } for ($i 0; $i $task-chunk_total; $i) { $chunkPath $tempDir . / . $i . .part; if (!file_exists($chunkPath)) { fclose($outHandle); throw new RuntimeException(分片 {$i} 不存在文件不完整); } $inHandle fopen($chunkPath, rb); if (!$inHandle) { fclose($outHandle); throw new RuntimeException(分片 {$i} 无法打开); } // 用流式复制而不是 file_get_contents原因同前面 stream_copy_to_stream($inHandle, $outHandle); fclose($inHandle); } fclose($outHandle); // 合并完成后对最终文件做一次快速哈希比对确认合并过程没有损坏数据 $finalHash hash_file(sha256, $finalPath); if ($finalHash ! $task-computed_hash) { throw new RuntimeException(哈希校验失败合并后的文件与上传内容不一致); } }hash_file()在合并完成后又对完整文件读取了一遍那岂不是又出现了一开始提到的全量哈希开销确实合并后需要重新读一遍完整文件来做最终校验。但要注意这里的读取是验证合并过程不是验证传输过程。传输过程的验证已经通过增量哈希完成了合并过程是从多个分片拼装成一个文件理论上不会改字节但磁盘写入异常、断电、文件系统错误等极端情况仍可能导致数据不一致所以多一次全量读取的保险是有必要的。如果对性能特别敏感这个最终比对还可以做成异步任务合并完成先返回成功给客户端后台队列里慢慢做hash_file()比对。我在产线项目的实测中一个 3GB 的视频文件合并耗时约 20 秒hash_file()比对耗时约 15 秒异步化之后用户体验提升明显。写在这里可能已经有人反应过来了既然流式增量哈希在分片上传过程中已经完整地让数据流过哈希算法那最终比对时是不是可以不读整个文件只比对分片数量和数据大小理论上可以但工程上不推荐。数据分片在磁盘上的排列跟最终文件完全一致、分片大小总和等于文件总大小这些并不能 100% 保证合并后的文件可用中间一次 bit 翻转可能不改变文件大小也可能恰好不改变每个分片的边界。所以最终一次hash_file()比对是我强烈保留的校验步骤它能兜底所有中间环节的隐患。4. 生产环境的工程细节与异常处理实测中容易踩的坑4.1 Nginx 和 PHP 配置参数怎么调才不坑分片上传不是把代码写完就完事了运行环境的配置往往决定了方案能否真正跑起来。我在现场遇到了两个典型的坑都跟配置有关。第一个坑是 Nginx 的client_max_body_size限制。很多人以为只要 PHP 的upload_max_filesize调大就万事大吉结果分片请求一进来直接被 Nginx 拦截返回 413 Request Entity Too Large。Nginx 会先于 PHP 检查请求体大小默认才 1MB。分片方案里单个请求的分片大小是 5MB 或 10MB所以 Nginx 配置必须改# 单个分片最大 50MB结合业务需求留点余量 client_max_body_size 50m; # 请求体的读取超时时间农业环境网络波动大放宽到 10 分钟 client_body_timeout 600s;第二个坑是 PHP 的post_max_size和内存限制。虽然我们在代码里对流式读写了数据但post_max_size太小仍然会导致请求被 PHP 直接拒绝。推荐至少设置成post_max_size 60M比单个分片的最大值大一点即可。memory_limit倒是不需要跟着调大因为流式计算不依赖大内存保持 256MB 就行。还有一个必须提的配置是 PHP-FPM 的request_terminate_timeout。如果这个值设成 30 秒而分片上传在弱网环境下耗时超过 30 秒PHP-FPM 会直接 kill 掉 worker 进程导致分片上传到一半就失败。我建议设成 300 秒或更长确保一个分片在极端网络下也能完成传输。4.2 多进程并发分片请求哈希上下文状态在哪儿存这是方案 B 在生产环境落地最让人头疼的问题。PHP-FPM 默认是多进程模型同一个file_id的分片请求可能被负载均衡转发到不同的 PHP worker 进程。我前面代码里把哈希上下文序列化存到了数据库所以不同进程之间可以通过数据库这条共享通道传递状态这是可行的但有一个并发隐患需要额外处理。假设分片 1 上传时初始化了上下文并写入数据库分片 2 几乎同时上传读取数据库时拿到的是分片 1 写入的上下文。但分片 3 如果跟分片 2 并行上传两边同时读到了分片 2 的上下文就会产生覆盖问题——其中一次hash_update()的状态会丢失因为两个请求都基于同一个旧上下文各自做了更新后写库的覆盖先写库的。解决这个问题有两个思路。第一个思路是加锁。在更新上下文之前先对file_id对应的数据库行做行级锁。SELECT * FROM upload_task WHERE file_id ? FOR UPDATE;但行级锁要求两个并发的 PHP-FPM 请求都要访问同一个数据库连接而且事务隔离级别不同表现差异很大实现复杂度比较高还需要额外的超时重试机制。第二个思路更省事客户端串行上传分片。前端控制在同一时刻只发一个分片请求上一个分片得到服务端确认后再发下一个。这个方案对用户体验的影响其实很小——网络传输本身就是顺序依赖的一个分片传输过程中再并发传另一个分片并不会提高整体速度反而因为上下文覆盖问题导致哈希错误。我在项目里最终就是这么做的。实测中串行分片上传一个 3GB 的视频在车间局域网内大约耗时 4 分钟。并发分片可能把时间压到 3 分半但换来的是哈希计算的确定性这笔账非常划算。如果你确实需要并发分片加速那就不要用方案 B 的增量哈希直接回到方案 A每个分片独立算哈希最后合并后再整体算一次。4.3 断点续传和重传场景下的上下文一致性断点续传是汽车制造场景里的刚需。车间网络不稳定一个 3GB 的视频分片排在前面某个位置传失败了总不能让操作工重新来过。我在代码里通过upload_chunk表来判断哪些分片已经上传成功客户端查询后跳过这些分片重传失败的部分。但断点续传对增量哈希方案有一个隐蔽的影响重传分片时hash_update()会把同一个分片的内容再喂一遍哈希上下文。假设分片 5 第一次上传成功数据已经写进上下文并持久化到了数据库后来因为某种原因客户端重传了分片 5代码里没有做去重判断就会把分片 5 的数据重复写入哈希上下文最终算出来的computed_hash跟源端哈希对不上。处理办法是在分片落地之后、更新上下文之前先判断upload_chunk表中是否已经存在这个分片记录$existChunk UploadChunk::where(file_id, $fileId) -where(chunk_index, $chunkIndex) -first(); if ($existChunk) { // 分片在服务端已存在这里直接返回不再重复写入哈希上下文 return response()-json([code 0, msg 分片已存在]); }这个分支的另一个作用体现在“客户端拿到确认后网络超时服务端其实已经写成功”的场景。如果不需要重传客户端会认为传输失败而重发服务端直接定位到已存在分片并返回成功即可同时保证哈希上下文不会被污染。4.4 临时文件的磁盘空间与清理机制分片上传会把所有分片先写到临时目录全部上传完成后再合并。如果产线高峰期同时有 10 个 3GB 的视频在上传临时目录峰值占用就是 30GB 以上不加控制的话磁盘很快就满了。我设置了双保险。第一层是上传任务状态控制合并完成并验证通过后立即删除临时目录里的所有.part分片文件。第二层是兜底定时任务每天凌晨检查upload_tmp目录删除创建时间超过 24 小时且对应任务不是“上传中”状态的临时文件。因为正常的传输流程不会让分片在临时目录里躺超过一天超过的要么是残留垃圾要么是打开任务但一直没有完成的废弃数据。定时清理的脚本我用一个简单的 Shell cron 实现find /data/storage/app/upload_tmp -type f -mtime 1 -exec rm -f {} \; find /data/storage/app/upload_tmp -type d -empty -mtime 1 -exec rmdir {} \;清理掉那些“孤儿”临时文件之后还需要清理数据库中对应的upload_chunk记录这个可以在 PHP 的定时任务里处理按文件 ID 关联删除。加这一层的原因是防止upload_task表里残留大量 status3 的失败任务时间久了表数据膨胀影响查询效率。4.5 实测性能数据与资源占用项目上线后的实测数据我整理了一张表方便大家预估自己的场景能不能接受。视频文件大小总耗时局域网增量哈希阶段额外耗时合并后 hash_file 校验耗时峰值内存占用1GB约 1 分 10 秒约 2 秒约 6 秒2.5MB3GB约 4 分 05 秒约 6 秒约 15 秒2.5MB5GB约 6 分 50 秒约 11 秒约 23 秒2.5MB增量哈希的额外耗时主要花在hash_update()的 CPU 计算上。我用的是 SHA-256 算法实测吞吐量大约 500MB/s 到 800MB/s跟 CPU 主频和是否支持 SHA 指令集有关。现代 Intel 和 AMD 处理器基本都支持 SHA-NI 指令集PHP 的 hash 扩展会自动利用所以这个额外耗时比很多人想象中低得多——3GB 文件全部喂进哈希计算器大约只要 5 秒。内存方面因为始终用的是 1MB 的缓冲区流式读取加上HashContext自身占用的几百字节整个 PHP 进程的峰值内存增量可以控制得很低。这对于云上容器化部署、内存配额吃紧的场景非常友好。5. 扩展到其他校验场景的经验不只是视频文件能用这套方案这套“分片上传 流式增量哈希”的组合拳实际上是一个通用的数据完整性验证框架。在汽车制造企业里我发现它能复用的场景远不止质检视频。焊装机器人的离线程序包、涂装车间的工艺配方文件、总装线的电子作业指导书 PDF这些文件的体积虽然比不上视频但是对完整性的要求一点也不低。工艺配方文件哪怕一个字节损坏可能导致整条喷涂线颜色参数调错后果非常严重。把它们从文件服务器同步到边缘节点时我用同样的增量哈希方案做校验效果很好。还有一个典型场景是产线数据上云。整车厂每个月要上传一批车辆下线数据到集团数据中心单个批次打包压缩后经常有 10GB 以上。过去用 FTP 传输 校验清单的方式数据对不上的情况时有发生。改造后客户端按 20MB 分片、服务端流式哈希整个过程可以全自动化失败分片自动重传最终哈希不一致自动告警运维人员从手工对账中解脱了出来。这套方案的扩展方向有几个值得琢磨的点。第一哈希算法可以按需切换。SHA-256 在安全性和性能之间比较均衡如果企业内部安全要求不那么高追求速度可以换成 MD5 或 xxHash。PHP 的hash_init()支持传入算法名切换成本极低。但强烈不建议在同一个业务里混用两种算法否则排查共识会和历史数据对不上。第二可以接入消息队列做异步校验。分片接收和合并的操作都比较重如果上传频率高建议把mergeChunks和hash_file比对放到 Redis 队列或 RabbitMQ 里异步执行API 接口只负责确认分片落盘然后立即返回给客户端。这样接口的响应时间能控制在毫秒级客户端体感非常流畅。第三文件归档到对象存储或 NAS 时可以先算清楚哈希把哈希值作为文件名的后缀一起入库。比如QC_20240612_aoi_line3_a1b2c3d4....mp4将来做防重、比对、审计都非常方便不需要再重新读一遍文件算哈希。回到开头的汽车制造案例整套方案上线之后月底质检视频集中传输的问题彻底消失了。操作工在系统里上传完一个 3GB 的视频界面上能看到实时进度传完之后的完整性校验结果自动展示。鉴权链路也不再因为超时或者哈希不一致导致视频必须重拍重传质量管理部门对这套系统的信任度明显提升了。如果让我给后来者一个最实在的建议不要一上来就撸代码先把文件大小分布、网络环境稳定性、并发上传量这三个指标摸清楚。文件平均多大、网络掉包率多少、同时上传的终端有几台这三个答案基本决定了你该选方案 A 还是方案 B也决定了分片大小设多少合适。以我的经验分片 5MB 到 10MB 是黄金区间网络差就调小网络好就调大但尽量不要超过 50MB因为单个分片越大Nginx 和 PHP 的配置要求就越高失败重传的成本也越高。最后分享一个小技巧在分片上传接口里打日志时记得记录每个分片的chunk_index、chunk_size和耗时。排查哈希不一致问题时有了这三个字段你能很快定位到底是哪个分片出了问题。这个日志在项目初期可能觉得是多余的开销但到了线上跑一个月再回看你会发现它就是救命的排查线索。