视频流代理:从 Java 到 Nginx 的性能优化实践

数据归集系统的视频流代理(FLV/HLS)由 Java Controller 实现,每路视频流占用一个 Tomcat 线程做 8KB 缓冲区转发。并发路数多时会耗尽线程池。本文记录了将 FLV 代理迁移到 Nginx 的完整方案,以及 HLS 仍需走 Java 代理的技术原因。

🎧 文章导读

🎵 背景音乐

背景

水文监测系统需要播放 Lisa 视频平台的实时监控视频。后端通过反向代理解决前端无法直接访问内网视频流的问题。

原有实现是纯 Java 方案:

1
2
前端 → Java(play接口) → FLV/HLS 都返回 Java 代理地址
前端 → Java(流代理) → Lisa上游

问题:每路视频流占用一个 Tomcat 线程,做 8KB 缓冲区转发。当同时播放 20 路视频时,Tomcat 线程池压力很大。

方案设计

架构对比图
图1:Java 代理模式 vs Nginx 代理模式的架构对比

新增 Nginx 流代理,与 Java 代理并存,通过配置切换:

1
2
3
4
5
6
7
8
【Nginx 代理模式】(新增,生产环境推荐)
前端 → Java(play接口) → FLV 返回 Nginx 代理地址,HLS 返回 Java 代理地址
前端 → Nginx(flv代理) → Lisa上游 ← FLV 走 Nginx,不吃 Tomcat 线程
前端 → Java(hls代理) → Lisa上游 ← HLS 走 Java,需要改写 M3U8 内容

【Java 代理模式】(当前,保持不变)
前端 → Java(play接口) → FLV/HLS 都返回 Java 代理地址
前端 → Java(流代理) → Lisa上游

关键决策:FLV 走 Nginx,HLS 走 Java。

为什么 HLS 不能走 Nginx?

Nginx 是纯透传代理,无法改写 M3U8 文件中的 TS 片段路径。Lisa 平台 M3U8 中的 TS 为相对路径,经 Nginx 代理后 HLS.js 解析出的 TS 地址没有 url 参数,请求会失败。

1
2
3
4
5
6
Lisa 原始 M3U8:
#EXTINF:10.0,
segment001.ts ← 相对路径

经 Nginx 代理后,HLS.js 解析出:
http://nginx:18001/segment001.ts ← 缺少 url 参数,404

Java 代理会改写 M3U8 内容,将 TS 地址替换为 Java 代理地址:

1
2
3
改写后的 M3U8:
#EXTINF:10.0,
http://java:9223/video/proxy/hls/ts?url=xxx&segment=001.ts ← 完整代理地址

为什么 FLV 可以走 Nginx?

FLV 是单条连续流,不需要改写内容,Nginx 透传完全没问题。而且 FLV 才是长时间占用 Tomcat 线程的大头(一个摄像头可能持续播放几小时)。

Nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
server {
listen 18001;
server_name _;

location /video-proxy/ {
# CORS 预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Range, If-Range' always;
add_header 'Access-Control-Max-Age' 3600 always;
add_header 'Content-Length' 0;
return 204;
}

# 动态代理:$arg_url 从查询参数获取目标地址
resolver 211.136.192.6 valid=30s;
set $target $arg_url;
proxy_pass $target;

# 代理配置
proxy_http_version 1.1;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# Range 请求支持(视频拖拽)
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;

# 关闭缓冲,实时转发
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

# CORS 响应头
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
}
}

关键配置说明:

配置项 作用
proxy_buffering off 关闭缓冲,实时转发视频流
proxy_read_timeout 3600s 长连接超时 1 小时
set $target $arg_url 从查询参数动态获取代理目标
resolver DNS 解析器,用于解析 $target 中的域名

Java 代码改动

修改 LisaVideoServiceImpl.javarewriteUrls() 方法,根据配置决定 FLV 的代理目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Value("${lisa.video.proxy-type:java}")
private String proxyType;

