
凌晨一点产线值班同事打电话给我探针台的测试视频传到一半又断了FTP服务器上躺着一堆半截文件第二天良率会议等着用。这种场景在半导体工厂做过数据平台的人应该都不陌生。芯片从晶圆到封装每一步测试环节都会录下大量视频探针压到焊盘上的接触过程、封装件被顶进测试座时引脚弹跳的画面、AOI检测捕捉到的可疑缺陷。这些几十分钟的视觉记录解压后动辄就是2GB到10GB。更要命的是视频内容直接暴露芯片版图、批次信息和工艺缺陷属于工厂内部高敏感数据不能随便走明文通道也不能靠U盘拷贝。后来我彻底放弃FTP和网盘改用HTMLPHP搭了一条专门跑在车间内网的加密传输链路。思路很简单前端用HTML5 File API把大视频切成若干分片逐片做AES加密再通过HTTPS通道传到PHP后端后端校验分片完整性、拼接合并最后归档到加密存储区。这套方案不依赖商业软件适合半导体制造、封测工厂里做数据回传、良率分析、MES周边系统的开发和IT工程师参考。我会把前端分片、PHP接收合并、AES-GCM加密、密钥管理以及我在车间部署过程中踩过的坑全部拆开讲。1. 先想清楚问题半导体测试视频为何必须走分片加密链路1.1 视频从哪来测试工序里的视觉证据在芯片制造流程里测试不是最后一道工序而是贯穿晶圆制造、封装、成品出厂全过程的质量闸口。CP测试Chip Probe晶圆测试时探针台会控制探针扎到晶圆上每一个Die的Pad上探针下方的高倍CCD相机记录整个扎针动作和接触过程。这段视频有什么用举个例子一批芯片电测异常失效分析工程师要回看探针接触的瞬间判断是不是发生了虚接、探针磨损或是晶圆表面有异物导致接触异常。封装后的FT测试Final Test同样依赖视频。芯片被机械臂顶进测试座Socket里的弹片反复接触引脚如果接触瞬间发生弹跳哪怕只有几十毫秒也可能造成电测误判。这时候测试录像就是最直接的证据比测试日志更直观、更有说服力。外观检测环节的AOI设备也会产生大量高分辨率图片和视频流用来复判焊点、金线、封装表面有无缺陷。这些数据有的来自测试机台自带相机有的来自独立检测设备最终都要回到良率分析平台或者MES系统做归档。可以说没有这些视频工程师的很多结论都只能靠猜。1.2 这类数据的三个硬特点我把测试视频的数据特征归纳为三个词大、多、敏感。单个文件非常大。一次完整lot的CP测试视频可以达到2GB到10GB如果测试机台的相机分辨率再往上升文件会更大。一个中等规模的封测厂每天新增的视频数据量以TB计是很正常的事情。文件数量非常多。一个车间里几十台测试机台三班倒生产每个班次都会产出大量视频。这些文件不是一次性的往往要按批次保留数月甚至数年做追溯。内容高度敏感。视频画面里会有芯片的版图布局、晶圆批次号、测试参数、缺陷形态这些都是半导体企业重点保护的信息。很多公司对这类数据有明确的DLP策略禁止明文落地、禁止私自带出。1.3 直接整包传输为什么不行如果只是把文件搞过去方案早就有了但直接传会有三个绕不开的问题。第一FTP是明文协议。用户名密码和文件内容都在网络里裸奔生产网虽然相对隔离但不是没有人抓包。一旦内网有异常流量分析工具FTP账号和数据内容都会暴露。而且FTP对断点续传的支持并不友好几十个客户端同时连上来服务器负载和磁盘IO很容易被打满。第二单次HTTP大文件上传不稳定。把一个2GB的文件一次性POST提交浏览器要长时间维持同一个连接Nginx要等待请求体完整接收PHP后端也要一次性接收全部数据。任何一个环节超时或闪断这次上传就白费了重头再传一遍。会话过程中服务器内存和临时目录的压力也很大。第三U盘和移动硬盘在芯片工厂属于灰色地带。严格的数据防泄漏政策不允许员工私自拷贝尤其涉及新产品、未量产的芯片型号。所以分片上传、加密传输、断点续传这三个需求不是Web开发者凭空想出来的噱头而是半导体产线环境下的真实刚需。1.4 为什么选HTMLPHP而不是更重的方案有人可能会问这种场景不是应该上Java、Go或者直接买商业MFT软件吗我的考虑很实际。前端用HTML5是必须的。产线上的工控机大部分是Windows系统浏览器升级到新版Chrome或Edge即可不需要装任何客户端软件。相比之下Java Applet、ActiveX那套早就该淘汰了还要维护一堆运行环境光是版本冲突就够头疼的。后端用PHP不是因为PHP比其它语言更先进而是因为在这种工厂内网场景里它足够顺手。NginxPHP-FPM是极其成熟的组合部署维护门槛低和MES、质量系统的HTTP接口打通也方便。单机承受几十台测试机台的同时上传PHP完全扛得住。真正决定系统质量的不是语言本身而是分片、校验、加密这套逻辑设计得对不对。2. 全链路架构从测试机台到服务端的传输拓扑与分层加密设计2.1 服务端放在哪里、网络怎么走半导体工厂的生产网通常和办公网隔离测试机台在洁净车间里网络接入属于生产网段。我部署的Web服务放在车间附近的辅助机房通过Nginx对外提供两个入口一个是上传页面的HTTP访问一个是接收分片的HTTPS接口。数据库和文件存储不直接暴露给车间网段。整体数据流是浏览器工控机加载上传页面选择本地视频文件前端分片完成后逐片提交到HTTPS接口Nginx把请求转给PHP-FPMPHP把分片写入临时目录等所有分片到齐后合并成完整文件再经过存储层加密移到归档目录最后把元数据写入MySQL。这个链路里没有复杂的微服务也没有中间件绕来绕去每跳一步都是可控的。工厂运维环境里少一个组件就少一个出问题的环节。如果你的车间规模更大可以在这个基础上把文件归档改接到对象存储但整体结构保持一样。2.2 三层加密设计各管一段别想互相替代我把加密拆成三层每一层解决的问题不一样不能相互替代。第一层是TLS传输加密。页面和接口都必须走HTTPSTLS协议版本至少1.2。这层解决的是数据在网络上传输时被别人监听的问题。注意它只管传输过程一旦数据到了服务器TLS就管不到了。第二层是应用层AES加密。视频分片在浏览器端就完成加密网络上出现的始终是密文。这样做最大的价值是即使某个内网设备对流量做了镜像分析或者HTTPS被某个网关解密后记录到日志对方看到的也不是能直接播放的视频内容。第三层是存储层加密。合并后的完整视频文件用独立的存储密钥再次加密后落盘。这样即使有人拿到了服务器硬盘或者运维人员误操作把文件拷贝到别的机器上没有密钥也打不开。三层加密之间的关系我用一句话概括TLS防链路嗅探AES防数据在合法通道里被中间环节碰触存储加密防磁盘失窃。把希望全部压在HTTPS上在半导体数据这种敏感场景里是不够的。2.3 元数据和业务数据分离视频文件本身是业务数据但日常查询、追溯、清理流程靠的是元数据。我把这两类数据彻底分开管理。文件系统和对象存储只负责存放加密后的视频文件MySQL里记录一条元数据记录包括测试批次号、机台ID、测试类型CP/FT/AOI、晶圆ID、上传用户、上传时间、文件大小、总分片数、最终文件SHA256、使用的密钥ID。这样的设计让追溯变得非常高效。工程师想查某个批次的所有测试视频先查数据库拿到文件路径再根据密钥ID去密钥服务取出对应的解密密钥。不用在文件系统里一层一层翻目录也不用把所有视频扫一遍。对半导体行业而言元数据和文件分离还有一个隐形的好处做数据生命周期管理时可以先把过期批次在数据库里标记为待清理确认审计期满后再删文件避免误删。3. 前端分片怎么切HTML5 File API与断点续传的实现细节3.1 分片大小不是拍脑袋按照带宽来算前端分片时第一个问题是每片切多大我见过有人把整片设为50MB结果上传频繁超时也有人把分片设为256KB结果请求数量暴增服务器PHP进程被频繁占满总吞吐反而下降。分片大小的设计逻辑是单片上传耗时尽量控制在3到5秒。如果车间内网实际可用带宽是20MB/s5MB分片大约需要0.25秒确实偏小如果带宽只有2MB/s5MB分片需要2.5秒还在合理窗口内。所以我的默认值是5MB并提供配置项根据网络状况动态调整。视频文件动辄几个GB时5MB一片意味着产生几百到上千个分片这个数量级对服务器来说完全可接受关键是并发要控制住。3.2 核心切分逻辑File API Blob.slice在浏览器端切分视频的核心代码其实很短const file uploadInput.files[0]; const chunkSize 5 * 1024 * 1024; // 5MB const chunks []; let start 0; while (start file.size) { chunks.push({ index: chunks.length, blob: file.slice(start, Math.min(start chunkSize, file.size)) }); start chunkSize; }Blob.slice()返回的仍然是一个Blob对象可以直接作为后续加密和上传的数据源。这个API在主流浏览器上都很成熟但有一点必须提醒老版本Safari可能要求使用webkitSlice或mozSlice上线前建议直接在产线浏览器上做一次兼容测试。我建议直接把浏览器升级到新版Chrome或Edge能省掉大量兼容性工作量。3.3 并发控制并发设成多少才合理分片切完之后如果一次性把几百个分片全发出去服务端会吃不消。原因有三个PHP-FPM进程数量有限每个请求都要占用一个worker每个分片请求都要经历磁盘写入并发太高会把磁盘IO打满Nginx的连接数也会成为瓶颈。我的做法是在前端控制并发数量用一个简单任务队列始终只有4个上传请求在同时进行async function uploadChunks(chunks, limit 4) { let index 0; const workers []; for (let i 0; i limit; i) { workers.push((async () { while (index chunks.length) { const chunk chunks[index]; await uploadOne(chunk); } })()); } await Promise.all(workers); }并发数取3到5是经验值。太少了浪费带宽太多了容易拖垮服务器。尤其是当车间有多台机台同时上传时每台机台的并发加起来才是服务器实际承受的并发量所以不能只看单客户端的数字。3.4 断点续传以服务端记录为准别依赖本地存储断点续传是这套系统里用户感知最明显的功能。如果传输到一半网络断了、页面刷新了、甚至电脑重启了用户最不希望看到的就是从头再传一遍。有些前端方案用localStorage记录已经传完的分片索引但问题很明显换了浏览器、清了缓存、或者换一台机器记录就丢了。我的做法是让后端当账本先生前端在创建上传任务时后端生成一个file_id并记录任务状态前端每次上传前调用查询接口拿到后端已接收的分片索引集合上传时自动跳过这些分片只补传缺失的部分前端进度条按已传分片数占总分片数的百分比显示而不是只看当前这一片的进度。这样无论用户刷新多少次页面只要服务端的分片还在就能无缝续传。当然服务端必须配套做过期分片清理否则那些永远不完整的任务会一直占用磁盘空间。4. PHP后端如何接住分片接收、校验、合并的关键代码逻辑4.1 接收分片基础字段和PHP配置项前端上传分片时我设计的POST字段是file_id、chunk_index、chunk_total、chunk_hash、file_hash、token。其中token是上传任务的唯一凭证后面细说。分片二进制数据放在$_FILES[chunk]里。PHP接收分片的代码核心是这样的$fileId $_POST[file_id]; $index (int)$_POST[chunk_index]; $total (int)$_POST[chunk_total]; $tmp $_FILES[chunk][tmp_name]; // 分片完整性校验必须先做 $hash hash_file(sha256, $tmp); if ($hash ! $_POST[chunk_hash]) { http_response_code(400); echo json_encode([error chunk hash mismatch]); return; } $dir sys_get_temp_dir() . /upload_ . $fileId; if (!is_dir($dir)) { mkdir($dir, 0750, true); } move_uploaded_file($tmp, $dir . / . sprintf(%08d, $index)); echo json_encode([saved true]);这里有几个配置项是整套方案能不能跑起来的基础很多人第一次部署时就是栽在配置上。PHP的upload_max_filesize默认只有2MB如果分片是5MB请求直接失败。post_max_size默认8MB也要同步调大。max_execution_time默认30秒接收大分片时可能不够。Nginx的client_max_body_size默认1MB更是必须改。我在生产环境用的配置是upload_max_filesize 20M post_max_size 25M max_execution_time 300Nginx对应配置client_max_body_size 25m;基本原则是一个分片的大小再加点余量不要配置成无限大否则等于把服务器变成一个公开的垃圾桶。4.2 合并逻辑用流式写入避免内存爆炸所有分片都到齐后前端会调用一个触发合并的接口。合并最忌讳的做法是把所有分片一次性读进内存再拼成一个数组然后写文件。视频如果是30GBPHP内存分分钟被撑爆。我的合并接口用流式方式逐片打开、逐片拷贝$fullPath $finalDir . / . $fileId . .mp4; $out fopen($fullPath, wb); for ($i 0; $i $total; $i) { $part $tmpDir . / . sprintf(%08d, $i); if (!file_exists($part)) { fclose($out); echo json_encode([error missing chunk {$i}]); return; } $in fopen($part, rb); stream_copy_to_stream($in, $out); fclose($in); } fclose($out);这段代码里最值得注意的一点不管分片大小是5MB还是50MB内存占用都只取决于当前这一片缓冲区的实际大小而不是整个文件的总大小。合并完成后还需要用hash_file(sha256, $fullPath)重新计算整个文件的哈希和前端最初提供的file_hash比对。不一致就说明某些分片虽然单独校验通过了但合并过程仍然有问题这时候宁可删除重传也不要直接入库。4.3 上传令牌每个分片都要验明身份半导体工厂内网虽然相对隔离但Web接口一旦被扫描到任何人都可以往服务器上POST数据。为了防止伪造上传我在创建任务时生成一个随机令牌和file_id、上传用户、过期时间绑定存入数据库。之后每个分片请求都必须携带这个token否则直接拒绝。$token bin2hex(random_bytes(32));使用random_bytes而不是mt_rand原因很简单上传令牌是要防伪造的伪随机数在安全场景下不可接受。令牌的过期时间一般设为24小时超过时间任务作废分片目录清空。4.4 幂等处理重复分片、缺片和合并加锁分片网络传输过程中前端可能因为超时重发同一片后端可能已经保存了该片。所以接收接口要对同一个chunk_index做到幂等如果临时目录里已存在相同index且哈希一致直接返回成功不再重复写盘。合并操作还需要防止两个并发请求同时触发。我用了一个简单的文件锁$lockFile $finalDir . / . $fileId . .lock; $lock fopen($lockFile, c); if (!flock($lock, LOCK_EX | LOCK_NB)) { // 已有合并任务进行中 fclose($lock); return; }这个锁保证了同一个file_id同一时刻只有一个合并进程。缺口、重传、锁这三件事处理好分片上传才算真正可靠。5. 加密不止HTTPS应用层AES与密钥管理的落地选择5.1 为什么要做应用层加密HTTPS不够吗这是我在分享时被问到最多的问题。如果你的认知是公司内网是可信的HTTPS就够了在半导体行业可能要重新考虑。内网并不等于绝对安全镜像端口、运维审计系统、第三方远程支持通道都可能让流量记录落到不被你控制的地方。HTTPS一旦在某个网关或WAF节点被合法解密后面的数据就变成明文日志了。应用层加密让每一片视频在进入网络之前就变成密文即使日志记录了请求内容和响应内容也只是密文。这是对所有中间环节的一层兜底。另外采用带认证的AES-GCM模式还可以在服务端检测出分片是否被篡改、被替换、被乱序这正好对应前面强调的分片完整性需求。5.2 前端加密用Web Crypto API而不是自研算法前端加密我推荐使用浏览器原生Web Crypto API而不是引入CryptoJS或自研加密算法。理由很直接原生API经过浏览器厂商的审计和硬件加速性能更好安全性也更有保障。唯一需要注意的坑是Web Crypto API只在HTTPS环境下可用。如果你的页面是HTTP访问crypto.subtle很可能是undefined。所以HTTPS对这套系统不是可选项而是必须项。关键代码片段如下async function encryptChunk(key, chunkData, fileId, chunkIndex) { const iv crypto.getRandomValues(new Uint8Array(12)); const aad new TextEncoder().encode(${fileId}:${chunkIndex}); const ciphertext await crypto.subtle.encrypt( { name: AES-GCM, iv, additionalData: aad }, key, chunkData ); return { iv, ciphertext }; }这里面的additionalDataAAD非常关键。它把file_id和分片序号和密文绑定在一起。攻击者如果试图把A文件的分片挪到B文件或者调换同一文件里两个分片的顺序服务端解密一定会失败。这一点对上一章节提到的分片账本是一个很好的加密层补充。5.3 PHP端解密openssl_decrypt的正确姿势服务端收到前端传来的iv、tag和密文后用PHP的openssl扩展解密。代码大概是这样的$ciphertext $payload[data]; $iv $payload[iv]; $tag $payload[tag]; $aad $fileId . : . $chunkIndex; $plaintext openssl_decrypt( $ciphertext, aes-256-gcm, $dataKey, OPENSSL_RAW_DATA, $iv, $tag, $aad ); if ($plaintext false) { // 解密失败分片要么被篡改要么密钥不对 echo json_encode([error decrypt failed]); return; }这里最容易被忽略的是$tag。AES-GCM在加密时会生成一个16字节的认证标签这个标签必须随密文一起传给服务端。如果漏传tag或者tag不匹配openssl_decrypt会直接返回false。我建议前端提交分片时把密文格式固定为[12字节IV 16字节TAG 密文]服务端按偏移量截取避免JSON里反复做base64编码带来额外开销和出错概率。5.4 密钥管理数据密钥和主密钥不能放一起应用层加密的密钥不能写死在前端JS里。我采用的方式是每次上传任务开始时后端生成一个一次性的数据密钥用主密钥加密后下发给前端前端内存中持有这个数据密钥用它加密所有分片后端解密后把数据密钥用独立的存储密钥再次加密与元数据放在一起。这里的关键原则是数据密钥、主密钥、存储密钥三者不能同时出现在同一个地方。主密钥放在独立的密钥服务或者加密机里Web服务器就算被攻破拿到的也只是被加密过的数据密钥没有主密钥照样解不开。6. 工厂环境实战我在部署这套方案时踩过的坑和根因分析6.1 第一个坑分片设成50MB结果请求频繁超时我最初把分片大小设为50MB觉得分片越少、请求越少、逻辑越简单。结果上线第一天就发现上传任务总是中途失败。排查链路是这样的先在Nginx错误日志里看到大量client timed out再翻PHP-FPM日志又看到max_execution_time exceeded。实测一个50MB分片从车间传回服务器在高峰期需要30秒以上超过了Nginx默认的fastcgi_read_timeout配置。当时的修复方案有两步把分片降到5MB并把Nginx相关超时参数调到300秒。降分片后单片耗时降到1到3秒超时问题基本消失。这个教训让我意识到分片大小不是一个随心所欲的数值它要和超时配置、并发数一起考虑。6.2 第二个坑临时目录爆满磁盘被写满导致服务全挂分片上传时所有分片都落在PHP服务器的临时目录里只要任务没合并或者合并失败分片就会一直留在那里。某一次几十台机台同时产生上传任务临时目录很快就涨到40多GB直接把系统盘写满了。排查时用du -sh /tmp/*一眼就看到/tmp/upload_xxx占了大量空间。那之后我做了三件事上传成功合并后立即删除临时分片目录每个上传任务写入一行状态记录超过24小时未完成的任务由脚本定时清理临时目录单独挂载一块独立磁盘避免拖垮系统盘。如果你也想复现这套方案建议一上来就把分片目录和最终归档目录放在不同的磁盘分区千万别跟系统盘混在一起。工厂服务器一旦磁盘写满影响的不只是一个传输服务可能是整条产线的数据采集链。6.3 第三个坑合并后的视频播放到某段时间花屏排查了一整夜这是整套系统里最隐蔽的一个问题。有一个分片在网络闪断时前端浏览器认为上传成功后端也返回了200但实际上传的数据只有完整分片的一半。当时接口没有做分片哈希校验后端照单全收。合并后视频整体能播放但播放到第23分钟开始花屏、解码失败。我按这条链路排查用ffmpeg -v error file.mp4 -f null -定位损坏位置根据时间点估算对应的分片序号检查该分片文件大小发现只有正常分片的一半翻上传日志看到那一片前端确实返回了200但日志里记录的实际接收字节数和预期不符。根因是浏览器在发送缓冲区尚未完全写完时连接就断了前端却误判为成功。修复方案分两层前端在分片发送前计算一次该分片的SHA256后端接收后立刻重新计算并比对不一致直接报错合并完成后整体再算一次file_hash和前端初始计算值比对。从那以后这类悄悄损坏再也没有出现过。分片上传不是把文件切碎传上去这么简单每一片的完整性校验都不能省。6.4 第四个坑合并大文件时PHP内存溢出早期合并视频时我用的是最省事的写法file_get_contents读一个分片然后file_put_contents追加到目标文件。当时还觉得每一片才5MB内存应该没问题。等到第一次接收30GB级别的视频PHP进程直接OOM整个合并任务失败。改用stream_copy_to_stream之后不管分片多大单进程内存占用始终保持在几十MB以内。这个经验其实不新鲜但每次都会有人踩进去处理大文件永远用流永远别把数据一次性铺在内存里。6.5 跨网段和多网卡IP白名单为什么靠不住车间工控机的网络环境比办公网复杂得多很多机器有多个网卡一个连测试设备一个连文件服务器。我最初用IP白名单来保护上传接口结果发现同一个工位今天能传、明天不能传因为系统拿到了不同的网卡地址。后来和网络组协同改用端口级限制和交换机准入策略才真正解决了这个时好时坏的问题。所以做工厂内网服务时不要把IP白名单当成唯一的访问控制手段尤其车间网段经常有地址规划调整。7. 留痕与合规审计日志、元数据管理以及后续演进7.1 每一次传输都必须留痕半导体行业对数据追溯的审计要求非常严格。每个测试视频从产生、上传、合并、存储、查看、删除都要有完整日志。我在上传接口里记录的关键字段包括操作员账号、机台ID、车间位置、文件ID、原始文件名、文件大小、任务开始时间、完成时间、总分片数、失败重传次数、最终文件SHA256、加密算法标识。这些日志的价值在出现质量事故或安全事件时最能体现。比如工程师怀疑某批视频被替换过直接查数据库里文件SHA256和当前计算值是否一致几秒就能出结论。没有这套记录光靠人工比对几十个GB的视频文件工作量无法想象。7.2 和MES/EAP系统的衔接视频文件上传成功之后不能只躺在服务器上。我会通过消息队列或直接调用MES接口把该批次测试视频的归档信息同步过去携带的内容包括文件ID、测试批次号、机台ID、视频时间范围等。这样良率工程师在MES里点开某个批次可以直接跳到视频播放页面而不用跑到文件系统里层层翻目录。从系统集成角度看这也是HTMLPHP方案最顺手的地方。PHP写一个回调脚本、对接一个HTTP接口都非常方便不需要引入复杂的企业服务总线就能和现有系统完成数据握手。7.3 后续演进转码、抽帧、对象存储这套方案上线后我又陆续做了几个优化。第一是转码抽帧大视频直接归档很占空间我按需用ffmpeg抽关键帧生成预览图工程师不用下载整个视频就能初步判断缺陷形态。第二是冷热分离近期频繁访问的视频放高速存储老批次迁移到低成本归档区。第三是规划对象存储分片上传逻辑完全可以兼容对象存储由PHP负责调度和元数据管理把本地磁盘IO压力卸到对象存储服务上。不过这些属于优化层。底层最核心的分片校验、加密传输、断点续传、审计日志才是这套系统真正不能出错的部分。任何一次数据损坏或者丢失在半导体追溯场景里都可能是严重事故。项目做完之后我给自己团队定了一条规矩凡是涉及大文件传输校验和审计永远不能省。加密算法可以换密钥管理可以调但分片哈希校验、AAD绑定、完整日志这三件事必须从一开始就做进去。如果你也打算在工厂内网搭类似系统我建议先跑一个最小版本前端分片、后端合并、HTTPS传输确认链路通了再补AES应用加密、断点续传、MES对接。不需要一开始就追求复杂架构把核心链路打磨扎实后面加功能会顺畅很多。