工程建筑视频上传实战:PHP分片上传与分布式存储设计
工程建筑行业做视频上传,跟在互联网产品里传短视频完全是两回事。施工现场的工序验收视频、塔吊运行监控、监理巡检记录,单段时长动不动就超过二十分钟,分辨率再往上走,一个文件一两GB是家常便饭。又赶上工地现场网络条件复杂,上行带宽经常只有几Mbps,还存在点位漂移、4G信号不稳的情况。传统方式让用户把整个文件一次性丢给PHP接口,基本可以预见结果:传一半断掉、重传、再断、用户投诉。
当初接到这个需求时,标题问的其实是两个问题合在一起:工程建筑行业背景下,视频该用什么方式传到服务端,服务端又该怎么把文件存到分布式存储里。很多人会把注意力完全放在“分片”和“分布式存储”这两个技术词上,但真做下来你会发现,工程行业现场的约束条件才是整个设计的起点。这篇文章把我的设计过程和最终定下的参数直接摊开讲,如果你正在做建筑工地视频回传、项目资料归档、施工过程留痕这类系统,可以直接参考里面的接口时序、字段设计、存储规划,以及我在线上踩过的坑。
1. 工程建筑视频上传的第一个认知:这不是容量问题,是一个场景问题
1.1 工地视频的类型和它们对系统提出的要求
工程建筑行业里的视频,大概能分成几类:隐蔽工程验收视频、施工进度记录、安全晨会和交底录像、塔吊和升降机等大型设备的运行监控、监理日常巡查等等。每一类的采集场景都不一样,但对系统的要求很相似。
隐蔽工程验收视频往往在钢筋绑扎完成、混凝土浇筑之前拍摄,拍摄位置可能在地下室、基坑里,部分区域信号很差。这种视频内容重要,一旦遗失会直接影响后续验收和结算。设备运行监控视频是长时间连续录制,一个项目几十台塔吊,每台每天产生大量视频,而且这些视频不能随便覆盖,将来一旦出现安全事件,需要追溯当时的完整画面。
监理巡查视频则更碎片化,经常是现场随手拍一段,然后在项目部或者路上用手机上传。你还要考虑网络切换,可能刚才是施工现场的Wi-Fi,走到电梯里就变成4G,一断网整个连接就没有了。
这种业务场景决定了视频系统必须具备几个能力:大文件可断点续传,弱网环境下能稳定上传;视频和所属项目的对应关系要清楚;文件保存周期要足够长,不只是存三个月,而是要跟随项目整个生命周期,甚至到项目结束、质保期结束之后还能调出来。
1.2 直接整文件上传在工程场景中为什么站不住
早期很多工程类信息化系统用的是普通表单上传,或者简单的HTTP PUT整文件上传。项目刚起步时视频量少,看起来没有问题。等到实际项目铺开,问题马上暴露。
最直接的问题是超时。PHP-FPM默认的max_execution_time一般是30秒,nginx转发给PHP的proxy_read_timeout一般也就60秒。一个500MB的视频在工地2Mbps上行带宽下,理论耗时都将近34分钟,无论如何都超过任何服务端超时时间。你可能会说自己改超时时间,但改长了之后又面临新问题:一台服务器长时间占据一个PHP进程,上传还没结束,其他请求全部堵塞。
其次是存储空间无法规划。所有文件都落在业务服务器本地磁盘,或者挂载到NFS网络磁盘。业务服务器磁盘满掉,整个业务宕掉。视频文件分散,没有统一命名规则,久了之后想清理、想迁移,根本无法追踪。
还有中断恢复的体验。工程现场的人不会操作复杂系统,传了一小时最后告诉你“上传失败,请重新上传”,人直接会炸掉。视频不是一段文本,重来成本太高。整文件上传在这个场景里先天就不成立,这不是技术选型问题,是产品形态问题。
1.3 分片上传和分布式存储在这个场景里各自的边界到底在哪里
我后来把两个问题拆开想:分片上传解决的是“传输可靠性”,分布式存储解决的是“存储扩展性和归属性”。这两个问题不能混在一起设计。
分片上传的核心逻辑是:把一个大视频横向切成若干小片,客户端一片一片传,传完的片由服务端记录下来,客户端断网之后能从最后一片未传的位置继续传,而不是从头再来。这里的重点在于“可恢复”和“可校验”。至于小片最终落到哪个磁盘,分片上传并不关心。
分布式存储的核心逻辑是:视频文件不能只存在某一台服务器的本地目录里,而应该存到具备水平扩展能力的存储集群,或者云对象存储。文件的位置是一个寻址路径,而不是一个写死的目录。它在整个设计里的职责,是保证文件不会因为一台机器故障或者磁盘损坏而永久丢失,并且容量能随时扩容。
这两件事单独拿出来都不难,难的是在一个PHP为主的后端体系里把它们衔接好。视频分片从哪里上传、PHP要不要经手视频字节、分片信息记录在哪、什么时候触发合并、合并后的文件存到什么位置,这一整条链路,才是真正需要设计的地方。
2. 系统骨架设计:把“上传链路”和“存储策略”分开思考
2.1 客户端到PHP再到存储的链路里,哪些环节会变成瓶颈
我先画了个链路图,虽然只是一张草稿,但对整个设计帮助很大。链路大致是:施工现场的App或监控设备端首先发起上传,经过现场网络、公网入口,到达后端的API层,API层再调用存储服务,最终把数据写到分布式存储节点。这里面真正容易成为瓶颈的,不是分布式存储本身,而是PHP应用层和网络出口。
PHP作为动态语言,处理字符、业务逻辑和状态流转很快,但让它承担大量视频字节流的搬运是浪费。一个PHP-FPM进程最多处理一个请求,视频每秒搬运几MB数据,内存占用高,进程被长时间占住,并发一上来整个接口集群就会瘫掉。另一个瓶颈是集中出口带宽。所有工地的视频都汇聚到一台服务器,再由这台服务器转存到分布式存储,出口带宽会成为唯一通道。这个带宽一旦满了,任何分片算法都解决不了。
所以我在设计上定了两个原则:第一,PHP只负责生成上传凭证、记录分片状态、发起合并指令,不直接接收和转发视频二进制数据;第二,客户端直连分布式存储的上传端点,PHP通过预签名URL或上传凭证来控制权限。如果你们的存储方案是自建MinIO或云OSS一类的对象存储,这个模式几乎是标准的。
不过话说回来,如果项目里用的不是对象存储,而只是分布式文件系统,比如HDFS或者自研文件集群,客户端不能直连上传,那就得有一层Gateway来支持HTTP分片上传。这种场景下Gateway接收分片后写入分布式存储,Gateway由PHP封装成接口。简单说,不要让PHP进程变成文件的搬运工,要把它变成行为的决策者。
2.2 分片直传与PHP中转的取舍,不能只按技术惯性来选
直传模式听起来最“正统”,但并不是所有工程建筑项目都适合。我见过一些业主方的内网环境,客户端并不能直接访问对象存储的内网地址,只能通过业务API域名出去。这种情况下就不能强上纯直传,只能采用PHP中转,或者部署一个独立的Upload Gateway服务。
直传有一个隐性问题:客户端必须先知道要传到哪个存储节点,以及用什么凭证。如果你们用的是云厂商对象存储,那就需要开通跨域、配置签名,客户端SDK集成成本会高一些。而且施工现场App往往是外包团队开发的,你让它做复杂的直传签名逻辑,它未必愿意,有时候会直接把问题踢回给你。
PHP中转也有它的价值,尤其在初期项目量不大、视频文件数量可控的情况下。中转模式最大的好处是统一入口,客户端只认一个API域名,鉴权、流量控制、上报进度都很好做,开发工作量最小。缺点是消耗应用层带宽。
我当时做的是折中方案:视频分片的数据流默认通过PHP上传网关中转,网关内部使用异步方式把分片写入对象存储;后续如果某个项目的网络和客户端能力允许,可以切换成直传模式,通过配置开关控制。这样一个接口层同时兼容两种路径,不把架构锁死。
2.3 用状态机制管理分片任务
视频分片上传不是一步到位的操作,它是客户端和服务端之间的一次长对话。你必须在数据库和缓存里贯穿一个任务状态,否则任何一步断掉,服务端都不知道这个任务进行到了哪一步。
我设计的状态至少包含这些:已初始化、等待上传分片、部分分片已上传、可合并、合并中、合并完成、合并失败。用upload_id来标识一次完整的上传任务,所有分片都挂在同一个upload_id下。客户端首次发起初始化接口时,服务端返回upload_id和分片大小、分片数量,同时把任务记录写入MySQL,状态置为init。
之后每上传一个分片,服务端记录该分片编号、存储地址、ETag和大小,然后更新任务的已上传分片计数。等到客户端通知服务端“所有分片都传完了”,服务端先核对已上传的分片编号和总数是否一致,一致才允许进入合并流程。这个状态管理看着啰嗦,但它是后续断点续传和校验的基础。
另外,状态机里一定要有“过期清理”状态。工地现场网络不稳定,客户端很可能在某一部断了之后再也没回来。如果不清掉这些僵尸任务,临时分片会把存储空间慢慢占满。我会加一个定时任务,把超过24小时仍未完成且没有活跃上传记录的任务标记为过期,然后删除已经上传的临时分片。
2.4 存储抽象层:一开始就要当作会换存储来做
对象存储市场上有云厂商的OSS/COS,也有开源自建的MinIO、Ceph RGW,每个产品API有细微差别。如果代码里直接写死某一种存储客户端,将来要么扩容受限,要么被厂商锁定,想要切到另一个存储方案时,涉及的所有上传接口都要返工。
最简单有效的办法是定义一个PHP接口,把所有存储操作收敛到后面。初始化分片上传、生成上传凭证、保存已传分片、合并分片、删除临时文件、生成访问下载地址,六个操作封装成一个StorageAdapter。底层如果是MinIO,就实现MinIOAdapter;如果切到云对象存储,再写一个CloudAdapter。业务代码只依赖接口,不感知底层是什么。
不要小看这个抽象,在工程建筑项目里,有时候一个集团下面同时有公有云项目和企业内网项目,存储底座可能完全不同。有了适配层,你就可以在一个上传服务里同时对接不同存储后端,按项目维度路由到对应的适配器。这个设计几乎没增加成本,却解决了以后扩容时的最大麻烦。
3. PHP侧的上传时序设计与代码实现
3.1 初始化一个分片上传任务
初始化接口是整个上传流程的起点,我通常是设计在POST /api/v1/video/uploads上。客户端把文件名、文件大小、所属项目ID、视频类型这些信息传过来,服务端根据文件大小决定需要切多少片,并返回上传标识。
判断分片大小不能拍脑袋定死。工程视频经常有20分钟甚至更长的监控录像,文件可能超过3GB。如果固定一个8MB分片,超大文件会产生好几百个分片,网络稍有波动,就有一堆分片要管理。我在服务端按文件大小做了两档划分:大于64MB才走分片逻辑,其中小于3GB的视频用8MB分片,3GB以上的用20MB分片。这样既能兼顾较小的监理视频,又能减少超大监控视频的分片数量。
以下是初始化接口的简化代码逻辑:
public function initUpload(array $params): array
{
$projectId = $params['project_id'];
$fileSize = (int) $params['file_size'];
$fileName = $params['file_name'];
// 工程视频按项目做归属,必须校验项目是否存在且用户有上传权限
$this->assertProjectPermit($projectId, $params['user_id']);
// 64MB以下可整传,但设计上仍走分片协议,只是只有一片
$usePartUpload = $fileSize > 64 * 1024 * 1024;
if (!$usePartUpload) {
$partSize = $fileSize;
} elseif ($fileSize > 3 * 1024 * 1024 * 1024) {
$partSize = 20 * 1024 * 1024;
} else {
$partSize = 8 * 1024 * 1024;
}
$partTotal = (int) ceil($fileSize / $partSize);
$uploadId = $this->generateUploadId($params['user_id']);
$this->taskRepository->create([
'upload_id' => $uploadId,
'project_id' => $projectId,
'file_name' => $fileName,
'file_size' => $fileSize,
'part_size' => $partSize,
'part_total' => $partTotal,
'status' => 'init',
'created_at' => time(),
]);
return [
'upload_id' => $uploadId,
'part_size' => $partSize,
'part_total' => $partTotal,
'expire_at' => time() + 7200,
];
}
一个容易被忽略的细节:上传任务的过期时间。工程视频上传经常是断断续续的,用户上午开始传,中午可能因为工地停电断了一次,下午继续传。过期时间定太短会导致用户重新走初始化流程,很伤体验;定太长又会堆积大量临时分片。我实践中习惯把过期时间定在2小时到4小时之间,同时在每次上传分片时刷新任务的有效期,保证在活跃上传状态下不会被后台清理任务误删。
3.2 上传指定分片
初始化完成之后,客户端会得到upload_id、part_size和part_total。接下来客户端按照这些信息把本地视频切分,逐个上传分片。上传接口我设计为POST /api/v1/video/uploads/{upload_id}/parts,每个请求携带分片编号part_number和文件二进制。
这里最需要校验的是分片编号的合法性和大小的边界,服务端不接受超出预期数量的分片,也不接受超过part_size太多的大小。因为我走的是PHP中转网关,每个分片到达后,PHP不会立即写入最终存储位置,而是先放到一个临时存储区,再把临时位置注册到任务分片表里。
实现伪代码如下:
public function uploadPart(int $uploadId, int $partNumber, $fileStream): array
{
$task = $this->taskRepository->findByUploadId($uploadId);
if (!$task || $task['status'] === 'completed') {
throw new UploadTaskException('任务不存在或已完成');
}
if ($partNumber < 1 || $partNumber > $task['part_total']) {
throw new PartOutOfRangeException('分片编号越界');
}
$partSize = (int) $fileStream->getSize();
if ($partSize > $task['part_size'] * 1.1) {
throw new PartTooLargeException('分片大小超过允许范围');
}
// 同一分片重复上传时,直接返回之前的结果,保证幂等
$exists = $this->partRepository->findByPartNumber($uploadId, $partNumber);
if ($exists) {
return $exists;
}
// 分片先存临时目录或临时bucket
$objectKey = vsprintf('tmp/uploads/%s/%05d.part', [
$uploadId,
$partNumber,
]);
$etag = $this->storageAdapter->putTemporaryPart($objectKey, $fileStream);
$this->partRepository->create([
'upload_id' => $uploadId,
'part_number' => $partNumber,
'object_key' => $objectKey,
'etag' => $etag,
'size' => $partSize,
]);
// 刷新任务最后活跃时间
$this->taskRepository->touchActiveTime($uploadId);
return ['part_number' => $partNumber, 'etag' => $etag];
}
工程现场的网络再差,也会出现重复提交同一个分片的情况。App的网络库超时重试,很可能服务端已经把分片收下来了,但客户端没收到响应,于是又传了一遍。所以接口必须做幂等,判断到同一upload_id下的同一分片编号已存在,就直接返回已保存的分片信息,不再重复写。否则合并时可能出现重复分片,写入存储后文件就坏了。
3.3 合并全部完成的分片并做完整性确认
所有分片都传完后,客户端调用一下合并接口POST /api/v1/video/uploads/{upload_id}/complete。服务端不能无条件信任客户端的声明,需要自己先去查分片表,确认已上传分片数量和总数一致,再按分片编号从小到大依次合并,合并成大文件并写入最终的对象存储目录。
工程行业里有一个常见事故:视频文件不大,但合并出来的文件看着大小差不多,播放器就是打不开。后来排查发现,某几个分片的上传顺序是乱的,或者有几个分片用的是旧版本重复覆盖。合并之前我会额外做一次校验,对于视频文件,最好能记录每个分片的MD5或ETag,然后把各分片的有序列表传给存储的合并接口。只要有一个分片缺失或ETag不符,就直接拒绝合并,让客户端重新上传异常分片。
下面是我合并接口的核心逻辑片段:
public function completeUpload(int $uploadId): array
{
$task = $this->taskRepository->findByUploadId($uploadId);
$uploadedParts = $this->partRepository->listByUploadId($uploadId);
// 核对分片数量是否齐全
if (count($uploadedParts) !== $task['part_total']) {
throw new PartMissingException('分片不完整,无法合并');
}
// 按分片编号升序排序,防止乱序
usort($uploadedParts, fn($a, $b) => $a['part_number'] <=> $b['part_number']);
// 核对编号是否连续
for ($i = 0; $i < count($uploadedParts); $i++) {
if ($uploadedParts[$i]['part_number'] !== $i + 1) {
throw new PartMissingException('分片编号不连续');
}
}
$objectKey = $this->buildFinalObjectKey($task);
// 把临时分片按顺序合并成最终文件
$this->storageAdapter->mergeTemporaryParts(
$uploadId,
$objectKey,
array_column($uploadedParts, 'etag')
);
// 删除临时分片目录
$this->storageAdapter->cleanupTemporaryParts($uploadId);
$this->taskRepository->markCompleted($uploadId, $objectKey);
return ['object_key' => $objectKey, 'download_url' => $this->createSignedDownloadUrl($objectKey)];
}
还需要注意合并操作本身的耗时。对象存储通常有专门的MultipartUpload合并接口,但如果是文件系统方案,服务端需要遍历并拼接分片,这个操作吃CPU和磁盘I/O。所以合并接口不应在PHP同步请求里无限等待,我最终是把合并任务投递到消息队列,由Workers异步完成,再通过回调或轮询把状态通知客户端。前端在合并期间显示“文件处理中”,用户体验上完全可以接受。
3.4 分片查询、续传和垃圾回收
断点续传不是说客户端断开后能记住自己传了哪些分片,而是服务端必须能被查询。客户端重新连上网络时,会先调用GET /api/v1/video/uploads/{upload_id}/parts,拿到已上传分片编号列表,只把缺少的分片重新上传。
这一步在工程建筑场景里是刚需。工地上的视频传输往往是“利用闲时上传”,可能白天拍完,晚上挂机传一部分,第二天到了项目部切换到稳定的Wi-Fi再传剩下的。如果服务端没有已上传分片查询能力,客户端每次都要把整个视频重传,那基本等于废了。
同时还要有后台垃圾回收机制。任务过期、用户取消、项目被删除等场景都会留下临时分片,必须定期清理。我写了一个CLI脚本,每小时扫描一次,把超过有效期且仍然处于init或partially状态的任务找出来,删除其临时分片数据和任务记录。这个脚本用crontab跑,不能省略。
下面是查分片的简化实现:
public function listUploadedParts(int $uploadId): array
{
$parts = $this->partRepository->listByUploadId($uploadId);
return ['uploaded_part_numbers' => array_column($parts, 'part_number')];
}
3.5 接口鉴权与滥用防护
视频文件是工程项目的重要数据,不是谁都可以随意上传的。每一个接口都要做项目级别的权限控制,而不是只做登录校验。比如总包方的劳务人员只能上传统筹项目下的安全交底视频,监理单位可以上传监理日志相关的视频,但不能动塔吊运行监控数据。
上传凭证上,我在初始化接口返回的响应里加了签名字段。客户端调用上传分片时必须在Header带上签名,签名由业务参数、用户ID和过期时间组合生成,服务端用同样的算法校验,防止有人抓包以后伪造上传请求。所有接口统一加频率限制,服务端限制同一IP和同一用户每分钟最多调用N次。
还有一个小坑要提醒:很多工程公司项目现场用了大内网,多台设备共享同一个公网出口IP做上传,如果频率限制按IP限制得太死,反而会把正常上传误杀。我一般把按IP的限流阈值放宽,而按用户和按upload_id的限流收紧,既能防刷又不影响真实业务。
4. 分布式存储策略的落地:目录规划、分级存储与容灾设计
4.1 工程建筑行业存储选型怎么比较务实
分布式存储的选项很多,但落到工程建筑行业,选型逻辑和互联网公司不太一样。工程建筑企业很少会养一支专业的存储运维团队,他们的核心诉求通常是:稳定、容量可扩展、能长期保存、成本可控。下面是我会考虑的几个选项。
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 自建服务器本地磁盘+RAID | 成本低、实施快 | 扩展性差、容灾弱 | 单项目临时系统、演示系统 |
| 分布式文件系统(如GlusterFS、CephFS) | 可扩展、可自控 | 运维门槛高、易出问题 | 有一定运维团队的企业内网 |
| 自建对象存储(如MinIO) | S3兼容、API成熟、部署相对简单 | 需要自己保证高可用和容灾 | 中大型工程企业私有化部署 |
| 云对象存储 | 弹性扩容、生命周期管理完善 | 长期大量存储成本偏高,受限于网络出口 | 公有云部署或多地域项目协同 |
如果是在一个集团层面统一建设,我更倾向于首选云对象存储或自建MinIO,二选一都行。重点是不要用一台业务服务器的磁盘容量去估“够不够”,工程项目的视频量在主体施工阶段增长得非常快,等结构封顶再做扩容已经来不及了。
4.2 视频对象命名规则和元数据怎么解耦
对象存储里的“文件路径”某种意义上就是一个逻辑key,它不像传统文件系统有目录层级,但我们可以用前缀来模拟。命名规则要能够支持两种场景:一个项目下的视频能按时间顺序扫描;一个具体视频能通过业务系统迅速找到。
我最终采用的object key格式是:
video/{projectCode}/{category}/{yyyyMMdd}/{uploadVideoId}_{userId}.mp4
举例来说,一个属于编号PJ2024008项目、类型为safety(安全交底)的视频,路径就会是:
video/PJ2024008/safety/20250121/UP7823123_u1024.mp4
projectCode用来区分项目,category用来区分视频类型,日期可以方便后续做冷热迁移,最后的uploadVideoId保证唯一,userId便于追溯上传责任。其中category的值我会固定枚举,不允许客户端随便传,避免出现各种奇怪的目录。
元数据和文件本身要彻底解耦。对象存储只保存二进制文件,存放位置、时间、上传人等全部记录在MySQL里,存储key作为连接两者的唯一外键。一旦某个文件需要从热存储迁移到低频存储或者归档存储,业务层不需要改变,只需要存储适配层处理,同时更新元数据表里的存储类型字段。
4.3 冷热数据分层:在途观看、验收追溯、竣工归档
工程视频的生命周期非常长。视频刚上传完成的那几天,甲方、监理、施工员经常需要反复观看确认,这些是热数据;项目进入后期,主体已经竣工,验收视频被拿去审计和资料归档的频率降低,但依然很重要,万一出现质量纠纷需要拿出来做证据;再往后,进入保修期,甚至项目公司可能都注销了,视频还要能被法定责任主体调取。这种情况下,一套存储策略里如果不区分冷热,存储成本会失控。
我用的是三级存储模型。上传完成后的前90天,文件留在标准存储池里,业务系统访问速度快,方便回放和审核。超过90天未访问的视频,进入低频存储池,费用下降,但访问时需要几秒的解冻延迟。项目竣工验收并完成备案后,把需要长期留存的视频转为归档存储,这个阶段的存储成本最低,只保留备查能力。
从技术实现上说,工程建筑领域的全量访问需求比较少,合理的冷热分层能省下可观的费用。我用对象存储的生命周期规则自动完成标准转低频、低频转归档的操作。需要留意的是,归档存储里的文件不能直接下载,又要先发起解冻请求,而且解冻会有小时级别的延迟。所以归档策略只用在明确不会再频繁调用的历史视频上,不能一上来就把所有文件丢到归档存储。
4.4 容灾与一致性需要考虑的最小集合
有不少工程公司会把所有视频放在同一个存储节点上,一旦节点所在机房断电或磁盘阵列坏了,历史数据就全部消失。这种风险在工程行业是不能接受的,因为一些施工过程的视频资料可能涉及项目责任认定。
分布式存储本身的副本机制能解决单节点故障,但如果整个集群只有一个机房,机房网络故障或断电还是会造成可用性中断。如果预算支持,尽量把集群分散到不同地域的节点或不同的可用区。小型项目可以退一步,至少保证同集群内多副本,加上每天一次全量备份到异地的冷存储。
我用一套最小但有效的一致性约束来避免脏数据。第一,上传过程中文件不进入正式存储,正式存储里的文件一定是一个完整、经过校验的视频;第二,元数据表里记录的状态和对象存储里的实际状态保持一致,每次合并完成即事务性更新任务状态;第三,定期跑一个巡检任务,把元数据表里状态是已完成的视频与对象存储里是否存在对应key做比对,发现缺失则报警,并支持从备份恢复。
5. 线上踩坑和参数调整实录
5.1 深基坑网络接入点的“假信号”问题
第一个让我印象深刻的坑出现在工地的深基坑区域。监理在基坑下面拍了一段钢筋绑扎验收视频,上传时手机信号显示满格,但进度条就是不动,过了一会儿直接报网络错误。
排查下来发现,所谓的“满格”只是信号强度,实际基站能分配给上行链路的带宽极其有限。客户端和服务器之间的TCP连接建立很快,但数据传输速率极低,稍有不慎连接就被运营商基站回收或被nginx判定超时。
这个问题的解法不止靠代码,还靠业务策略。第一,客户端在弱网下初始不要使用过高分辨率上传,服务端初始化接口可以获取一个客户端上报的网络等级参数,决定分片大小和上传并发数。第二,分片上传必须搭配合理重试,且分片一旦传完就不会受后续网络波动影响。第三,如果现场允许,我会引导用户先回项目部连接Wi-Fi再上传,针对大文件提供一个“仅Wi-Fi自动上传”的选项,这个功能在工地场景里特别实用。
5.2 分片大小为什么定成8MB和20MB两档
分片大小是最容易被拍脑袋定下来的参数。我一开始参考一些教程设成2MB,结果一个3GB的视频要切1500多个分片,每个分片都要走一次HTTP请求,光建连耗时和请求头开销就非常大,上传反而更慢。
分片大小应该根据网络带宽、往返时延和失败重试成本来估算。如果上行带宽比较稳定,比如项目公司办公网能有30Mbps,那单分片大小可以放到8MB甚至更大;如果带宽只有2Mbps,8MB的分片上传一次需要32秒,一旦在末尾断掉,重试代价就有点高。这时候反而建议用4MB分片。反过来,如果带宽太差连4MB都传不过去,那是一味的调小分片解决不了的,应该想办法切换网络,比如建议用户到信号好的区域再传。
最终我采用了分段设定:1GB以内的视频默认4MB分片,1GB到3GB用8MB分片,3GB以上用20MB分片。大文件使用大分片,请求数量少,失败率低;小文件使用小分片,单次请求的时长可控。前端拿到分片大小后,还会结合当前实时测速结果向服务端申请一个建议值,服务端也允许在初始化接口返回时遮罩成客户端指定的合法值。
5.3 PHP-FPM超时报错以后,我却先把合并操作移到了异步队列
项目刚上线时,分片上传本身很顺畅,但合并接口频繁出现504。我发现问题不是分片没传完,而是PHP在等待对象存储合并完成时超出了网关的超时时间。
对象存储的合并接口看起来是同步的,但内部要记录几百个分片的元数据,并触发后台任务拼接,个别时候耗时能达到几十秒。PHP-FPM等待一个几十秒的远程调用,肯定会触发nginx的504。
我把合并操作改成异步执行后,状态就不再是同步返回,而是先返回“合并中”的状态给前端。前端轮询查询任务状态,后端在Worker进程里完成合并后再更新状态。这一个改动不仅解决了超时,还降低了接口集群被长请求占满的风险。工人端的体验只是从“等待几十秒”变成“过一会儿看到已完成”,几乎无感。
5.4 所有视频都在,就是有几个文件播放不了
有一次系统运行一段时间后,甲方反馈说有几个视频在项目验收系统里播放不了。我看视频文件大小都正常,几个GB的文件都在,没有0字节的情况,但播放器就是打不开。
后来把文件从对象存储取回来,用ffprobe分析才发现,视频的moov元数据在文件末尾,而分片合并时的文件顺序有误,导致没有形成有效的MP4结构。根源是后端从数据库拉已上传分片时直接用了字符串排序,把10、11这种分片编号排在2前面了,排序错乱造成合并后的字节顺序错误。
修起来就是两行代码的问题:必须把part_number转成整数再用升序排序,而且在合并前先检查分片编号是否连续。同时我在流程里加了校验:合并完成后用ffprobe读取文件头,确认文件能被解析为视频后,才把任务状态更新为已完成。如果校验失败,文件不会立即可见,而是会把组合的分片标记为异常,方便重新上传对应分片后再次合并。
5.5 存储客户端连接池、并发上传和预签名地址的调优
存储客户端直接使用预签名方式让终端上传时,签名URL的有效期和接口层签名不太一样。如果有效期过长,万一URL泄露出去,别人就能往你的桶里上传任意文件。如果有效期过短,客户端可能上传到一半签名过期,后续分片全部失败。实际使用中,我把签名的有效期定为30到40分钟,对于一个传几分钟的分片来说足够,又不会暴露太长时间。
分片并发数不能无限大。同一个文件被切分成上百个分片后,如果客户端同时开20个并发上传,很容易把现场网络打满,其他办公系统也会变卡。后端在初始化任务时可以下发一个max_concurrency参数,我默认设置为5,客户端按这个数值控制并发。过去习惯并发越多越快,但这里的瓶颈不在服务端,而在客户端所在的工地网络质量。
PHP侧连接对象存储也要注意连接复用。如果每个分片请求都重新new一个客户端,在大量并发上传时会频繁建立TCP连接和TLS握手,既消耗CPU,又可能打满对象存储的连接数。我在PHP-FPM内部采用进程内单例复用连接,减少握手开销,服务端负载明显下降。
6. 后续演进:从“上传视频”到工程媒体底座
6.1 统一媒体上传服务,支撑安全巡查和质检拍照等更多端
分片上传这套机制不仅适用于视频,照片、施工日志附件、安全巡更图片,本质都是一样的。如果只为一个视频模块维护这套分片逻辑,性价比不高。项目往后走,可以把它抽取成统一的媒体上传服务,对外提供一个通用上传接口,通过文件类型参数区分走视频转码、图片压缩,还是原样存储。
工程建筑企业里,常见的接入场景还有无人机航拍视频、工地摄像头定时抓拍、移动端的巡检照片。这些媒体文件一旦接入统一底座,就能复用已经建好的项目隔离、权限控制、生命周期存储策略,不用每个新业务都从零造一套上传轮子。
6.2 接入视频抽帧、智能识别等分析流程
视频传到分布式存储之后,不只是让人来看的,工程行业越来越多的需求是做视频分析。比如从塔吊监控里识别违规操作,从工地入口视频里识别安全帽佩戴情况,从施工进度视频里自动对比计划进度和实际现场。这些分析都要求视频先完成分片上传、合并、入库,再被分析服务消费。
可以以文件完成上传作为事件,把它推送到消息队列,下游的分析服务订阅事件后,从对象存储读取视频,转码或抽帧进行处理。这样的设计能避免分析任务占用上传链路的I/O,也让新增智能分析模块时不动原有上传流程。
我在做分析服务接入时会留一个字段,在视频上传时标注它的业务用途,比如是“质量验评”还是“安全管理”。分析服务拿到视频后,按业务用途执行对应的算法链,而不是把所有视频都跑一遍昂贵的识别模型。这个“用途”字段在存储目录规划里就预留好了,这也再次说明,视频采集阶段就要把元数据设计到位。
6.3 长期项目中的维护建议
最后说一个很现实的体会。工程建筑类的软件项目周期长,一套系统可能从项目开工一直用到竣工结算,后面还要维护好几年。长期维护中,最容易出问题的不是上传逻辑,而是没有人再去清理临时分片、没有人去检查过期任务、存储容量不足时也没有告警。
建议运维阶段至少要配三个监控:临时分片总量的监控、对象存储容量使用率的监控、异常上传任务数量的监控。这三个数据能反映整个媒体系统的健康状况。临时分片异常增长,说明前端上传中断或重试逻辑有Bug;容量突增,可能是有异常客户端在刷上传;异常任务数量上升,通常意味着网络故障或者代码发布引入问题。
分片、合并、签名、存储,这些机制每个都值得打磨,但真正让一套系统长期扛住工程现场的轮番考验,是持续巡检和快速排障的能力。控制面简单、存储面可靠,后面业务发展再快,基础都不会散掉。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)