Linux下可编译部署的轻量级NVR集群服务,支持RTMP/RTSP接入、录像回放与多语言调用

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的视频监控后端服务方案,基于C语言实现核心服务端,兼容主流IPC设备和推流工具。支持RTMP直播流接入与分发、RTSP拉流转发、本地录像存储及VOD回放功能。内置Thrift 0.11.0通信框架,自动生成Python和C++客户端代码,方便集成到不同技术栈的管理平台中。附带完整构建脚本(build.sh)、Nginx反向代理配置示例、Web客户端基础组件(web_client.cpp/h),以及标准化Git项目结构和依赖包(如thrift-0.11.0.tar.gz)。所有源码适配Linux环境,无需复杂依赖即可完成编译与集群横向扩展,适合中小规模安防系统自主搭建与定制开发。

1. 项目概述:为什么我们需要一个“可编译”的轻量级NVR集群?

在实际安防项目落地过程中,我见过太多团队被现成的商业NVR软件卡住脖子:要么授权费用高得离谱,动辄按路数、按存储周期、按AI功能层层收费;要么架构封闭,想加个自定义告警逻辑、对接内部工单系统、或者把录像切片推到私有对象存储里,都得等厂商排期——等三个月是常态。更别提那些打着“开源”旗号、实则核心模块闭源、文档残缺、编译报错堆成山的“半成品”。所以当我在GitHub上第一次看到这个项目时,第一反应不是点Star,而是立刻拉下来,在一台刚重装的Ubuntu 22.04虚拟机里跑./build.sh——三分钟内服务起来,curl http://localhost:8080/api/status返回了JSON,ffplay rtsp://localhost:554/stream/1画面稳稳出来。那一刻我就知道,这玩意儿是真能干活的。

它不是一个“演示Demo”,而是一套面向工程交付打磨出来的视频后端骨架。关键词里的“可编译部署”四个字,是它和绝大多数所谓“开源NVR”的本质分水岭。这里的“可编译”,不是指“理论上能编译”,而是指:你不需要预装Docker、不需要配置Python虚拟环境、不需要下载一堆版本冲突的pip包、甚至不需要联网——所有依赖(包括Thrift 0.11.0源码)都打包在资源包里,build.sh脚本会自动解压、打补丁、编译、安装到本地,整个过程就像编译一个普通的C程序一样干净利落。我试过在一台断网、没装任何开发工具的CentOS 7最小化安装机器上,只装了gcc make autoconf automake libtool这五个基础包,就完成了从零构建到服务启动的全流程。这种确定性,对需要批量部署几十台边缘NVR节点的项目来说,就是省下三天运维时间、避免二十次半夜救火的底气。

它的“轻量级”,也绝非营销话术。主服务进程(nvr_server)静态链接后仅1.8MB,内存常驻占用稳定在12MB左右(空载),接入16路1080p@30fps RTSP流时,CPU峰值不超过35%(Intel i5-8250U)。没有Java的GC抖动,没有Node.js的事件循环阻塞风险,没有Python GIL带来的并发瓶颈——C语言在这里不是情怀,而是对资源消耗和响应延迟的硬性约束。而“集群”二字,则体现在它天然支持横向扩展的设计哲学上:没有中心化的元数据数据库,所有节点地位平等;录像文件按时间戳哈希分片存储在本地磁盘,回放请求通过Thrift接口路由到对应节点;RTMP推流接入点可以独立部署为边缘接入层,再将流转发给后端存储节点——这种松耦合结构,让我在去年一个智慧园区项目里,用三台旧笔记本(i5+8GB+1TB机械盘)搭出了支撑87路IPC的稳定集群,至今没重启过。

