十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

赛事平台高峰分流:四层架构设计应对瞬时高并发

赛事平台高峰分流:四层架构设计应对瞬时高并发 做赛事平台最怕的就是决赛夜。星逐赛事源码这个项目上线初期我印象最深的一次是某场关键比赛刚开赛API网关的QPS从几百直接冲到几万数据库连接数瞬间耗尽比赛直播间画面卡在加载转圈后台监控一片红。那几天我几乎住在工位上把整个调用链从接入层、业务层、数据层到流媒体层重新过了一遍最终形成了一套完整的高峰分流设计方案。下面我从实际改造的角度把每一层为什么要拆、怎么拆、源码里哪些参数必须改一次讲清楚。不管你是做赛事直播、在线教育还是电商大促只要系统会在某个时间点被流量集中冲击这套思路可以直接抄作业。1. 高峰分流设计为什么赛事源码必须拆四层来做1.1 赛事的流量特征和传统单点架构的痛点赛事和其他互联网业务最大的区别在于“可预测的瞬时流量”。普通电商有双11赛事则是每场比赛的固定时段。星逐赛事源码这个项目做的是赛事数据与直播平台平时在线人数可能只有几百但热门场次开赛前10分钟用户会像约好了一样同时进场拉取赛程、进入直播间、发表弹幕、参与竞猜后台API的QPS能在几十秒内翻几十倍。传统单点架构在这种场景下非常脆弱。我接手这个项目时请求链路是浏览器/App - 一台Nginx - 一台Spring Boot - 一台MySQL流媒体还是源站直接输出RTMP给播放器。平时没问题但高峰时出现三类典型故障第一个是连接数被打爆Nginx的worker_connections和MySQL的max_connections同时触顶服务直接拒绝新连接第二个是带宽被流媒体耗尽直播源和Web服务共用机房带宽画质一高接口请求跟着超时第三个是单条慢SQL拖垮整个库比分轮询接口每次全表扫描高峰期直接把数据库CPU打到100%。这些问题的本质不是硬件不够而是所有流量都堆在同一个出口。直播间拉流带宽、API请求、写库操作互相干扰任何一个环节出问题都会形成雪崩。要做高峰分流第一步不是加机器而是把流量按性质拆开让每个层次都只处理自己该处理的事。1.2 四层拆分的基本思路和拆层原则赛事源码里的高峰分流设计核心是把整个系统拆成四层接入层负责连接管理和协议解析业务层处理无状态逻辑数据层管有状态存储流媒体层单独走一条带宽密集型链路。这个拆法不是随意定的而是按流量性质来的Web请求是短时、高频、计算型流媒体是长连接、大带宽、传输型数据读写则是对一致性和持久性最敏感的部分。把它们放在同一批机器上必然互相拖累。拆层时有几条原则是我们在踩坑后总结出来的每层只做一件核心事。接入层不做业务计算业务层不直接操作数据库数据层不承担转发任务流媒体层不掺和API请求。层与层之间的接口必须设超时和熔断。某层出问题不能无限等待否则会把故障传染到上游。有状态的东西一律下沉。Session、在线状态、比分快照都放到数据层或缓存保证业务节点可以随时扩容缩容。流媒体和业务流量在物理网络层面也要隔离。条件允许的话用不同的网卡、不同的交换机至少用不同的端口和域名。拆完之后高峰期扩容就变得非常轻松接入层扛不住就加Nginx节点业务层扛不住就水平扩展无状态服务数据层扛不住就做读写分离和分片流媒体层扛不住就扩CDN边缘节点。下面我按这条链路逐层展开。2. 接入层入口流量的第一道分流闸门2.1 为什么优先用HTTPDNS而不是裸DNS做区域分流接入层最早遇到的问题是DNS解析混乱。星逐赛事源码初期只有一个域名各地用户解析到的都是同一个源站IP华南和华北的请求全打到一个机房跨地域延迟高不说某个机房故障时也没办法快速切流量。后来改成了“智能DNS HTTPDNS”双方案。智能DNS的作用是按用户来源IP和运营商线路把域名解析到不同区域的接入集群。比如电信用户解析到电信机房的VIP联通用户解析到联通机房的VIP这样运营商之间的跨网延迟能降一大半。但裸DNS有缓存污染和生效慢的问题尤其移动端App运营商LocalDNS经常缓存错误结果。所以App端直接集成HTTPDNS SDK通过HTTP接口拿到最优IP列表绕过本地DNS缓存。这里有个需要注意的点HTTPDNS下发IP后为了避免单个接入集群过载要在SDK侧做加权随机或轮询而且必须带健康检查。我见过一个项目HTTPDNS下发两个机房IP客户端概率均分结果其中一条链路出现丢包用户刷不出来但流量还是源源不断进来排查了很久才发现是客户端没有感知到故障。星逐赛事源码里App端做了一套简化逻辑ListIpNode ipList httpDnsService.getIpList(api.star-event.com); ListIpNode healthyList ipList.stream() .filter(ip - healthChecker.isHealthy(ip)) .collect(Collectors.toList()); String targetIp loadBalancer.select(healthyList); // 加权轮询裸DNS和HTTPDNS的区别可以看这张表对比项裸DNSHTTPDNS解析精度受运营商LocalDNS影响大按客户端地域和IP精确调度生效速度受TTL和缓存影响实时下发秒级生效故障切换依赖DNS提供商与TTL刷新客户端主动探活可以秒切适用对象PC Web、简单场景App端、对延迟敏感场景2.2 Nginx层如何做连接分流和请求分流接入层进入机房后先经过四层负载均衡LVS/DPVS或云上SLB再进到七层Nginx集群。四层做的是连接级分流把TCP连接均匀分配到多个Nginx节点解决单机连接数上限问题七层做请求级分流按URL前缀、Header参数把请求分到不同后端服务组。这里最关键的是不要所有请求都走一遍应用层。静态资源、直播分片、API可以拆成多个域名和Nginx location互不影响。星逐赛事源码的Nginx配置里有几个参数非常重要worker_processes auto; worker_rlimit_nofile 655350; events { use epoll; worker_connections 65535; multi_accept on; } http { keepalive_timeout 30; keepalive_requests 1000; upstream api_cluster { least_conn; server 10.0.1.11:8080 max_fails3 fail_timeout5s; server 10.0.1.12:8080 max_fails3 fail_timeout5s; } server { listen 80; location /api/ { proxy_pass http://api_cluster; proxy_http_version 1.1; proxy_set_header Connection ; proxy_connect_timeout 2s; proxy_read_timeout 5s; } } }比较容易被忽略的是worker_connections和系统文件句柄数需要一起调。只改Nginx配置不改ulimitworker_connections设得再大也会受限。还有proxy_set_header Connection 这行它的意思是把后端连接升级成HTTP keep-alive可以大幅减少Nginx到后端服务的重复建连开销在高峰期能省下不少CPU。2.3 TCP层源码级调优参数接入层的连接数问题很多Linux内核参数都直接决定成败。某次压测时我们发现Nginx单机连接数到了2.5万就不涨了CPU和内存都没满查系统日志才看到accept队列溢出。原因是net.core.somaxconn太小导致三次握手完成后的连接进不了accept队列。赛事场景里大量是App客户端的长轮询和直播播放器的长连接所以TCP参数要按“高并发建连、保持长连接”去调。下面是我在一台8核16G的接入节点上使用的核心参数net.core.somaxconn 65535 net.core.netdev_max_backlog 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_fin_timeout 15 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_max_tw_buckets 65536这里要特别提醒tcp_tw_reuse只对客户端出站连接生效服务器上不能靠它解决TIME_WAIT堆积。服务端想快速复用端口需要开tcp_tw_recycle但这个参数在NAT环境下会产生严重问题坑过很多人。更稳妥的做法是让客户端主动断连接或者把短连接改成长连接保活减少连接创建频率。接入层这块的经验就是连接是成本能复用就复用不要为每次请求都重新建TCP连接。3. 业务层无状态化与动态负载均衡3.1 先把服务改成无状态再谈扩容星逐赛事源码原来的业务层是典型的有状态服务用户登录态存在本地Session里弹幕在线人数存在进程内Map里竞猜下注状态也有一部分放在内存。这种架构在单机时很顺但高峰想要加节点负载均衡把请求分到新机器新机器上没有用户Session用户就被踢下线体验非常差。改造的核心原则就一句话所有跨请求的数据都移到外部存储。登录态用JWT或Redis Session房间在线状态放到Redis的Hash结构比分快照放到Redis的String业务进程只保留可以随时丢弃的本地缓存。改造完成后新加一台业务节点只需要把代码发布上去注册到服务发现中心流量会自动打过去不需要人工处理任何迁移。无状态化有个附带的好处可以对不同节点做差异化流量分配。比如把低配机器权重调低把高配机器权重调高利用Nginx或负载均衡的weight参数完成。星逐赛事源码在业务高峰期一般是直接扩容一倍节点配合弹性伸缩策略节点启动到接入流量大概只需要40秒。如果是有状态服务这个过程至少要小时级起步。3.2 服务发现和按房间维度的一致性哈希路由业务层拆分后服务节点数量会动态变化这时候需要服务注册发现。我们用的是Nacos也可以用Consul、etcd或者K8s原生Service关键不在于选哪套中间件而在于负载均衡策略要按业务场景设计。常规的round-robin适合普通API但赛事场景里弹幕、竞猜这类写操作有极强的房间维度热点。同一个直播间里的用户可能同时刷几万条弹幕如果这些请求被轮询到不同的业务节点每个节点都要去抢同一个房间的锁性能会非常差。更合理的路由方式是按roomId做一致性哈希同一个房间的写请求尽量落到同一批节点上这样本地缓存命中率高锁竞争也小。星逐赛事源码里的路由逻辑类似这样// 一致性哈希虚拟节点设为160个 ConsistentHashServiceNode hashRing new ConsistentHash(serviceNodes, 160); ServiceNode node hashRing.get(String.valueOf(roomId));虚拟节点的数量很关键。节点太少会导致哈希倾斜几个热门房间全落在同一台机器上。设置到160到200个虚拟节点跑下来效果比较好。但一致性哈希也有限制当节点数量变化时会有部分请求重新路由高可用还是要靠下游幂等和重试兜底不能只依赖路由策略。3.3 限流熔断降级高峰期的三条安全线赛事平台的业务接口不是每个都需要在高峰期保证同等质量。我的做法是把接口按核心程度分级一级是直播流地址获取、实时比分、登录鉴权属于不可降级二级是弹幕发送、竞猜下注要求可用但可以限速三级是礼物榜单、历史赛程、数据统计高峰期可以直接关闭或降级为本地缓存。限流用Sentinel或直接基于Guava的RateLimiter都可以更推荐在网关层做集中限流这样不用在每个服务里重复写代码。星逐赛事源码里按房间维度做了一个令牌桶限制// 每个房间每秒最多处理5000条写请求突发可以到8000 RateLimiter limiter RateLimiter.create(5000.0); if (!limiter.tryAcquire()) { // 降级直接丢弃非关键消息记录日志 metrics.recordDropped(barrage_ roomId); return Result.degraded(); }熔断方面业务层调用数据层时全部设置了快速失败机制。比如查询竞猜列表Redis超时时间设为300ms超过就直接返回一个默认空结果不让请求继续打到MySQL。这样做会牺牲一点数据新鲜度但能保证主流程不挂。降级开关要做到配置中心里改配置秒级生效不要用代码叠if判断否则每次都要发版。真正到决赛夜那种量级现场改代码是不可能的必须提前把所有降级预案都配置好。4. 数据层读写分离、分库分表和缓存兜底4.1 先用监控数据决定读写分离策略数据层改造不能拍脑袋。星逐赛事源码初期DBA给了我们一份高峰期的MySQL status快照直观看到了问题的来源Com_select每秒超过8万Com_insert每秒只有不到3000QPS高得吓人但写入其实不多。这说明大量SELECT请求是重复地在查同一批比分和房间数据完全没有必要直接打MySQL。所以我们先做了读写分离主库只承担写入和事务性查询从库扛读流量。中间用ProxySQL做读写分离和连接池管理业务层只配一个数据源地址SQL自动路由mysql -h 127.0.0.1 -P 6033 -u service -pProxySQL的查询规则大概长这样INSERT INTO mysql_query_rules (rule_id, active, match_digest, destination_hostgroup, apply) VALUES (10, 1, ^SELECT.*, 1, 1);应用层所有SELECT都指向从库组INSERT、UPDATE、DELETE走主库组。赛事场景还有个特殊地方比分数据短期内有极高读频率但允许秒级延迟所以从库同步延迟设成2秒都不会有用户感知。为了降低延迟我们把从库也放在同一内网MySQL半同步复制开启后主从延迟在正常情况下低于100ms。4.2 分库分表规则这样定热点账号才不会打爆单库读写分离只解决了读压力房间、用户这类核心表的写入请求依然集中在同一个库。随着在线用户和竞猜活动增多必须做分库分表。我们的分片方案很简单但有效按用户ID哈希分16个库解决用户登录、订单、竞猜记录等读写均匀问题按比赛日期做月表分区历史数据自动归档热门直播间单独建“热点房间表”不走普通分片路由。这里最容易踩坑的是热点账号。大明星开直播的时候一个房间的在线人数可能超过普通房间总和的百倍按哈希分片会把房间数据打散到某个库上这个库瞬间成为热点。星逐赛事源码的解决方法是在Redis里维护一个hot_room列表当房间热度超过阈值就把这个房间的运营数据和比分快照全部放缓存写操作异步落库数据库层面只保留最终状态不实时更新计数器。分库分表后跨库查询会非常麻烦所以业务层从一开始就要约束查询维度能带userId或roomId的查询必须带不允许出现全库扫描的报表SQL。统计分析需求全部走离线数仓用DataX定时把数据库数据同步到分析集群线上库不承担复杂聚合。4.3 Redis缓存如何防穿透、防击穿、防雪崩数据层的缓存是保命手段。星逐赛事源码的缓存策略是Cache Aside Pattern读的时候先查Redis命中直接返回没命中查MySQL再回填写的时候先更新MySQL然后删除Redis缓存。之所以删除而不是更新是因为更新缓存遇到并发写会产生脏数据删除后让下一次读请求重新构建简单可靠。高峰期最怕三类缓存问题穿透查询一个不存在的roomId每次都不命中缓存直接打到数据库。解法是缓存空值并设置短过期时间或者在服务启动时加载布隆过滤器把所有合法房间ID放进过滤器拦截非法查询。击穿某个热点key的缓存刚好过期一瞬间上万人同时来查全部落到MySQL。解法是互斥锁只让一个请求回源其他请求等待或者热点key设置永不过期。雪崩大量key在同一时间过期数据库压力瞬间暴涨。解法是过期时间加随机扰动不要让key集体失效。互斥锁的简单实现可以参考String cacheValue redis.get(key); if (cacheValue ! null) { return cacheValue; } String lockKey lock: key; if (redis.setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS)) { try { cacheValue loadFromDb(key); redis.setex(key, expireRandom(), cacheValue); return cacheValue; } finally { redis.delete(lockKey); } } else { // 等待100ms后重试 Thread.sleep(100); return getWithLock(key); }Redis本身的高可用我们用的是哨兵模式一主两从加三个哨兵。写操作走主节点读操作从从节点读。赛事场景对缓存一致性不是强要求所以读写分离后偶尔读到旧值问题不大。如果业务对一致性有要求就必须牺牲性能走主节点读或引入版本号校验这一点要根据项目承受能力取舍。5. 流媒体层赛事直播的带宽与延迟博弈5.1 转码和转发分开直播链路才不会互相拖累流媒体层是整个赛事平台最容易“藏雷”的地方。普通API接口挂掉用户刷新一下可能就恢复了直播卡顿用户会直接流失。星逐赛事源码的直播链路一开始是摄像头推流 - 源站服务器直接转发RTMP - 播放器拉流所有转码工作都堆在源站导致一场比赛把源站CPU打到90%其他比赛也跟着卡。后来我们把链路改成“接入-转码-切片-分发”四段分离# 接入RTMP/WebRTC推到源站 10.0.6.10:1935 # 转码根据网络情况输出多码率 ffmpeg -i rtmp://10.0.6.10/live/main_1080p \ -c:v h264 -b:v 4M -s 1920x1080 -r 25 -preset veryfast \ -c:a aac -b:a 128k -f flv rtmp://10.0.6.20/transcode/hd \ -c:v h264 -b:v 2M -s 1280x720 -r 25 -preset veryfast \ -c:a aac -b:a 96k -f flv rtmp://10.0.6.20/transcode/sd转码节点只做转码不负责对外拉流。转码后的流要么继续推到分发节点切成HLS分片要么直接在转码节点混合成多码率流再交给CDN回源。这样某一个环节出问题其他比赛不会全挂。切片的编码参数也很讲究。HLS切片时长在2到6秒之间切片太短会导致CDN回源请求量大太长会导致直播延迟高。我们线上用4秒切片播放延迟控制在8到10秒左右。如果对延迟要求更高的比赛会考虑LL-HLS或者WebRTC低延迟链路但这两者对CDN和播放器都有额外要求不是默认部署。5.2 CDN边缘节点、上行推流与下行拉流怎么分赛事直播有一个容易忽略的流量分布问题推流只有一路或几路拉流却有成千上万路。如果把推流和拉流走同一个入口源站上行带宽会被拉流请求耗尽所以架构上必须把上行和下行分开上行推流主播通过RTMP/WebRTC推流到最近的城市级接入节点优先走内部专线保证低延迟和稳定。下行拉流播放器从CDN节点拉HLS或FLVCDN回源时只回源到分发层不会直接打到转码服务器。CDN调度一般由云厂商GSLB完成但我们要做好两件事一是播放地址签名防止别人盗用直播流同时方便按房间维度限流二是边缘节点缓存策略。HLS的m3u8文件是动态列表不能缓存太久但ts分片可以缓存很长时间所以要让CDN对.ts文件强制缓存对.m3u8文件设置短TTL否则高峰期边缘节点每个切片都回源源站带宽再大也会被打穿。星逐赛事源码里生成播放地址的逻辑类似// 生成带鉴权参数的播放地址 const expire Date.now() / 1000 3600; // 1小时有效 const sign md5(${roomId}-${expire}-${secret}); const url https://play.star-event.com/live/${roomId}.m3u8?expire${expire}sign${sign};5.3 流媒体服务器源码级优化要点如果直接用开源流媒体服务器比如SRS或ZLMediaKit有几个参数值得研究。SRS的listen队列、gop_cache开关、TCP收发缓冲区直接影响并发能力和首开延迟。gop_cache的意思是缓存最近一组GOP新用户加入时能瞬间看到画面不用等关键帧代价是内存和带宽占用增加。在高并发场景下我一般会把这个开关单独做成配置热门房间开冷门房间关# SRS config http_server { enabled on; dir ./objs/nginx/html; gop_cache on; }ZLMediaKit在源码级别也有一些可以调的空间比如修改config.ini里的maxStreamWaitMS、keepaliveTimeout以及编译时调整tcp的sendBufferSize。如果遇到大并发拉流TCP吞吐上不去可以从内核层面改echo 16777216 /proc/sys/net/core/wmem_max echo 16777216 /proc/sys/net/core/rmem_max还有一个很实用的经验不要把HLS切片写进普通磁盘最好用tmpfs或SSD并且及时清理过期分片。赛事高峰期一场比赛4秒一个切片一小时就是900个文件24小时连续直播会产生大量小文件普通磁盘inode不够会直接导致写盘失败流直播瞬间中断。这个坑我们当时在压测里差点没发现建议所有做流媒体的人提前检查inode配额。6. 实战一次高峰期全链路压测复盘与排障经验6.1 压测场景怎么设计才有参考价值很多人压测就是拿一个工具猛打接口看极限QPS这样做出来的数字其实没有意义。星逐赛事源码做高峰压测时我们把场景设计成“模拟一场关键比赛前15分钟的状态”100万用户通过App同时进入直播间其中80万用户在等待过程中轮询实时比分20万用户进入直播间并弹幕互动同时有3场普通比赛在并行直播。混合场景比单点压测能暴露更多问题。压测工具上API接口用JMeter和wrk流媒体拉流用自研的模拟播放器脚本直接从CDN外层打模拟真实用户的拉流路径。每轮压测我们都分四个阶段推进先压接入层单节点找到单台Nginx的吞吐上限再压业务层看无状态服务的CPU和GC接着压数据层观察主从延迟和慢SQL最后串起来做全链路。每个阶段都记录峰值流量、错误率、P99延迟三项核心指标。6.2 压测中真实遇到的瓶颈和调整方案第一轮全链路压测结果非常难看API成功率刚开始有99.9%10分钟后一路跌到85%。排查过程大概是这样的先是接入层Nginx错误日志没有异常但TCP连接数在缓慢上涨同时TIME_WAIT数量从几千涨到十万。原因是我们压测工具每秒钟都会新建立大量短连接系统默认参数无法处理这么多TIME_WAIT随后调整了net.ipv4.tcp_fin_timeout和端口范围并把客户端压测脚本改成连接复用问题马上缓解。接着是业务层日志里出现大量Redis超时。查下去发现是某个统计接口每次请求都要用KEYS命令扫描缓存高峰期CPU被打满。KEYS命令在Redis里有名的不适合生产环境改成记录房间ID到ZSET用ZRANGEBYSCORE取范围CPU瞬间降下来。这里也提醒大家压测不只是压出容量上限更是压出那些平时看不出来的慢操作。最后是流媒体层CDN回源率突然飙升到40%。原因是边缘节点对m3u8列表和ts切片都设置了60秒缓存但m3u8每4秒更新一次导致60秒内每次拉流都会回源拿新列表同时ts分片因为URL里带时间戳参数缓存Key不固定根本没有命中。调整后m3u8缓存设成2秒ts切片缓存设成1小时URL签名里只对路径做签名、不改变ts文件名回源率从40%降到2%以内。6.3 常见问题速查表把这次压测和线上运行抠出来的问题整理成一张速查表后续每次大促前我们都会拿它过一遍现象可能原因排查命令/工具解决方案连接数到某个数值后不再上涨内核accept队列溢出dmesg、ss -lnt调大net.core.somaxconn后端服务也同步调大listen backlogTIME_WAIT堆积短连接过多netstat -ant客户端连接复用、调整tcp_fin_timeout服务端慎用tcp_tw_recycle接口超时集中在Redis使用KEYS等慢命令redis-cli --latency、slowlog改用SCAN或换成按索引查询超时时间设置合理数据库CPU打满但TPS不高慢SQL没有走索引show processlist、慢查询日志增加联合索引拆分大查询热点查询走RedisCDN回源率飙升动态文件缓存Key碎片化CDN日志、回源统计m3u8短TTL、ts长缓存统一文件命名规则直播卡顿但源站CPU不高下行拉流把带宽耗尽iftop、zabbix带宽监控上下行分离、限制单路拉流带宽、扩容边缘节点分片写盘失败磁盘inode耗尽df -i定时清理过期分片、换SSD、使用tmpfs排查问题要有顺序我的习惯是从入口往出口走先看接入层连接是否正常再看业务层日志接着看Redis和数据库最后再看流媒体回源和带宽。只要每一层都有监控面板和错误码高峰期的定位时间能从小时级压缩到分钟级。最后分享一点个人经验全链路分流设计的红利并不只在高峰期体现。它会让系统平时更稳、更容易扩展、故障半径更小。星逐赛事源码这套方案做完之后后面再上新玩法比如竞猜、聊天室、多视角直播都只是往对应层加模块而已。不要等到线上炸了才想起来搞架构先把四层拆清楚比什么优化都管用。
返回列表