直播高并发环境从0到1搭建:SRS、WebSocket与Redis实战

直播高并发环境从0到1搭建:SRS、WebSocket与Redis实战 去年年底接了个带视频直播的业务项目团队里没专人搞过流媒体我自己平时也就是写写接口、做做增删改查的水平。硬着头皮从装流媒体服务器开始到后面扛住一场在线几千人的直播活动整个过程踩了不少坑。这篇文章就是我这套直播高并发环境从0到1的完整搭建笔记后端小白可以放心照抄因为我就是从小白视角一路趟过来的。直播这个东西乍一听很唬人什么推流、拉流、转码、CDN、低延迟一堆名词砸过来很容易懵。但说穿了一条直播链路的核心就三件事视频怎么进来、视频怎么转发出去、业务流量怎么撑住。前两件事有现成的开源方案第三件事才是后端工程师真正的考卷。我把这套环境从零搭起来之后最大的感受是——高并发不是某一个组件的孤军奋战它需要从入口到出口整条链路一起配合。1. 先想清楚一件事直播高并发到底在并发什么1.1 拆开直播这条链路四个角色缺一不可刚开始接到需求的时候我的第一反应是找直播开发全套方案结果搜出来一堆SDK、云服务越看越糊涂。后来自己动手才理清楚一套完整的直播环境里最少有四个角色推流端主播那侧的设备用OBS这种软件把摄像头画面编码后推给服务器。流媒体服务负责接收推流、转码、切片、分发给观看端是整个直播的中转站。播放端观众看到的网页或App从流媒体服务拉取视频流。业务后端处理用户登录、直播间列表、弹幕、礼物、在线人数统计这些和视频无关、但同样高并发的业务接口。这四个角色搞清楚之后压力的分布就一目了然了。推流端只有一个或少数几个压力不大播放端可能有几千上万人但因为走的是流媒体协议和业务后端是分离的而业务后端要承接的是所有观看用户产生的接口请求比如进入直播间、发弹幕、刷礼物、心跳上报这些请求短平快但数量巨大这才是高并发的主战场。1.2 高并发压力的重灾区在哪里我拿自己搭建这套环境时的真实数据来说明。当时一场活动直播间在线人数峰值到了5000人左右看上去不算特别夸张但看后端监控就发现进入直播间这个接口的QPS瞬间冲到了800多弹幕WebSocket的消息广播量每秒接近两万条数据库连接数一度被打满。这个数字背后反映出直播业务高并发的三个典型特征突发性强活动开始前一分钟大量用户同时涌入流量曲线像心电图一样瞬间拉满。读多写少绝大多数请求是查直播间信息、查弹幕历史、查排行榜这些读操作占了九成以上。长连接多每开一个观看页面就会建立一条WebSocket长连接单机撑不住太多并发连接这跟普通HTTP接口的压力模型完全不同。这三条特征决定了优化思路不能照搬传统Web项目的套路而是要有针对性地设计缓存策略、连接管理方案和削峰手段。1.3 后端小白最容易踩的三个认知误区我一开始也走了不少弯路总结下来有三个典型的认知误区建议新手直接绕开。第一个误区是以为高并发就是多买几台服务器。服务器多确实能扛更高流量但如果没有负载均衡、没有横向扩展能力、没有合理的架构分层买再多机器也只是让流量均匀地打垮每一台机器。先有架构再有机器。第二个误区是忽视带宽和连接数的限制。直播场景里一台流媒体服务器能支撑的同时拉流数量很大程度上取决于带宽而不是CPU。我自己当时就差点忽略了这点用一台1Gbps带宽的机器去支撑5000人同时观看即使按每人2Mbps的码率计算光视频流量就已经接近瓶颈更别提还有弹幕和接口流量。第三个误区是把所有压力都放在业务后端上。直播里视频流的转发应该交给流媒体服务或者CDN去处理业务后端只负责接口和消息推送如果让后端去代理视频流很快就会被带宽打爆。这个边界必须划分清楚。2. 从0开始搭推拉流环境让视频先跑起来2.1 选型对比SRS、nginx-rtmp还是直接上云搭建直播环境的第一步是选择一个流媒体服务。我对比了三种主流方案各有各的适用场景。nginx-rtmp-module是最老牌的方案基于Nginx做扩展配置简单社区资料多坏处是功能比较基础HLS切片、鉴权、回源这些需要自己在Nginx层面做各种配置而且性能上限不高适合学习或者规模很小的场景。SRSSimple Realtime Server是目前开源领域做直播流媒体比较能打的方案原生支持RTMP、HTTP-FLV、HLS、SRT、WebRTC内置了鉴权、转发、集群、GOP缓存等一堆直播场景需要的功能配置起来也不复杂。我最终选的就是SRS主要看中它的功能和性能均衡既能撑住几千人同时拉流又不用自己造轮子。直接买云服务商的直播产品也可以优势是省心、有SLA保障、自带CDN劣势一是按流量计费挺贵的二是像鉴权、回调、数据上报这些细节要要在别人的平台上绕来绕去不够灵活。如果只是做内部系统的小规模直播或者想自己掌控全链路开源方案其实是更合适的。2.2 用SRS搭一个最小可用的推拉流服务SRS的安装非常简单直接从GitHub拉取源码编译就行或者用官方提供的Release包。我当时的操作是下载源码后在Linux服务器上执行wget https://github.com/ossrs/srs/archive/refs/tags/v5.0-r4.tar.gz tar zxvf v5.0-r4.tar.gz cd srs-v5.0-r4 ./configure make -j4编译完成后在conf目录下建一个自己的配置文件我起了个名字叫live.conf内容如下listen 1935; max_connections 10000; daemon on; srs_log_tank file; srs_log_file ./objs/srs.log; http_api { enabled on; listen 1985; } http_server { enabled on; listen 8080; dir ./objs/nginx/html; } vhost __defaultVhost__ { hls { enabled on; } http_remux { enabled on; mount [vhost]/[app]/[stream].flv; } }然后启动./objs/srs -c conf/live.conf这里有几个参数值得展开说说。listen 1935是RTMP协议的默认端口推流和拉流走这个端口max_connections设到10000是为了后续并发测试留余量实际能撑多少还要看带宽和文件描述符限制。hls enabled on会开启HLS切片功能把直播流切成一个个几秒的ts文件再播兼容性很好但延迟会高一些。http_remux是SRS的一个核心特性能把RTMP流转成HTTP-FLV直接通过HTTP 8080端口拉流延迟比HLS低很多这一项在后面低延迟优化里非常有用。2.3 OBS推流与播放器验证全流程流媒体服务起来之后我用OBS做了一次完整的推拉流验证。OBS的推流设置里填写服务器rtmp://你的服务器IP:1935/live推流码teststream需要注意的是这里的live对应的是SRS配置里的app名teststream对应stream名播放地址里也要拼上这两个值。推流成功后在浏览器里打开播放器验证。我用了最直接的方式用ffplay播放ffplay rtmp://你的服务器IP:1935/live/teststream ffplay http://你的服务器IP:8080/live/teststream.flv ffplay http://你的服务器IP:8080/live/teststream.m3u8三条地址全部能出画面说明推拉流链路已经通了。在验证过程中顺手做了一件事把码率调到2Mbps分辨率设为1080p模拟真实主播的使用场景这样后续压测的数据更有参考价值。2.4 低延迟播放的关键配置直播里低延迟是个重要指标做互动直播比如带货、连麦、互动答疑时尤其关键。观众看到的内容比真实时间落后太多互动体验就很差。我在这套环境里做了几个关键调整把端到端延迟从HLS模式下的10秒以上降到了HTTP-FLV模式下的2到3秒。第一个调整是播放端优先使用HTTP-FLV而不是HLS。HLS因为切片和索引机制天然会有几秒延迟而HTTP-FLV是直推直拉时间差主要在网络传输上小很多。第二个调整是在SRS里开启GOP缓存同时把GOP大小控制在2秒以内。GOP是编码器生成关键帧的间隔播放器只有在拿到关键帧之后才能开始解码。如果GOP太大观众进直播间时可能要等好几秒才能等到下一个关键帧。我的处理方式是让主播在OBS里把关键帧间隔设置成2秒同时在SRS里开启了默认的GOP缓存这样新用户进入时能立刻拉到最近一个关键帧开始播。第三个调整是使用WebRTC推流和播放来进一步降低延迟。SRS从4.0版本开始支持WebRTC我把这个能力也打开了一套实测延迟能压到500毫秒以内但这套方案对前端播放器的要求也更高普通网页要用WebRTC播放器才能兼容。我当时的做法是核心互动场景用WebRTC普通围观场景用HTTP-FLV兼顾低延迟和兼容性。提示如果你只是做活动直播或内容直播延迟在5秒以内观众基本无感知没必要一开始就上WebRTC。先跑通HTTP-FLV等确认有强互动需求再升级开发成本会低很多。3. 业务后端的高并发改造真正考验后端功底的部分3.1 业务并发和流媒体并发是两件事视频流跑起来之后更大的挑战来了——业务后端。我一开始也很天真以为流媒体服务能扛住并发就万事大吉结果压测一打后端的Spring Boot服务先垮了。这里要把一个概念讲清楚流媒体服务负责的是视频数据的转发每秒转发多少MB数据取决于带宽业务后端负责的是登录、进房、弹幕、礼物这类业务逻辑每秒能处理多少次请求取决于应用性能。两者的并发模型完全不同优化手段也完全不一样。流媒体主要愁带宽业务后端主要愁QPS、数据库连接和线程资源。基于这个认知我重新设计了一套后端架构核心思路是分层拆分接入层用Nginx做负载均衡和静态资源处理应用层用Spring Boot做业务接口数据层用Redis扛热点读用MySQL存最终数据再用RocketMQ做削峰和异步解耦。这套架构不算新但用在小团队的直播项目上非常合适每一层都有明确的职责边界。3.2 弹幕网关用WebSocket撑住万人同时发言直播场景里最考验后端功底的接口往往是弹幕。一组数据可以说明问题5000人在线时假设有20%的人同时发弹幕瞬时消息量就是1000条每秒而且要广播给所有在线用户广播量就达到5000乘以1000的量级虽然靠推送通道可以优化但这个量级对Web框架来说已经是很大的压力。我的方案是用Spring WebSocket加Redis Pub/Sub做了一套弹幕网关。Spring WebSocket负责管理连接和收发消息Redis Pub/Sub负责消息的广播转发。每个直播间的消息通过Redis广播出去消息到达WebSocket服务端后再推送给这个房间里所有连接的客户端。核心配置如下Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(barrageHandler(), /ws/barrage) .addInterceptors(new AuthHandshakeInterceptor()) .setAllowedOrigins(*); } Bean public WebSocketHandler barrageHandler() { return new BarrageHandler(); } }这里有一个关键点就是要设置心跳机制。我在前端配置了30秒一次的ping心跳后端在60秒内没收到心跳就主动断开连接这样能及时清理死连接避免连接数被僵尸连接占满。另一个关键点是鉴权手写了一个AuthHandshakeInterceptor在握手阶段校验token校验不通过直接拒绝连接避免有人伪造身份发弹幕。WebSocket这个方案实测下来单机撑2000左右的长连接很轻松配合两台机器做负载均衡就能覆盖5000人在线场景。如果人数再上一个量级就要考虑用Netty替换Spring WebSocket或者接入专业的IM云服务但那是另一个话题了。3.3 Redis在处理在线状态和热点数据上的正确姿势直播间的高并发流量里很多可以靠Redis解决。我一开始的错误做法是每个接口都直接查MySQL很快数据库连接池就爆了。后来把Redis好好用起来才把数据库的压力降了下去。在线人数统计是最典型的场景。如果用MySQL记录进入和离开事件5000人进进出出每次都要写库数据库根本扛不住。我的方案是直接用Redis的HyperLogLog来做去重统计每个直播间对应一个HyperLogLog key用户进入时执行PFADD统计人数时执行PFCOUNT误差在可接受范围内但性能比Set和数据库高了好几个量级。直播间的基础信息标题、主播名、封面、在线人数等我也做了三级缓存本地缓存 - Redis - MySQL。热点直播间的数据从Redis直接返回QPS打到几千都没有压力。需要注意的是缓存把并发挡在了Redis层之后Redis单机也能撑住很高的QPS所以这套方案在小规模直播场景下完全够用。另一类适合Redis的场景是排行榜。比如送礼物的实时榜单用Redis的ZSet存储每次送礼物执行ZINCRBY排行榜按分数倒序取前50性能远胜数据库查询。直播间结束后再异步把最终结果刷到MySQL作为历史数据长期保存。3.4 用消息队列削峰踢开高并发大门的第二把钥匙即使做了缓存还是有一些写操作躲不开比如用户进入直播间的日志、礼物记录、弹幕持久化。这些操作如果跟着用户请求一起同步落库高峰期数据库一样扛不住。我的做法是引入RocketMQ把所有非关键写操作改成异步。以礼物记录为例用户送礼物的请求到达后端后后端只做一件事——把礼物消息投递到RocketMQ然后立刻返回成功。真正写MySQL的动作放到消费端去执行。这样即使瞬时请求量很大数据库的压力也是平稳的因为消费端可以按自己的节奏慢慢处理积压的消息这就是所谓的削峰填谷。我选的RocketMQ而不是Kafka理由是RocketMQ在这类业务场景里更友好消息的可靠性和事务消息都支持得比较好控制台也比较直观。如果团队里已经有Kafka了用Kafka也完全没问题原理是一样的。有两点经验值得一提。第一是消息一定要设置好消费幂等网络抖动可能导致消息重复投递消费端要设计好去重逻辑否则会出现礼物数量翻倍、弹幕重复入库的问题。第二是消息积压要能及时预警我写了个定时任务去监控消费堆积量超过阈值就推送告警防止数据库在高峰期之外的时段被异步流量打垮。4. 负载均衡与系统参数让流量均匀落下来4.1 Nginx反向代理与负载均衡策略选择业务后端部署了两台应用服务器之后用Nginx做统一入口。很多人会把Nginx只当成静态资源服务器但对直播这种长连接和突发流量并存的场景Nginx的配置甚至比业务代码还要讲究。先说负载均衡策略。常规Web接口我用的是轮询因为每个请求是独立的谁空闲就分给谁。但WebSocket长连接不能简单轮询因为一旦建立连接后续的所有消息都走这条连接不能再被转发到别的机器上。这里用了ip_hash策略保证同一个用户的请求都落在同一台机器上。upstream ws_backend { ip_hash; server 10.0.0.2:8080; server 10.0.0.3:8080; } map $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 80; location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }Upgrade和Connection这两个头不配置的话WebSocket通过Nginx代理时会被断开这是做长连接代理最容易踩的坑我当时花了不少时间才排查出来。proxy_read_timeout默认是60秒如果不调大长连接会因为没有数据传输而被Nginx掐掉这也是一个很隐蔽的问题。另外Nginx的worker进程数和keepalive也要调。我的配置是worker_processes auto、worker_connections 65535、keepalive_timeout 65。注意worker_connections和后面的操作系统的文件描述符限制是绑定的如果系统只允许打开1024个文件你配置了65535也不会生效。4.2 操作系统层需要动的一组参数负载均衡和应用层的配置做完了还有一个容易被忽略的部分是操作系统层的调优。默认的Linux系统参数是为普通应用设计的应付直播这种高连接数、高并发场景时需要调整几项。我当时在服务器上执行了这样一组调整ulimit -n 100000 sysctl -w net.core.somaxconn65535 sysctl -w net.ipv4.tcp_max_syn_backlog65535 sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout30每个参数的作用我简单解释一下。ulimit -n限制的是进程能打开的文件描述符数量一个TCP连接就要占用一个文件描述符所以这个值必须足够大否则连接数一到上限新连接直接进不来。somaxconn和tcp_max_syn_backlog是内核层面接收连接请求的队列长度在高并发握手阶段如果队列太小客户端会连接超时。ip_local_port_range是允许分配的本机端口范围如果范围太小做压力测试时源端口会不够用出现connect失败的情况。这些参数改完之后记得确认修改是持久化的。ulimit要写到/etc/security/limits.confsysctl可以写到/etc/sysctl.conf否则重启后又会恢复默认值。4.3 压力测试拿数据说话配置都说完了最终效果还是要用压测数据说话。我用wrk分别对HTTP接口和消息推送链路做了压力测试压测命令如下wrk -t8 -c1000 -d60s --latency http://localhost:8080/api/live/info注意这里-c1000表示模拟1000个并发连接-t8是8个线程-d60s是持续60秒。这个命令跑出来的数据有两个指标我要解释一下。QPS是每秒处理的请求数这代表系统的最大处理能力p99延迟表示99%的请求都能在这个时间内返回这个指标比平均延迟更有参考价值因为平均延迟容易被极端值拉低。我当时的压测结果大致是单台应用服务器在1000并发下QPS能到5000左右p99延迟38毫秒这个水平对直播业务来说是够用的。发现的问题也很有意思压测到1500并发时QPS不升反降p99延迟飙到500毫秒以上。定位发现是线程池打满了——Spring Boot内置的Tomcat默认最大线程数是200一旦所有线程都忙于等待数据库查询新的请求只能排队等待出现了线程饥饿现象。这个问题的解决方案是优化查询、加缓存而不是单纯调大线程池因为线程太多反而增加上下文切换的开销。5. 前后端分离场景下的鉴权与安全细节5.1 推拉流地址为什么要鉴权直播地址如果没有任何保护任何人拿到地址都能直接拉流观看甚至可以直接把地址分享出去导致流量白白被别人消耗。我在搭建环境时一开始也没注意这个问题直到发现有人在非白名单渠道播放我的直播流才意识到地址鉴权有多重要。SRS支持在推流和拉流时触发回调把推拉流请求上报给业务后端后端返回允许或拒绝。同时也可以给直播地址加一个周期性过期的签名参数只有携带合法签名的播放器才能拉流成功。我采用的方案是两者结合推流端要求带固定的推流码播放端要求带基于时间戳的签名。推流码比较长普通人拿不到签名地址有有效期即使泄露了过期后也自动失效。5.2 Token生成与校验一个可以直接抄的签名方案签名方案的具体逻辑如下生成一个包含直播路径和过期时间戳的字符串用HMAC-SHA256算法加上服务端密钥对这个字符串做哈希然后把原始串和哈希拼在一起作为播放地址的token参数。前端拿到后拼成完整的播放地址http://你的服务器IP:8080/live/teststream.flv?tokeneyJwYXRoIjoiL2xpdmUvdGVzdHN0cmVhbSIsImV4cCI6IjE3MDAwMDAwMDAifQ.signature后端在SRS的on_play回调里校验token。校验有两步先检查时间戳是否在有效期内再对同一段字符串做HMAC运算比对结果是否一致。只要有一个不通过就拒绝这次拉流请求。private boolean verifyToken(String playPath, String token) { String[] parts token.split(\\.); String payload parts[0]; String signature parts[1]; String decoded new String(Base64.getUrlDecoder().decode(payload)); String expire decoded.split(:)[1]; if (Long.parseLong(expire) System.currentTimeMillis() / 1000) { return false; } String expected hmacSha256(secret, payload); return expected.equals(signature); }这个方案有个小细节要注意HMAC比对要用固定时间的比较函数避免因为字符串比较的短路特性导致时序侧信道问题。虽然直播场景被攻击的概率不高但写后端代码时养成好习惯很重要。前端播放器对接的时候也有一个坑使用原生HTML5的video标签播放HTTP-FLV时浏览器有跨域限制需要在Nginx加上跨域头add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET;如果是用flv.js或hls.js播放器它们底层走的还是HTTP请求同样需要在服务端配置CORS头。这个是典型的前后端分离联调问题我因为没配置跨域白折腾了好几个小时。6. 常见问题与排查实录替你们提前踩一遍坑6.1 跨域、断流、并发告警等高频问题速查搭完这套环境并经过一段时间运行之后我把遇到的典型问题整理成了一个速查表希望能帮后面做直播的人少走弯路。问题现象根因解决办法网页播放器拉不到流服务端未配置CORS跨域头在Nginx或SRS的HTTP响应中增加跨域头WebSocket连接频繁断开Nginx未配置Upgrade头或read_timeout过短配置proxy_set_header Upgrade和Connection调大超时时间直播间同时在线几十人就卡系统文件描述符限制未调整执行ulimit -n设置到100000以上弹幕发送后部分用户看不到Redis Pub/Sub消息丢失或消费端处理不及时改用消息队列或设计消息持久化机制高峰期数据库连接池爆满大量同步写操作打满连接池把写操作改为异步MQ消费Redis承接热点读直播延迟越来越大GOP设置过大或未开启GOP缓存OBS设置关键帧间隔2秒SRS开启GOP缓存播放地址泄露被他人使用未做拉流鉴权给播放地址加带过期时间的HMAC签名压测时QPS上不去但CPU不高线程池默认太小导致请求排队调大Tomcat线程数或改用异步Servlet模型这张表我在后面做复盘时反复对照过基本上所有突然xxx的问题都能在里面找到影子。特别是CORS和文件描述符这两个属于看起来跟直播毫无关系、但实际上最容易卡住新手的地方。6.2 一次直播高峰期的真实排障过程再分享一次实际排障过程比单独罗列问题更能说明问题。活动开始后大概10分钟监控报警提示某台应用服务器的CPU使用率持续在95%以上。我登录服务器一看JVM的GC日志显示频繁Full GC每次耗时都在几秒。当时情况很危险再持续下去服务基本就不可用了。我先从数据库入手排查把慢查询日志打开发现有一个进入直播间时查用户等级的SQL执行了2.3秒。查了一下用户表的数据量也才二十多万条不应该是全表扫描后来发现是SQL里有个关于直播间的条件字段没有索引导致每次查询都会先扫全表。加上复合索引之后这个SQL的执行时间降到30毫秒以内。但这只是解决了CPU高的一个因素。GC频繁的另一个原因是压测时创建的定时任务在高峰期同时触发每秒钟往消息队列里投递了大量日志数据导致堆内存快速堆积。我调整了定时任务的执行频率把日志批量投递改成异步批量提交之后GC频率明显降了下来。排障过程中我最大的体会是高并发场景下遇到问题时先看指标再动代码不要凭感觉瞎调。我当时就是先看监控面板确认是GC问题才去查代码和SQL整个过程用了不到20分钟。如果一上来就去改代码可能半天也定位不到根因。还有一个小技巧后端服务要提前做好健康检查接口返回当前线程池占用率、最近一分钟QPS、JVM内存使用情况这些关键指标配合Prometheus和Grafana做成监控面板。这样在出现问题的第一时间就能看到是哪一块崩了而不是一台一台登录服务器去翻日志。最后说点实在的。这套直播高并发环境搭建下来我最深的体会是高并发没有银弹每一层都有每一层的优化空间但是先跑通再优化永远比一开始就设计一个巨型架构要靠谱。我一开始也没想到一个后端小白真的能从装流媒体服务器开始一步步把推流、拉流、弹幕网关、消息队列、负载均衡全链路搭起来。如果你也想做直播项目建议从SRS加Spring Boot这套组合开始把最小的闭环跑通再去考虑集群和更高阶的架构。等你能扛住几千人的在线直播时回头看最开始那些让你困惑的并发概念会发现它们其实一点都不神秘。