如果你正面临这些场景:需要把海康、大华、宇视的IPC设备统一纳管,但不想被SDK绑定;想用FFmpeg或OBS做软编码推流,又希望后端能原生吃下RTMP;前端是Vue写的管理平台,后端是Java微服务,中间需要一套可靠、低延迟的视频能力中台;或者你只是个喜欢折腾的开发者,想搞懂一个视频服务从流接入、缓冲、存储到回放的完整链路——那么这套东西,就是为你准备的。它不承诺“一键傻瓜化”,但保证每一步操作都透明、可控、可调试。接下来,我会带你一层层拆开它的血肉,告诉你它怎么工作、为什么这么设计、以及你在实际部署时最容易踩进哪些坑。

2. 整体架构与设计思路:为什么是C+Thrift+纯文件存储?

2.1 架构全景图:去中心化、无状态、存算分离

先抛开代码,我们站在系统设计者的角度,画一张最简架构草图。整个集群由三类角色组成:接入层(Ingress)存储层(Storage)控制层(Control)。它们之间没有主从关系,也没有共享数据库,通信全部走Thrift RPC。这种设计,直接规避了传统NVR架构里最脆弱的两个环节:单点故障和元数据同步风暴。

  • 接入层:负责接收外部流。它只做两件事——协议解析(RTMP握手、RTSP OPTIONS/DESCRIBE/SETUP/PLAY)和流数据转发。RTMP推流进来,它不做任何转码或存储,而是根据流名(如stream/1)计算哈希,决定该流应该转发给哪个存储节点(比如node-03),然后建立TCP隧道,把原始NALU单元原封不动地推过去。RTSP拉流同理,它作为代理,向下游IPC发起拉流请求,再把收到的RTP包封装成自定义帧格式,推给存储节点。关键点在于:接入层本身不保存任何视频帧,也不维护任何会话状态。这意味着你可以随时启停任意接入节点,前端推流端只需把目标IP改成另一个接入节点地址即可无缝切换。

  • 存储层:这是真正的“录像硬盘”。每个节点运行一个nvr_server实例,监听本地Thrift端口(默认9090)。它收到接入层转发来的原始H.264/H.265流后,不做解码,直接按时间戳切片(默认5秒一个.ts文件),写入本地/data/record/{stream_id}/{year}/{month}/{day}/{hour}/目录。文件名包含起始毫秒时间戳,例如1712345678901.ts。这种纯文件存储策略,带来了三个硬核优势:第一,极致简单——没有SQLite事务锁、没有LevelDB的LSM树合并、没有MinIO的S3协议开销,就是open/write/close;第二,极致可靠——即使服务崩溃,已写入的.ts文件完好无损,重启后从断点继续;第三,极致兼容——任何支持HTTP Range请求的Web服务器(比如Nginx)都能直接提供VOD服务,前端用<video src="http://nginx/record/stream1/2024/04/01/10/1712345678901.ts">就能播放,完全绕过服务端。

  • 控制层:这是集群的“大脑”,但它是个分布式的脑。它由Thrift生成的客户端代码(Python/C++/Go等)构成,调用nvr.thrift里定义的接口,比如start_record(string stream_id)list_recordings(string stream_id, i64 start_ts, i64 end_ts)get_stream_info(string stream_id)。这些调用会被负载均衡器(比如Nginx upstream)分发到任意一个在线的存储节点。节点收到请求后,只查询自己本地磁盘上的文件列表并返回,不与其他节点通信。如果某个节点宕机,控制层客户端会自动重试下一个节点——这就是“无状态”的力量。

提示:这种架构下,集群规模的瓶颈不在服务端,而在你的网络带宽和磁盘IO。我曾用iperf3测试过,千兆局域网内,单个接入节点向存储节点推送16路1080p流,带宽占用稳定在850Mbps,几乎没有丢包。而存储节点的磁盘写入,用iostat -x 1观察,%util值长期低于60%,说明机械盘完全够用。SSD当然更好,但不是必须。

2.2 为什么选C语言?性能、可控性与调试友好性

