ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

直播后端从0到1:高并发架构搭建与踩坑实战

直播后端从0到1:高并发架构搭建与踩坑实战 做后端这几年最怕听到的词可能就是“高并发”。尤其是直播这种业务它跟普通的电商秒杀还不太一样——秒杀是瞬间流量集中直播是长时间、大连接的持续压力既要保带宽又要保延迟还得兜住弹幕、礼物、在线人数这类实时互动流量。作为一个后端小白我在没有任何直播项目经验的情况下硬是把手里的这套直播环境从0到1手搓了出来中间废了好几版方案也踩了不少坑。这篇笔记就是把我当时选型、搭建、调优、排查的完整过程记录了下来适合刚接触直播后端、或者想从增删改查跨到高并发场景的开发者参考。不是官方文档式的说明书是我自己实操过的内容能直接照着抄。整个搭建过程我大概花了两周时间从一穷二白的服务器到能同时支撑万人直播间的基本架构。先别急着上组件先把直播高并发的核心逻辑盘清楚——为什么它会比普通业务难这么多瓶颈到底在哪然后我们再一步步把环境搭起来。1. 直播高并发的核心思考先搞懂瓶颈在哪1.1 直播场景和普通业务高并发的本质区别我知道很多人一看到“高并发”三个字第一反应就是加缓存、加消息队列、搞集群。但直播场景最坑的一点是它的高并发不是只集中在数据层而是在网络连接层就被打满了。拿普通Web业务来说用户一个请求进来后端处理几毫秒返回一个JSON连接就断开了。就算QPS做到几千对连接本身的消耗也不大。但直播是什么一个观众从进入直播间到离开可能保持1小时甚至更久的长连接。1000人在线就是1000个长连接挂在服务器上10万人在线就是10万个长连接。这些连接不仅要保持还要实时接收弹幕、礼物消息通过WebSocket推送过来。这就意味着你的服务器每时每刻都在执行“读事件、写事件、分发消息”CPU没空转内存被连接占着网络带宽被消息和流媒体占着三个维度一起承压。还有一点特别容易被忽略直播的流量曲线是“突发式”的。比如一个主播开播粉丝瞬间涌入可能从几百人在线直接跳到几万人在线前后不到一分钟。这时候如果架构没有提前做好水平扩展、限流和削峰单机节点很容易直接被打挂。普通的Web接口挂了可以重启直播连接断了观众体验就是画面卡住、弹幕消失流失率极高。所以在动手之前我心里先列清楚了直播高并发的三个核心瓶颈连接数瓶颈、带宽瓶颈、实时消息分发瓶颈。后面所有的方案选型都是围绕这三件事来做的。1.2 整体架构选型与组件清单想清楚瓶颈之后我开始画架构图。我们的业务形态是主播推流观众拉流观看所有人可以发弹幕、送礼物直播间有在线人数统计热门直播间有一个排行榜。这是一个很典型的“直播IM互动”场景而且并发压力主要集中在热门直播间。我最终选型的核心组件是这样的DNS层多线路解析 按区域调度把用户流量导到最近的机房节点。接入层Nginx做主入口后面挂业务API和WebSocket网关。流媒体层推拉流走自建服务使用HTTP-FLV协议边缘节点接了CDN做分发加速。服务层Java Spring Boot写业务APINetty写WebSocket长连接网关。数据层Redis负责在线人数、弹幕限流、热门榜等热数据Kafka做消息削峰和广播通道MySQL存用户、礼物记录、直播回放等冷数据。基础设施Docker Compose单机编排起步后面迁移到Kubernetes做容器编排Prometheus Grafana做监控告警。组件选型上面我踩过一个认知误区这里必须先说清楚不是组件越多越高级。一开始我想上微服务全家桶后来发现直播业务的核心链路其实很简单真正复杂的是连接管理和消息分发。如果一上来就把服务拆得很散链路一长延迟就上去了排查问题也困难。我做了一个折中方案——核心链路是单体服务但把WebSocket网关独立出来因为它有完全不同的伸缩维度。2. 接入层搭建从带宽计算到Nginx调优2.1 带宽与服务器规格先算明白再动手很多新手搭建直播环境上来就装软件结果等流量一进来才发现带宽不够或者服务器规格不对只能全部推翻重来。我特别建议大家先把容量估算做了再着手搭建这笔账其实很简单小学数学就能算明白。先说观看带宽。比如一个主播推流码率是2Mbps如果1000个观众同时在线观看理论出口带宽就是2Mbps × 1000 2000Mbps约等于2Gbps。这还只是一个机房的出口带宽自建机房基本都是百兆到千兆的带宽成本如果你让源站直接扛观看流量光是带宽费用就能把预算烧穿。这也是我必须接入CDN的根本原因——观众的拉流流量由CDN边缘节点承担源站只承担一路推流和CDN回源的流量回源带宽通常只有总观看流量的5%到10%。再说连接数。我用的是4核8G的云主机做WebSocket网关节点按经验估算一个4核8G的节点在不做额外优化的情况下大约能支撑5万左右的长连接如果内存和内核参数优化到位可以到10万。但我们还要留CPU给消息处理不能顶着上限跑。我按单节点3万在线来规划目标支撑6万在线就至少需要2台网关节点。这只是网关还不包括API服务节点。还有推流带宽。主播端推流一般也要占带宽但推流数量远小于拉流数量所以源站的推流入口带宽压力不大重点在于推流节点的稳定性不能因为单台故障导致主播断流。做完这三笔账我心里就有底了源站不需要太大的带宽但对连接数、CPU和内存的要求更高CDN是必须的不能省网关节点一定要能水平扩展。2.2 四层负载均衡选型与Nginx配置接入层我用了两级负载均衡先一层四层(L4)负载均衡负责流量分发再一层七层(L7)Nginx负责HTTP和WebSocket的反向代理。四层这层我最后选了云厂商的负载均衡(SLB/TGW)原因是自建LVS虽然可控性强但对后端小白来说运维成本太高而且云LB天然带高可用挂了自动漂移少操心很多。七层Nginx这里我要多说几句配置细节。下面是我当时用的核心配置片段worker_processes auto; worker_rlimit_nofile 1048576; events { worker_connections 102400; use epoll; multi_accept on; } http { upstream ws_gateway { least_conn; server 10.0.1.11:8080 max_fails3 fail_timeout10s; server 10.0.1.12:8080 max_fails3 fail_timeout10s; keepalive 256; } server { listen 80; server_name live.example.com; location /ws { proxy_pass http://ws_gateway; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } }这里有三个非常关键的参数worker_connections、worker_rlimit_nofile、proxy_read_timeout。第一worker_connections要尽可能调大因为WebSocket长连接会占用并发连接数。默认的1024完全不够我这边设成了102400同时把worker_rlimit_nofile也调到了1048576这个参数代表单个worker进程能打开的最大文件描述符数量。如果这两个参数不配套调整连接数一上来Nginx就会报“too many open files”。第二proxy_read_timeout和proxy_send_timeout必须调大。因为WebSocket连接是长连接如果还用默认的60秒超时客户端一分钟没发消息就会被Nginx断开。这里我设成了3600秒也就是1小时配合heartbeat机制实际使用中很少出现被动断连。第三least_conn是WebSocket网关的最佳负载均衡策略。因为每个WebSocket连接占用的资源差不多但连接存活时间差异很大如果按轮询分发可能出现某个节点分配到的都是长连接负载偏重。least_conn会把新连接分发到当前活跃连接最少的节点更均衡。2.3 直播流协议选型与CDN接入直播流这块我也纠结过挺久。HLS延迟太高10秒以上不适合互动场景RTMP推流可以但拉流在浏览器里没法直接播放WebRTC延迟最低但搭建复杂。我最后选择了HTTP-FLV作为拉流协议配合WebSocket做IM延迟能控制在1到3秒评论区互动和画面基本对得上。HTTP-FLV的优点是基于HTTP协议能无缝复用Nginx、CDN这些基础设施延迟低在浏览器里可以通过flv.js直接播放不需要原生播放器。缺点是不支持iOS原生Safari播放需要做兼容处理但我们的场景主要是在移动App和Web端能接受这个代价。CDN接入我走的流程是先在流媒体服务器拿到可用的HTTP-FLV播放地址然后把自己的推流域名和播放域名在CDN控制台接入配置好源站地址。要注意的是CDN只是分发加速源站必须稳定因为CDN边缘节点没有命中的时候要回源拉流。回源策略我配的是“回源到源站的边缘存储节点”这样主播推流只写一份多个边缘节点按需回源成本可控。3. 服务层实现WebSocket网关与直播核心服务3.1 为什么要把WebSocket网关单独拆出来最开始我也想过把WebSocket直接挂在Spring Boot业务服务里毕竟开发起来最简单一个ServerEndpoint注解就搞定了。但后来我算了一笔账WebSocket连接是长连接每一条连接都会长期占用一个线程或者一个协程资源而业务API是短连接用完就释放。这两个东西混在一起最大的问题是故障域没法隔离——弹幕流量一涨网关把CPU打满连带着登录、送礼这些API接口也跟着超时整个直播间接瘫痪。所以我把WebSocket网关单独拆成了一个独立服务用的Netty不依赖Spring MVC那套容器原因有两个。第一Netty是事件驱动的NIO框架一个线程可以处理成千上万个连接内存占用比“一个连接一个线程”的模式低一个数量级。第二网关服务可以独立扩缩容——弹幕流量大了我单独加网关节点就行完全不影响业务API的稳定性。网关和业务服务的交互我用的是Redis发布订阅。网关收到弹幕消息后不直接处理业务而是把消息丢到Redis的channel里业务服务订阅这个channel去做敏感词过滤、存储、统计。这样做的好处是解耦坏处是如果消息量巨大Redis单机的发布订阅会成为瓶颈所以后面我又在消息量更大的场景引入了Kafka一会儿再说。3.2 网关节点水平扩展与连接状态管理WebSocket网关单独拆出来了新的问题来了怎么让多个网关节点协同工作首先遇到的是会话状态问题。如果用户连的是网关A下一次请求被转发到网关BB没有这个用户的会话信息推送就失败了。所以网关节点必须无状态化。我的做法是用户连接上来时把userId 网关节点信息 连接ID注册到Redis同时维护一个全局的连接映射。业务服务需要给某个用户推送消息时先查Redis看这个用户连接在哪个网关节点然后通过内部RPC或者Kafka把消息投递给对应网关再由它推给用户。这里有一个很容易踩的坑网关节点挂了它维护的连接全部断开但Redis里的注册信息还残留着导致后续消息推送到一个不存在的节点。解决方式是做“心跳检测 过期清理”。每个连接在Redis里设置一个过期时间比如30秒网关节点每10秒发送心跳更新过期时间网关节点下线时主动清理注册信息万一节点被强制杀掉Redis的过期机制也能兜底清理。还有一个问题就是在线人数统计。我之前在MySQL里实时count在线人数到了几千人就开始响应慢了因为每次count都是全表扫描。后来我改成用Redis的SET结构每个直播间一个SET用户连接时SADD进去断开时SREM在线人数就是SCARD的返回结果时间复杂度O(1)百万人在线都扛得住。3.3 核心链路从开播到弹幕的完整流程有了网关我再完整梳理一下直播间的核心链路方便你理解各组件是怎么配合的。主播端主播推流到流媒体服务流媒体服务把HLS/FLV切片分发到CDN同时推流成功回调通知业务服务业务服务修改直播间状态为“直播中”并在Redis里创建直播间在线人数集合。观众端用户进入直播间前端通过业务API拿到直播流地址和WebSocket连接地址。然后前端建立WebSocket连接连接到WebSocket网关网关校验token通过后从Redis读取用户信息建立会话同时网关把自己的节点信息注册到Redis并把用户加入直播间的在线人数集合。互动消息用户发弹幕前端通过WebSocket发送到网关。网关不直接处理而是把消息发布到Redis channel或Kafka业务服务订阅后进行敏感词过滤、存储入库、统计同时把弹幕消息广播给同一直播间内的其他连接。广播方式我采用的是直播间维度分组——每个网关节点在本地维护roomId - SetChannel的映射业务服务广播消息时通过消息队列把消息投递到所有网关节点网关节点再遍历本地的直播间连接集合逐个推送。这套链路跑下来单直播间数千人弹幕基本没有压力瓶颈主要在网络带宽和广播风暴上。4. 数据层与高并发利器Redis、消息队列与数据库优化4.1 Redis为什么能扛住直播的热数据压力直播场景里Redis几乎是神一样的存在。我的用法主要集中在四个方面。第一是在线人数统计。前面提到了用SET结构一个直播间一个SET秒级出结果。第二是弹幕限流。如果不做限制一个用户1秒可以刷几百条弹幕服务器直接被刷爆。我用了Redis的滑动窗口限流INCR一个key设置过期时间比如danmu:limit:{userId}:{timestampWindow}如果这个key的值超过了阈值比如每秒5条就直接拒绝消息返回“发送太快”。第三是热门直播间排行榜。这个用Redis的ZSET最合适每个直播间一个scorescore可以是在线人数、礼物金额等指标ZREVRANGE取前100名就是排行榜毫秒级响应。第四是分布式锁。比如发礼物、扣库存这种需要原子操作的场景用Redis的SETNX实现分布式锁防止并发情况下超卖。这里要注意锁的过期时间要设置合理我一般设置为30秒同时启用看门狗机制自动续期避免锁过期导致业务还没执行完就被其他线程抢到。Redis的部署我建议至少一主一从加哨兵。我踩过一个坑一开始图省事只部署了单机Redis结果直播间流量上来之后主节点CPU飙到100%然后Redis进程OOM被系统杀掉所有读请求瞬间打到数据库数据库连接池直接被打爆。那次事故之后我老老实实加了从节点做读写分离热点数据走从库写操作走主库压力降了一大半。4.2 消息队列选型与削峰Kafka vs RabbitMQ在消息队列选型上我对比过Kafka和RabbitMQ最后选了Kafka。原因是直播场景的消息量大广播消息多Kafka吞吐量更高而且天然支持发布订阅模式。Kafka在我的架构里承担两个核心职责一个是弹幕消息的削峰缓冲另一个是跨网关消息广播。弹幕消息的写入是高频小消息如果直接打到数据库几千人同时发弹幕数据库的写入压力是灾难性的。所以我把弹幕消息先发到Kafka业务服务异步消费批量写入数据库。高峰期就算瞬时消息暴涨到每秒几万条Kafka也能扛住消费者按自己的处理能力慢慢消费数据库永远不会被打爆。Kafka的配置我当时是这样调的num.partitions设置为3的倍数因为我们有3台业务消费节点replication.factor设为2保证数据冗余。每个分区可以对应一个消费者线程提高消费并行度。这里要特别提醒一下retention.hours要想清楚再设置。我把弹幕消息保留时间设置成3天因为要做弹幕回放。如果你不需要回放功能可以设置成1小时避免磁盘浪费。RabbitMQ也有它的优势比如延迟队列、死信队列、多路由规则等更擅长复杂的消息路由场景。但在“量大、简单、广播”的直播弹幕场景Kafka是更省心的选择。4.3 MySQL连接池、读写分离与热点表拆分直播业务最终的数据还是要落到MySQL里。但MySQL在高并发下是最容易出问题的环节所以我在数据层做了三层防御。第一层是连接池限流。我用的是HikariCP连接池maximum-pool-size我设置成20minimum-idle设置成5。很多人喜欢把连接池调得很大好像连接数越多越好其实这是一个误区。每个MySQL连接占用的内存大概在10MB左右20个连接就已经占200MB内存了而且MySQL的并发能力有一道天花板连接数再多也只是排队等待反而增加上下文切换开销。我实测下来20个连接足够支撑大多数直播业务的读写压力。第二层是读写分离。直播间的弹幕、礼物记录是高频写而用户信息、主播信息等是高频读。我把写请求打到主库读请求打到从库通过中间件我用的是ShardingSphere做读写分离路由。这样主库压力减轻很多从库可以水平扩展。第三层是热点表拆分。弹幕表是直播场景最大的热点表。我一开始把所有弹幕都存到一张表里到了百万条数据之后查询就变慢了。后来我按照直播间ID做分片把弹幕表拆分成多个物理表比如按直播间ID取模查询时带上直播间ID直接定位到对应的分片表查询速度从几百毫秒降到了几十毫秒。礼物记录表也是类似的思路按时间分表每个月一张表历史数据定期归档。4.4 缓存穿透、击穿、雪崩的防御数据层这一环如果只加了缓存就完事那是会出大事的。我必须把缓存三大经典问题的防御方案写出来因为我在直播环境里全踩了一遍。缓存穿透请求一个不存在的key缓存没有数据库也没有每次请求都打到数据库。我做了两层防御第一层是参数校验非法的直播间ID直接拒绝第二层是布隆过滤器把所有合法的直播间ID预加载到布隆过滤器里查询前先判断ID是否存在不存在直接返回数据库零压力。缓存击穿某个热点key比如热门直播间的状态过期的一瞬间大量请求同时打到数据库。防御方式是加互斥锁只允许一个请求去数据库查并重建缓存其他请求等待缓存重建完成。我用Redis的SETNX实现了这个锁。缓存雪崩大量key在同一时间过期导致数据库被击穿。防御方式有三个一是给缓存过期时间加随机值打散过期时间二是热点数据用逻辑过期不设置物理过期时间三是增加多级缓存比如本地缓存Redis缓存Redis出问题了还有本地缓存兜底。5. 容器化部署、压测与监控告警5.1 Docker Compose一键拉起整套环境环境搭建阶段我用Docker Compose把整套中间件管理起来了。不是我不喜欢Kubernetes而是对于中小规模的直播环境来说Compose的部署和排查成本低很多等真正需要跨多机编排时再迁移K8s也不迟。这是我的docker-compose.yml核心片段version: 3.8 services: nginx: image: nginx:1.24 container_name: nginx-lb restart: always ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - gateway1 - gateway2 networks: - live_network gateway1: image: live/gateway:1.0.0 container_name: ws-gateway-1 restart: always environment: - SPRING_PROFILES_ACTIVEprod - SERVER_PORT8080 networks: - live_network gateway2: image: live/gateway:1.0.0 container_name: ws-gateway-2 restart: always environment: - SPRING_PROFILES_ACTIVEprod - SERVER_PORT8080 networks: - live_network redis: image: redis:7.0 container_name: redis-master restart: always command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data networks: - live_network mysql: image: mysql:8.0 container_name: mysql-master restart: always environment: - MYSQL_ROOT_PASSWORD${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASElive_db volumes: - mysql-data:/var/lib/mysql networks: - live_network kafka: image: bitnami/kafka:3.5 container_name: kafka-broker restart: always environment: - KAFKA_CFG_NODE_ID0 - KAFKA_CFG_PROCESS_ROLEScontroller,broker - KAFKA_CFG_LISTENERSPLAINTEXT://:9092,CONTROLLER://:9093 - KAFKA_CFG_ADVERTISED_LISTENERSPLAINTEXT://kafka-broker:9092 volumes: - kafka-data:/bitnami/kafka networks: - live_network prometheus: image: prom/prometheus:v2.47.0 container_name: prometheus restart: always volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro networks: - live_network grafana: image: grafana/grafana:10.1.0 container_name: grafana restart: always ports: - 3000:3000 environment: - GF_SECURITY_ADMIN_PASSWORD${GRAFANA_PASSWORD} networks: - live_network volumes: redis-data: mysql-data: kafka-data: networks: live_network: driver: bridge有几个编排细节值得提出来。第一所有的敏感信息都用环境变量注入不要硬编码在Compose文件里。第二Nginx做流量入口挂在最前面网关节点通过内部网络通信不暴露公网端口。第三每个容器都设置了restart: always容器挂了能自动重启这在直播场景里很重要——主播不会等你手动恢复服务。5.2 压测怎么做才可信搭建完环境最关键的环节是压测。很多新手压测就是拿Jmeter随便打几个请求看到不报错就觉得系统没问题这是完全错误的。真正的压测要模拟真实的直播业务场景。我的压测方案分三路并行。第一路是API接口压测用Jmeter模拟用户登录、进入直播间、获取直播流地址等HTTP接口。第二路是WebSocket长连接压测我用的是go-stress-testing配合自研的WebSocket客户端模拟器每个虚拟用户建立WebSocket连接后按固定频率发送弹幕消息。第三路是推流压测用ffmpeg模拟推流源不断推送测试视频流。压测时我重点盯三个指标QPS、响应时间、错误率。比如API接口我期望的指标是单机QPS 2000P95响应时间在200ms以内错误率低于0.1%。WebSocket网关预期单节点支撑3万连接消息分发延迟低于500ms。这里有一个血的教训压测一定要在独立的压测服务器上跑不要在和应用服务器共享资源。我第一次压测的时候为了省机器压测工具直接跑在应用服务器上结果压测流量一上来压测工具本身先把CPU吃满了数据完全不可信浪费了我整整两天时间查所谓的“性能问题”。另外一个重要指标是连接数曲线。压测WebSocket时我会每秒新增一定数量的连接观察连接数攀升过程中CPU、内存、文件描述符的变化。如果连接数到某个阈值时CPU突然飙升说明操作系统打开了太多文件描述符需要调整内核参数fs.file-max、net.ipv4.tcp_max_syn_backlog、net.core.somaxconn这几个参数要提前调好。5.3 Prometheus Grafana盯住哪些指标监控是整个环境的眼睛。没有监控系统出了问题你只能靠猜。我安装了Prometheus Grafana配置了四类关键指标。服务器类指标CPU使用率、内存使用率、磁盘使用率、网络带宽使用率、文件描述符数量。这些指标任何一个超过阈值都要告警比如CPU使用率连续5分钟超过80%就要警惕是否需要扩容了。应用类指标QPS、响应时间、错误率、活跃连接数。这些指标暴露在Spring Boot的Actuator端点Prometheus定时抓取。中间件指标Redis的命中率、连接数、内存使用率、慢查询数量MySQL的连接数、慢查询数、主从延迟Kafka的消息堆积量、消费延迟。Kafka的消息堆积量是最重要的指标之一如果消费者消费速度跟不上生产者生产速度堆积量会越来越大最终导致消息延迟过高弹幕用户会感觉到明显的延迟。业务类指标直播间在线人数、弹幕每秒发送量、礼物每秒数量。这些是业务侧的实时数据需要业务代码埋点输出到Prometheus。监控大盘上同时能看到在线人数曲线和弹幕吞吐曲线哪个直播间出现问题一眼就能定位到。告警规则我用了Prometheus的Alertmanager配置了三个级别的告警WARNING预警、ERROR严重、CRITICAL紧急。比如“在线人数超过X万”属于预警“Nginx连接数打满”属于严重“服务宕机”属于紧急会直接发短信和电话通知。6. 常见问题与排查实录血泪避坑清单6.1 连接数打满CPU却很低这是我遇到的第一个诡异问题。某天晚上直播间流量涨上来用户开始反馈直播间卡顿、弹幕发不出去。我检查服务器发现CPU使用率才30%内存也没满但Nginx日志里疯狂报“accept() failed: Too many open files”。后来排查明白了根本不是CPU不够是文件描述符被耗尽了。WebSocket长连接每个连接都会占用一个文件描述符Linux系统对单个进程能打开的文件描述符有上限。默认的1024根本不够用即使我在Nginx配置里把worker_rlimit_nofile调大了操作系统的全局限制也还是要一并调整。解决方法分两步。第一步修改操作系统的/etc/security/limits.conf把nofile软硬限制都调到1048576第二步修改/etc/sysctl.conf把fs.file-max调大然后sysctl -p生效。这个坑非常隐蔽因为如果你只看CPU和内存永远找不到原因必须关注文件描述符指标。6.2 推拉流延迟忽高忽低延迟不稳定这个问题困扰了我很久。明明用的是HTTP-FLV理论上延迟应该在1到3秒但实际运行时高峰期延迟能飙到10秒以上。排查过程是这样的首先排除了CDN的问题因为边缘节点的缓存配置和回源带宽都正常。后来我在源站上抓包分析发现推流端上传的带宽被限速了导致主播推流数据在源站堆积CDN回源拉不到最新数据观众看到的就是明显延迟。源头是云主机的带宽包只有5Mbps推流码率是2Mbps看起来够用但HTTP-FLV拉流的回源流量和推流流量共用这5Mbps回源流量一大推流就被挤占了。解决方法是把源站的上行带宽升级到50Mbps同时为主播推流单独走一条专用带宽不和回源流量抢。还有一个优化点是在流媒体服务器上开启了GOP缓存这只缓存关键帧拉流端可以快速起播减少首屏时间。6.3 WebSocket断连重连风暴直播间同时在线人数多的时候会出现一种很可怕的现象大量WebSocket连接同时断开然后所有客户端同时发起重连请求把服务端瞬间打爆。这个过程像滚雪球一样服务端越处理不过来客户端的重连间隔越短最终导致整个网关瘫痪。第一次遇到这个问题时我以为是网络故障后来在日志里发现是Nginx的proxy_read_timeout设置得太短WebSocket超过60秒没数据就被Nginx主动断开了。但按理说断开了客户端会走重连逻辑问题在于我的客户端重连逻辑是无脑立即重连没有做退避处理。修复方案分两路。服务端proxy_read_timeout调大并在WebSocket协议层增加心跳机制客户端每30秒发送一个Ping帧服务端响应Pong帧。客户端重连策略改为“指数退避抖动”第一次断线等1秒第二次等2秒第三次等4秒……最多等30秒每次重连时间加一个随机抖动避免所有客户端同时重连。6.4 热点直播间把Redis打穿有一个头部主播开播他的直播间在线人数一下子飙到几万人我们发现Redis的主节点CPU使用率直接冲上100%整个Redis服务响应变慢所有依赖Redis的功能——在线人数、弹幕限流、排行榜——全部受影响。原因有两个。第一是这个直播间的在线人数SET非常大几万个成员每次SCARD计算还好但频繁的SADD和SREM操作会带来大量的内存分配和回收。第二是弹幕限流所有用户发的弹幕都走同一个Redis key做计数这个key的访问频率极高。我用三个方法解决了这个问题。一是热点大KEY拆分把在线人数按照用户ID取模分成10个SET统计时把10个SET的SCARD结果相加单KEY的压力就降下来了。二是弹幕限流改成Token Bucket在网关本地实现不再每次请求都查Redis。三是给Redis加了一层本地缓存热点数据在网关节点内存里缓存几秒减少对Redis的访问频率。6.5 数据库连接数被打爆有一次弹幕服务重启导致Kafka消费者在短时间内全部暂停Kafka里的消息堆积了几十万条。等消费者恢复后积压的消息像洪水一样涌进来消费线程疯狂往MySQL里写数据连接池瞬间被耗尽其他正常的业务请求比如用户登录也拿不到数据库连接整个服务处于假死状态。这次事故的根本原因是消费者消费速度没有做限流。修复方案我在消费逻辑里加了信号量限流最多允许同时10个线程写数据库超出部分排队等待。同时在Kafka消费者配置里把max.poll.records从500调低到200让每次拉取的消息数量降下来防止瞬间拉取过多消息导致消费线程被打爆。还有一个长期优化是数据库写入做了批量合并。弹幕消息在Kafka里是逐条消费的我在消费者里攒够100条或者每100毫秒批量写一次库写入性能提升了好几倍数据库的压力也大幅下降。6.6 直播流量高峰期的固定动作最后分享一个我高峰期前必做的固定动作。每次收到头部主播的开播预热通知我会提前一小时做这些事情检查各节点的文件描述符和连接数确认没有逼近上限扩容网关节点提前把流量分散到更多实例上检查Kafka的磁盘剩余空间确保消息堆积时有足够的缓冲区域检查CDN回源带宽如果需要回源限速提前配好执行一次全链路压测演练用低于预期30%的流量确认所有环节正常。这套动作看着简单但能把大部分风险消灭在流量进来之前。直播高并发这种事情永远是“防守大于进攻”等到线上出了问题再补救损失的是用户口碑和主播信任。另外再提一个关于部署的经验。我建议所有配置项都做成可动态调整的比如直播间人数上限、弹幕频率限制、礼物并发阈值不要写死在代码里。直播运营会根据活动需要随时调整这些参数如果每次调整都要重新发版不仅效率低还容易出错。我用的方式是放在配置中心改完配置实时生效运维和运营各拿各的权限谁也不会干扰谁。这套环境从搭到跑已经稳定运行了几个月经历了多次万人直播间的考验。我个人的体会是直播高并发并没有想象中那么玄乎关键是把每一个环节都算明白、压到位、盯仔细。如果你也正在从0搭建直播后端希望这篇笔记能帮你少走一些弯路。等技术沉淀得更成熟之后我还会写一篇关于转码服务、弹幕回放和成本优化的笔记继续把这些实战经验分享出来。
返回列表