Ubuntu 22.04 下 Nginx-RTMP 高并发直播服务器实战部署
1. 为什么是 Nginx-RTMP 而不是 FFmpeg 或其他方案在 Ubuntu 22.04 上搭一个能扛住几十路并发推流、同时支持 Web 端低延迟播放的视频服务器很多人第一反应是“直接用 FFmpeg 转发不就完了”——我试过也踩过坑。去年给一家本地教育机构做直播系统时最初就是用ffmpeg -i rtmp://localhost:1935/live/src -c copy -f flv rtmp://localhost:1935/live/out这种链式转发跑起来的。表面看能用但第三天就崩了学生端卡顿率飙升到 40%后台日志里全是Connection refused和Broken pipe重启 FFmpeg 后不到两小时又挂更麻烦的是一旦要加 HLS 切片、录制回放、多分辨率自适应就得堆砌七八个 FFmpeg 实例CPU 占用直冲 98%htop里密密麻麻全是ffmpeg进程根本没法监控和限流。后来我们彻底重做选了 Nginx-RTMP 模块。不是因为它“新”而是它把流媒体服务里最硬的几块骨头——连接管理、协议转换、状态跟踪、资源隔离——全压进 Nginx 这个久经考验的事件驱动引擎里。Nginx 本身在 Ubuntu 22.04 上已深度适配 systemd、AppArmor 和内核 TCP 栈优化而 RTMP 模块只是在其 upstream 机制上做了轻量扩展不碰主线程模型也不 fork 子进程。这意味着单机 2C4G 的 VPS 就能稳稳支撑 80 路 RTMP 推流实测峰值带宽 120 MbpsHLS 切片延迟稳定在 8–12 秒且 CPU 峰值长期压在 35% 以下。这不是理论值是我们在线上跑了 17 个月的真实数据。关键区别在于架构层级。FFmpeg 是“流处理器”本质是命令行工具链它处理的是“一帧一帧的数据”但不管“谁在连、连了多久、断了几次、带宽多少”。而 Nginx-RTMP 是“流媒体网关”它天然具备连接上下文每个 RTMP 客户端连接进来模块会生成唯一 session ID记录推流名、时间戳、码率、客户端 IP、User-Agent如果推流端带了、甚至首帧 PTS这些信息全暴露在/stat页面里还能通过on_publish/on_play回调脚本实时干预。比如我们加了一段 Python 脚本在on_publish阶段校验推流密钥从 Redis 读取非法推流直接拒绝整个过程耗时 8ms完全不影响推流握手速度。这种能力FFmpeg 做不到也没法优雅地做。再看生态兼容性。Ubuntu 22.04 默认源里的nginx是 1.18 版本但 Nginx-RTMP 模块要求 Nginx ≥ 1.2.6 且需重新编译。很多人卡在这一步以为必须手动下载源码、装一堆 dev 包、改 configure 参数。其实大可不必——我们验证过用 Ubuntu 官方提供的nginx-full包含所有常用模块作为基础仅需 patch 两个小文件就能启用 RTMP全程不碰./configure。这个细节后面会细说。现在先明确一点你不是在“装一个流媒体软件”而是在 Ubuntu 22.04 的成熟 Web 服务底座上“打开一个流媒体开关”。这个认知差决定了你是花三天调通还是花三周反复重装系统。提示别被“RTMP 已淘汰”的说法带偏。HLS/DASH 确实更适合公网 CDN 分发但 RTMP 在局域网推流、OBS 直连、低延迟互动场景仍是事实标准。Nginx-RTMP 不是让你只用 RTMP而是以 RTMP 为入口自动转出 HLS、DASH、FLV、甚至 JPEG 截图它是个协议翻译中枢不是协议孤岛。2. 编译前的系统级准备绕开 Ubuntu 22.04 的三个默认陷阱Ubuntu 22.04 LTSJammy Jellyfish的底层变化比表面看起来深刻得多。它默认启用systemd-resolved做 DNS 解析内核升级到 5.15iptables被nftables全面接管而最关键的——nginx包从nginx-light/nginx-full拆分为nginx-corenginx-modules架构。这三点不提前处理编译 Nginx-RTMP 时会遇到三类典型报错undefined reference to clock_gettime链接器找不到实时库、error: ‘struct sockaddr_storage’ has no member named ‘ss_family’内核头文件版本错配、以及最让人抓狂的nginx: [emerg] unknown directive rtmp模块根本没加载成功。第一个陷阱clock_gettime链接失败。Ubuntu 22.04 的libc6-dev默认不链接librt而 Nginx-RTMP 的ngx_rtmp_cmd_module.c里用了clock_gettime(CLOCK_MONOTONIC, ts)。解决方法不是加-lrt到 LDFLAGS那会污染全局而是精准打补丁在objs/Makefile生成后、执行make前用 sed 替换$(LINK) $(LDFLAGS)行追加-lrt。但更稳妥的做法是在./configure前设置环境变量export LIBS-lrt ./configure --add-module../nginx-rtmp-module --with-http_ssl_module --with-compat注意这里--with-compat是关键它让模块能被动态加载避免重编译整个 Nginx。我们实测发现漏掉这个参数即使编译成功nginx -t也会报module not found。第二个陷阱sockaddr_storage成员缺失。这是内核头文件与 Nginx 源码的兼容问题。Ubuntu 22.04 的linux-libc-dev包里/usr/include/asm-generic/socket.h对ss_family的定义方式变了。Nginx-RTMP 模块的ngx_rtmp_handler.c第 123 行直接访问((struct sockaddr_storage *)sa)-ss_family但在新头文件下ss_family被包裹在联合体里。修复只需一行把原代码if (sa-ss_family AF_INET) {改成if (((struct sockaddr_in*)sa)-sin_family AF_INET) {——别嫌土这是最直接有效的绕过方式。我们测试过 12 种 patch 方案只有这个在 22.04/20.04/18.04 三套系统上全部通过。第三个陷阱模块未加载。Ubuntu 22.04 的nginx-full包默认禁用所有第三方模块加载路径。你必须显式配置load_module指令并确保路径绝对正确。很多教程让你把.so文件丢进/usr/lib/nginx/modules/但实际路径是/usr/share/nginx/modules/注意是share不是lib。更隐蔽的问题是 SELinux/AppArmor 干预Ubuntu 22.04 默认启用 AppArmor其abstractions/nginx模板不包含对.so模块的读取权限。临时关闭 AppArmorsudo systemctl stop apparmor能快速验证但生产环境必须加规则sudo nano /etc/apparmor.d/local/usr.sbin.nginx # 添加一行 /usr/share/nginx/modules/*.so mr,然后sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx重载。最后强调一个易忽略的依赖libpcre3-dev。Ubuntu 22.04 的pcre2库已成主流但 Nginx-RTMP 仍依赖老版 PCRE1 的pcre_compile函数。不装libpcre3-devconfigure 会静默跳过 PCRE 支持导致后续location ~* \.(m3u8|ts)$这类正则匹配全部失效。验证方法很简单编译完运行nginx -V 21 | grep -o pcre输出应为pcre而非空。3. 动态模块编译实战从零构建可热加载的 rtmp.so现在进入核心操作。我们放弃“从头编译 Nginx”这种重模式采用 Ubuntu 22.04 官方nginx-full包 动态模块加载的轻量方案。全程无需卸载现有 Nginx不改动任何系统配置所有操作在普通用户家目录完成15 分钟内可完成。第一步安装基础依赖并获取源码。注意这里不用apt install nginx而是装nginx-full的 dev 包它包含头文件和静态库sudo apt update sudo apt install -y \ build-essential zlib1g-dev libpcre3-dev libssl-dev \ nginx-full nginx-full-dev git curl wgetnginx-full-dev是关键它提供/usr/src/nginx/下的完整源码树和debian/rules构建脚本。接着拉取 Nginx-RTMP 模块用官方主仓库别用 forkcd ~ git clone https://github.com/arut/nginx-rtmp-module.git第二步打上前面提到的两个补丁。先修复sockaddr_storage问题sed -i s/sa-ss_family/((struct sockaddr_in*)sa)-sin_family/g \ nginx-rtmp-module/ngx_rtmp_handler.c再修复clock_gettime链接问题在nginx-rtmp-module/config文件末尾追加echo CORE_LIBS\$CORE_LIBS -lrt\ nginx-rtmp-module/config第三步利用 Ubuntu 的debian/rules构建系统生成动态模块。这是最省心的方式——它复用官方打包脚本保证 ABI 兼容cd /usr/src/nginx sudo make modules \ MODULES_PATH/usr/share/nginx/modules \ MODULESnginx-rtmp-module \ MODULES_CONFdebian/modules/nginx-rtmp-module.conf注意MODULES_CONF参数指向一个配置文件我们需提前创建它echo obj-$(shell dpkg-architecture -qDEB_HOST_GNU_TYPE)/nginx-rtmp-module.o | \ sudo tee /usr/src/nginx/debian/modules/nginx-rtmp-module.conf执行make modules后会在/usr/src/nginx/objs/下生成ngx_rtmp_module.so。把它复制到模块目录sudo cp /usr/src/nginx/objs/ngx_rtmp_module.so /usr/share/nginx/modules/第四步配置 Nginx 加载模块。编辑/etc/nginx/nginx.conf在user指令下方、events块之前添加load_module /usr/share/nginx/modules/ngx_rtmp_module.so;然后创建 RTMP 配置文件/etc/nginx/conf.d/rtmp.confrtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; allow publish 127.0.0.1; allow publish 192.168.0.0/16; deny publish all; } } }这里allow publish是安全底线只允许本机和局域网推流公网 IP 默认拒绝。别信“先开放再加固”的说法线上第一行配置就该是防火墙。第五步验证模块加载。执行sudo nginx -t输出必须是syntax is ok且无警告。如果报unknown directive rtmp99% 是load_module路径错了或 AppArmor 拦截了。此时运行sudo strace -e traceopenat nginx -t 21 | grep rtmp能看到 Nginx 实际尝试打开的路径对照修正即可。注意不要用sudo systemctl restart nginx立即生效。先sudo nginx -s reload它会平滑重启 worker 进程不中断已有流。我们线上所有更新都走 reload零秒切换。4. RTMP→HLS→DASH 全链路配置不只是“能播”而是“播得稳”Nginx-RTMP 的价值不在 RTMP 本身而在它能把一路推流实时、无损、低开销地转出多种格式。很多人只配了hls on就以为完工结果发现 HLS 播放卡顿、切片丢失、DASH manifest 404——问题全出在参数没吃透。先看 HLS 配置。这是最常用的 Web 播放格式但默认参数极不友好application live { live on; hls on; hls_path /var/www/html/hls; hls_fragment 3s; hls_playlist_length 30s; hls_nested on; }这段代码有四个致命隐患。第一hls_path必须是 Nginx worker 进程有写权限的目录/var/www/html/hls默认属主是www-data但如果你用sudo nginx -t测试root 用户能写worker 却可能因权限不足无法创建文件。解决方案sudo chown -R www-data:www-data /var/www/html/hls sudo chmod -R 755 /var/www/html/hls。第二hls_fragment 3s太短会导致切片数量爆炸30 秒 playlist 就要存 10 个.ts文件磁盘 I/O 压力大且浏览器缓存效率低。实测5s是平衡点既保证首屏时间 8 秒又控制文件数在合理范围。第三hls_playlist_length 30s意味着 m3u8 文件只保留最近 30 秒的索引但若网络抖动导致某次切片延迟旧索引被删而新索引未生成就会出现空白期。我们改为60s并加hls_cleanup on自动清理过期文件。第四hls_nested on开启后切片路径变成hls/streamname/index.m3u8但很多播放器如 Video.js默认不识别嵌套路径需在 HTML 里显式指定srchls/streamname/index.m3u8不如关掉用扁平路径。修正后的 HLS 配置application live { live on; hls on; hls_path /var/www/html/hls; hls_fragment 5s; hls_playlist_length 60s; hls_cleanup on; hls_nested off; hls_continuous on; # 关键避免切片间隙 hls_sync 100ms; # 强制音视频 PTS 对齐防音画不同步 }再看 DASH。它比 HLS 更复杂因为需要生成 MPDMedia Presentation Description文件和分片。Nginx-RTMP 的 DASH 支持是实验性的但经过我们 11 个月压测已足够稳定application live { live on; dash on; dash_path /var/www/html/dash; dash_fragment 5s; dash_playlist_length 60s; dash_cleanup on; }注意DASH 和 HLS 可以共存于同一 applicationNginx 会自动为同一推流生成两套切片。但dash_path和hls_path必须不同目录否则文件名冲突。DASH 的dash_fragment必须与 HLS 的hls_fragment严格一致否则 MPD 里的duration字段会错乱播放器解析失败。我们曾因hls_fragment 5s而dash_fragment 4s导致 dash.js 报Invalid MPD: duration mismatch排查了 6 小时才发现是这个参数。最后是 FLV 直播流。很多人忽略它但它对低延迟场景至关重要application live { live on; play /var/www/html/flv; # 指向静态文件目录 }这行配置让 Nginx-RTMP 开启 HTTP-FLV 服务URL 为http://your-server/flv?streamstreamname。OBS 或 ffmpeg 可直接用rtmp://your-server/live/streamname推流前端用 flv.js 播放端到端延迟压到 1.2 秒以内实测数据。flv.js 的优势是纯 JS 实现不依赖 MSE兼容 IE11且启动快。我们对比过 hls.js 和 flv.js相同网络下flv.js 首帧时间平均快 2.3 秒卡顿率低 67%。提示所有切片目录hls_path,dash_path,play目录必须在 Nginx 配置中显式声明为 locationlocation /hls { types { application/vnd.apple.mpegurl m3u8; } root /var/www/html; add_header Cache-Control no-cache; }否则浏览器会因 MIME 类型错误拒绝加载 m3u8。5. 生产级加固从“能跑”到“敢上线”的七道防线配置完基本功能离真正上线还差很远。我们总结出七道必须落地的防线每一道都来自真实故障教训。第一道连接数与带宽硬限。Ubuntu 22.04 默认ulimit -n是 1024Nginx worker 进程最多打开 1024 个文件描述符而每个 RTMP 连接至少占 3 个 fdsocket、log、hls file100 路推流就爆了。修改/etc/security/limits.confwww-data soft nofile 65536 www-data hard nofile 65536并在/lib/systemd/system/nginx.service的[Service]段加LimitNOFILE65536 Restarton-failure RestartSec10然后sudo systemctl daemon-reload sudo systemctl restart nginx。第二道推流认证。allow publish只能按 IP 过滤无法防密码泄露。我们在on_publish回调里集成 JWT 验证application live { live on; on_publish http://127.0.0.1:8080/auth; }后端用 Python Flask 写一个/auth接口解析 URL 中的?tokenxxx校验签名和有效期返回200 OK或403 Forbidden。Nginx-RTMP 会阻塞推流握手直到回调结束超时设为 3 秒on_timeout 3s避免拖慢正常推流。第三道自动录制与归档。教育直播必须存档。record all会录所有流但磁盘会撑爆。我们用record_unique on生成带时间戳的文件名并加record_max_size 1G限制单文件大小application live { live on; record all; record_path /var/www/html/record; record_suffix -%d-%b-%y-%H-%M-%S.flv; record_unique on; record_max_size 1G; record_interval 3600s; # 每小时切一个文件 }record_suffix里的%d-%b-%y是日期%H-%M-%S是时间生成streamname-01-Jan-23-14-30-22.flv方便按天归档。第四道HTTP 状态页与监控。/stat页面暴露所有连接详情但默认只允许 127.0.0.1 访问。我们加一层 Basic Authlocation /stat { rtmp_stat all; rtmp_stat_stylesheet stat.xsl; auth_basic Restricted; auth_basic_user_file /etc/nginx/.htpasswd; }用openssl passwd -apr1生成密码存入.htpasswd。这样运维人员可随时查在线人数、码率、延迟。第五道日志精细化。默认access_log只记 HTTP 请求RTMP 推拉流不记录。加rtmp_log指令rtmp_log /var/log/nginx/rtmp.log main; log_format main $remote_addr - $remote_user [$time_local] $command $app/$flashver $status $bytes_sent $session_time $bytes_received $connect_time;$command是publish或play$session_time是会话时长$connect_time是握手耗时——这些字段对定位卡顿、断连问题至关重要。第六道防火墙白名单。Ubuntu 22.04 默认用ufw必须精确放行sudo ufw allow from 192.168.1.0/24 to any port 1935 # 局域网推流 sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw deny 1935 # 拒绝公网 RTMP第七道定期健康检查脚本。我们写了一个check_rtmp.sh每 5 分钟用curl -I http://localhost/hls/streamname/index.m3u8检查 HLS 是否可访问失败则发邮件告警。脚本放在/etc/cron.d/简单有效。6. 推流与播放端实操OBS、FFmpeg、Web 三端联调指南配置再完美推流和播放端出错也白搭。我们整理出三端最简、最稳的联调方案覆盖 95% 场景。OBS Studio 推流Windows/macOS/Linux 通用这是最常用场景。OBS 设置要点服务选择“自定义”服务器rtmp://your-server-ip/live注意结尾是/live不是/live/streamname流密钥填streamname如class101关键设置视频编码器x264别用 NVENCOBS 28 对 NVENC 的 RTMP 封装有 bug码率2500 kbps1080p或1200 kbps720p关键帧间隔2s必须等于 Nginx 的hls_fragment预设veryfast平衡质量与 CPU音频AAC码率128 kbps采样率44.1kHz推流后立刻访问http://your-server-ip/stat看到publish状态为activebytes数字在跳动说明成功。FFmpeg 推流Linux/macOS 终端适合自动化或无人值守场景。命令模板ffmpeg -re -i /path/to/video.mp4 \ -c:v libx264 -preset veryfast -b:v 2500k -maxrate 2500k \ -g 100 -sc_threshold 0 \ -c:a aac -b:a 128k \ -f flv rtmp://your-server-ip/live/streamname关键参数解释-re按原始帧率读取避免 ffmpeg 疯狂刷帧-g 100GOP 长度100 帧 ≈ 2 秒假设 50fps必须匹配hls_fragment-sc_threshold 0禁用场景切换检测防止 GOP 错乱-maxrate硬限码率防突发流量打爆带宽实测发现不加-sc_threshold 0某些 MP4 文件推流后 HLS 切片会频繁中断日志报hls: fragment not closed。Web 端播放HTML5 兼容方案必须兼顾现代浏览器和老旧设备。我们采用双播放器策略Chrome/Firefox/Safari用hls.js播 HLSIE11/Edge Legacy用flv.js播 HTTP-FLVHTML 示例div idvideo-container video idvideo controls autoplay/video /div script srchttps://cdn.jsdelivr.net/npm/hls.js1.4.2/script script srchttps://cdn.jsdelivr.net/npm/flv.js1.8.2/script script const video document.getElementById(video); const streamName streamname; let hls, flv; // 检测 HLS 支持 if (Hls.isSupported()) { hls new Hls(); hls.loadSource(http://your-server/hls/${streamName}/index.m3u8); hls.attachMedia(video); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生 HLS video.src http://your-server/hls/${streamName}/index.m3u8; } else { // 降级到 FLV flv flvjs.createPlayer({ type: flv, url: http://your-server/flv?stream${streamName}, isLive: true, enableStatis: false, }); flv.attachMediaElement(video); flv.load(); } /script注意enableStatis: false关闭统计上报减少额外请求。isLive: true启用直播模式自动处理追帧。最后分享一个血泪经验所有播放 URL 必须用http://别用https://除非你已配好 TLS 证书。Nginx-RTMP 的 HLS 输出不支持 HTTPS 重定向浏览器会直接报Mixed Content错误。我们曾因在开发环境用http上线后切https却忘了改播放地址导致全站直播黑屏 2 小时。7. 故障排查黄金链路从“播不了”到“根因定位”的完整路径线上最怕的不是报错而是“一切看似正常但就是播不了”。我们梳理出一条标准化排查链路按顺序执行90% 的问题 10 分钟内定位。第一步确认 Nginx-RTMP 模块已加载执行sudo nginx -V 21 | grep -o rtmp输出应为rtmp。若为空检查load_module路径和 AppArmor 权限。第二步检查 RTMP 端口是否监听sudo ss -tlnp | grep :1935应看到nginx: master process。若无输出检查rtmp { server { listen 1935; } }是否在配置中且nginx -t通过。第三步验证推流是否到达 Nginx用tcpdump抓包sudo tcpdump -i any -nn port 1935 -w rtmp.pcap推流 10 秒后停止用 Wireshark 打开rtmp.pcap过滤rtmp应看到connect、createStream、publish等握手包。若只有connect没有publish说明推流端被allow publish拦截。第四步检查 HLS 切片是否生成ls -la /var/www/html/hls/streamname/应有index.m3u8和若干*.ts文件。若无检查hls_path权限和hls on是否开启。第五步验证 m3u8 文件内容curl http://your-server/hls/streamname/index.m3u8输出应为标准 m3u8 格式包含#EXTINF:5.000,和streamname-xxxx.ts。若返回 404检查 Nginx 的location /hls配置和 MIME 类型。第六步用 VLC 直播测试VLC 是终极验证工具打开 VLC → 媒体 → 打开网络串流 → 输入rtmp://your-server/live/streamname若能播说明 RTMP 正常再输http://your-server/hls/streamname/index.m3u8若能播说明 HLS 正常。VLC 不受浏览器 CORS 限制是排除前端问题的利器。第七步分析 Nginx 日志重点看/var/log/nginx/error.logclient denied by ruleallow publish规则拒绝hls: fragment not closedGOP 设置错误stat: failed to open filehls_path权限不足on_publish timeout认证接口响应超时我们曾遇到一个诡异问题error.log里大量hls: fragment not closed但hls_fragment明明设了 5s。最终发现是推流端OBS的keyframe interval设成了0导致 GOP 不固定。把 OBS 的关键帧间隔从0改成2s问题消失。提示所有排查步骤必须按顺序执行跳步只会浪费时间。我们内部 SOP 规定运维接到“播不了”告警必须严格走完这七步每步截图留痕。不是为了甩锅而是让问题可追溯、可复现、可沉淀。8. 性能压测与容量规划单机到底能扛多少路流“能跑”和“能扛”是两回事。我们用真实压测数据告诉你 Ubuntu 22.04 上 Nginx-RTMP 的真实边界。测试环境云服务器2 核 4GSSD 磁盘100 Mbps 带宽推流端10 台机器每台用 FFmpeg 循环推 720p1200kbps 视频播放端200 个 Chrome 标签页用 hls.js 播放同一index.m3u8压测结果并发推流路数CPU 使用率内存占用HLS 延迟切片丢失率2022%1.1G7.2s0%5048%1.8G8.1s0.02%8076%2.9G9.5s0.15%10092%3.7G12.3s1.8%关键发现瓶颈在磁盘 I/O不在 CPU。当推流 60 路时iostat -x 1显示await平均等待时间从 1ms 升至 15ms%util达 98%。这是因为每个.ts文件写入都是小 IOHLS 切片频率高SSD 也扛不住。解决方案把hls_path挂载到内存盘tmpfssudo mount -t tmpfs -o size2G tmpfs /var/www/html/hls sudo chown -R www-data:www-data /var/www/html/hls压测后100 路时await降至 0.3ms延迟稳定在 8.5s丢失率归零。带宽不是线性增长。100 路 1200kbps 推流理论带宽 120 Mbps但实测出口带宽仅 85 Mbps。原因是 Nginx-RTMP 的 HLS 转码有压缩冗余且 TCP 传输有开销。我们按“理论带宽 × 0.7”做容量规划留足缓冲。连接数有隐性上限。Ubuntu 22.04 的net.core.somaxconn默认 128当并发连接 100 时新连接会排队。需调大echo net.core.somaxconn 4096 | sudo tee -a /etc/sysctl.conf sudo sysctl -p容量规划公式所需服务器数 ceil(总推流路数 × 1.2 / 单机安全路数)其中单机安全路数 60H