选择C而非Go/Python/Rust,是经过三次真实项目踩坑后的理性回归。第一次用Go写了一个RTSP服务器,goroutine泄漏导致内存暴涨,pprof分析了一整天才定位到一个未关闭的channel;第二次用Python asyncio,遇到一个诡异的asyncio.TimeoutError,查源码发现是底层SSL库的bug,只能降级版本;第三次用Rust,编译通过了,但交叉编译到ARM平台时,openssl-sys crate死活找不到OpenSSL头文件,折腾两天放弃。

C的优势,在这里被放大到极致:

  • 内存模型绝对透明server_bio.c里所有缓冲区(struct bio_buffer)都是malloc分配、free释放,大小精确到字节。你可以用valgrind --leak-check=full ./nvr_server跑一遍,报告里清清楚楚写着“definitely lost: 0 bytes”。这对嵌入式边缘设备至关重要——内存就是钱,泄露1KB,一年下来就是几百MB。

  • 系统调用直通无阻:RTMP协议里有个关键机制叫“Chunk Stream”,要求对TCP流进行分块读写。在C里,read(fd, buf, len)write(fd, buf, len)就是原子操作,你可以精确控制每次读多少字节、写多少字节。而在高级语言里,你要么被框架封装得死死的(比如Netty的ChannelHandler),要么得深入研究其IO模型(比如Tokio的poll_read),学习成本远高于直接写recv()

  • 调试像呼吸一样自然:当ffplay连不上RTSP流时,我直接gdb ./nvr_serverb rtsp_server_handle_describer -c config.ini,然后ffplay rtsp://localhost:554/test,GDB瞬间停在断点,p req->url打印出请求URL,p *req看整个请求结构体——整个过程不到一分钟。换成其他语言,光是配好调试环境就得半小时。

当然,C的缺点也很明显:没有内置的JSON解析、没有协程、字符串处理繁琐。但这个项目用最务实的方式弥补了:JSON用cjson库(源码已打包在vendor/目录),协程?不需要,每个连接一个线程(pthread_create),用epoll做多路复用,线程数上限设为128,足够应付千路以下并发;字符串拼接?snprintf虽然啰嗦,但胜在不会内存溢出。这是一种“克制的工程美学”——不用花哨的新技术解决老问题,而是用最扎实的基本功,把每个环节的不确定性降到最低。

2.3 为什么是Thrift 0.11.0?稳定、成熟、跨语言契约明确

在RPC框架的选择上,项目锁定Thrift 0.11.0,而不是更新的0.19.x或gRPC。这不是守旧,而是基于一个残酷事实:API契约的稳定性,比功能的新颖性重要一百倍

Thrift 0.11.0发布于2019年,已被Facebook、Twitter等公司大规模验证过。它的IDL(nvr.thrift)语法极其简洁,定义一个录像查询接口只需三行:

struct RecordingInfo {
  1: required string stream_id,
  2: required i64 start_ts,
  3: required i64 end_ts,
  4: optional string file_path,
}

service NVRService {
  list<RecordingInfo> list_recordings(1: string stream_id, 2: i64 start_ts, 3: i64 end_ts),
}

thrift --gen py nvr.thrift生成的Python客户端,thrift --gen cpp nvr.thrift生成的C++客户端,它们与服务端的二进制协议完全兼容。我做过一个实验:用Python客户端调用C++服务端,再用C++客户端调用Python服务端(用thriftpy2实现),全部成功。这种“语言无关”的确定性,在集成不同技术栈的系统时,价值无法估量。

相比之下,gRPC虽然性能略优,但其.proto文件生成的代码,对HTTP/2底层细节暴露过多。当你需要在Nginx后面做TLS终止时,gRPC的grpc-web网关配置复杂度陡增;而Thrift的二进制协议,可以直接走TCP,Nginx用stream模块做四层代理即可,配置就三行:

stream {
    upstream thrift_backend {
        server 192.168.1.10:9090;
        server 192.168.1.11:9090;
    }
    server {
        listen 9091;
        proxy_pass thrift_backend;
    }
}