@Value("${lisa.video.proxy-base-url}")
private String proxyBaseUrl;

@Value("${lisa.video.nginx-base-url:}")
private String nginxBaseUrl;

private void rewriteUrls(JSONObject data) throws UnsupportedEncodingException {
JSONObject urls = data.getJSONObject("urls");
if (urls == null) return;

for (String key : urls.keySet()) {
String url = urls.getString(key);
if (url == null || !url.startsWith(upstreamHost)) continue;

if (key.equals("flvPlayUrl")) {
// FLV 根据配置选择代理
if ("nginx".equals(proxyType)) {
urls.put(key, nginxBaseUrl + "/video-proxy/?url=" + URLEncoder.encode(url, "UTF-8"));
} else {
urls.put(key, proxyBaseUrl + "/dataCollection/video/proxy/flv?url=" + URLEncoder.encode(url, "UTF-8"));
}
} else if (key.equals("hlsPlayUrl")) {
// HLS 始终走 Java 代理(需要改写 M3U8 内容)
urls.put(key, proxyBaseUrl + "/dataCollection/video/proxy/hls/m3u8?url=" + URLEncoder.encode(url, "UTF-8"));
}
}
}

配置项

配置项 说明
lisa.video.proxy-type nginxjava 决定 FLV 走 Nginx 还是 Java,默认 java
lisa.video.proxy-base-url http://10.144.32.219:9223 Java 代理地址
lisa.video.nginx-base-url http://10.144.32.219:18001 Nginx 代理地址

切换方式:修改 Consul 配置 lisa.video.proxy-type=nginx,重启或热更新即可。

验证步骤

阶段一:Nginx 代理单独验证

  1. 部署 Nginx 配置,确认 18001 端口可访问
  2. 调 play 接口拿一个上游 FLV 地址
  3. 直接访问 http://nginx:18001/video-proxy/?url=<上游flv地址>,确认返回 video/x-flv

阶段二:Java 配置切换验证

  1. Consul 配置 lisa.video.proxy-type=nginx
  2. 重启 Java 服务
  3. 调用 POST /dataCollection/video/play,确认返回的 FLV URL 指向 Nginx,HLS URL 指向 Java
  4. 用 flv.js 播放 FLV 验证
  5. 用 hls.js 播放 HLS 验证

阶段三:回退验证

  1. Consul 改回 lisa.video.proxy-type=java
  2. 确认 FLV 和 HLS 都恢复为 Java 代理模式

风险点

风险 应对
Nginx DNS 解析器配置不对 根据服务器环境配置正确的 resolver,或直接用 IP
Nginx 未部署或端口不通 proxy-type 保持 java,不影响现有功能
Nginx 无 host 白名单(开放代理) 内网环境风险低;如需加固,可在 Nginx 层加 if 校验

性能对比

性能对比图表
图2:Java 代理 vs Nginx 代理的性能指标对比

指标 Java 代理 Nginx 代理
每路视频占用 1 个 Tomcat 线程 Nginx worker 连接
内存开销 8KB 缓冲区 × 并发数 极低
最大并发路数 受 Tomcat 线程池限制(默认 200) 数千路
CPU 开销 较高(Java 内存拷贝) 极低(内核态转发)

经验总结

  1. Nginx 适合纯透传场景——不需要改写内容的代理(如 FLV)交给 Nginx,需要改写内容的(如 HLS M3U8)留在 Java
  2. 配置切换比代码部署更灵活——通过 Consul 配置 proxy-type 可以随时切换代理模式,不需要重新部署
  3. 视频流代理要关闭缓冲——proxy_buffering off 是关键配置,否则 Nginx 会缓冲整个视频再返回
  4. 超时时间要足够长——视频流可能持续几小时,proxy_read_timeout 要设为 3600s 或更长

本文涉及文件:LisaVideoServiceImpl.java、Nginx 配置、Consul 配置。