☰
Docker部署SRS直播集群:从单点到源站边缘架构实战
2026/10/19 7:03:08 网站建设 项目流程

做直播服务的同学应该都听过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 1935(RTMP)、TCP 8080(SRS HTTP服务)、TCP 1985(SRS HTTP API)。
  • 文件描述符上限:ulimit -n检查,生产环境建议调到65535以上。SRS在高并发连接下非常吃fd。
  • 机器数量:我这次实验环境是4台云主机,两台做源站,两台做边缘节点。如果只是本地学习,可以用docker compose在一台机器上模拟多节点,效果也是成立的。

注意:如果你是在云厂商的安全组里操作,记得同步改安全组入方向规则,只放行必要的端口。我之前因为只改了系统防火墙忘了改安全组,排查了整整半小时才发现流量根本没到宿主机。

2.2 SRS镜像版本怎么选

SRS官方镜像的命名是ossrs/srs,tag有很多,这个项目里我用的是ossrs/srs:5.0-r0。为什么选5.x而不是4.x?因为5.x对HTTP回调、HTTP-FLV、WebRTC的支持更完善,而且容器镜像的启动逻辑做了优化,更适合用在Docker部署场景。

建议使用固定版本tag,不要用latest。这个习惯我踩过坑:有一次明明没改任何配置,重启容器之后推流却失败了,最后发现是镜像的latest标签在某个时间点被重新打了,里面SRS的默认配置变了。固定到具体版本号之后,这类问题再没出现过。

2.3 端口与网络规划

SRS容器里常用的端口有三个:

  • 1935:RTMP推流和拉流端口,核心端口。
  • 1985:HTTP API端口,用来查看服务器状态、踢流等运维操作。
  • 8080:HTTP服务端口,提供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_TANK=console 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容器把日志输出到stdout;gop_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 故障演练:边缘挂了会怎样

集群的意义在于容错,所以必须做故障演练。我的测试方法是:

  1. 让一台边缘节点正常服务。
  2. 手动docker stop掉这台边缘节点。
  3. 观察正在播放的客户端表现。

结果:正在播放的客户端画面中断,短视频缓冲之后自动重连到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 --tail=200 -f实时追踪日志,观察推流和回源过程。

关键日志级别可以调,SRS的SRS_LOG_TANK=console会把日志输出到控制台,默认级别是info。在线上排查问题时,可以临时把日志级别调到debug,定位更底层的连接问题:

environment: - SRS_LOG_TANK=console - SRS_LOG_LEVEL=debug

这里有个坑: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 done

6.4 一例真实的回源异常排查记录

最后分享一个实际踩坑的案例。集群上线第二天,边缘节点的日志里出现了大量source stream not found的报错,播放端表现是间接性卡顿。

排查步骤:

  1. 先看源站API,确认源站1和源站2的流状态正常,streams字段没有异常下降。
  2. 再看边缘节点配置,发现两台边缘节点的origin地址写的是源站1的宿主机IP和源站2的宿主机IP。
  3. 手动在边缘容器里执行telnet 源站IP 1935,发现源站1的1935端口通,源站2的1935端口有时通有时不通。
  4. 最后定位到原因:源站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-FLV

RTMP协议现在很多浏览器默认不支持,播放端往往需要借助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穿透要求比较高,跨运营商场景不够稳,所以目前线上主力还是RTMP+HTTP-FLV,WebRTC只用在特定内部场景。如果你有兴趣,可以把SRS的RTC配置单独拉出来练练,但建议先确保RTMP集群稳定运行一个月再动这个。

基于我个人这些项目的体会,SRS集群搭建的复杂度不在于SRS本身,而在于你得把推流、拉流、回源、负载均衡、容器生命周期管理这几件事同时想清楚。用Docker去编排集群,最大的价值是把“可以重复”变成“必然重复”,新的节点只要套用同一套Compose配置就能拉起,故障恢复的时间从小时级降到了分钟级。如果你正准备上直播集群,我建议先按文章里的最小单点配置跑通,再把源站和边缘节点分开部署,最后引入负载均衡。每一步都验证无误后再进入下一步,这套流程走下来,你对SRS集群的理解会比直接抄一份现成配置深刻得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询