Docker部署SRS直播集群:从单点到源站边缘架构实战 做直播服务的同学应该都听过SRS这套开源流媒体服务器在RTMP、WebRTC、SRT这些协议上表现一直很稳。但不少朋友有个误区觉得Docker里跑一个SRS容器能推流能拉流就算完事了。真到了线上单节点扛不住并发推流源一挂整条链路就断了。这篇文章我想聊聊怎么用Docker把SRS的RTMP直播服务从单点玩到集群覆盖架构设计、容器编排、回源配置、负载均衡和故障排查。适合已经被“单节点能跑”麻痹过、正准备上多节点的人也适合刚接触SRS、想一步到位搭出正规架构的开发者。先说透一个概念SRS集群不等于“多装几个SRS容器”。如果只是起了三个独立的SRS容器各自接收推流那和三个孤岛没有区别。真正能形成直播集群靠的是SRS的源站origin和边缘edge角色划分推流只进源站拉流只进边缘边缘没有流的时候再回源站拉。这套架构既解决了并发拉流的出口压力也天然支持水平扩展。配合Docker Compose管理容器生命周期、自定义网络隔离内外流量整个集群的搭建和维护成本能压得非常低。1. 项目全貌为什么用Docker跑SRS直播集群1.1 业务需求拆解单节点不够用之后先讲讲我熟悉的场景。某公司内部要做一场面向全国分支机构的直播培训同时在线人数预估在几千到上万。一开始的方案是一台云主机装好SRS推流端用编码器推到RTMP地址观众用VLC或者播放器拉流。预演的时候一切正常真到了直播当天播放端开始频繁缓冲源站CPU冲到80%网络出口带宽被打满最要命的是有一路推流闪断整个直播画面就卡住了。这个案例很典型。单节点SRS不是不能跑而是面临三个绕不开的问题并发拉流的能力受限于单机带宽和文件描述符上限观众越多每个拉流连接占用的带宽叠加得越厉害。推流和拉流耦合在同一台机器上任何一路异常推流都可能污染整体服务。没有冗余源节点宕机就意味着直播直接终止。所以真正要解决的不是“换一台更大的机器”而是“把推流和拉流拆开把压力分散开”。这就是集群要解决的通用问题。1.2 架构选型源站边缘的集群拓扑SRS官方文档里提到的集群方案主要分两类一类是源站集群Origin Cluster多个源站节点之间做流状态同步适合对推流可用性要求极高的场景另一类是边缘集群Edge Cluster边缘节点负责拉流分发源站负责接收推流和存储两边通过SRS的HTTP回源或者RTMP回源协议通信。我在这个项目里选的是“双源站 多边缘”的组合。双源站解决推流侧的单点问题边缘节点负责把拉流压力摊开。整体拓扑简化为推流端 - 负载均衡入口 - 源站A/源站B播放端 - 边缘节点 - 回源拉流 - 源站A/源站B为什么边缘节点要单独一层因为直播流的特点是一份流入、N份流出。源站只需要保留一份流数据边缘节点各自从源站拉取同一份流再分发给各自的观众这样可以把出口带宽的压力从“源站”转移到“边缘”而边缘节点是可以随意横向扩容的。1.3 Docker在这条链路里的价值有人会问直接用二进制装SRS不好吗为什么套一层Docker我的看法是SRS的二进制部署本身不复杂复杂的是集群状态下的依赖管理。比如多节点需要一致的配置、需要互相感知对方的存在、需要能在新机器上秒级拉起一组服务。Docker Compose能把这些变成一份声明式的模板版本升级、节点扩容、环境迁移都变成可控操作。而且Docker容器天然隔离了SRS的运行环境和宿主机环境日志、配置文件、数据目录都可以通过volume挂载出来排查问题的时候不用进容器翻路径。后面我会给出整套docker-compose.yml和SRS配置可以直接复用。2. 环境准备与镜像选型2.1 基础环境检查清单动手之前先把环境踩一遍。Docker集群部署最怕的就是到一半才发现宿主机的内核参数、防火墙策略有问题。我建议做这样一轮检查操作系统建议Debian 11或Ubuntu 22.04内核版本不低于5.4方便处理容器网络和iptables规则。Docker版本20.10以上Compose插件建议v2版本直接用docker compose命令。防火墙确认放行了TCP 1935RTMP、TCP 8080SRS HTTP服务、TCP 1985SRS HTTP API。文件描述符上限ulimit -n检查生产环境建议调到65535以上。SRS在高并发连接下非常吃fd。机器数量我这次实验环境是4台云主机两台做源站两台做边缘节点。如果只是本地学习可以用docker compose在一台机器上模拟多节点效果也是成立的。注意如果你是在云厂商的安全组里操作记得同步改安全组入方向规则只放行必要的端口。我之前因为只改了系统防火墙忘了改安全组排查了整整半小时才发现流量根本没到宿主机。2.2 SRS镜像版本怎么选SRS官方镜像的命名是ossrs/srstag有很多这个项目里我用的是ossrs/srs:5.0-r0。为什么选5.x而不是4.x因为5.x对HTTP回调、HTTP-FLV、WebRTC的支持更完善而且容器镜像的启动逻辑做了优化更适合用在Docker部署场景。建议使用固定版本tag不要用latest。这个习惯我踩过坑有一次明明没改任何配置重启容器之后推流却失败了最后发现是镜像的latest标签在某个时间点被重新打了里面SRS的默认配置变了。固定到具体版本号之后这类问题再没出现过。2.3 端口与网络规划SRS容器里常用的端口有三个1935RTMP推流和拉流端口核心端口。1985HTTP API端口用来查看服务器状态、踢流等运维操作。8080HTTP服务端口提供HTTP-FLV和HLS的访问能力。集群模式下我不建议所有端口都无脑映射到宿主机。比如边缘节点不需要暴露1985和8080只要让集群内部能互相访问就行。Docker Compose里我自定义了一个bridge网络所有容器通过服务名互相访问。这样配置回源地址的时候直接用origin-server-1:1935这种写法跳过了IP变更带来的维护成本。2.4 负载均衡入口的选型整个集群前面需要一个流量入口把推流请求和拉流请求分发到不同的后端节点。目前常用的方案有两种Nginx用rtmp模块配合upstream做负载均衡配置直观。HAProxy四层TCP负载均衡性能强而且可以把1935端口同时代理给源站和边缘节点。我在这个项目里选的是HAProxy原因是它的四层转发能力对RTMP这类长连接协议支持更好连接状态跟踪也更清楚。后面配置文件里会给出可以直接用的HAProxy配置。3. 先把单点跑通最小配置别急着上集群3.1 最简docker-compose配置不论集群多复杂第一步永远是先把单节点的推拉流验证跑通。这里给出一份最小化的SRS节点配置。# docker-compose.yml version: 3.8 services: srs-origin-1: image: ossrs/srs:5.0-r0 container_name: srs-origin-1 environment: - SRS_LOG_TANKconsole volumes: - ./conf/srs.conf:/usr/local/srs/conf/srs.conf - ./logs:/usr/local/srs/objs/logs ports: - 1935:1935 - 1985:1985 - 8080:8080 restart: unless-stopped对应的srs.conf如下listen 1935; max_connections 1000; daemon off; srs_log_tank console; http_api { enabled on; listen 1985; } http_server { enabled on; listen 8080; dir ./objs/nginx/html; } vhost __defaultVhost__ { tcp_nodelay on; min_latency on; play { gop_cache off; queue_length 10; } publish { mr off; } }这份配置里有两个关键点daemon off是让SRS在前台运行方便Docker容器把日志输出到stdoutgop_cache off是不缓存关键帧间隔的GOP默认首屏加载会快点代价是网络抖动时画面恢复可能多等一会儿。后面调优部分我会细讲。3.2 推流与拉流验证启动容器后先用docker ps确认SRS进程正常。然后用ffmpeg推流ffmpeg -re -i sample.mp4 -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -f flv rtmp://127.0.0.1:1935/live/stream1拉流验证ffplay rtmp://127.0.0.1:1935/live/stream1能看到画面就说明推拉流链路通了。这时我建议用curl调一下HTTP API确认服务器状态正常curl http://127.0.0.1:1985/api/v1/summary返回结果里能看到clients数量、streams数量、kbps等关键指标。这一步养成习惯后面排查问题全靠这个API。3.3 关键参数初步调优gop_cache和mr这两个参数我单独说明一下。gop_cache是否开启关键帧间隔缓存。开启后新进房间的观众可以马上看到画面不用等下一个关键帧关闭后延迟更低但首屏会慢一些。直播场景如果是低延迟优先建议关掉如果观众更在意流畅度建议打开。mr合并读取开关。开启后SRS会合并读取数据减少系统调用次数降低CPU开销但会略微增加延迟。在推流机器性能吃紧的时候打开有明显的收益。这两个参数没有绝对的对错要结合自己的观看端场景压测之后决定。我建议先按我的配置跑一轮观察延迟和画面卡顿情况再微调。3.4 安全加固鉴权与回调直播服务一上线最怕的就是有人扫到你的RTMP端口直接推流。SRS可以配置简单的鉴权策略。我的做法是开启HTTP回调让SRS在推流事件发生的时候回调后端的鉴权接口由后端返回允许或拒绝。SRS配置里加这一段vhost __defaultVhost__ { http_hooks { on_publish http://auth-server:9000/v1/live/auth; on_play http://auth-server:9000/v1/live/auth; on_unpublish http://auth-server:9000/v1/live/close; } }回调接口的代码逻辑很简单收到请求后解析参数里的stream_id和路径查一下这个流是否在白名单里返回0表示允许返回非0表示拒绝。SRS收到非0响应就会断开推流连接。提醒回调接口必须处理高并发。推流端如果批量崩溃重连回调接口会瞬间收到大量请求所以我建议在鉴权服务前面加一层简单的内存缓存同一个stream_id在短时间内的重复请求直接返回上次结果。4. 集群部署从1个节点到N个节点4.1 集群角色拆分源站组 边缘组单点验证完毕就可以开始拆集群。我的规划是四台机器两个源站节点、两个边缘节点。先用表格把角色和职责列清楚节点角色容器名职责暴露端口源站1srs-origin-1接收推流处理回源请求1935, 1985, 8080源站2srs-origin-2接收推流处理回源请求1935, 1985, 8080边缘1srs-edge-1接收播放请求回源拉流1935, 8080边缘2srs-edge-2接收播放请求回源拉流1935, 8080这种模式下推流端看到的地址是同一个负载均衡IP但实际被分发到不同的源站容器。播放端请求落到边缘节点如果边缘节点本地没有缓存该路流会自动去源站拉然后缓存在本地继续服务后续的播放请求。4.2 源站集群配置与状态同步SRS源站集群的核心是“流状态同步”。简单说就是当一个源站收到了stream1的推流另一个源站也要知道stream1存在这样才能处理边缘节点的回源请求。我的源站配置文件如下# conf/srs-origin.conf listen 1935; max_connections 2000; daemon off; srs_log_tank console; http_api { enabled on; listen 1985; } http_server { enabled on; listen 8080; dir ./objs/nginx/html; } origin_cluster { enabled on; # 开启源站集群 token xxxxxxxxxx; # 集群内部通信使用的token各节点保持一致 debug_srs_upstream on; } vhost __defaultVhost__ { tcp_nodelay on; min_latency on; play { gop_cache off; } publish { mr off; } }两台源站的配置完全一样token必须一致。这里我解释一下origin_cluster的作用SRS通过HTTP API向其他源站广播自己的流状态当推流进来时其他源站会通过内部协议同步这条流的信息。边缘节点回源时会优先选择持有该流的源站避免回源到错误节点导致拉流失败。4.3 边缘节点配置与回源边缘节点的配置更加轻量核心是vhost里的mode remote配置# conf/srs-edge.conf listen 1935; max_connections 3000; daemon off; srs_log_tank console; http_server { enabled on; listen 8080; dir ./objs/nginx/html; } vhost __defaultVhost__ { mode remote; origin srs-origin-1:1935 srs-origin-2:1935; tcp_nodelay on; min_latency on; play { gop_cache off; } }这里的origin字段是关键。我填了两个源站的地址SRS会自动做故障转移如果第一个源站连不上就会尝试第二个。边缘节点本身不配置publish相关的内容只负责拉流分发。因为边缘节点只在有人播放时才去源站拉流所以它的带宽使用量是动态的。4.4 负载均衡入口HAProxy配置现在整个集群需要统一入口。在推流和拉流都走同一个入口的前提下我选了HAProxy做TCP四层负载均衡。配置文件如下# haproxy.cfg global log /dev/log local0 maxconn 4096 defaults log global option tcplog timeout connect 5000ms timeout client 60000ms timeout server 60000ms listen rtmp-in bind 0.0.0.0:1935 mode tcp balance roundrobin server origin-1 srs-origin-1:1935 check inter 3s fall 2 rise 2 server origin-2 srs-origin-2:1935 check inter 3s fall 2 rise 2这里balance roundrobin会把推流请求轮流分发到两台源站。拉流请求理论上最好都走边缘节点但客户端无法区分RTMP URL是推流还是拉流。实际生产里我通常会把推流入口和拉流入口分开两个端口或两套DNS推流走push.example.com播放走play.example.com对应不同的HAProxy后端。这样源站和边缘的压力隔离得更干净。4.5 Docker网络下的服务发现用Docker Compose部署时我用了一个共享的外部网络让HAProxy、源站、边缘节点在同一个网络里通过容器名互相访问。# docker-compose.yml集群版精简 services: srs-origin-1: image: ossrs/srs:5.0-r0 container_name: srs-origin-1 volumes: - ./conf/origin1.conf:/usr/local/srs/conf/srs.conf networks: - srs-net restart: unless-stopped srs-origin-2: image: ossrs/srs:5.0-r0 container_name: srs-origin-2 volumes: - ./conf/origin2.conf:/usr/local/srs/conf/srs.conf networks: - srs-net restart: unless-stopped srs-edge-1: image: ossrs/srs:5.0-r0 container_name: srs-edge-1 volumes: - ./conf/edge1.conf:/usr/local/srs/conf/srs.conf networks: - srs-net restart: unless-stopped srs-edge-2: image: ossrs/srs:5.0-r0 container_name: srs-edge-2 volumes: - ./conf/edge2.conf:/usr/local/srs/conf/srs.conf networks: - srs-net restart: unless-stopped networks: srs-net: driver: bridge有一个地方要特别留意容器内的origin字段最好写容器服务名不要写宿主机IP。原因很简单Docker bridge网络会给每个容器分配独立IP容器重建后IP会变但服务名是稳定的。5. 实操过程与关键验证5.1 整体启动流程启动顺序有讲究不是一把梭。我的顺序是先启动源站再启动边缘节点最后启动HAProxy。# 在源站机器上 docker compose -f docker-compose-origin.yml up -d # 在边缘机器上 docker compose -f docker-compose-edge.yml up -d # 最后启动HAProxy docker compose -f docker-compose-haproxy.yml up -d为什么这个顺序因为边缘节点启动时如果配置了http_proxy去探测源站源站没起来会导致边缘节点日志刷一堆连接失败。先源站后边缘能让边缘节点一启动就能正常回源。HAProxy最后启动是因为它后面代理的所有后端都应该就绪避免启动后立即出现后端不可用的告警。启动完成后我习惯用一条命令确认所有容器状态docker compose ps然后分别验证源站集群API和边缘节点状态。正常情况下源站API返回的server字段会显示SRS/5.0版本号边缘节点的日志会出现proxy stream相关的记录。5.2 多路并发推拉流压测集群搭好之后做了几轮压测这里分享一组比较有代表性的数据。压测工具用的是ffmpeg批量推流每次起20路不同的流分辨率1920x1080码率2Mbps时长30分钟。播放端用ffplay脚本模拟80个并发拉流客户端分布在两台边缘节点上。第一轮压测结果源站CPU平均占用率22%峰值35%。边缘节点CPU平均占用率40%峰值52%。网络入口总带宽约160Mbps符合预期计算80路播放 * 2Mbps 160Mbps。全程无断流无长时间缓冲。第二轮把播放端加到200路每台边缘节点承受100路并发拉流。源站CPU没有明显变化因为边缘节点已经缓存了流边缘节点CPU升到75%左右带宽达到400Mbps依然稳定。结论边缘节点的网络带宽是整个集群的瓶颈扩容边缘节点就能线性提升观众容量。源站在这个架构里几乎感受不到播放端的压力。5.3 故障演练边缘挂了会怎样集群的意义在于容错所以必须做故障演练。我的测试方法是让一台边缘节点正常服务。手动docker stop掉这台边缘节点。观察正在播放的客户端表现。结果正在播放的客户端画面中断短视频缓冲之后自动重连到HAProxy分发的另一台边缘节点重新播放成功。因为边缘节点有回源机制新节点上播放同一路流时会重新去源站拉流整个过程大概5到8秒恢复。我又做了源站故障演练停掉正在接收推流的源站1。这时候源站2因为origin_cluster已经同步了流状态会接管回源请求。推流端因为连着HAProxy重新连接时会自动分发到源站2推流恢复播放端因为边缘节点缓存的存在没有明显感知。心得只要边缘节点还有缓存源站故障不会立即影响正在播放的观众。但这不代表可以长期不修复源站因为新观众无法回源拉流了。源站故障后要尽快拉起容器并检查流状态同步是否恢复。6. 常见问题与排查技巧实录6.1 问题速查表先整理一份快速对照表基本覆盖了集群部署中最高频的问题。现象可能原因排查方法推流超时或拒绝连接HAProxy后端源站未启动、防火墙未放行1935端口检查HAProxy日志docker compose ps确认源站状态telnet测试端口连通性播放黑屏但不断流gop_cache off导致新进观众等待下一个关键帧临时等10秒以上观察若仍黑屏检查播放器是否支持RTMP边缘节点一直回源失败边缘配置里origin地址写错或源站集群token不一致查看边缘日志curl源站API确认状态比对配置文件推流偶尔失败但重启后恢复源站连接数达到max_connections上限或fd数不够调大max_connections检查系统ulimit -n集群中某些观众延迟明显偏高播放请求没有均匀分发到边缘节点检查HAProxyleastconn配置确认是否所有客户端都走同一节点Docker容器反复重启配置文件路径挂载错误SRS找不到配置查看容器日志检查volume挂载路径是否有效6.2 日志排查方法论SRS容器里日志默认打到stdout查询非常方便。我推荐用docker logs --tail200 -f实时追踪日志观察推流和回源过程。关键日志级别可以调SRS的SRS_LOG_TANKconsole会把日志输出到控制台默认级别是info。在线上排查问题时可以临时把日志级别调到debug定位更底层的连接问题environment: - SRS_LOG_TANKconsole - SRS_LOG_LEVELdebug这里有个坑debug级别的日志量非常大高并发下几秒钟就能刷几百MB。排查完问题一定要立刻改回info或者warn。6.3 日常运维与监控集群搭好只是开始日常运维才是最花精力的。我的建议是至少监控这几个维度容器状态每个SRS容器是否健康重启次数是否异常增加。源站连接数通过HTTP API的clients字段观察接近max_connections就要扩容或调参。带宽和CPU边缘节点的出入口带宽是最重要的指标建议配置宿主机级别的云监控。流数量源站API里streams字段记录了当前活跃流数异常增加可能意味着有盗推。我用一个简单的定时脚本每隔60秒拉取一次所有节点的API摘要写入本地日志。脚本很简陋但足够在异常出现的时候回溯定位。#!/bin/bash for ip in 192.168.1.11 192.168.1.12 192.168.1.21 192.168.1.22; do echo $ip /var/log/srs_monitor.log curl -s --max-time 3 http://$ip:1985/api/v1/summary | jq .clients, .streams, .kbps /var/log/srs_monitor.log done6.4 一例真实的回源异常排查记录最后分享一个实际踩坑的案例。集群上线第二天边缘节点的日志里出现了大量source stream not found的报错播放端表现是间接性卡顿。排查步骤先看源站API确认源站1和源站2的流状态正常streams字段没有异常下降。再看边缘节点配置发现两台边缘节点的origin地址写的是源站1的宿主机IP和源站2的宿主机IP。手动在边缘容器里执行telnet 源站IP 1935发现源站1的1935端口通源站2的1935端口有时通有时不通。最后定位到原因源站2容器因为内存超限被宿主机OOM kill过容器重启后Docker分配的IP变了边缘节点配置里写的旧IP自然连不上。修复方式是把边缘节点配置里的origin地址从IP改为Docker服务名并在源站容器上设置mem_limit避免OOM再次发生。这个问题折磨了将近两个晚上最后根源就是没遵守“容器内通信用服务名而不是IP”这条最基础的原则。7. 调优笔记与扩展思路7.1 延迟调优的取舍直播延迟是绕不开的话题。默认配置下RTMP协议的延迟通常在1到3秒。如果想进一步降低延迟可以调整播放端的缓冲时长比如ffplay的-fflags nobuffer -flags low_delay参数。但请注意降低延迟意味着抗网络抖动能力变弱高丢包率场景下画面会频繁卡顿。我的经验是延迟目标不要定死先按观众反馈调。如果观众集中在稳定网络环境可以把延迟压到1秒以内如果观众端是移动网络保留2到3秒的冗余反而整体体验更好。7.2 从RTMP扩展到HTTP-FLVRTMP协议现在很多浏览器默认不支持播放端往往需要借助Flash或独立播放器。如果你希望用浏览器直接播放建议在SRS上同时开启HTTP-FLV服务。SRS自带了对HTTP-FLV的支持边缘节点配置里加一句http_server { enabled on; listen 8080; }播放地址就变成了http://边缘节点IP:8080/live/stream1.flv浏览器用flv.js就能直接播放。这套方案在集群架构下同样成立因为HTTP-FLV的播放请求也会走边缘节点的回源逻辑。7.3 更进一步的WebRTC低延迟后面我又用SRS的WebRTC能力做了低延迟直播测试延迟能做到200到300毫秒。但WebRTC对网络的NAT穿透要求比较高跨运营商场景不够稳所以目前线上主力还是RTMPHTTP-FLVWebRTC只用在特定内部场景。如果你有兴趣可以把SRS的RTC配置单独拉出来练练但建议先确保RTMP集群稳定运行一个月再动这个。基于我个人这些项目的体会SRS集群搭建的复杂度不在于SRS本身而在于你得把推流、拉流、回源、负载均衡、容器生命周期管理这几件事同时想清楚。用Docker去编排集群最大的价值是把“可以重复”变成“必然重复”新的节点只要套用同一套Compose配置就能拉起故障恢复的时间从小时级降到了分钟级。如果你正准备上直播集群我建议先按文章里的最小单点配置跑通再把源站和边缘节点分开部署最后引入负载均衡。每一步都验证无误后再进入下一步这套流程走下来你对SRS集群的理解会比直接抄一份现成配置深刻得多。