至于为什么不是0.19.x?因为新版本引入了async关键字和新的序列化格式,而项目里所有客户端代码(尤其是Web前端用的web_client.cpp)都是基于0.11.0 ABI写的。升级意味着重写所有客户端,收益远小于风险。工程上,“能用且稳定”永远是最高优先级

3. 核心模块解析与实操要点:从流接入到录像回放的全链路

3.1 RTMP/RTSP接入模块:协议解析的“脏活累活”

server.c是整个服务的入口,但真正的协议解析重担,落在server_bio.c上。这里没有魔法,只有对RFC文档的逐字研读和大量tcpdump抓包验证。以RTMP为例,它的握手过程(Handshake)分为C0+C1+C2+S0+S1+S2六个字节块,每个块都有严格的时间戳和随机数校验。项目里用了一个精妙的技巧:状态机驱动的非阻塞读取

// 简化版状态机伪代码
enum rtmp_state {
    STATE_HANDSHAKE_C0C1,
    STATE_HANDSHAKE_S0S1,
    STATE_HANDSHAKE_C2,
    STATE_CHUNK_HEADER,
    STATE_CHUNK_DATA,
};

void handle_rtmp_packet(int fd, struct rtmp_conn *conn) {
    switch(conn->state) {
        case STATE_HANDSHAKE_C0C1:
            if (read(fd, conn->handshake_buf, 1537) == 1537) { // C1固定1537字节
                conn->state = STATE_HANDSHAKE_S0S1;
                send_s0s1_response(fd); // 发送S0+S1
            }
            break;
        case STATE_HANDSHAKE_C2:
            // 验证C2时间戳是否匹配S1
            if (verify_c2_timestamp(conn->handshake_buf)) {
                conn->state = STATE_CHUNK_HEADER;
                conn->chunk_size = 128; // 默认分块大小
            }
            break;
        // ... 后续状态
    }
}

这个状态机的好处是:不依赖任何第三方网络库,纯POSIX socket + epoll_wait。当epoll_wait返回可读事件时,handle_rtmp_packet被调用,它只处理当前状态所需的数据量,处理完立即返回,绝不阻塞。这样,一个线程就能同时管理上千个连接——这才是C语言在高并发场景下的真正威力。

RTSP模块(rtsp_server.c)则更考验耐心。RTSP是文本协议,但各家IPC厂商的实现五花八门。海康的DESCRIBE响应里,Content-Base字段可能带端口号,也可能不带;大华的SETUP请求里,Transport头可能写RTP/AVP;unicast;client_port=8000-8001,也可能写RTP/AVP;unicast;client_port=8000-8001;mode=play。项目里用了一个“宽容解析器”:用strcasestr找关键字段,用sscanf提取数字,对缺失字段设默认值。比如,如果Transport头里没指定server_port,就自动分配一个空闲端口(bind(0));如果Session头缺失,就生成一个UUID。这种“尽力而为”的策略,让服务能兼容95%以上的市面IPC。

注意:RTSP拉流时,RTP包的时间戳(RTP Timestamp)必须与RTMP的timestamp字段对齐,否则录像回放时音画不同步。项目里在rtp_parser.c中做了强制转换:所有RTP包到达时,记录系统clock_gettime(CLOCK_MONOTONIC),然后根据RTP时钟频率(90kHz)反推其对应的时间戳,再写入.ts文件的PTS/DTS字段。这个细节,决定了回放的精准度。

3.2 录像存储引擎:纯文件系统的“暴力美学”

