
多路推流这个词懂的人自然懂。做无人值守直播、做监控直播、做同源多平台分发的基本都绕不开它。简单说就是一路视频源同时推到多个直播平台让不同平台的观众都能看到同一画面。我这边有一路7x24小时滚动直播源需要稳定分发到三个平台最早以为只是把推流地址多填几个而已结果真正跑了一个月被各种边缘问题折腾到怀疑人生。这篇文章就把我踩过的坑完整整理出来围绕服务端转发架构、断流自愈、时间戳修复、日志与文件句柄这些只有长期运行才会暴露的细节给准备做多路推流或者正在被多路推流折磨的朋友一份可以直接套用的经验。1. 多路推流不是“多填几个地址”那么回事先说一个很多人容易忽略的前提每一路推流本质上都是一条独立的RTMP长连接。RTMP的工作方式是客户端和服务器建立一条TCP长连接然后持续往里面塞FLV格式的音视频数据。多路推流并不是把一个文件复制几份那么简单而是每一次推流都要维护独立的连接状态、独立的发送缓冲、独立的断线重试逻辑。这就导致同一个编码器同时推多个平台时只要其中一路网络抖动整个进程都可能出问题。短期直播比如一场活动、一次商品讲解用OBS多开几个“自定义流媒体服务器”确实够用。但长期7x24小时无人值守跑客户端方式会暴露很多问题一是上行带宽按路数成倍占用比如源流码率4Mbps同时推三路就是12Mbps的上行需求普通家庭上行根本扛不住二是编码器进程一旦崩溃或者被平台踢掉没有一套自愈机制直播就默默断了三是多个平台的心跳策略、超时策略各不相同一个平台断线重连时可能会影响其他平台的推流稳定性。1.1 为什么客户端同时推多路不靠谱我最早也想用OBS插件直接多路推试了一周就放弃了。最明显的痛点是重连风暴网络稍有波动三个平台的连接同时断开OBS的自动重连机制会同时发起重连瞬间占满上行带宽然后继续超时、继续重连形成恶性循环。就算带宽充足Windows系统下网络栈的稳定性也扛不住长时间高并发长连接。另外还要考虑CPU。如果每路推流单独编码那是灾难性的三路1080p编码能把中端CPU吃到80%以上如果只是复用同一条编码流CPU压力小一些但内存和TCP连接管理依然绕不开。长期跑下来客户端进程本身的内存泄漏、显卡驱动崩溃、系统休眠这些不可控因素太多了。1.2 服务端分发的优势与我的最终选型后来我改成服务端分发架构思路很简单采集端只推一路RTMP到自己的服务器然后由服务器负责复制成多路并转推到各平台。这样做的好处非常明显。采集端到服务器只占一份上行带宽服务器下行带宽才是按路数增长而服务器带宽是可控的。故障域被隔离。每个平台一个独立的转推进程一个平台挂了不会拖垮其他平台。可以在服务端做统一的时间戳修复、参数修正、断线重试不用依赖客户端编码器的行为。可以自动化监控和自愈无人值守也敢让它长期跑。我还简单对比过第三方聚合推流平台确实省事但费用高且对接自己的鉴权、回调逻辑受限。我的需求是长期稳定、可控、可排查所以最终选了自建Nginx-RTMP收流 FFmpeg转推这条路线。2. 服务端分发的搭建nginx-rtmp收流 FFmpeg转推整体架构一句话就能说清楚采集端OBS或采集卡推流到自建服务器的Nginx-RTMP节点服务器通过Nginx接收这一路原始流然后由FFmpeg分别转推到各个目标平台。每个目标平台对应一个独立的转推任务互不干扰。2.1 两种转推方式nginx自带的push和独立FFmpeg进程Nginx-RTMP模块本身支持push指令在收到流后直接复制到其他地址配置极简单rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; push rtmp://platform-a.example.com/live/streamA; push rtmp://platform-b.example.com/live/streamB; } } }只要推流到这台服务器的live应用Nginx就会自动把流复制推送出去。这个方案效率高、零额外进程适合目标平台地址固定不变的情况。但它有个致命短板一旦要动态修改推流地址、加入鉴权参数、修复时间戳或者想对某一路做不同的处理push就无能为力了。况且Nginx的push一旦连接被平台断开重连逻辑并不精细长期稳定性全看运气。所以我实际采用的是第二种方式Nginx只负责接收和鉴权转推工作由独立的FFmpeg进程完成。每个平台一个FFmpeg进程用systemd守护手动重启、监控、日志全部都独立。配置文件可以灵活定制这是长期无人值守运行最稳妥的方案。2.2 nginx-rtmp收流配置Nginx侧只做三件事接收流、鉴权、提供本地拉流探测。配置如下rtmp { server { listen 1935; chunk_size 4096; max_message 1M; application live { live on; record off; meta copy; # 推流鉴权校验通过才允许推流 on_publish http://127.0.0.1:8080/auth; # 允许本机拉流探测 allow play 127.0.0.1; allow play ::1; deny play all; } } }on_publish指向一个本地鉴权服务推流方带着key来鉴权服务校验通过才放行。如果要限制外网直接推流还可以再包一层防火墙策略只放行采集端的固定IP。allow play 127.0.0.1是为了后面监控脚本可以在本机拉流探测不影响转推链路。注意生产环境一定要加推流鉴权。否则任何人都可以往你的live应用推流流名冲突会让你的转推进程全部断掉重连严重的直接被冒名顶替。2.3 FFmpeg转推命令的参数细节每个平台一个转推任务核心命令长这样/usr/local/bin/ffmpeg -hide_banner -loglevel warning \ -rw_timeout 5000000 \ -i rtmp://127.0.0.1:1935/live/main \ -fflags genpts \ -c copy \ -f flv rtmp://platform-a.example.com/app/streamKey?tokenxxxxx拆开看几个关键参数-rw_timeout 5000000单位是微秒5秒。如果上游输入5秒内没有数据FFmpeg会判定超时并退出方便systemd拉起重启。这是断流自愈的关键。-fflags genpts重新生成PTS时间戳避免源端时间戳抖动影响下游播放器。-c copy编码流直接复制不转码CPU开销极低。前提是源编码格式和目标平台兼容。-f flvRTMP推流使用FLV封装目标平台服务器才认识。如果你的源流音频采样率或者编码格式不被某平台支持则需要加上转码参数。比如把Opus音频转成AAC/usr/local/bin/ffmpeg -hide_banner -loglevel warning \ -rw_timeout 5000000 \ -i rtmp://127.0.0.1:1935/live/main \ -fflags genpts \ -c:v copy \ -c:a aac -b:a 128k -ar 48000 \ -f flv rtmp://platform-b.example.com/app/streamKey-c:v copy不动视频只转音频开销也不算大。但要注意音频采样率最好固定为48000Hz或44100Hz立体声这是国内主流平台兼容性最好的组合。3. 稳定运行一个月的核心自愈、守护与监控配置能推流只是第一步稳定跑一个月靠的是后面这套守护体系。我把所有转推任务都交给systemd管理配合探活脚本和日志轮转基本做到了无人值守。3.1 用systemd管好每个转推进程每个平台一个systemd service文件比如/etc/systemd/system/relay-platform-a.service[Unit] DescriptionRelay live stream to Platform A Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userrelay Grouprelay ExecStart/usr/local/bin/ffmpeg -hide_banner -loglevel warning -rw_timeout 5000000 -i rtmp://127.0.0.1:1935/live/main -fflags genpts -c copy -f flv rtmp://platform-a.example.com/app/streamKey?tokenxxxxx Restartalways RestartSec5 LimitNOFILE65536 [Install] WantedBymulti-user.target几个关键点Restartalways而不是默认的on-failure。因为FFmpeg转推进程被平台断开时退出码不一定非零可能显示正常退出on-failure根本不会触发重启。用always最保险。RestartSec5留5秒缓冲避免平台还没释放旧连接就立刻重连导致被拒。LimitNOFILE65536防止长时间运行后文件句柄耗尽。这个坑我在后面详细说。用独立用户relay运行不碰root权限降低风险。写好后执行systemctl daemon-reload systemctl enable relay-platform-a systemctl start relay-platform-a3.2 断流自动恢复与拉流探活systemd保证进程死了能拉起但如果上游流源断了FFmpeg进程会一直卡在等待数据状态直到-rw_timeout触发。这个设计是够用的上游断流超过5秒转推进程退出systemd重启它重启后FFmpeg重新连接上游上游恢复后链路自动恢复。但“进程活着”不等于“流在正常推”。有时候FFmpeg进程挂在那里网络连接半开平台侧早就把连接断了进程却感知不到。这时候需要主动探活。我写了一个探活脚本定期去本机拉流探测源流是否正常#!/bin/bash # check_stream.sh STREAM_URLrtmp://127.0.0.1:1935/live/main SERVICE_NAMErelay-platform-a for i in $(seq 1 10); do timeout 20 ffprobe -v error -f live_flv -i $STREAM_URL \ -show_entries formatformat_name -of defaultnoprint_wrappers1 /dev/null 21 if [ $? -eq 0 ]; then exit 0 fi sleep 3 done logger -t check_stream Stream probe failed, restarting $SERVICE_NAME systemctl restart $SERVICE_NAME脚本逻辑连续10次探测本机源流都失败就认为源流已经不正常重启对应转推服务。配合crontab每2分钟执行一次即可。注意探测时用timeout包住防止ffprobe挂死。如果要对某个具体平台的推流状态做精确检查就需要调平台侧的推流状态API或者用rtmpdump去目标地址拉一小段流验证。目标平台一般不允许外部随意拉流所以更实际的做法是看服务器到平台的TCP连接是否还活着用ss命令定期快照。3.3 日志、文件句柄、带宽的日常体检长期运行时最容易被温水煮青蛙的是日志和句柄。FFmpeg的-loglevel warning已经比默认的info安静很多但网络抖动时依然会刷错误日志。如果不做轮转跑一个月日志文件能到几十GB直接把磁盘写满所有直播任务跟着崩溃。我的做法是按应用拆分日志并用logrotate按天切割保留7天。logrotate配置示例/var/log/relay/*.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate }文件句柄是另一个暗坑。Nginx和FFmpeg都需要大量TCP连接系统默认的1024或4096句柄限制根本不够。除了systemd里的LimitNOFILENginx主配置里也要调大worker_rlimit_nofile 65535; events { worker_connections 20480; }带宽方面我会每周看一次网卡流量趋势确认多路总码率没有超过服务器上行带宽的70%。如果长期逼近带宽上限任何突发流量都会导致全链路质量下降。4. 那些跑了一个月才暴露的隐藏坑下面这几条是我觉得最值得分享的经验。它们不是配置文档里会写的东西全是运行一个月过程中被实际逼出来的。4.1 平台“静默踢连接”与TCP空闲连接回收第一个月刚开始我隔三差五发现某一平台路数断了但FFmpeg进程还活着日志里什么错误都没有。查了半天才明白很多直播平台和中间网络设备会对长时间没有数据收发的TCP连接做空闲回收而且是静默断开不发送任何RST或FIN信息。连接已经死了进程却还以为是好的所以数据一直在发送但对方永远收不到。解决办法分两层。第一层是让连接永不空闲保证推流过程中音频流持续发送。RTMP推流是持续的音视频数据流只要上游真的在推音频帧会不断过来转推侧也会保持数据流动连接就不会“空闲”。怕的就是源流画面静止且某些编码器在静止时降帧甚至停发视频帧这时至少要保证音频帧连续。第二层是启用TCP keepalive。在服务器上调整系统参数sysctl -w net.ipv4.tcp_keepalive_time60 sysctl -w net.ipv4.tcp_keepalive_intvl10 sysctl -w net.ipv4.tcp_keepalive_probes6同时在Nginx监听配置里加上so_keepaliveonlisten 1935 so_keepaliveon;这样TCP协议栈会在连接空闲时主动发探测包尽早发现半开连接。4.2 时间戳漂移才是“偶发卡顿”的元凶跑到第二周某个平台的播放端开始出现偶发卡顿不是网络问题因为其他平台正常。用ffprobe抓包一查源流视频包的时间戳存在周期性跳变个别包PTS会突然往后跳几百毫秒然后下一次又恢复正常。这种时间戳漂移在短时间直播时基本感知不到但积累一个月播放器端的缓存队列会越积越长表现为延迟增大和卡顿交替出现。解决办法就是在转推命令里加-fflags genpts重新生成时间戳。如果源端时间戳彻底没有参考价值还可以用墙钟时间戳覆盖/usr/local/bin/ffmpeg -hide_banner \ -use_wallclock_as_timestamps 1 \ -i rtmp://127.0.0.1:1935/live/main \ -fflags genpts \ -c copy \ -f flv rtmp://platform-a.example.com/app/streamKey-use_wallclock_as_timestamps让FFmpeg以收包时的系统时间作为时间戳基准源端时间戳直接忽略这样时间戳曲线就是单调平滑的。副作用是如果服务器系统时间发生跳变也会带偏时间戳所以使用这个参数时要配合NTP平滑同步不要用ntpdate那种直接跳跃的方式。排查时间戳问题的命令也分享一下ffprobe -v error -select_streams v \ -show_entries packetpts_time \ -of csvp0 -read_intervals %10 \ rtmp://127.0.0.1:1935/live/main正常情况下25fps流的相邻包PTS间隔在40ms左右30fps在33ms左右。如果出现大量间距几十倍异常的值说明源时间戳有问题。4.3 编码格式兼容H.264和AAC最稳中途我想过提升画质把视频改成HEVC编码因为同画质下码率更低。测试时发现部分平台拉流没问题但播放器无法解码表现为黑屏或反复缓冲。国内主流直播平台对HEVC的支持参差不齐RTMP封装下HEVC本身就不是标准能力很多平台服务端会直接丢弃视频轨。同样的问题也出现在音频上。有一次我接了一个Opus音频源转推时偷懒没转码结果某平台有画面没声音后台日志又没有任何报错。排查了半天才发现是平台解码器不支持Opus。所以我的经验是要追求最大兼容性视频统一H.264音频统一AAC采样率48000Hz或44100Hz立体声。在这个框架内优化码率和编码参数不要碰非标准的东西。4.4 日志炸弹、僵尸进程和密钥过期日志炸弹前面提过还有一个具体案例某平台网络抖动持续了20分钟FFmpeg每秒钟打印一条重连报错错误日志直接暴涨到3GB把系统盘写满。那次所有转推进程全部崩溃场面极其酸爽。从那以后我强制所有转推日志走logrotate并且磁盘告警阈值设到80%。僵尸进程则出现在我早期用Nginx的exec指令启动转推进程时。Nginx虽然会管理子进程但某些场景下转推进程并不会被正确回收变成僵尸状态占用进程表项时间久了整个服务器越来越卡。换用systemd独立管理后这个问题彻底消失。密钥过期也值得一提。部分平台推流地址中的token不是永久的最长的可能一个月有效。跑着跑着某一天突然断流查日志发现是token过期。解决思路是写一个定时任务定期调用平台API刷新密钥然后重启对应转推service。密钥刷新逻辑要用脚本实现模板化不要把密钥写死在service文件里否则每次过期都要手改。5. 常见问题速查与一次完整排查实录最后把我这一个月遇到的高频问题整理成一张速查表遇到同类问题直接对照处理。5.1 一个月内遇到最多的8个问题速查表现象可能原因排查方法解决方案推到平台秒断推流地址错误、token过期、编码格式不支持看FFmpeg日志用ffprobe检查源流参数重新生成推流地址检查编码格式画面周期性卡顿时间戳抖动、GOP过大、B帧乱序ffprobe抓包检查PTS间隔加-fflags genpts限制GOP有画面无声音音频编码或采样率不支持ffprobe查看音频流编码和采样率转码为AAC 44.1/48kHz所有路同时断上行带宽打满、OOM、系统盘满iftop看流量dmesg查OOMdf查磁盘限码率、清理日志、扩充资源重启后不自动恢复systemd未配置Restart或RestartSec不合理systemctl status查看状态改用Restartalways日志飞快增长loglevel过高、日志无轮转du/df查看磁盘占用降低loglevel配置logrotateFFmpeg进程卡死半开TCP连接未检测到断开ss -tn查看连接状态调TCP keepalive加探活脚本平台延迟缓慢增大GOP过大、播放器缓冲堆积ffprobe检查关键帧间隔控制GOP 2-4秒转码时固定GOP5.2 一次“平台端延迟越来越大”的排查过程这是我觉得最有代表性的一次排障。现象是平台A的播放端延迟从正常的10秒慢慢涨到1分钟其他平台正常。一开始我怀疑是平台A的网络路由问题但拉取服务器到平台的TCP丢包率正常。后来用ffprobe抓本机流检查发现视频流的GOP间隔很不规律有的地方两个关键帧之间隔了100多帧远超4秒。原因定位到采集端编码器画面上有大范围静态区域时编码器会降低帧率并自动跳过场景切换关键帧间隔被拉长。平台A的播放器为了保持播放流畅会缓存更多GOP导致延迟越积越大。其他平台对GOP不敏感所以没有这个问题。修复分两步一是把采集端编码器的GOP固定为60帧关闭场景切换检测二是转推时如果源头改不了就加一层视频转码强制GOP/usr/local/bin/ffmpeg -hide_banner \ -i rtmp://127.0.0.1:1935/live/main \ -c:v libx264 -preset veryfast -tune zerolatency \ -g 60 -keyint_min 60 -sc_threshold 0 \ -c:a copy \ -f flv rtmp://platform-a.example.com/app/streamKey注意-g 60是GOP大小60帧对应2秒30fps符合平台建议值-sc_threshold 0禁止编码器在场景切换时不按GOP插入关键帧保证关键帧严格每60帧一个。5.3 我的标准修复流程现在只要转推链路出问题我都按这个顺序排查基本不动脑子看告警信息确认是哪个平台、哪条链路。systemctl status relay-platform-a先确认进程还活着。journalctl -u relay-platform-a -n 200看最近日志尤其关注重连和超时报错。ss -tn sport :1935 or dport :1935确认到平台侧的TCP连接是否存在。ffprobe拉本机源流确认源流本身是否正常。如果源流正常但平台连接异常重启转推服务如果源流异常去查采集端。每改一个变量观察至少10分钟不要一次性改多个配置。这套流程帮我解决了不少问题核心原则是“先判断故障在哪一层再动手改”避免盲目重启造成二次污染。一个月跑下来我最深的感受是多路推流本身并不复杂真正考验人的是长期无人值守运行时那些细碎的问题。不要相信“配好就能一直跑”一定要把进程守护、日志轮转、连接探活、密钥刷新这些自动化手段提前做好。最后再分享一个小细节别把转推进程的日志和Nginx的日志写到同一个文件分开写、按天切割出问题的时候能少很多折腾。希望这份用一个月时间换来的经验能帮你少踩几个坑。