边缘视频分析平台的架构设计与性能优化——从750ms到190ms的调优之路

一个省赛一等奖项目的技术复盘,涵盖异步管道、MQ削峰、熔断降级、缓存防护等Java后端核心技术栈。


一、项目背景

在智慧城市场景下,灯杆上挂载了摄像头和多种传感器(温湿度、光照、电压、电流)。传统方案把视频流传回云端分析------带宽成本高、延迟大。这个项目的思路是把AI推理下沉到边缘节点,本地处理完只上传结构化结果。

边缘节点的硬件很普通:4核CPU、8G内存。这台机器要同时跑视频帧抓取、YOLOv8推理、传感器数据入库、告警判定、WebSocket长连接------所有服务挤在一起,性能瓶颈很快就暴露了。

这篇文章记录我从"能跑"到"跑得稳"的优化过程。


二、整体架构

7个容器,Docker Compose一键编排:
#mermaid-svg-VhLtflbv3SCe9biG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VhLtflbv3SCe9biG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VhLtflbv3SCe9biG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VhLtflbv3SCe9biG .error-icon{fill:#552222;}#mermaid-svg-VhLtflbv3SCe9biG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VhLtflbv3SCe9biG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VhLtflbv3SCe9biG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VhLtflbv3SCe9biG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VhLtflbv3SCe9biG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VhLtflbv3SCe9biG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VhLtflbv3SCe9biG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VhLtflbv3SCe9biG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VhLtflbv3SCe9biG .marker.cross{stroke:#333333;}#mermaid-svg-VhLtflbv3SCe9biG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VhLtflbv3SCe9biG p{margin:0;}#mermaid-svg-VhLtflbv3SCe9biG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VhLtflbv3SCe9biG .cluster-label text{fill:#333;}#mermaid-svg-VhLtflbv3SCe9biG .cluster-label span{color:#333;}#mermaid-svg-VhLtflbv3SCe9biG .cluster-label span p{background-color:transparent;}#mermaid-svg-VhLtflbv3SCe9biG .label text,#mermaid-svg-VhLtflbv3SCe9biG span{fill:#333;color:#333;}#mermaid-svg-VhLtflbv3SCe9biG .node rect,#mermaid-svg-VhLtflbv3SCe9biG .node circle,#mermaid-svg-VhLtflbv3SCe9biG .node ellipse,#mermaid-svg-VhLtflbv3SCe9biG .node polygon,#mermaid-svg-VhLtflbv3SCe9biG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VhLtflbv3SCe9biG .rough-node .label text,#mermaid-svg-VhLtflbv3SCe9biG .node .label text,#mermaid-svg-VhLtflbv3SCe9biG .image-shape .label,#mermaid-svg-VhLtflbv3SCe9biG .icon-shape .label{text-anchor:middle;}#mermaid-svg-VhLtflbv3SCe9biG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VhLtflbv3SCe9biG .rough-node .label,#mermaid-svg-VhLtflbv3SCe9biG .node .label,#mermaid-svg-VhLtflbv3SCe9biG .image-shape .label,#mermaid-svg-VhLtflbv3SCe9biG .icon-shape .label{text-align:center;}#mermaid-svg-VhLtflbv3SCe9biG .node.clickable{cursor:pointer;}#mermaid-svg-VhLtflbv3SCe9biG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VhLtflbv3SCe9biG .arrowheadPath{fill:#333333;}#mermaid-svg-VhLtflbv3SCe9biG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VhLtflbv3SCe9biG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VhLtflbv3SCe9biG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VhLtflbv3SCe9biG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VhLtflbv3SCe9biG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VhLtflbv3SCe9biG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VhLtflbv3SCe9biG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VhLtflbv3SCe9biG .cluster text{fill:#333;}#mermaid-svg-VhLtflbv3SCe9biG .cluster span{color:#333;}#mermaid-svg-VhLtflbv3SCe9biG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VhLtflbv3SCe9biG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VhLtflbv3SCe9biG rect.text{fill:none;stroke-width:0;}#mermaid-svg-VhLtflbv3SCe9biG .icon-shape,#mermaid-svg-VhLtflbv3SCe9biG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VhLtflbv3SCe9biG .icon-shape p,#mermaid-svg-VhLtflbv3SCe9biG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VhLtflbv3SCe9biG .icon-shape .label rect,#mermaid-svg-VhLtflbv3SCe9biG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VhLtflbv3SCe9biG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VhLtflbv3SCe9biG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VhLtflbv3SCe9biG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Nginx :80

