Docker部署SRS直播集群:从单机瓶颈到Origin-Edge横向扩展
一台服务器撑不住一万人在线看直播这个问题跟SRS本身的关系不大更多是单节点物理上限的问题。前阵子帮朋友调一个RTMP直播服务单台SRS跑测试一切正常推流不卡、画面延迟也稳定结果活动一上线上千路播放请求同时进来服务器连接数和带宽双双打满直播直接开始抽风。折腾了几天最后我用Docker重构成了SRS-RTMP直播服务集群把推流和播放彻底拆开问题才算真正解决。这篇文章先把集群为什么能解决这个问题的原理讲清楚再把我的完整Docker部署步骤、两个节点的配置文件、验证方法和踩过的坑全部写出来给打算上集群的朋友当个参考。1. 单机瓶颈为什么RTMP直播服务要上集群1.1 单机SRS到底能扛多少流量先给一个概念单台SRS的并发能力受三个因素限制——带宽、连接数、CPU。带宽这块最容易算。假设一路1080p直播码率4Mbps1000个观众同时拉流出口带宽就是4Gbps。普通服务器的千兆网卡直接打满万兆网卡也只剩一半余量。这还没算推流上行、API请求、进程间通信的消耗。所以判断单机能不能扛第一个要问的不是SRS能处理多少并发而是这台服务器的出口带宽能喂饱多少路并发播放。连接数这块容易被忽略。Linux默认的文件描述符上限通常是1024就算调高到65535一个SRS进程能维持的TCP连接也是有限的。RTMP长连接和HTTP-FLV连接都会占用fdHLS请求虽然是无状态的但异常或频繁的请求同样会耗尽连接资源。连接满了之后新观众就只能排队或者被拒。很多线上故障不是因为CPU爆了而是fd被占满后服务直接拒绝新连接。CPU和内存主要消耗在转封装和切片上。SRS把RTMP流转成HTTP-FLV是纯内存操作开销不算大但HLS切片需要持续写文件、维护窗口磁盘IO和CPU都有消耗。如果还开了SRS内置的转码CPU负载会直线上升。把这些资源约束摆出来就能理解单机方案的扩容空间其实非常有限。1.2 集群要解决的三个核心问题真正推动我上集群的是三个单机方案解决不了的问题。第一个是播放带宽的水平扩展。单机出口带宽有上限集群可以把播放请求分散到多个节点的带宽上整体容量接近所有节点带宽之和。第二个是推流和拉流的资源隔离。直播场景里推流路数少但每路码率稳定拉流路数多且波动剧烈把两者放在不同节点上推流节点的性能就不会被拉流高峰拖累。第三个是故障隔离。单机模式下进程一崩或者网络一断所有观众全部断开有了集群某一个Edge节点故障只影响连到这个节点的观众其他节点继续服务。虽然不是自动容灾但至少把爆炸半径控制住了。这三个问题对应到SRS的架构里就是一个很成熟的方案Origin-Edge。1.3 为什么选择SRS而不是其他方案SRS是开源流媒体服务器对RTMP、HTTP-FLV、HLS、WebRTC都有支持关键是它对集群场景有原生设计。我也对比过其他方案Nginx加RTMP模块部署简单但本质是单机模块集群要自己用upstream做负载均衡边缘缓存、回源合并这些能力都得二开ZLMediaKit性能很强API设计现代但文档和社区资料相对分散集群相关配置不如SRS这么直给。SRS这套Origin-Edge配置总共就几行官方还维护了Docker镜像确实省事。选择SRS还有一个现实原因它的HTTP API非常完整。启动后可以用curl直接查连接数、带宽、进程状态这对集群运维太重要了。而SRS的Edge节点是第一个观众触发回源后续观众共享连接这种合并回源机制在RTMP时代就已经非常成熟。2. 架构选型Origin-Edge模式是怎么工作的2.1 Origin源站与Edge边缘节点的分工先明确角色。Origin就是源站负责接收推流端推上来的RTMP流是整个集群里流的唯一权威来源。Edge是边缘节点它自己不接收推流只接收播放请求当一个观众请求某个流时Edge会去Origin拉取同一路流然后在本地缓存并分发给这个观众。有人会问播放为什么不能直接连Origin直接在Origin上播放也不是不能用但前面说过所有播放流量都压在Origin上带宽和连接数会瞬间被打满。Edge存在的价值就是充当流量缓冲层它把播放请求的负载承接下来并且只对Origin维持极少的回源连接。2.2 一次完整的推拉流数据路径推流路径推流端OBS或ffmpeg用RTMP协议把流推到rtmp://Origin服务器的1935端口/live/流名Origin接收后把流存在内存里同时按需做HLS切片、HTTP-FLV封装。播放路径观众播放器请求rtmp://Edge服务器的1935端口/live/流名Edge收到请求后先看本地有没有这路流的缓存没有就向Origin发起一条RTMP回源连接拉到流之后开始给当前观众转发。第二个观众请求同一个流时Edge直接复用刚才那条回源连接把流同时分发给多个观众。这条路径里最核心的一句话是无论有多少观众连EdgeEdge对Origin只维持一条回源连接。这就是SRS能支撑大规模并发播放的关键。2.3 Edge节点如何合并回源用生活化的比喻Origin是出版社只负责出一本书Edge是书店。读者不会全涌到出版社去而是去附近书店。书店只需要从出版社拿一本书放在店里就能卖给所有到店的读者。书是流读者是观众书店就是Edge节点。这个合并机制带来的收益是巨大的。假设1000个观众看同一路流不经过Edge时Origin要维持1000条播放连接、占1000份带宽经过Edge后Origin只维持1条回源连接带宽占用只有一份。Edge到观众之间的1000份播放带宽由Edge集群的各节点分摊。在实际部署里Edge节点可以放在不同机房或区域观众从网络距离最近的Edge拉流整体延迟和带宽体验都会更好。这也是直播CDN的基本逻辑。3. Docker编排镜像、端口与Compose文件设计3.1 选哪个SRS镜像版本SRS官方在容器仓库维护了多个版本的镜像最常见的是4和5两个大版本。我的建议是直接用5.x也就是ossrs/srs:5标签。它已经是目前的主线版本对HTTP-FLV、HLS、WebRTC支持更完整配置格式也和官方文档同步更新。用4.x不是不行但新项目没必要从旧版本起步。大规模部署时可以进一步固定小版本标签比如ossrs/srs:5.0-r0避免镜像更新带来不确定因素。本地练习和中小规模场景用ossrs/srs:5就够。3.2 端口规划把冲突扼杀在配置阶段SRS有三个默认端口需要特别注意1935是RTMP1985是HTTP API8080是HTTP服务HLS切片和HTTP-FLV播放。容器内部的Origin和Edge都必须监听这些端口——是的Edge和Origin默认都一样。这就带来Docker部署的核心矛盾宿主机端口不能重复映射。如果Origin把1935映射到宿主机1935Edge就不能再把1935映射到宿主机1935。解决思路很简单给Edge的端口做偏移。服务角色容器端口宿主机端口Origin宿主机端口EdgeRTMP193519352935HTTP API198519852985HTTP服务808080809080这里的原则是Origin用标准端口方便推流端配置Edge用非标准端口区分节点。后续加第二个Edge就再偏移一层比如2936、2986。3.3 docker-compose.yml完整示例下面是整个集群的编排文件关键信息写在注释里。version: 3.8 services: srs-origin: image: ossrs/srs:5 container_name: srs-origin restart: always ports: - 1935:1935 - 1985:1985 - 8080:8080 volumes: - ./conf/origin.conf:/usr/local/srs/conf/srs.conf - ./data/origin:/usr/local/srs/objs/nginx/html command: [./objs/srs, -c, conf/srs.conf] ulimits: nofile: soft: 65535 hard: 65535 srs-edge: image: ossrs/srs:5 container_name: srs-edge restart: always depends_on: - srs-origin ports: - 2935:1935 - 2985:1985 - 9080:8080 volumes: - ./conf/edge.conf:/usr/local/srs/conf/srs.conf - ./data/edge:/usr/local/srs/objs/nginx/html command: [./objs/srs, -c, conf/srs.conf] ulimits: nofile: soft: 65535 hard: 65535两个服务用的是同一个镜像只是挂载的配置文件不同、映射的端口不同。command指定SRS启动时加载的配置文件路径容器内的配置文件统一被覆盖成srs.conf外部只需要改挂载目录里的conf文件即可。3.4 配置挂载与数据持久化为什么要把srs.conf挂载成宿主机文件SRS镜像里带了默认配置但默认配置不适合集群场景每次进容器改配置太反人类。挂载之后可以直接用编辑器改宿主机文件然后执行docker-compose restart就能生效。数据目录./data/origin、./data/edge用来挂载HLS切片输出目录。演示场景也建议挂出来至少想看切片文件时不用进容器翻。生产环境这里要换成持久化存储避免容器重建导致历史HLS文件丢失。另外如果业务只用RTMP和HTTP-FLVOrigin可以不开HLS切片减少磁盘IO这点后面调优部分再展开。4. 核心配置详解区别只在几行参数4.1 origin.conf源站的完整配置listen 1935; max_connections 1000; daemon off; http_api { enabled on; listen 1985; } http_server { enabled on; listen 8080; dir ./objs/nginx/html; } vhost __defaultVhost__ { hls { enabled on; hls_path ./objs/nginx/html; hls_fragment 6; hls_window 120; } http_remux { enabled on; } }逐项说listen是RTMP监听端口max_connections控制最大连接数daemon off必须写Docker容器是前台进程模型SRS如果以守护进程方式启动容器会立刻退出。http_api提供HTTP接口后面我们靠它验证集群状态。http_server是SRS内置的静态文件服务HLS切片要通过HTTP访问就靠它。vhost __defaultVhost__是默认虚拟主机不写域名匹配的推拉流都会进入这个vhost。hls块开启HLS切片hls_fragment 6表示每6秒生成一个切片hls_window 120表示播放器最多可回看最近120秒的窗口。http_remux开启后RTMP流同时能以HTTP-FLV形式对外播放。4.2 edge.conf边缘节点的关键三行listen 1935; max_connections 2000; daemon off; http_api { enabled on; listen 1985; } http_server { enabled on; listen 8080; dir ./objs/nginx/html; } vhost __defaultVhost__ { mode remote; origin srs-origin:1935; hls { enabled on; hls_path ./objs/nginx/html; } http_remux { enabled on; } }和Origin对比Edge的配置几乎一样唯一的关键是vhost里多了两行mode remote;告诉SRS这个vhost是边缘模式自己不接收推流所有播放请求都转为回源请求origin srs-origin:1935;指定源站地址。这里用的是docker-compose服务名srs-origin在同一个compose网络里可以被正确解析。如果Origin和Edge不在同一台机器这里就填Origin所在服务器的内网IP或域名比如origin.internal.example:1935。max_connections可以比Origin放宽一些因为Edge承担的是播放连接压力。4.3 为什么Edge也要开HLS和HTTP-FLV很多人配置Edge时只关注RTMP回源不开HLS和HTTP-FLV结果播放器用HLS访问Edge时直接404。原因很简单观众请求http://Edge的8080端口/live/流名.m3u8时请求打到了Edge上如果Edge自己不提供HLS服务这个请求就没人处理。Edge的hls enabled on开启后SRS Edge会向Origin发起HLS回源把Origin生成的m3u8和ts切片拉到本地再通过Edge自己的http_server输出给观众。HTTP-FLV同理Edge开启http_remux后才能对外提供flv播放地址。所以Edge节点的http_server和对应协议开关都要开不只是RTMP。4.4 vhost对齐回源失败的隐藏原因SRS里vhost可以理解成虚拟房间。推流地址如果不带vhost就走默认的__defaultVhost__如果带?vhostxxx或者用域名就走对应vhost。集群里Edge回源时会带上自己vhost的信息去找Origin要求Origin也按对应的vhost处理流。如果Origin和Edge的vhost命名不一致回源会失败或者出现找不到流的诡异问题。最稳妥的做法是两边都用默认vhost__defaultVhost__不做花活。等业务需要多域名了再在两边同步增加同名的vhost配置保证Origin和Edge的vhost一一对应。这个细节平时不起眼出问题时却特别难排查。5. 完整部署流程从空目录到双节点在线5.1 规划目录结构先在服务器上建一个干净的目录我习惯用/opt/srs-cluster。mkdir -p /opt/srs-cluster/conf mkdir -p /opt/srs-cluster/data/origin mkdir -p /opt/srs-cluster/data/edge cd /opt/srs-cluster然后按上一章的代码创建conf/origin.conf和conf/edge.conf。最终目录结构如下/opt/srs-cluster/ ├── docker-compose.yml ├── conf/ │ ├── origin.conf │ └── edge.conf └── data/ ├── origin/ └── edge/5.2 启动集群并确认节点状态把docker-compose.yml也放到/opt/srs-cluster后执行docker-compose up -d第一次启动会拉取镜像耐心等一会儿。启动完成后看容器状态docker-compose ps正常情况是两个服务都在Up状态。如果有服务不断重启或退出大概率是配置文件有问题看日志docker-compose logs srs-origin docker-compose logs srs-edge日志里最关注的是有没有出现start server successfully或类似的启动成功字样。SRS启动过程中如果配置语法错误日志会明确指出第几行出错很好排查。5.3 用API确认Origin和Edge都在工作SRS的HTTP API是最方便的确认手段。在宿主机上执行curl http://localhost:1985/api/v1/summaries curl http://localhost:2985/api/v1/summaries如果返回JSON里面有server状态、version、connections等字段说明两个节点的API都活着。这一步很值得做——容器进程活着不代表SRS真的起来了只有API能响应才说明核心服务正常。我有一次遇到容器在跑但API无响应最后发现是配置里漏开了http_api。6. 功能验证与故障演练证明集群真的有效6.1 推流测试用ffmpeg推一路测试流到Origin用ffmpeg生成一路测试视频流推给Originffmpeg -re -f lavfi -i testsrcsize640x360:rate30 -f lavfi -i sinefrequency1000 -vcodec libx264 -preset veryfast -tune zerolatency -acodec aac -f flv rtmp://localhost:1935/live/demo这里不需要准备视频文件testsrc和sine都是ffmpeg自带的测试源跑起来就是一段带声音的动态画面。推流地址里的live是应用名demo是流名后面拉流地址要与之保持一致。推流后在Origin的API里查一下当前连接curl http://localhost:1985/api/v1/clients返回的JSON里应该能看到一条类型为publisher的客户端说明推流成功打到了Origin。6.2 拉流测试从Origin和Edge分别播放先在Origin直接播放验证源站的流本身是通的ffplay rtmp://localhost:1935/live/demo能弹出画面就说明源站没问题。关掉ffplay再从Edge播放ffplay rtmp://localhost:2935/live/demo这里的端口是2935对应的是Edge节点。如果配置正确画面同样能出来。这个播放动作就是触发Edge回源的关键操作——第一个来自Edge的播放请求会促使Edge向Origin发起回源连接。6.3 验证合并回源的关键证据这是整个验证里最有意思的一步。在Edge播放的同时分别查询两个节点的APIcurl http://localhost:1985/api/v1/clients curl http://localhost:2985/api/v1/clients你会看到Origin这边多了一条类型为player的连接这是Edge的回源连接Edge这边也有一条播放连接这是你本地ffplay的连接。如果再开第二个、第三个播放器连EdgeEdge这边的播放连接数会增加但Origin那边的回源连接始终只有一条。这组数据就是集群生效的最有力证据观众连接数的增长不会传导到OriginOrigin的负担不会随观众数线性增长。6.4 故障演练Origin挂了会怎样最后做一个破坏性测试。把Origin容器停掉docker-compose stop srs-origin此时已经连到Edge的播放会短暂中断或直接退出因为Edge失去了回源来源。再尝试新建播放连接也会因为回源失败而拉不到流。重启Origindocker-compose start srs-originOrigin恢复后新播放请求又能通过Edge正常播放Edge会自动重建回源连接。这个测试说明了一点SRS的Origin-Edge模式解决的是播放流量的水平扩展它本身不是自动容灾方案。如果希望Origin故障时播放不中断需要在上层做Origin主备切换比如用负载均衡把Edge的回源请求指向备用源站或者配合DNS切换。这个边界一定要清楚别把集群和容灾画等号。7. 实战踩坑最容易翻车的地方与调优建议7.1 容器秒退daemon off这个关键配置我第一次用Docker跑SRS就遇到容器刚启动就退出的问题。查日志发现SRS默认以daemon模式运行进程fork到后台后容器里前台没有进程Docker就判定容器结束。解决办法是在配置文件里写死daemon off;让SRS以前台进程方式运行。这个配置对Docker部署是刚需本地裸机跑的时候不写反而没事所以特别容易被忽略。7.2 回源地址写成127.0.0.1Edge把请求打给了自己这是集群搭建时最经典的坑。如果在edge.conf里写了origin 127.0.0.1:1935;Edge回源时会连自己的1935端口——等于对着镜子喊话永远连不到源站。因为容器内127.0.0.1指向的是容器自身不是宿主机更不是另一台服务器。正确写法分两种情况两个节点用docker-compose部署在同一个网络里直接用服务名srs-origin:1935如果不在同一网络用Origin所在服务器的内网IP或公网IP。我调试时一直看到日志报connect origin server failed把地址改对才恢复。这个排查过程很能说明问题先看网络通不通再看地址填得对不对。7.3 端口映射冲突与防火墙放行Docker部署SRS集群的另一个高频问题是端口。两个容器内部都监听1935如果都给宿主机映射1935第二个容器会启动失败报端口被占用。方案就是给Edge用非标准端口比如2935或者干脆部署在不同宿主机上。端口映射完了还有防火墙。云服务器的安全组如果只放行了1935那1985的API访问不到8080的HLS播放也会超时。最好的习惯是部署完成后把要用到的端口一次性放行RTMP的1935、HTTP API的1985或Edge映射后的2985、HTTP服务的8080或Edge映射后的9080。7.4 生产环境的规模感从Demo到上线的差距上面这套Demo能跑通但离生产环境还有几步路要走。一是连接数上限。容器默认的ulimit可能不够大并发预期高的话要在compose文件里给每个服务加上ulimits配置把nofile调到65535甚至更高这个我在示例里已经加上了。二是资源规划。Origin和Edge尽量别部署在同一台机器上Origin是核心资源内存和CPU都要优先保证Edge可以做多个副本分摊播放流量上层用负载均衡把观众引导到不同Edge。如果预期并发上万Origin的带宽和连接数都要按峰值预留不能只按平均值估。三是监控。SRS的1985端口HTTP API就是为监控准备的定时抓取/api/v1/clients和/api/v1/summaries的数据把当前连接数、总带宽、内存占用等指标收集起来做成告警。没有监控的集群等出问题时往往已经晚了。最后是老生常谈的日志级别。测试环境可以开debug生产环境建议info或warn否则日志文件会以肉眼可见的速度吞噬磁盘。另外如果业务只走RTMP和HTTP-FLVOrigin可以关闭HLS切片把磁盘IO留给更需要的环节需要HLS时再打开Node端的配置同步调整就好。文章写到这儿我最大的感受是SRS集群本身并不复杂Docker又把这些复杂性进一步收拢了真正考验人的是对业务的理解——你要清楚自己的播放场景是RTMP多还是HLS多观众分布在哪里Origin有没有足够的带宽冗余。先把这些想明白再动手部署会比盲目抄配置踏实得多。