资讯动态

视频AI中台实战:Docker多架构镜像与K8s弹性调度全解析

发布时间:2026/9/10 6:34:24 来源:尧图企业网站定制
1. 视频AI中台的架构思路与整体拆解1.1 为什么提“异构算力”这个概念先说个背景。我之前所在的项目组做的是一个视频AI中台核心业务是从各个前端摄像头拉取实时视频流在服务端做AI分析入侵检测、车牌识别、客流统计这类然后把结果推给业务方。原本这套系统跑在几台物理机上用的是一套古老的单体服务视频流靠ffmpeg进程硬扛AI推理靠几块GPU卡直接分配。业务量小的时候没问题但是当接入的摄像头从几十路涨到几百路、甚至上千路的时候问题就爆发了有的摄像头是GB28181协议的国标设备有的是海康/大华私有SDK还有的直接暴露RTSP地址来源五花八门有的点位是1080P有的是4K同一路流的码率波动极大白天晚上差异很大AI推理模型又有不同架构的需求有些模型只能跑NVIDIA GPU有些推理服务恰恰更适合在ARM CPU上跑低分辨率检测单机部署时某一台机器一旦宕机或GPU故障整条链路就断了而且资源无法互相调剂。这才有了后面整套方案的演进用Docker的多架构镜像解决“同一套服务在不同芯片上都能跑”的问题用K8s的弹性调度解决“算力怎么动态分配”的问题再叠加GB28181/RTSP视频接入的标准化处理形成一套完整的视频AI中台。这套东西上线之后给我最大的感受是视频接入不再是一个一个适配的“手工活”AI推理资源也不再是一块一块“静态分赃”而是一个统一的、能伸缩的资源池在按需分配算力。1.2 视频接入层GB28181、RTSP与私有SDK的取舍做视频AI中台第一道坎就是接入层。市面上摄像头和NVR的主要视频输出协议大概就三类协议/方式优点缺点适用场景GB28181国标统一跨厂商互通支持信令媒体分离SIP信令复杂部分厂商实现得比较“野”平安城市、雪亮工程、大平台对接RTSP简单直接推拉流都方便工具链成熟需摄像头/编码器直接支持部分设备并发数受限小规模接入、本地调试、私有化部署厂商私有SDK功能全能拿设备状态、报警等附加信息强绑定厂商开发成本高SDK维护困难深度集成场景建议在边缘网关做屏蔽我们最终选了“GB28181为主、RTSP为辅”的双通道接入方案。原因不复杂对客户来讲GB28181已经是很多项目招标的硬性要求国标设备无须二次开发就能注册进来对内部团队来讲GB28181的SIP信令比厂商SDK更利于抽象统一我们把GB28181的注册、心跳、Invite、ACK、BYE这些都封装成了标准化的媒体接入接口RTSP则作为“补位通道”存在遇到不支持GB28181的老摄像头或者第三方流媒体服务时直接通过ffmpeg拉RTSP流转成内部统一媒体格式。这里有一个容易被忽略的点GB28181的媒体流默认走RTP/RTCP而直连RTSP时大多走RTSP over TCP或UDP所以内部媒体网关要支持两种协议的互相转换。我们内部定的统一媒体流标准就是转成RTSP或者直接推流给ZLMediaKit这样下游的AI分析服务拿到的永远是一路“干净的RTSP地址”不用关心上游到底是国标还是海康SDK。1.3 整体分层架构设计整套系统跑起来之后我把它总结成四个层次方便团队内部对焦接入接入层负责对接GB28181设备端SIP服务器自建、RTSP拉流、以及少量厂商SDK。这一层将各路流统一转成内部RTSP流。媒体处理层负责解码、抽帧、缩放、编码、切片、录制。这里大量使用Docker容器每个处理单元都是一个可水平扩展的实例。AI推理层负责运行业务模型检测、分类、跟踪等按需用CPU/GPU/NPU通过K8s调度到合适的节点。业务开放层把AI结果、录像检索、告警事件等通过API/Webhook暴露给上层业务系统。既然是“中台”核心目标就是让上层业务系统不需要关心摄像头怎么接入的、AI推理怎么部署的只关心“给一个设备ID给我一路流”和“给一个时间段给我识别结果”。2. Docker多架构镜像构建的实战经验2.1 为什么要同时考虑x86和ARM架构视频AI中台中算力种类很多x86服务器上插NVIDIA GPU跑重型模型ARM架构的边缘盒子比如瑞芯微RK3588、海思Hi3559跑轻量级模型还有一部分纯CPU节点做视频转码。如果每一类硬件都单独写一套部署脚本维护成本会迅速失控。特别是边缘节点数量上来之后手工rsync、手工docker build的方式根本没法看。Docker多架构镜像解决的正是这件事同一个镜像tag在不同CPU架构的机器上docker pull时会自动拉取对应架构的镜像层。用户无感知但底层是manifest list也叫镜像索引在做“按需分发”。示意一下镜像索引的逻辑registry.cn-hangzhou.aliyuncs.com/your-project/video-ai:latest ├── linux/amd64 → amd64平台的镜像层 ├── linux/arm64 → arm64平台的镜像层 └── linux/arm/v7 → 32位ARM平台的镜像层视需要决定要不要用docker manifest inspect可以查看一个多架构镜像的详细信息。上线部署时K8s节点上的kubelet会根据节点自身的Architecture字段自动选择正确的镜像变体来拉取。2.2 用buildx做一次构建、多平台输出多架构镜像的构建我推荐直接用Docker官方提供的buildx插件。这个工具底层使用了BuildKit支持在一个构建节点上同时产生多个平台的目标镜像。一个典型的构建命令大概是这样的# 先创建支持多平台的构建器builder docker buildx create --name multiarch --use --platform linux/amd64,linux/arm64 # 然后构建并推送 docker buildx build \ --platform linux/amd64,linux/arm64 \ -t your-registry.com/video-ai-mediagate:2026.01 \ --push \ -f docker/Dockerfile.media_gate .实际项目中我为“媒体网关”这个组件写了单独的Dockerfile.media_gate核心步骤包括基于ubuntu:22.04基础镜像安装FFmpeg、ZLMediaKit运行时依赖拷贝编译好的二进制媒体网关本体处理时区、CA证书、非root用户启动等细节通过buildx同时产出amd64和arm64版本。这里有几个非常值得注意的坑如果不加--platform参数docker build只会构建当前机器的架构。这是一个很常见的误区以为buildx装上就等于多架构了。ARM版本的二进制要么交叉编译要么在ARM节点上构建。如果是纯解释型语言比如Python、Node.js直接做成多架构镜像问题不大但像C写的媒体网关建议走交叉编译链或者用QEMU模拟。我自己的经验是复杂二进制不建议依赖buildx的模拟构建否则构建时间可能翻5倍而且有些依赖库在模拟环境下编译会报“Illegal instruction”。给镜像打多个tag时注意保留架构后缀tag方便排障。我会在CI里同时推一个latest-arm64、latest-amd64的tag出问题时候排查更快。2.3 视频AI镜像里最容易踩的三个坑视频处理相关的镜像跟普通Web服务镜像不太一样下面这几个坑比较有代表性第一个坑是底层依赖库对CPU指令集的要求。比如FFmpeg在编译时如果开启了--cpunative那编出来的二进制就绑定在特定CPU型号上了换一台新机器可能会直接段错误。建议生产镜像构建FFmpeg时用通用指令集尽量不开启native优化宁可损失一点点性能换兼容性。尤其是ARM平台同是arm64不同核心的指令集差异更大。第二个坑是字体、CA证书、时区这类“小东西”。AI分析的结果往往需要在画面上画框绘制中文标签就需要中文字体拉取HTTPS的RTSP流时需要根证书视频录像的时间戳必须使用Asia/Shanghai时区。这些在镜像里不配好运行起来就是各种“不可描述”的乱码和错误。我的习惯是在Dockerfile里显式安装并设置时区字体也都打进去。ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone RUN apt-get install -y fonts-noto-cjk ca-certificates第三个坑是容器内以root运行时留下的隐患。安全扫描经常报这个而且K8s强制runAsNonRoot时根本起不来。我的做法是在镜像里创建一个低权限用户并给需要写入的目录显式授权。RUN useradd -r -s /sbin/nologin videoai \ mkdir -p /data/records /data/cache \ chown -R videoai:videoai /data USER videoai3. K8s弹性调度算力怎么按需分配3.1 调度与亲和性让容器跑到最合适的机器上K8s集群里如果只有一种规格的节点调度策略相对简单但视频AI中台的节点往往是“混血”的有GPU节点、有纯CPU计算节点、有ARM边缘节点甚至可能还有存储型节点挂着大容量磁盘。这时候调度策略必须显式声明。我梳理了几类必须做的调度约束需求实现方式说明推理服务必须跑到GPU节点nodeSelectornvidia.com/gpu资源给GPU节点打标签gputrue推理Pod里声明nvidia.com/gpu: 1视频转码服务尽量跑到CPU核数多的节点nodeAffinitypreferred设置preferredDuringSchedulingIgnoredDuringExecution优先调度到高CPU节点边缘盒子必须跑ARM镜像nodeSelector 镜像自动选择边缘节点打标签archarm64调度时绑定到对应节点避免同一路视频的主备分析实例放在同一节点podAntiAffinity同一设备的两个分析实例不要调度到同一台机器有一点需要特别说明K8s原生调度器默认不会识别GPU资源需要安装NVIDIA Device Plugin。装好插件后Pod声明nvidia.com/gpu: 1K8s才会把GPU当作一种可调度的资源来处理。如果没装插件就算把Pod调度到GPU节点上容器里也看不到GPU设备。3.2 HPA弹性伸缩视频负载下的扩容策略视频AI中台的负载特征非常“非线性”白天车流量大、晚上几乎没有事件某些节假日前一天晚上这种峰值压力特别明显。如果按峰值时刻采购常备资源成本不合理所以必须让K8s的HPAHorizontalPodAutoscaler来接这个“削峰填谷”的活儿。标准的HPA基于Pod的CPU/内存利用率扩容但对视频AI服务来说CPU利用率其实不够“直觉”——视频解码和AI推理都会吃CPU/GPU但它们的波动非常快500毫秒内就可能从20%跳到90%。单纯按CPU平均值扩缩容经常出现“刚扩容完峰值已经过去了”的尴尬局面。我们的生产配置里综合用了两类指标K8s内置资源指标CPU、内存利用率适合兜底防止资源耗尽自定义指标Prometheus Adapter比如“当前正在分析的视频路数”“消息队列积压数”这类业务指标对扩容决策才是最直接的。举一个简化版的HPA配置示例apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: ai-analyzer-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: ai-analyzer minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Pods pods: metric: name: ai_video_stream_active target: type: AverageValue averageValue: 50这里有两条红线要提醒HPA不要针对GPU利用率直接扩容因为GPU调度是排他性的扩容出来的Pod可能因为GPU资源不足而Pending。更稳妥的做法是让HPA扩容后由调度器决定Pod落在哪个GPU节点上同时给不同优先级的Pod设置调度优先级。缩容策略要加稳定窗口。视频AI业务正在分析中途如果Pod被杀了会导致一路视频分析中断影响体验。建议behavior.scaleDown.stabilizationWindowSeconds设为600秒以上确保负载下降后不会立刻把Pod回收掉。3.3 只读权限、CPU Throttling与集群安全K8s生产集群里运维安全绝对不是一个可选项。我在实践中做过两件比较典型的加固只读根文件系统和只读权限用户。“只读权限用户”这个需求跟Docker镜像里的非root启动是配套的。在K8s层面我们通过securityContext强制约束spec: securityContext: runAsNonRoot: true runAsUser: 1001 containers: - name: ai-analyzer securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: [ALL] volumeMounts: - name: tmp mountPath: /tmp - name: cache mountPath: /data/cache视频分析服务经常要写临时文件抽帧、缓存、日志所以readOnlyRootFilesystem: true之后必须把/tmp、/data/cache这些目录以emptyDir或PVC方式挂载进去否则一启动就报“Read-only file system”。再来说CPU Throttling。这是K8s里一个非常经典且容易被忽略的问题。当你在Pod的resources.limits.cpu里设置了CPU上限比如limits.cpu: 2Linux CFS带宽控制会在100ms的周期内限制Pod的CPU用量。如果一个视频服务在一段时间内试图超过限制就会触发cpu throttling表现为CPU使用率看起来不高但响应延迟猛增视频处理出现卡顿。排查方式很直白进容器看cpu.stat。cat /sys/fs/cgroup/cpu.stat nr_periods 100000 nr_throttled 35000 throttled_time 95900000000如果nr_throttled占比很高说明容器频繁被限流。这时候最直接的解法是提升limits.cpu或者直接把limits调成跟requests一致这样虽然预留了资源但避免被限流到“想用用不了”的尴尬。另外对于视频转码这类持续消耗CPU的Pod不建议设置过低的CPU限频宁可让调度器把它放到空闲节点上。4. GB28181/RTSP接入与流媒体分发实现4.1 GB28181信令要点与常见业务流GB28181本质上是一套基于SIP的信令协议加上RTP媒体传输的规范。很多初次接触的人会被SIP协议吓到头韵、sdp、cseq都是一堆抽象名词。用大白话讲清楚就是设备注册摄像头作为SIP用户代理UA主动向SIP服务器我们自建在K8s里的信令网关发送REGISTER请求携带设备ID、密码等信息。服务器应答后设备就进入“在线”状态。实时音视频点播SIP服务器向设备发送INVITE请求里面携带SDP参数描述我们期望接收的媒体格式视频编码H.264/H.265、码率、分辨率等。设备回复200 OK并携带设备侧的SDP后双方进入“媒体传输”阶段。媒体传输设备用RTP协议把PS流Program Stream推过来后台需要做“RTP/PS → ES流”的解析再封装成RTSP流或者直接喂给解码器。语音对讲方向反过来服务器向设备发送音频RTP流用于广播/对讲。在实际项目中我见过不少设备在国标对接时出现“注册成功但点播失败”或者“点播成功但画面迟迟不出来”的问题。排查时优先看三个地方信令日志中的SSRC是否正确、SDP中的媒体IP是否可达、RTP端口是否被防火墙拦截。尤其是某些摄像头虽然注册到了公网IP但实际媒体流从内网IP发送服务器回包时直连内网IP就会失败。4.2 RTSP拉流与转发的坑RTSP协议本身不复杂但在大规模拉流场景下坑全在细节里。最常见的两个问题一是拉流地址带鉴权信息时的拼接。海康/大华的RTSP地址格式通常是rtsp://username:passwordip:554/Streaming/Channels/101。这类地址一旦密码里有特殊字符就需要URL编码处理否则认证失败。更稳妥的做法是使用ffmpeg时通过-rtsp_transport tcp和显式提供凭据来拉流。二、并发拉流导致设备/编码器过载。很多网络摄像头的RTSP并发数限制很低4路、6路如果中台的解码服务重复去拉同一路流摄像机就受不了。我们的做法是再加一层“流聚合网关”对同一路上游RTSP永远只维持一个会话下游所有需要该流的业务AI分析、录像、转发都从网关内部分发。样例命令用ZLMediaKit作为流媒体网关时可以直接通过API动态发起拉流curl -X POST http://127.0.0.1:8080/index/api/addStreamProxy \ -H Content-Type: application/json \ -d { vhost: __defaultVhost__, app: live, stream: camera_001, url: rtsp://admin:password192.168.1.64:554/Streaming/Channels/101, rtp_type: 0 }这个API返回成功后ZLMediaKit会保持上游RTSP流并对外提供rtsp://127.0.0.1:554/live/camera_001这样的统一地址给下游消费。4.3 浏览器播放从RTSP到Web端的最短路径有一个高频需求是“在浏览器里看视频”但浏览器不支持RTSP协议。所以必须做一层流媒体转换。我们内部做过两个方案老方案用FFmpeg把RTSP转成HLS直播流新方案用ZLMediaKit/Mediamtx直接转成WebRTC或HTTP-FLV。HLS的优点是兼容性强Apple、Android、PC浏览器都能播但延迟在3~10秒对于AI告警联动这种对实时性要求高的场景不够友好。WebRTC延迟能压到1秒以内体验好很多但部署复杂度更高需要信令服务和TURN/STUN配置。我的建议是对外展示类场景优先做WebRTC对录像回放场景保留HLS即可。中间这套转换逻辑可以沉淀为平台的基础能力上层业务不需要自己处理。5. 落地过程中遇到的高频问题与排查实录5.1 国标接入的“403”与注册失败GB28181对接时403 Forbidden是很典型的错误。绝大多数情况是密码错误、设备ID不在允许列表、或者SIP服务器的认证机制Digest校验不兼容。排查方法是同时抓设备端和服务器端的SIP日志比对Authorization头的摘要算法有设备只支持MD5有设备需要SHA-256后端如果只实现了一种算法就会导致部分设备反复403。另外有一些设备在初次注册时用的是默认密码上线时没改后台又强制要求复杂密码这种属于“配置问题”而不是“协议问题”。我的习惯是做一个“一键抓包”小工具在信令网关容器里直接tcpdump -i any -s0 -w /tmp/sip.pcap port 5060把SIP报文留下来然后用Wireshark的voip_calls视图分析很快能定位是注册阶段还是Invite阶段失败。5.2 RTSP拉流花屏、卡顿与时间戳问题RTSP拉流时如果出现花屏大概率是RTP包乱序或者丢包。常见解法是把传输方式从UDP强制改成TCPffmpeg -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 -c:v copy -an -f flv rtmp://127.0.0.1:1935/live/camera_001改用TCP后乱序问题基本消失但TCP拥塞控制也可能导致延迟变大。所以真正可靠的做法是保持UDP传输但在接收侧做大缓冲区并做RTP排序ZLMediaKit对这块处理得比较成熟我建议优先使用开源的ZLMediaKit而不是自己造轮子。时间戳问题则更隐蔽。有些摄像头在RTP包头里的时间戳单位不一致导致转封之后播放时快进/慢放或音画不同步。排查技巧是看FFmpeg的-debug输出或ZLMediaKit的毫秒级统计如果发现时间戳跳变异常可以在拉流时加-use_wallclock_as_timestamps 1这种参数强制使用系统时钟统一时间戳。5.3 容器网络模式与防火墙导致的“拉流不通”在K8s里跑媒体网关网络模型不像docker run --networkhost那么简单直接。默认情况下Pod网络是OverlayCalico/Flannel等从Pod内发起的RTP/RTCP包走的是VXLAN或IPIP隧道。部分网络插件对组播或大量UDP包支持差容易导致视频卡顿。视频媒体处理类服务我强烈建议使用hostNetwork模式让Pod直接复用宿主机网络栈spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet原因很简单媒体网关需要大量并发UDP收发Overlay网络会有性能损耗而且排查问题时多一层网络封装会让人非常痛苦。hostNetwork模式下RTSP的554端口、RTP的10000-20000端口、SIP的5060端口都直接暴露在宿主机的网络上配合防火墙规则反而更容易管理。5.4 监控告警全家桶Prometheus Node Exporter Grafana视频AI中台不带监控就跑生产等于裸奔。我的做法是标准三件套Prometheus采集存储、Node Exporter做节点指标、Grafana做展示和告警。Node Exporter采集的指标很多但磁盘告警规则是最值得先配的。视频录像和AI缓存会疯狂消耗磁盘空间如果不做告警等磁盘满了才发现整个集群的Pod可能都处于Evicted状态。一条经典的磁盘告警规则长这样groups: - name: disk-alerts rules: - alert: DiskUsageHigh expr: (1 - (node_filesystem_avail_bytes{fstype!~tmpfs|overlay} / node_filesystem_size_bytes{fstype!~tmpfs|overlay})) * 100 85 for: 5m labels: severity: warning annotations: summary: 磁盘使用率超过85% description: 节点 {{ $labels.instance }} 磁盘使用率已超过85%请立即清理或扩容。除了磁盘CPU Throttling的监控也很关键可以通过容器指标container_cpu_cfs_throttled_periods_total直接计算限流占比超过阈值就发出告警。有了这套监控很多“明明跑着但感觉不对”的问题都能提前暴露。6. 演进过程中的实践经验与一点个人总结整个视频AI中台从最初“几台物理机 手工脚本”演进到现在“Docker多架构镜像 K8s弹性调度 GB28181/RTSP标准接入”最大的收获并不是“用了K8s”这个事实而是团队处理问题的方式变了原来接一个摄像头要改代码、拉分支、发版现在只需要在平台里录入一条设备配置媒体网关自动完成注册、拉流、转流AI分析服务自动通过K8s调度获得算力。新接入一个点位的时间从按天算缩短到按分钟算。有几点经验想单独说多架构镜像一定要在项目早期就推下去而不是先只做amd64、等ARM设备进场再补。补历史镜像的坑比一开始双架构构建的维护成本高得多。K8s的弹性能力要在业务压力真正出现之前就验证过否则压力峰值来了HPA扩出来的Pod全是Pending等于没有弹性。我建议每季度做一次“压测日”把视频路数人为打上去看调度、扩缩容、录像是怎么表现的。视频媒体的网关服务尽量保持“有状态”的感知但要设计成无状态部署。有状态指的是它需要知道某路流的会话状态无状态指的是多个网关实例之间不互相依赖任何一台挂了另外一台可以接管。我们的做法是把会话状态存储放到Redis里网关实例只保持本地内存态重建时从Redis恢复。GB28181这类国标协议看着很复杂但有标准文档可查。遇到问题先抓包比看日志猜问题和问厂商客服要高效得多。最后再分享一个小技巧在和摄像头厂商联调时不要只给对方看结果把信令包、报文时间线、媒体传输的码率统计一起发过去两边对齐效率会高很多——这个习惯帮我们解决过好几个悬而未决的“玄学问题”。视频AI中台这条路还很长但这套“异构算力 统一接入 弹性调度”的骨架确实帮我们节省了大量重复劳动也让我们在面对新项目时能把精力聚焦在AI算法本身而不是被底层接入和运维问题反复摩擦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价