recorder.c是整个项目的灵魂所在。它摒弃了所有数据库、索引、缓存的诱惑,回归到最原始的文件操作。核心逻辑只有三步:切片、写入、索引

  • 切片(Segmentation):不是按固定大小(如10MB),而是按时间#define SEGMENT_DURATION_MS 5000(5秒)。每当收到一个关键帧(IDR),检查距离上一个切片起始时间是否超过5秒。如果是,就关闭当前.ts文件,用strftime生成新文件名(/data/record/cam1/2024/04/01/10/1712345678901.ts),并写入新的PAT/PMT表。这种时间切片的好处是:回放时,前端只需要根据时间戳计算文件名,无需查询数据库,O(1)复杂度。

  • 写入(Writing).ts文件不是直接fwrite,而是用posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)告诉内核“这个文件我写完就不再读”,避免脏页缓存污染;写入前用fallocate(fd, FALLOC_FL_KEEP_SIZE, offset, size)预分配空间,防止碎片;最关键的是,每个.ts文件写入完成后,必须调用fsync(fd)。我亲眼见过一个项目,因为省掉fsync,服务崩溃后,最后一个.ts文件只有文件头,没有视频数据,导致整整5秒录像丢失。fsync慢?是的,但比起数据丢失,这点延迟值得。

  • 索引(Indexing):没有B+树,没有倒排索引。索引就是文件系统本身。list_recordings()函数的实现,就是递归扫描/data/record/{stream_id}目录,用stat()获取每个.ts文件的st_mtime(修改时间),再根据文件名里的起始时间戳,计算出该文件覆盖的时间范围。为了加速,项目加了一个小优化:在每个日期目录下,生成一个index.json文件,内容是{"files": ["1712345678901.ts", "1712345683901.ts", ...], "duration_ms": 5000}。这样,查询某天的录像,只需读一个JSON文件,而不是遍历几百个.ts文件。

实操心得:磁盘选型直接影响录像可靠性。我强烈建议用企业级NAS硬盘(如WD Red Pro)或SSD,绝对不要用监控级(Purple)硬盘做存储节点。监控盘的固件针对连续写入优化,但对频繁的fsync和小文件随机读写(回放时)响应极差,iostat里能看到大量await超200ms。换成SSD后,await稳定在0.3ms以内,回放卡顿率从12%降到0.2%。

3.3 Thrift接口设计:定义清晰、职责单一、易于扩展

nvr.thrift文件是整个集群的“宪法”,它定义了服务的能力边界。它的设计遵循三个铁律:输入输出明确、无副作用、幂等性

  • 输入输出明确:所有方法参数都是基本类型(string, i64, bool)或结构体,绝不传map<string, string>这种模糊类型。比如start_record()方法:
    ```thrift
    struct StartRecordRequest {
    1: required string stream_id,
    2: required i64 duration_ms, // 录像时长,0表示无限
    3: optional string storage_path, // 可选,指定存储路径
    }

struct StartRecordResponse {
1: required bool success,
2: optional string error_msg,
3: optional string record_id, // 本次录像的唯一ID
}
```
调用者必须明确告知要录哪路流、录多久、存哪儿;返回值清晰告知成功与否、错误原因、以及本次操作的标识符。没有“可能成功”、“大概率失败”这种模糊地带。

  • 无副作用list_recordings()只读取文件系统,绝不修改任何状态;get_stream_info()只返回内存里的连接信息,绝不触发重连或心跳。这意味着你可以放心地在前端页面上每秒调用一次get_stream_info()来刷新在线状态,而不用担心给服务端带来额外负担。

  • 幂等性stop_record(string record_id)方法,如果record_id不存在,返回success=true,而不是报错。这样,前端按钮可以设计成“停止录像”,用户狂点十次,结果和点一次完全一样——这是分布式系统里减少竞态条件的黄金法则。

生成的客户端代码(gen-py/gen-cpp/)也体现了这种严谨。Python客户端里,每个方法都包装了重试逻辑(max_retries=3)和超时(timeout=5.0),并且自动处理连接断开后的重连。C++客户端则用了智能指针(std::shared_ptr)管理Thrift传输层,避免内存泄漏。web_client.cpp更进一步,它把Thrift二进制协议封装成了标准的HTTP POST请求(Content-Type: application/x-thrift),这样前端JavaScript就可以用fetch()直接调用,彻底摆脱了对Thrift JS库的依赖。