反向代理 + API限流
Spring Boot :8080

核心业务
MySQL :3306

数据持久化
Redis :6379

缓存 + 心跳 + 分布式锁
RabbitMQ :5672

传感器削峰 + 死信队列
Mosquitto :1883

MQTT指令下发
YOLOv8 Flask :5000

AI行人检测
LED灯杆设备
死信队列 DLQ

四条核心链路

链路 流程
视频帧抓取 HTTP → CompletableFuture(FFmpeg) → JPEG压缩 → Base64
传感器数据 HTTP → RabbitMQ → Consumer{DB+Redis+告警}
AI推理 HTTP → RestTemplate(Flask) → Resilience4j{重试+熔断} → fallback
设备控制 HTTP → MQTT Publish → 设备订阅

三、异步视频帧管道:750ms→190ms

3.1 原始实现的问题

最早是同步串行的:

java 复制代码
// 问题代码:每一步都阻塞当前线程
public VideoFrameDTO getCurrentFrame(Long lampId) {
    String frame = captureFrame(cameraUrl);     // FFmpeg抓帧 ~500ms
    String compressed = compressFrame(frame);    // JPEG压缩 ~200ms
    String base64 = encodeBase64(compressed);   // 编码 ~50ms
    return buildDTO(lampId, base64);             // 总计 ~750ms
}

4路摄像头同时请求时,Tomcat线程池瞬间打满。

3.2 异步化改造

java 复制代码
// 优化后:异步编排 + 超时保护
CompletableFuture<String> captureFuture = CompletableFuture.supplyAsync(
    () -> videoFrameHandler.captureFrame(cameraUrl),
    videoTaskExecutor  // 专用线程池,不占用Tomcat线程
);

String frameData = captureFuture
    .thenApply(videoFrameHandler::compressFrame)  // 异步链式编排
    .get(10, TimeUnit.SECONDS);  // 超时保护,防RTSP流卡死

线程池参数设计

参数 原因
corePoolSize 4 CPU核数,保底处理能力
maxPoolSize 7 IO密集型,阻塞系数≈1,理论值8个,留余量给其他服务
队列 LinkedBlockingQueue(100) 有界队列,防止无限堆积导致OOM
拒绝策略 CallerRunsPolicy 队列满时由调用者线程执行,形成自然背压

为什么CallerRunsPolicy? DiscardPolicy会丢帧(数据丢失不可接受),AbortPolicy会抛异常(增加调用方复杂度)。CallerRunsPolicy让Tomcat线程亲自执行任务------虽然慢一点,但下游处理完之前不会接收更多请求,形成自然背压。

3.3 效果

单帧处理 750ms → 190ms(↓74.7%),4路并发时Tomcat线程池保持健康水位。


四、传感器削峰:同步写库→MQ异步

4.1 问题

设备每5秒上报一次传感器数据(温度、湿度、光照、电压、电流共5个指标)。100个灯杆就是每秒20条数据。每条数据要:写MySQL、更新Redis缓存、检查告警规则------全同步链路,高峰期MySQL连接池被打满。

4.2 方案

引入RabbitMQ做异步削峰:

java 复制代码
// Producer:只投递到MQ,不阻塞
public void reportData(SensorDataDTO dto) {
    boolean sent = producer.sendSensorData(dto);  // 投递到RabbitMQ
    if (!sent) {
        processSync(dto);  // MQ不可用时降级同步兜底,保证数据不丢
    }
}

// Consumer:异步消费 + 死信队列保护
@RabbitListener(queues = "sensor.data.queue")
public void handleSensorData(SensorDataDTO dto) {
    // 1. MySQL insert
    // 2. Redis cache(随机TTL防雪崩)
    // 3. 告警判定
    // 消费失败 → Spring AMQP自动retry 3次 → 全部失败进死信队列sensor.data.dlq
}

