从 0 到 1 构建企业级视频智能分析平台:完整工程实践
一份可直接落地的全栈技术方案 覆盖相机接入、流媒体编排、存储治理、智能分析全链路 包含架构设计、核心代码、数据模型、运维体系与性能基准
目录
第一部分:平台基础与相机接入
第二部分:录像存储系统
第三部分:智能分析平台
第四部分:运维与演进
第四部分:运维与演进
18. 配置管理
18.1 应用配置模板
server: port: 8080
spring: datasource: url: jdbc:mysql://localhost:3306/video_analysis?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000
# 相机配置camera: onboarding: async-enabled: true sync-timeout-ms: 15000 duplicate-check: by-ip-port: true
onvif: default-port: 80 connect-timeout-ms: 3000 read-timeout-ms: 5000 heartbeat-timeout-ms: 3000 soap: strict-fault-check: true allow-capability-fallback: true
stream: naming: "cam_{ip}_ch{channel}_{subtype}" subtype-rule: main-min-width: 1280 retry: register-max-attempts: 3 register-backoff-ms: 1000
health: check-interval-ms: 30000 initial-delay-ms: 10000 offline-alarm-threshold-sec: 180
# 编解码配置codec: probe: ffprobe-bin: ffprobe timeout-ms: 3000 max-concurrency: 8
transcode: enabled: true assume-h265-when-unknown: true ffmpeg-bin: ffmpeg rtsp-transport: tcp stimeout-us: 5000000 probesize: 5000000 analyzeduration: 5000000 x264: preset: veryfast crf: 23 gop: 50 enable-audio: false
# MediaMTX 配置media: mtx: api-base-url: http://127.0.0.1:9997 rtsp-port: 8554 webrtc-port: 8889 api: path-add: /v3/config/paths/add/ path-get: /v3/config/paths/get/ path-list: /v3/config/paths/list path-delete: /v3/config/paths/delete/
# 录像配置recording: default: retention-days: 7 segment-duration: 10m record-format: fmp4
index: fixed-delay-ms: 30000 scan-depth: 4 lookback-hours: 24
storage: refresh-interval-ms: 15000 auto-switch-enabled: true migrate-batch-size: 20
# 算法任务配置ai: task: keepalive-timeout-seconds: 30 scene-keepalive-timeout-seconds: 30 dedup-running-seconds: 10
result: queue: size: 20000 worker: min: 4 max: 16 sample-interval-ms: 1000 batch: enabled: false size: 200 flush-ms: 500
preview: auto-start: true default-fps: 15 default-width: 1280 default-height: 720 default-bitrate: 3000k default-encoder: h264_nvenc
push: queue-size: 20000 flush-ms: 1000 routes-file: push_routes.json level-policy-file: level_policy.json
# WebSocket 配置ws: ai-engine: endpoint: /ws/ai-engine web-notify: endpoint: /ws/web-notify18.2 MediaMTX 配置模板
################################################ Global settings
# APIapi: yesapiAddress: :9997
# Metricsmetrics: yesmetricsAddress: :9998
################################################ RTSP server
rtsp: yesrtspAddress: :8554protocols: [tcp]encryption: "no"rtspAddress: :8554
################################################ WebRTC server
webrtc: yeswebrtcAddress: :8889webrtcICEServers: - urls: [stun:stun.l.google.com:19302]
################################################ HLS server
hls: yeshlsAddress: :8888hlsAlwaysRemux: nohlsVariant: lowLatencyhlsSegmentCount: 7hlsSegmentDuration: 1shlsPartDuration: 200ms
################################################ Recording
record: norecordPath: /recordings/%path/%Y-%m-%d/%H-%M-%SrecordFormat: fmp4recordPartDuration: 1srecordSegmentDuration: 10mrecordDeleteAfter: 7d
################################################ Path defaults
paths: all: sourceOnDemand: yes sourceOnDemandStartTimeout: 10s sourceOnDemandCloseAfter: 10s runOnDemandStartTimeout: 10s runOnDemandCloseAfter: 10s18.3 Docker Compose 部署模板
version: "3.8"
services: mysql: image: mysql:8.0 container_name: video-mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: video_analysis TZ: Asia/Shanghai ports: - "3306:3306" volumes: - ./data/mysql:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql command: --default-authentication-plugin=mysql_native_password
mediamtx: image: bluenviron/mediamtx:latest container_name: video-mediamtx restart: always ports: - "8554:8554" # RTSP - "8888:8888" # HLS - "8889:8889" # WebRTC - "9997:9997" # API - "9998:9998" # Metrics volumes: - ./mediamtx.yml:/mediamtx.yml - ./data/recordings:/recordings environment: TZ: Asia/Shanghai
app: image: video-analysis-platform:latest container_name: video-app restart: always depends_on: - mysql - mediamtx ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/video_analysis?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD} MEDIA_MTX_API_BASE_URL: http://mediamtx:9997 TZ: Asia/Shanghai volumes: - ./data/snapshots:/app/snapshots - ./logs:/app/logs
nginx: image: nginx:alpine container_name: video-nginx restart: always ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./data/recordings:/usr/share/nginx/html/recordings - ./data/snapshots:/usr/share/nginx/html/snapshots depends_on: - app19. 可观测性体系
19.1 核心指标定义
相机接入层:
camera_onboarding_total # 接入总数camera_onboarding_success_total # 接入成功数camera_onboarding_duration_seconds # 接入耗时camera_online_count # 在线设备数camera_offline_count # 离线设备数camera_offline_event_duration_seconds # 离线时长流媒体层:
stream_path_total # 路径总数stream_path_register_success_rate # 注册成功率stream_transcode_count # 转码路数media_gateway_request_duration_seconds # API 调用耗时录像层:
recording_plan_active_count # 活跃计划数recording_file_index_lag_seconds # 索引延迟storage_volume_usage_percent # 存储使用率storage_volume_full_count # 满盘卷数算法层:
ai_task_running_count # 运行中任务数ai_task_error_count # 异常任务数ai_engine_online_nodes # 在线节点数ai_result_queue_depth # 结果队列深度ai_result_drop_total # 丢弃结果数ai_alarm_push_success_rate # 推送成功率19.2 Prometheus 指标暴露
@Componentpublic class MetricsCollector {
private final Counter cameraOnboardingTotal; private final Counter cameraOnboardingSuccess; private final Histogram cameraOnboardingDuration; private final Gauge cameraOnlineCount; private final Gauge cameraOfflineCount;
public MetricsCollector(MeterRegistry registry) { this.cameraOnboardingTotal = Counter.builder("camera_onboarding_total") .description("Total camera onboarding attempts") .register(registry);
this.cameraOnboardingSuccess = Counter.builder("camera_onboarding_success_total") .description("Successful camera onboardings") .register(registry);
this.cameraOnboardingDuration = Histogram.builder("camera_onboarding_duration_seconds") .description("Camera onboarding duration") .register(registry);
this.cameraOnlineCount = Gauge.builder("camera_online_count", this::getOnlineCount) .description("Online camera count") .register(registry);
this.cameraOfflineCount = Gauge.builder("camera_offline_count", this::getOfflineCount) .description("Offline camera count") .register(registry); }
public void recordOnboardingAttempt() { cameraOnboardingTotal.increment(); }
public void recordOnboardingSuccess(long durationMs) { cameraOnboardingSuccess.increment(); cameraOnboardingDuration.record(durationMs / 1000.0); }
private double getOnlineCount() { return cameraRepository.countByStatus(1); }
private double getOfflineCount() { return cameraRepository.countByStatus(0); }}19.3 告警规则模板
groups: - name: camera_alerts interval: 30s rules: - alert: CameraOnboardingFailureRateHigh expr: | ( rate(camera_onboarding_total[5m]) - rate(camera_onboarding_success_total[5m]) ) / rate(camera_onboarding_total[5m]) > 0.05 for: 5m labels: severity: warning annotations: summary: "相机接入失败率过高" description: "过去 5 分钟接入失败率超过 5%"
- alert: CameraOfflineCountHigh expr: camera_offline_count > 10 for: 3m labels: severity: critical annotations: summary: "离线设备数量过多" description: "当前离线设备数: {{ $value }}"
- name: storage_alerts interval: 30s rules: - alert: StorageVolumeAlmostFull expr: storage_volume_usage_percent > 90 for: 3m labels: severity: warning annotations: summary: "存储卷即将满盘" description: "卷 {{ $labels.volume_id }} 使用率: {{ $value }}%"
- alert: RecordingIndexLagHigh expr: recording_file_index_lag_seconds > 120 for: 5m labels: severity: warning annotations: summary: "录像索引延迟过高" description: "索引延迟: {{ $value }} 秒"
- name: ai_alerts interval: 30s rules: - alert: AiResultQueueFull expr: ai_result_queue_depth > 18000 for: 2m labels: severity: critical annotations: summary: "结果队列即将满" description: "队列深度: {{ $value }}"
- alert: AlarmPushFailureRateHigh expr: ai_alarm_push_success_rate < 0.95 for: 5m labels: severity: warning annotations: summary: "告警推送成功率过低" description: "成功率: {{ $value }}"19.4 日志规范
日志级别使用:
- ERROR: 系统异常,需要人工介入
- WARN: 降级或重试成功的场景
- INFO: 关键业务事件 (接入成功、任务启动等)
- DEBUG: 详细调试信息
日志格式:
[时间] [级别] [线程] [类名] - [业务标识] 消息内容示例:
2026-02-13 14:30:00.123 INFO [main] c.e.CameraOnboardingService - [camera:1001] Device onboarded successfully: ip=10.0.1.20, manufacturer=Hikvision2026-02-13 14:30:05.456 WARN [task-1] c.e.MediaPathService - [path:cam_10_0_1_20_ch1_main] MediaMTX registration failed, retrying (1/3)2026-02-13 14:30:10.789 ERROR [task-2] c.e.OnvifManager - [camera:1002] ONVIF connection timeout: ip=10.0.1.21, timeout=3000ms20. 典型问题与解决方案
20.1 设备接入层
问题 1: ONVIF 连接超时
现象:
OnvifException: Connection timeout after 3000ms排查步骤:
ping设备 IP 确认网络连通性telnet ip 80确认端口开放- 浏览器访问
http://ip/onvif/device_service验证服务 - 检查防火墙规则
解决方案:
- 增加超时时间至 5-10 秒
- 配置网络路由
- 使用设备 Web 界面开启 ONVIF 服务
问题 2: WS-Security 认证失败
现象:
SOAP Fault: The security token could not be authenticated原因: 服务器与设备时间误差 > 5 秒
解决方案:
# 服务器端同步 NTPntpdate ntp.aliyun.com
# 设备端配置 NTP设备 Web 界面 -> 系统配置 -> 时间设置 -> NTP 服务器: ntp.aliyun.com问题 3: 在线但无画面
现象: camera_status=1, 但 RTSP 拉流失败
原因: 控制面可达但媒体面不可达
解决方案:
// 增加媒体可用性检查public boolean checkMediaAvailable(String rtspUrl) { try { ProcessBuilder pb = new ProcessBuilder( "ffmpeg", "-i", rtspUrl, "-t", "1", "-f", "null", "-" ); Process p = pb.start(); return p.waitFor(5, TimeUnit.SECONDS) && p.exitValue() == 0; } catch (Exception e) { return false; }}20.2 录像层
问题 4: 计划显示已开启但无文件
排查步骤:
- 检查
recording_plan.status是否为 1 - 检查 MediaMTX 路径配置:
GET /v3/config/paths/get/{pathName} - 检查存储卷状态:
storage_volume.status和is_full - 检查源 RTSP 可拉流性
解决方案:
// 增加健康检查@Scheduled(fixedDelay = 60000)public void checkRecordingHealth() { List<RecordingPlan> plans = planRepository.findByStatus(1);
for (RecordingPlan plan : plans) { // 检查最近 5 分钟是否有新文件 long recentFileCount = fileRepository.countByStreamNameAndTimeSince( plan.getStreamName(), LocalDateTime.now().minusMinutes(5) );
if (recentFileCount == 0) { log.warn("No recent files for plan {}, reapplying config", plan.getId()); planService.applyPlan(plan.toRequest()); } }}问题 5: 回放查询有空洞
现象: 物理文件存在,数据库查不到
原因: 索引任务只扫描最近窗口,漏扫历史补写文件
解决方案:
// 增加全量补扫任务@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点public void fullScan() { List<StorageVolume> volumes = volumeRepository.findByStatus(1);
for (StorageVolume volume : volumes) { scanVolumeFullRange(volume); }}20.3 算法层
问题 6: 任务显示运行中,但算法端无负载
排查步骤:
- 检查
ai_task_runtime.state是否真的为RUNNING - 检查节点 WebSocket 会话是否在线
- 检查算法端日志是否收到
START_TASK命令 - 检查
TASK_KEEPALIVE心跳上报
解决方案:
// 增加守护任务@Scheduled(fixedDelay = 10000)public void watchdogTask() { List<TaskRuntime> runtimes = runtimeRepository.findByState("RUNNING");
for (TaskRuntime runtime : runtimes) { long elapsed = Duration.between( runtime.getLastKeepalive(), LocalDateTime.now() ).getSeconds();
if (elapsed > 30) { log.warn("Task {} keepalive timeout, marking ERROR", runtime.getTaskId()); runtime.setState("ERROR"); runtimeRepository.updateById(runtime);
// 通知节点断连处理 onNodeDisconnected(runtime.getNodeId()); } }}问题 7: 结果队列堆积
现象: ai_result_queue_depth 持续增长,内存告警
原因: 处理速度跟不上生产速度
解决方案:
// 1. 启用非告警抽样@Value("${ai.result.sample-interval-ms:1000}")private long sampleIntervalMs;
// 2. 启用批量入库@Value("${ai.result.batch.enabled:false}")private boolean batchEnabled;
@Value("${ai.result.batch.size:200}")private int batchSize;
// 3. 增加处理线程int coreThreads = Runtime.getRuntime().availableProcessors();this.processExecutor = new ThreadPoolExecutor( coreThreads * 2, // 增加并发 coreThreads * 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));21. FAQ 与最佳实践
21.1 相机接入
Q: 是否必须支持 ONVIF?
A: 不是必须,但强烈建议。ONVIF 提供标准化的设备管理和流获取方式。不支持 ONVIF 的设备可以通过厂商兜底模式接入,但需要手动维护 RTSP 模板。
Q: 如何处理厂商差异?
A: 分层处理:
- 协议层做最大兼容 (Fault 解析、能力降级)
- 业务层提供厂商兜底开关
- 配置层维护厂商 RTSP 模板库
Q: 主码流和子码流如何选择?
A: 建议策略:
- 录像: 主码流
- 实时预览: 子码流
- 算法分析: 根据精度要求选择
21.2 流媒体
Q: 为什么推荐 MediaMTX 而不是 ZLMediaKit?
A: 两者都是优秀的开源方案,选择 MediaMTX 的原因:
- 单一二进制,部署简单
- RESTful API 设计现代化
- WebRTC 支持原生
- 社区活跃,文档完善
Q: H.265 一定要转码吗?
A: 不是。建议策略:
- 浏览器实时预览: 转 H.264
- 录像存储: 保留 H.265 (节省空间)
- 算法分析: 视算法输入要求而定
Q: 转码会增加多少延迟?
A: 软件转码: 1-3 秒 硬件转码 (NVENC): 500ms-1 秒
21.3 录像
Q: 切片时长怎么选?
A: 综合考虑:
- 1-2 分钟: 文件数量大,IO 压力高
- 5-10 分钟: 推荐,综合最优
- 30-60 分钟: 文件少,但回放拖拽粗糙
Q: FMP4 vs TS 格式?
A: FMP4 优势:
- 现代浏览器原生支持
- 元数据结构清晰
- 后期处理友好
TS 优势:
- 兼容性更广 (老设备/老浏览器)
- 容错性强 (部分损坏不影响播放)
Q: 如何避免磁盘满导致录像中断?
A: 多层防护:
- 水位线机制自动切卷
- 定时清理过期文件
- 磁盘使用率告警
- 降级策略 (关闭非关键路径录像)
21.4 算法
Q: 一个任务可以配置多少个场景?
A: 理论无上限,实际建议:
- 单节点模式: 3-5 个
- 拆分模式: 5-10 个
过多场景会导致:
- 推理开销增加
- 结果处理复杂
- 告警频率过高
Q: 任务失败如何重试?
A: 分级重试:
- 瞬时失败 (网络抖动): 自动重启,计数器 +1
- 持续失败 (设备离线): 标记 ERROR,等待设备恢复
- 配置错误: 不重试,需要人工修复
Q: 如何控制告警推送频率?
A: 使用推送模式:
- 高优先级:
IMMEDIATE - 中优先级:
COUNT=5, WINDOW=600s - 低优先级:
WINDOW=3600s
并配合冷却期避免风暴。
22. 性能基准与压测
22.1 接入性能基准
测试环境:
- CPU: 8C16G
- 网络: 千兆局域网
- 设备: 海康威视 DS-2CD2142FWD-I
测试结果:
| 并发数 | 接入成功率 | P50 耗时 | P95 耗时 | P99 耗时 |
|---|---|---|---|---|
| 10 | 100% | 850ms | 1200ms | 1500ms |
| 50 | 99.2% | 1100ms | 1800ms | 2400ms |
| 100 | 97.5% | 1450ms | 2500ms | 3200ms |
| 200 | 94.8% | 2100ms | 3800ms | 4800ms |
瓶颈分析:
- 网络 I/O 是主要瓶颈
- ONVIF SOAP 解析占用 15-20% CPU
- FFprobe 探测占用 30-40% CPU
优化建议:
- 接入任务异步化
- FFprobe 并发限制 (信号量)
- 设备信息缓存
22.2 录像性能基准
测试环境:
- CPU: 16C32G
- 存储: NVMe SSD
- 路数: 500 路并发录像
测试结果:
| 码率 | 切片时长 | 磁盘写入 | CPU 使用 | 内存使用 |
|---|---|---|---|---|
| 2Mbps | 10m | 125MB/s | 18% | 4.2GB |
| 4Mbps | 10m | 250MB/s | 22% | 5.8GB |
| 6Mbps | 10m | 375MB/s | 28% | 7.5GB |
索引性能:
- 扫描速度: 10000 文件/秒
- 索引延迟: P95 < 30 秒
22.3 算法性能基准
测试环境:
- GPU: NVIDIA RTX 3080
- 算法: YOLOv8m
- 输入: 1280x720 @ 15fps
单节点容量:
| 场景数/任务 | 并发任务数 | GPU 使用 | 推理延迟 |
|---|---|---|---|
| 1 | 50 | 68% | 45ms |
| 2 | 35 | 72% | 52ms |
| 3 | 25 | 78% | 61ms |
结果处理:
- 队列吞吐: 20000 条/秒
- 入库延迟: P95 < 200ms
- 告警推送: P95 < 500ms
23. 架构演进路线
23.1 当前架构 (v1.0)
特点:
- 单体应用
- MySQL 单实例
- 本地文件存储
- 单节点 MediaMTX
适用场景:
- 设备数 < 1000
- 并发流 < 200
- 算法任务 < 100
23.2 中期演进 (v2.0)
目标: 支撑 5000 设备、1000 并发流
改进点:
- 服务拆分
单体应用 → 接入服务 + 录像服务 + 算法服务- 存储优化
本地存储 → 对象存储 (MinIO/OSS) 分层存储: 热数据 SSD + 冷数据 HDD- MediaMTX 集群
单节点 → 多节点 + LVS 负载均衡 路径哈希分片- 数据库读写分离
主库: 写入 从库: 查询 + 统计23.3 长期演进 (v3.0)
目标: 支撑 10万+ 设备、万级并发流
改进点:
- 微服务化
按业务域拆分: - 设备管理服务 - 流媒体服务 - 录像服务 - 算法服务 - 告警服务
服务间通信: gRPC + MQ- 分布式存储
录像: Ceph 集群 元数据: TiDB 缓存: Redis 集群- 边缘计算
中心节点 + 边缘节点 边缘: 实时分析 + 本地录像 中心: 长期存储 + 全局调度- 云原生改造
容器化: Kubernetes 服务网格: Istio 可观测: Prometheus + Grafana + Jaeger总结
本文从零开始,构建了一个完整的企业级视频智能分析平台,覆盖:
- 相机接入: ONVIF 协议深度实践、流路径治理、编码决策
- 录像存储: 存储卷管理、文件索引、回放检索
- 智能分析: 多场景任务模型、节点调度、结果处理、告警推送
- 运维体系: 配置管理、可观测性、问题排查、性能优化
核心设计原则:
- 分层解耦: 控制面/媒体面、策略/执行、实时/批量
- 统一抽象: 路径模型、场景模型、状态模型
- 容错优先: 异步化、重试、降级、回滚
- 可观测: 日志、指标、链路、告警
技术栈总结:
| 层次 | 技术选型 |
|---|---|
| 协议 | ONVIF (SOAP/WS-Security) |
| 流媒体 | MediaMTX |
| 编解码 | FFmpeg/FFprobe |
| 存储 | MySQL + 文件系统 |
| 通信 | WebSocket + RESTful |
| 监控 | Prometheus + Grafana |
评论