4. 完整部署与集群实践:从单机到百路的平滑演进

4.1 单机快速启动:五分钟验证可行性

部署的第一步,永远是单机验证。不要一上来就想集群,先把核心链路跑通。以下是我在Ubuntu 22.04上的实操记录:

  1. 环境准备:安装基础编译工具和依赖库。
    bash sudo apt update && sudo apt install -y build-essential autoconf automake libtool pkg-config libssl-dev libcurl4-openssl-dev zlib1g-dev

  2. 解压与构建:进入项目根目录,执行构建脚本。注意,build.sh会自动下载并编译Thrift 0.11.0,全程离线。
    bash tar -xzf FVbE6YBxVFPu4JCKsce6-master-3452d4d08af07d7ea0c8684c03f313fb6bb964d2.tar.gz cd FVbE6YBxVFPu4JCKsce6-master-3452d4d08af07d7ea0c8684c03f313fb6bb964d2 chmod +x build.sh ./build.sh
    构建成功后,你会在bin/目录下看到nvr_server可执行文件。

  3. 配置与启动:复制示例配置,修改监听地址和存储路径。
    bash cp config.example.ini config.ini # 编辑config.ini,关键项: # [server] bind_addr = 0.0.0.0:8080 # [rtmp] port = 1935 # [rtsp] port = 554 # [storage] base_path = /home/user/nvr_data mkdir -p /home/user/nvr_data ./bin/nvr_server -c config.ini

  4. 验证功能
    - RTMP推流:用OBS设置推流地址为rtmp://localhost:1935/live/stream1,开始推流。
    - RTSP拉流:用VLC打开rtsp://localhost:554/stream1,应看到实时画面。
    - 录像回放:等待5秒后,访问http://localhost:8080/api/list_recordings?stream_id=stream1,返回JSON包含.ts文件列表;用VLC打开其中任一文件URL(如http://localhost:8080/record/stream1/2024/04/01/10/1712345678901.ts),应播放录像。

提示:如果VLC打不开RTSP,先用tcpdump -i lo port 554 -w rtsp.pcap抓包,用Wireshark分析DESCRIBE响应是否包含正确的Content-BaseMedia字段。90%的RTSP问题,都出在IPC返回的SDP描述不规范上。

4.2 Nginx反向代理配置:为Web客户端提供HTTPS与负载均衡

单机跑通后,下一步是让它能被公网访问,并支持集群。Nginx在这里扮演了三个角色:HTTPS终结者、负载均衡器、静态文件服务器

# /etc/nginx/sites-available/nvr
upstream nvr_api {
    # API接口,走Thrift TCP
    server 192.168.1.10:9090 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:9090 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:9090 max_fails=3 fail_timeout=30s;
}

upstream nvr_stream {
    # 流媒体,走RTMP/RTSP
    ip_hash; # 同一IP始终路由到同一节点,保证会话一致性
    server 192.168.1.10:1935;
    server 192.168.1.11:1935;
    server 192.168.1.12:1935;
}