为什么选RabbitMQ而不是Kafka?

维度 RabbitMQ Kafka 本项目选型
吞吐量 万级/秒 百万级/秒 传感器几十条/秒,RabbitMQ够用
路由灵活性 Exchange多类型路由 仅Topic订阅 需要按灯杆ID灵活路由
死信队列 原生支持,配置简单 需自行实现 RabbitMQ开箱即用
运维复杂度 高(需ZK/KRaft) 边缘节点资源有限

结论:不是越新的技术越好,匹配场景的才是最优解。


五、AI推理容错:不崩溃比什么都重要

YOLOv8是外部的Python Flask服务。HTTP调用天然不可靠------网络抖动、模型加载慢、GPU显存溢出都可能导致失败。

5.1 三层防护体系

java 复制代码
@CircuitBreaker(name = "yoloCB", fallbackMethod = "inferenceFallback")
@Retry(name = "yoloRetry", fallbackMethod = "inferenceFallback")
public InferenceResultVO inference(InferenceRequestDTO request) {
    // HTTP调用YOLOv8 Flask推理服务
    ResponseEntity<String> response = restTemplate.exchange(...);
    // ...
}

// 降级方法:不崩溃,返回空结果
private InferenceResultVO inferenceFallback(RequestDTO req, Throwable t) {
    return new InferenceResultVO(req.getLampId(), 0, "[]");
    // personCount=0,不阻塞主流程
}

配置参数解释

层级 配置 为什么这样配
RestTemplate超时 connect 2s / read 10s 2s连不上大概率网络不通;推理<10s正常
@Retry 3次 / 500ms / 指数退避×2 间隔500ms→1s→2s,给服务短暂恢复窗口
@CircuitBreaker 滑动窗口10 / 失败率50% / 熔断30s 10次调用统计,过半失败就熔断,30s后放3个请求探测
fallbackMethod personCount=0 宁可漏报不可崩溃

熔断状态机

text 复制代码
CLOSED(正常) → 失败率≥50% → OPEN(熔断,30s)
                                ↓
                          HALF_OPEN(放行3次)
                           ↙           ↘
                      成功→CLOSED    失败→OPEN

5.2 为什么不用Hystrix?

Hystrix已进入维护模式(Netflix官方声明),Spring Cloud官方推荐Resilience4j。而且Resilience4j是纯Java实现,不依赖Archaius/RxJava等外部库,启动更快、内存占用更小。


六、Redis缓存三板斧

getLatestData(lampId) 是最频繁的调用------前端轮询、告警检查、数据展示都调它。

6.1 穿透防护(Null Value Cache)

问题 :恶意请求不存在的lampId=99999,每次都穿透到DB。

java 复制代码
if (data == null) {
    // 缓存空值标记,TTL 5分钟
    redis.set("sensor:latest:99999", "__NULL__", 300, SECONDS);
    throw new BusinessException("未找到");
}

面试官可能会问"为什么不用布隆过滤器"?------布隆过滤器有误判率,且需额外维护bit数组。小规模场景空值缓存足够,大规模(千万级key)才上布隆。

6.2 击穿防护(Mutex Lock)

问题:热点灯杆的缓存刚好过期,瞬间100个请求同时查DB。

java 复制代码
// SETNX互斥锁:只有一个线程能获锁重建缓存
boolean locked = redis.setIfAbsent("sensor:lock:" + lampId, "1", 10, SECONDS);
if (locked) {
    try {
        // 双重检查:获锁后缓存可能已被其他线程重建
        cache = redis.get(key);
        if (cache != null) return cache;
        return rebuildFromDB(lampId);
    } finally {
        redis.delete(lockKey);
    }
}
// 未获锁→自旋等待(5次×50ms)→重试读缓存

6.3 雪崩防护(Random TTL)

问题:TTL全部设为3600s,1小时后所有key同时过期。

java 复制代码
long randomizeTtl(long base) {
    long offset = (long)(base * 0.1 * random());  // ±10%
    return base + (random() > 0.5 ? offset : -offset);
}

1000个key,TTL分布在3240s~3960s之间,不会同时过期,DB压力被均匀分散。


七、设备心跳协议

