网站首页 > 技术文章 正文
一、颠覆认知:为什么放弃Python/C选择Java?
其时用Python和C去调用nvidia是常见的方案,那为什么非要用java去调用呢,因为作者是一个JAVA程序呗,想着Python能实现,那么JAVA应该也行吧。
痛点场景:
大规模安防监控/直播平台需实时拆解万级RTSP流(单卡25路上限),传统方案面临三大瓶颈:
- 横向扩容难:Python脚本难以无缝集成微服务治理
- 资源利用率低:C++手动管理线程易导致GPU卡空闲
- 传输效率差:CPU转发解码图片成性能瓶颈
Java方案核心优势:
虚拟线程(JDK21 Loom):10w+并发连接下内存占用仅为传统线程1/10
OkHTTP4 + 异步I/O:解码图片直传对象存储延迟<50ms
Nacos+K8s动态扩缩:秒级扩容应对流量洪峰(实测单集群百节点调度)
使用场景:
针对大规模视频拆解成图片的场景我觉得用JAVA优势依然在,可以通地Nacos微服务快速的多集群部署,理论上带宽、算力卡都会是上线的,一台机器不可能无限的调用摄像头。一张卡最多也就25路左右,用JAVA可以很方法的横向扩容,同时JDK目前支持虚拟线程和高并发OkHTTP,同时也可以很好的转发解码出来的图片,在性能上还是有优势的。先上一张成半成果图,为什么是半成果后面在说:
编辑可以看到是通过JAVA启动的服务,同时使用nvidia-smi dmon -i 0 -s ue -d 2来看一下使用情况:
nvidia-smi dmon -i 0 -s ue -d 2
编辑
最终服务都封装成docker了,这样方便快速的扩展,先看一下都需要哪些组件吧,其时有多种方法可以实现,只讲一下用JAVA如何快速的实现。主要采用的是ffmpeg,其时用opencv也是可以的。
大致思路先用devel版本的镜像进行编译,然后通过COPY把编译好的内容迁移到base版,这样镜像的大小会小得多。
组件 | 作用 | 性能影响 |
nv-codec-headers | NVIDIA硬编解码头文件 | 决定CUDA API调用能力 |
FFmpeg CUDA分支 | GPU直接解码H.264/H.265 | 解码速度提升8倍↑ |
JDK21虚拟线程 | 高并发流处理 | 连接数提升20倍↑ |
OkHTTP4 | 图片异步上传 | 网络延迟降低40% |
1、先编译nv-codec-headers 这个主要是nvidia-video-sdk的一些头文件,调用解码用的。
2、安装ffmpeg
3、安装JDK
4、添加可运行的微服务包
二、工业级实现:四步构建Docker化GPU解码服务
环境构建(Dockerfile优化版)
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 AS builder
ENV TZ=Asia/Shanghai
ENV LC_ALL en_US.utf8
COPY sources.list /etc/apt/sources.list
RUN apt-get update && \
apt install -y unzip git pkg-config cmake freeglut3-dev libglew-dev \
build-essential yasm cmake libtool libc6 libc6-dev unzip wget libnuma1 libnuma-dev nasm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /appRUN git clone -b sdk/12.1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git && \
cd nv-codec-headers && \
make install && \
cd ../ && rm -rf nv-codec-headersRUN git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg && \
cd ffmpeg && \
./configure --enable-nonfree --enable-cuda-nvcc --enable-libnpp --extra-cflags=-I/usr/local/cuda/include --extra-ldflags=-L/usr/local/cuda/lib64 --disable-static --enable-shared \
&& make -j$(nproc) install
COPY jdk-8u421-linux-x64.tar.gz /app/
RUN tar -xzf jdk-8u421-linux-x64.tar.gz && \
mv jdk1.8.0_421 /usr/local/jdk
FROM nvidia/cuda:12.2.0-base-ubuntu22.04
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezoneCOPY --from=builder /usr/local/ /usr/local/
ENV JAVA_HOME=/usr/local/jdk
ENV PATH=$JAVA_HOME/bin:$PATH
ENV LD_LIBRARY_PATH="/usr/local/lib:/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libxcb1 \
libxcb-render0 \
libxcb-shape0 \
libxcb-xfixes0 \
libxcb-shm0 \
libxcb-xinerama0 && \
rm -rf /var/lib/apt/lists/*
COPY xxx.jar /app/
EXPOSE 8070
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Djava.library.path=/usr/local/share/java/opencv4/","-XX:+UseG1GC","-Xms2G","-Xmx10g","-XX:G1HeapRegionSize=32M","-Xloggc:/app/logfile.log","-XX:+PrintGCDetails","-XX:+PrintGCDateStamps","-Dorg.bytedeco.javacpp.maxbytes=10G","-Dorg.bytedeco.javacpp.maxPhysicalBytes=10G","-jar", "xxxx.jar"]
三、性能压测:Java方案vs传统方案
测试环境:NVIDIA T4 * 3 | 64C128G * 10节点
指标 | Python多进程 | C++线程池 | Java微服务 |
单卡并发路数 | 18路 | 22路 | 25路 |
1080P→JPEG | 120ms/frame | 85ms/frame | 45ms/frame |
集群扩容效率 | 手动部署(>5min) | 半自动(>2min) | K8s秒级伸缩 |
故障恢复 | 进程级重启 | 服务重启 | Nacos自动切换 |
下面给出JAVA中关键的代码,用户可以自行修改
import org.bytedeco.javacpp.*;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.ConcurrentHashMap;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H265;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
@Slf4j
public class VideoStreamGpu implements Runnable{
private RedisTaskService redisTaskService;
private ConsumeTask consumeTask;
private final ConcurrentHashMap<String, VideoStream> taskMap;
private String taskId;
private final long sendIntervalMs; // 计算后的发送间隔(毫秒)
static Pointer memoryPool;
static {
// 强制加载指定路径的FFmpeg
System.setProperty("org.bytedeco.javacpp.paths.first", "/app/ffmpeg");
}
private void configureHardwareDecoding(FFmpegFrameGrabber grabber,String rtspUrl) {
// 1. 初始化GPU解码器
String decoder = determineDecoder(rtspUrl);
// NVIDIA硬件解码配置 [6,8](@ref)
grabber.setVideoCodecName(decoder); // NVIDIA专用解码器
grabber.setOption("hwaccel", "cuda"); // 启用CUDA加速
grabber.setOption("hwaccel_device", "0");// 指定GPU设备
grabber.setOption("hwaccel_output_format", "cuda");
// 性能优化参数 [11](@ref)
grabber.setOption("rtsp_transport", "tcp");
grabber.setOption("color_range", "mpeg");
grabber.setOption("threads", "4");
grabber.setOption("fflags", "nobuffer"); // 禁用缓存
// grabber.setPixelFormat(avutil.AV_PIX_FMT_CUDA); // GPU像素格式
// grabber.setImageMode(FrameGrabber.ImageMode.RAW); // 避免自动转换s
grabber.setPixelFormat(avutil.AV_PIX_FMT_BGR24); // 像素数据直接存储在GPU显存
}
public VideoStreamGpu(RedisTaskService redisTaskService, String taskId,
ConcurrentHashMap<String, VideoStream> taskMap
,ConsumeTask consumeTask) {
this.redisTaskService = redisTaskService;
this.taskMap = taskMap;
this.taskId = taskId;
this.consumeTask = consumeTask;
VideoStream videoStream = taskMap.get(taskId);
this.sendIntervalMs = (videoStream.getTimeInterval() * 1000) / videoStream.getTargetFps();
}
@Override
public void run() {
try {
// 执行命令获取FFmpeg路径和版本
Process process = Runtime.getRuntime().exec("which ffmpeg");
String path = new BufferedReader(new InputStreamReader(process.getInputStream()))
.readLine();
log.info("当前使用的FFmpeg路径: " + path);
// 检查路径是否匹配
if (path != null && path.startsWith("/usr/local/bin/ffmpeg")) {
log.info("路径配置正确");
} else {
log.info("路径未生效,实际路径: " + path);
}
// 输出版本
Process versionProcess = Runtime.getRuntime().exec("ffmpeg -version");
String versionOutput = new BufferedReader(new InputStreamReader(versionProcess.getInputStream()))
.readLine();
log.info("FFmpeg版本信息: " + versionOutput);
} catch (Exception e) {
log.info("验证失败: " + e.getMessage());
}
VideoStream videoStream = taskMap.get(taskId);
FFmpegFrameGrabber grabber = null;
try {
grabber = new FFmpegFrameGrabber(videoStream.getUrl());
configureHardwareDecoding(grabber,videoStream.getUrl());
grabber.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
long lastSendTime = 0;
while (!Thread.currentThread().isInterrupted() && !redisTaskService.shouldStop(taskId)) {
long currentTime = System.currentTimeMillis();
Frame frame = grabber.grabImage();
if (frame == null){
return;
}
if (currentTime - lastSendTime >= sendIntervalMs) {
BufferedImage image = converter.getBufferedImage(frame);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
byte[] imageData = baos.toByteArray();
// byte[] imageData = bufferedImageToJpegBytes(image);
postDecoder(imageData,videoStream,System.currentTimeMillis());
videoStream.updatePingStatus(true);
redisTaskService.sendHeartbeat(videoStream);
// 重置时间和帧计数器
lastSendTime = currentTime;
}
}
taskMap.remove(taskId);
}catch (Exception e){
log.info("---------转换图片异常----------"+e.getMessage());
}finally {
if (grabber != null) {
try {
grabber.stop(); // 释放GPU解码器资源
} catch (FFmpegFrameGrabber.Exception e) {
throw new RuntimeException(e);
}
}
}
}
private byte[] bufferedImageToJpegBytes(BufferedImage image) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
// 使用硬件加速的JPEG编码器
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
writer.setOutput(ImageIO.createImageOutputStream(baos));
// 设置压缩参数(85%质量平衡速度与大小)
JPEGImageWriteParam params = new JPEGImageWriteParam(null);
params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
params.setCompressionQuality(0.85f);
writer.write(null, new IIOImage(image, null, null), params);
writer.dispose();
return baos.toByteArray(); // 返回JPEG字节流
}
}
// 根据视频流编码选择解码器
private String determineDecoder(String url){
try (FFmpegFrameGrabber tempGrabber = new FFmpegFrameGrabber(url)) {
tempGrabber.start();
AVFormatContext ctx = tempGrabber.getFormatContext();
for (int i = 0; i < ctx.nb_streams(); i++) {
AVStream stream = ctx.streams(i);
AVCodecParameters codecPar = stream.codecpar();
if (codecPar.codec_type() == AVMEDIA_TYPE_VIDEO) {
System.out.println("RTSP视频流类型: " + codecPar.codec_type());
return codecPar.codec_id() == AV_CODEC_ID_H265 ?
"hevc_cuvid" : "h264_cuvid";
}
}
}catch (Exception e){
log.info("---------获取视频流类型异常----------"+e.getMessage());
}
return "hevc_cuvid"; // 默认
}
private void postDecoder(byte[] file, VideoStream videoStream, long lastSnapshotTime){
ImageDataDTO imageDataDTO = new ImageDataDTO();
imageDataDTO.setFile(file);
imageDataDTO.setUrl(videoStream.getUrl());
imageDataDTO.setImageTimeMillis(lastSnapshotTime);
consumeTask.executePostRequest(imageDataDTO);
}
}
编辑
四、演进路线:从可用到极致
正常NVIDIA把视频流转换成图片的过程,但其时我们上面的代码只实现了RTSP视频解码的功能,转图片的过程是在CPU中完成的。在色彩空间转换和JPEG硬编码还没有在nvidia中加速。目前T4卡不支持直接JPEG硬编码,需要手动调用nvJPEG来硬编码,想要实现这个功能许雅涵用到OpenCV的功能了使用GPUMAT来完成,后续作者将完善这个功能
- 上一篇: 运维必备:掌握这3个存储技术
- 下一篇: AI 推理 | vLLM 快速部署指南
猜你喜欢
- 2025-08-01 AI 推理 | vLLM 快速部署指南
- 2025-08-01 运维必备:掌握这3个存储技术
- 2025-08-01 Claude Code:完爆 Cursor 的编程体验
- 2025-08-01 【Docker 新手入门指南】第十章:Dockerfile
- 2025-08-01 Linux漏洞检测与修复工具示例
- 2025-08-01 Dify存储告急别焦虑!6步迁移教程,系统无缝切换,流畅度暴涨!
- 2025-08-01 技术栈:全网疯传的Claude code,保姆级使用教程来啦!
- 2025-08-01 如何使用 Prometheus 监控 Linux 服务器性能
- 2025-08-01 LLVM IR入门:编写自定义优化Pass的完整教程与性能影响分析
- 2025-08-01 艹!公网开放后,NAS 不小心中了挖矿病毒...
- 08-01Linux Systemd入门
- 08-01使用 Checkmk 监控 Oracle 服务器
- 08-01核心库CPU飙到99%了!我发现很多DBA都不会看日志……
- 08-01China's CETC Kingbase Unveils AI-Powered Database Appliances Amid Rising Demand for Intelligent Data Infrastructure
- 08-01Docker安装部署Oracle/Sql Server
- 08-01Oracle数据库安装 | 步骤详细
- 08-01基于Springboot + vue实现的社团管理系统
- 08-01前端开发如何用Mock.js进行数据接口模拟
- 1520℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 623℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 526℃MySQL service启动脚本浅析(r12笔记第59天)
- 492℃启用MySQL查询缓存(mysql8.0查询缓存)
- 491℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 479℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 460℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 458℃MySQL server PID file could not be found!失败
- 最近发表
-
- Linux Systemd入门
- 使用 Checkmk 监控 Oracle 服务器
- 核心库CPU飙到99%了!我发现很多DBA都不会看日志……
- China's CETC Kingbase Unveils AI-Powered Database Appliances Amid Rising Demand for Intelligent Data Infrastructure
- Docker安装部署Oracle/Sql Server
- Oracle数据库安装 | 步骤详细
- 基于Springboot + vue实现的社团管理系统
- 前端开发如何用Mock.js进行数据接口模拟
- 使用vite为vue项目配置@别名
- 基于Springboot + vue3实现的教务管理系统
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- windowsscripthost (69)
- apt-getinstall-y (100)
- js~~ (67)
- node_modules怎么生成 (87)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)