从 0 到 1 构建企业级视频智能分析平台:完整工程实践(四)

从 0 到 1 构建企业级视频智能分析平台:完整工程实践
一份可直接落地的全栈技术方案 覆盖相机接入、流媒体编排、存储治理、智能分析全链路 包含架构设计、核心代码、数据模型、运维体系与性能基准
目录
第一部分:平台基础与相机接入
第二部分:录像存储系统
第三部分:智能分析平台
第四部分:运维与演进
第四部分:运维与演进
18. 配置管理
18.1 应用配置模板
# application.yml
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 配置模板
# mediamtx.yml
###############################################
# Global settings
# API
api: yes
apiAddress: :9997
# Metrics
metrics: yes
metricsAddress: :9998
###############################################
# RTSP server
rtsp: yes
rtspAddress: :8554
protocols: [tcp]
encryption: "no"
rtspAddress: :8554
###############################################
# WebRTC server
webrtc: yes
webrtcAddress: :8889
webrtcICEServers:
- urls: [stun:stun.l.google.com:19302]
###############################################
# HLS server
hls: yes
hlsAddress: :8888
hlsAlwaysRemux: no
hlsVariant: lowLatency
hlsSegmentCount: 7
hlsSegmentDuration: 1s
hlsPartDuration: 200ms
###############################################
# Recording
record: no
recordPath: /recordings/%path/%Y-%m-%d/%H-%M-%S
recordFormat: fmp4
recordPartDuration: 1s
recordSegmentDuration: 10m
recordDeleteAfter: 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 指标暴露
@Component
public 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 告警规则模板
# prometheus-alerts.yml
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=Hikvision
2026-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 秒
解决方案:
# 服务器端同步 NTP
ntpdate 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 |
贡献者
flycodeu
版权所有
版权归属:flycodeu