边缘设备通过4G/NB-IoT联网,网络极不稳定。简单的心跳超时会导致频繁误判。

方案:WebSocket长连接 + Redis INCR版本号

text 复制代码
设备连接 → Redis SET device:version:{id} = 0
收到心跳 → Redis INCR → 回复 heartbeat_ack{version}
断连     → Redis SET device:status:{id} = offline
前端判断 → 版本号跳跃=发生过断连,触发数据重新拉取

为什么INCR而不是SET? 版本号必须严格递增。前端通过版本连续性判断是否有数据丢失。INCR是Redis原子操作,多实例部署也不会有竞态。


八、测试与工程化

8.1 56个纯Mockito单元测试

全部使用 @ExtendWith(MockitoExtension.class),无需Spring容器,56个测试<2秒跑完。

复制代码
AlarmRecordServiceImplTest     9  覆盖超限/低限/正常/无规则/空值/多传感器
SensorDataServiceImplTest       9  覆盖MQ成功/降级/穿透/击穿/雪崩
VideoStreamServiceImplTest      8  覆盖正常/空/异常/超时/连续帧
DeviceWebSocketHandlerTest      8  覆盖连接/心跳/连续心跳/断连/非法JSON
AIInferenceServiceTest          5  覆盖推理成功/HTTP失败/记录查询
DeviceControlServiceTest        6  覆盖MQTT下发/离线/不存在/状态查询
DeviceHeartbeatServiceTest     11  覆盖初始化/校验/更新/断连/连续

8.2 为什么纯Mockito而不是@SpringBootTest?

@SpringBootTest 需要完整Spring上下文+MySQL+Redis,跑一次30秒起步。纯Mockito隔离外部依赖,CI友好,且能精确验证每个方法的调用参数和调用次数。


九、总结

经过9轮优化:

维度 优化前 优化后
视频帧延迟 750ms 190ms
传感器吞吐 同步写库 MQ异步削峰
AI推理 无保护 重试+熔断+降级
缓存 无防护 穿透/击穿/雪崩全覆盖
视频帧阻塞 永久阻塞 10s超时熔断
测试 0个纯单测 56个
部署 4容器 7容器

核心经验

  1. 先跑通,再优化------不要过早优化,但要知道瓶颈在哪
  2. 异步不是银弹------线程池参数要根据IO/CPU密集类型计算,拒绝策略要结合业务语义
  3. 容错是基本功------外部依赖总会挂,重试/熔断/降级不是可选项
  4. 缓存三板斧必须会------Java面试送分题,项目里有实现就是加分项
  5. 测试写多少都不嫌多------56个纯Mockito测试<2秒跑完,改完代码立刻验证

附录:项目信息

  • 技术栈:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis + RabbitMQ + MQTT + WebSocket + JavaCV/FFmpeg + YOLOv8(Flask) + Resilience4j + Docker Compose + Nginx + Prometheus + Springdoc + JUnit5/Mockito
  • 获奖:全国大学生物联网设计竞赛 湖南赛区一等奖
  • GitHubEdgeVideoAnalysis

本文由 BugFreeHunter 原创,转载请注明出处。

相关推荐
小刘|1 小时前
Spring AI Alibaba 集成和风天气 API 实战
java·服务器·前端
KANGBboy1 小时前
java知识五(继承)
java·开发语言
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第117题】【并发篇】第17题:线程有几种状态,之间如何转换?
java·开发语言·面试
DIY源码阁1 小时前
JavaSwing饮品管理系统 - MySQL版
java·数据库·mysql·eclipse
二哈赛车手2 小时前
新人笔记---最终版智能体图片分析完整方案,包括一些总结于经验,以及各种优化点讲解
java·笔记·spring·ai·springboot
泡^泡2 小时前
Spring AI简单高仿DeepSeek问答页面
java·人工智能·spring
带刺的坐椅2 小时前
Solon v4.0 正式发布,高考记忆版
java·ai·solon·flow·solon-ai
山东点狮信息科技有限公司3 小时前
企业级 MES 制造执行系统架构设计与实践
spring cloud·性能优化·系统架构·策略模式·点狮
xufengzhu4 小时前
第三方 Python 库 redis-py + hiredis 的使用
开发语言·redis·python