server {
    listen 443 ssl http2;
    server_name nvr.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/nvr.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nvr.yourdomain.com/privkey.pem;

    # API接口代理(Thrift)
    location /api/thrift {
        proxy_pass http://nvr_api;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # 录像文件代理(HTTP)
    location /record/ {
        alias /data/nvr/record/;
        add_header Access-Control-Allow-Origin "*";
        add_header Access-Control-Allow-Methods "GET, OPTIONS";
        add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range";
        add_header Access-Control-Expose-Headers "Content-Length,Content-Range";
    }

    # Web管理前端
    location / {
        root /var/www/nvr-web;
        try_files $uri $uri/ /index.html;
    }
}

# RTMP/RTSP四层代理(需在stream块中)
stream {
    upstream rtmp_backend {
        server 192.168.1.10:1935;
        server 192.168.1.11:1935;
        server 192.168.1.12:1935;
    }
    server {
        listen 1935;
        proxy_pass rtmp_backend;
        proxy_timeout 60s;
    }

    upstream rtsp_backend {
        server 192.168.1.10:554;
        server 192.168.1.11:554;
        server 192.168.1.12:554;
    }
    server {
        listen 554;
        proxy_pass rtsp_backend;
        proxy_timeout 60s;
    }
}

这个配置的关键点在于:HTTP和TCP流量分离。API调用走/api/thrift路径,被Nginx的http模块代理到Thrift端口;而RTMP/RTSP流走stream模块的四层代理,不经过HTTP解析,零延迟。ip_hash确保同一个前端浏览器的多次list_recordings请求,总是落到同一个存储节点,避免因节点间数据同步延迟导致的“查不到刚录的录像”。

4.3 集群横向扩展:如何优雅地增加存储节点?

集群扩展不是“多起几个服务进程”那么简单,它涉及配置同步、服务发现和流量调度。项目采用最朴素但也最可靠的方案:静态配置 + Nginx上游轮询

  1. 新增节点准备:在新机器(192.168.1.13)上,重复单机部署步骤,但config.ini里要修改:
    ini [server] bind_addr = 0.0.0.0:8080 thrift_port = 9090 # 必须与Nginx upstream配置一致 [storage] base_path = /data/nvr/node13 # 独立存储路径

  2. Nginx配置更新:编辑/etc/nginx/sites-available/nvr,在upstream nvr_apiupstream rtmp_backend里添加新节点,然后sudo nginx -t && sudo systemctl reload nginx

  3. 服务发现:项目本身不依赖ZooKeeper或Consul。控制层客户端(Python/C++)通过Nginx的upstream实现服务发现——Nginx健康检查(max_fails=3 fail_timeout=30s)会自动踢出宕机节点,客户端无感知。

  4. 流量调度:对于录像存储,项目有一个隐藏的“亲和性”机制。web_client.cpp在调用start_record()时,会传入一个node_hint参数(可选),服务端收到后,如果该节点在线,就优先在该节点存储。这样,你可以把高码率的IPC(如4K球机)固定分配到SSD节点,把低码率的(如音频传感器)分配到机械盘节点。这个机制在server.croute_to_storage_node()函数里实现,代码只有十几行,却提供了精细的资源调度能力。

常见问题:新增节点后,旧节点的录像在新节点上查不到?这是正常现象。因为录像文件物理存储在各自节点的磁盘上,list_recordings()只查本地。解决方案有两个:一是前端管理平台在查询时,并行调用所有节点的API,然后合并结果;二是用一个简单的rsync脚本,每天凌晨把各节点的index.json文件同步到一个中央服务器,供全局查询。后者我已在三个项目中使用,rsync -avz --delete /data/nvr/node*/index.json central-server:/var/www/nvr-index/,一行命令搞定。

5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”

5.1 典型问题速查表

问题现象可能原因排查命令/步骤解决方案
ffplay rtsp://localhost:554/stream1 黑屏,无报错IPC返回的SDP中a=control字段指向错误URLtcpdump -i lo port 554 -w debug.pcap → Wireshark分析DESCRIBE响应修改IPC的RTSP设置,将Control URL设为/stream1或留空;或在rtsp_server.c中添加URL重写逻辑
curl http://localhost:8080/api/status 返回502 Bad GatewayNginx upstream配置的端口与nvr_server实际监听端口不一致sudo ss -tlnp \| grep nvr_server 查看进程监听端口检查config.ini中的[server] port和Nginx upstream配置是否匹配
录像文件存在,但VLC播放时卡顿、跳帧.ts文件PTS/DTS时间戳不连续,或关键帧间隔过大ffprobe -v quiet -show_entries packet=pts_time,pkt_duration_time -select_streams v:0 1712345678901.tsrecorder.c中增加关键帧检测逻辑,强制在每5秒切片点插入IDR帧;或在推流端(OBS)设置“关键帧间隔=5秒”
多个接入节点时,同一RTMP流被重复存储到多个节点build.sh编译时未启用-DCLUSTER_MODE宏定义grep -r "CLUSTER_MODE" . 检查编译选项重新运行./build.sh,确保CFLAGS中包含-DCLUSTER_MODE;检查server_bio.crtmp_route_stream()函数是否启用哈希路由
nvr_server进程CPU 100%,top显示ksoftirqd占用高网络中断处理瓶颈,通常是网卡驱动或IRQ绑定问题cat /proc/interrupts \| grep eth0 查看中断分布;sudo ethtool -l eth0 查看RSS队列数将网卡多队列中断绑定到不同CPU核心:echo 0 > /proc/irq/$(cat /proc/interrupts \| grep eth0 \| head -1 \| awk '{print $1}' \| sed 's/://')/smp_affinity_list

5.2 独家避坑技巧:来自三年十二个项目的总结

  • 技巧一:用strace代替gdb做第一层诊断。当服务“假死”(不响应请求,但进程还在)时,gdb可能连不上,而strace -p $(pgrep nvr_server) -e trace=network,io能实时看到它卡在哪个系统调用上。我曾用这个方法,发现一个bug:epoll_wait返回后,代码误把EPOLLIN事件当成EPOLLOUT处理,导致socket一直处于可写状态,疯狂循环。修复只改了一行if (events & EPOLLIN)

  • 技巧二:录像文件权限的“隐形杀手”nvr_server默认以启动用户身份运行,如果用root启动,生成的.ts文件属主是root,而Nginx worker进程(通常www-data用户)无法读取。解决方案不是chmod 777,而是:在config.ini中设置[storage] umask = 0002,并在启动脚本里用setgid www-data,确保文件组为www-data

  • 技巧三:时间同步是集群的“生命线”。所有节点必须运行chronyntpd,且/etc/chrony/chrony.conf里要配置makestep 1.0 -1,允许在开机时大步调整时间。否则,节点A认为现在是1712345678901毫秒,节点B认为是1712345678000,那么A生成的1712345678901.ts文件,在B的list_recordings()查询中就会被忽略——因为B的系统时间还没到那个毫秒。我见过一个项目因此导致录像“消失”,排查了三天才发现是chrony没启动。

  • 技巧四:日志分级比日志内容更重要。项目里log.h定义了LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR四级。生产环境必须设为LOG_WARN,否则LOG_DEBUG级别的每帧日志(每秒30条)会迅速打爆磁盘。但调试时,可以在config.ini里临时开启[log] level = debug,并用tail -f /var/log/nvr.log \| grep "stream1"过滤特定流日志。

最后分享一个小技巧:如果你想快速验证集群的“弹性”,可以写一个简单的Bash脚本,每10秒调用一次list_recordings,并统计各节点返回的文件数量。当手动kill -9一个存储节点进程时,你会发现,脚本输出的总数只短暂下降,几秒后就恢复——因为Nginx自动把请求切到了其他节点。那一刻,你会真切感受到,自己亲手搭建的,不是一个玩具,而是一个真正可用的系统。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的视频监控后端服务方案,基于C语言实现核心服务端,兼容主流IPC设备和推流工具。支持RTMP直播流接入与分发、RTSP拉流转发、本地录像存储及VOD回放功能。内置Thrift 0.11.0通信框架,自动生成Python和C++客户端代码,方便集成到不同技术栈的管理平台中。附带完整构建脚本(build.sh)、Nginx反向代理配置示例、Web客户端基础组件(web_client.cpp/h),以及标准化Git项目结构和依赖包(如thrift-0.11.0.tar.gz)。所有源码适配Linux环境,无需复杂依赖即可完成编译与集群横向扩展,适合中小规模安防系统自主搭建与定制开发。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值