直播高并发环境从零搭建:Nginx、Redis、Spring Boot、WebSocket实战 📅 发布时间:2026/9/19 16:50:10 👁 浏览次数: 标题里写“手搓”其实是真“手搓”。我用了整整三个星期从一个只会写增删改查接口的后端小白到把一整套直播高并发环境在本地和云服务器上跑通中间经历了几十次配置失败、压测被打爆和日志翻到眼花的深夜。这篇笔记就是记录这套环境从 0 到 1 的完整搭建过程涉及 Nginx、Redis、Spring Boot、WebSocket、JMeter 压测这些技术点。如果你也想学高并发但又被各种分布式、微服务概念劝退这篇文章大概率能帮你节省几个星期的摸索时间。我也会把搭建过程中踩过的坑、调参的思路、压测时看到的真实现象都写出来尽量让每一步都像现场回放一样清楚。1. 为什么选直播场景练高并发目标和路线怎么定1.1 直播业务的高并发特征和普通 Web 并发根本不是一回事我最早以为“高并发”就是一个接口被访问很多次后来真正接触直播业务才发现完全不是这样。普通 Web 系统的并发大多数是一次请求一次响应请求结束后连接基本就释放了但直播场景里用户进入直播间之后会长时间挂在里面前后端之间保持长连接随时等待弹幕、礼物、状态变化这些实时消息推送。这就带来一个核心问题系统的压力不是来自“请求有多快”而是来自“连接能维持多久、同时能挂多少连接”。换句话说直播高并发环境里你会同时面对连接数、吞吐量、实时性三个维度的压力任何一个维度不行体验立刻崩。我后来跟朋友开玩笑说给普通接口做并发优化像是在练短跑给直播场景做并发优化像是在练马拉松前者冲刺一次就完了后者要一直保持状态。1.2 一个后端小白最初踩的三个坑第一个坑是盲目上分布式。我当时看别人说“高并发必须上微服务、必须上 K8s”于是花两周折腾了一堆组件最后本地起都起不来。后来我才想明白高并发的核心是把每一层的瓶颈吃透而不是靠组件数量堆安全感。先把单机跑炸再谈扩展这个顺序不能反过来。第二个坑是把数据库扛在主力上。最开始写弹幕功能我直接往 MySQL 里插记录然后让前端轮询接口。并发量一上来一个热门房间几百人在轮流刷数据库连接池立刻被打满接口平均响应时间从 30ms 涨到 3 秒多。后来把实时数据全部改到 Redis 和 WebSocket 推送MySQL 只负责落历史数据问题直接缓解。第三个坑是想当然地配置并发参数。Nginx 的 worker_connections、Tomcat 的 maxThreads、Redis 的连接池上限这些数字不是拍脑袋定的而我一开始根本没去想它们之间的约束关系直到用 JMeter 压测才意识到连接数瓶颈到底卡在哪一层。1.3 这套环境的最终目标和验收标准说到最终目标我想清楚了一点搭建这套直播高并发环境不是为了做出一个能上线的商业产品而是为了把自己置身于一个“高并发问题会真实爆发”的环境里去感受瓶颈是怎么出现的、资源配置错误会引发什么现象以及调优之后指标有什么变化。所以我的验收标准很简单先用 JMeter 把 500 并发打上去看接口 RT 和错误率再优化到 2000 并发还能稳定运行。这里也得说清楚我并没有把压力一下子拉到几十万在线。对个人学习来说一台 8 核 16G 的服务器搭好这套环境在合理配置下跑个两三千并发练手已经完全够用了。真到了上万并发的规模更多是集群、网关、资源调度的整体设计问题这个后面单独展开说。2. 技术选型与架构设计先把地基打对2.1 一套适合小白的直播高并发技术栈我在搭建这套环境时技术栈定得非常克制没有搞花活前端用 Vue3 加 WebSocket 客户端负责展示观众列表、弹幕流和在线人数Web 服务用 Spring Boot 2.x提供 REST API 和 WebSocket 推送负载和直播接入用 Nginx配合 nginx-rtmp-module 做直播推拉流Redis 管在线状态、弹幕热数据、限流计数MySQL 只存直播记录、用户资料这类低频写数据。这套组合的好处是每个组件都是各自领域的“主力”学习资料多出了问题也好查。而且对高并发学习来讲透明度和可观测性都很重要组件越复杂越难定位问题。我当时的原则是只在确实需要的位置引入复杂度绝不为“看起来高级”买单。2.2 为什么直播画面走 Nginx-RTMP弹幕走 WebSocket先说直播流。主流的直播拉流方案有 RTMP、HLS、HTTP-FLV。RTMP 延迟低但默认走 TCP 的 1935 端口浏览器原生不支持HLS 兼容性好但切片会产生几秒到十几秒延迟弹幕对不上画面HTTP-FLV 是折中方案延迟能控制在 1 到 3 秒浏览器可以通过 flv.js 播放。为了练手我推流端用了 OBS以 RTMP 协议推到 Nginx拉流端通过 HTTP-FLV 分发这样既避开了浏览器不能直接播 RTMP 的问题又把延迟压了下来。再说弹幕弹幕这类高频小消息用 HTTP 轮询非常浪费因为大部分轮询请求其实拿不到新消息却仍然占用连接和线程。我改成 WebSocket 后服务端只需在事件产生时通过长连接把消息推到客户端连接数从“每个用户每几秒一个请求”变成“每个用户一个长连接”网络开销下降了不止一个数量级。这两种方式一个管视频流、一个管实时消息各干各的活互不干扰。2.3 Redis 在这里扮演的角色远不止缓存Redis 表面上是缓存实际上在这个环境里管了四件大事。第一在线状态用 Hash 结构存每个直播间的在线用户 ID 和最后心跳时间心跳过期后由定时任务清理这样在线人数是实时算出来的而不是靠数据库 count。第二弹幕热区用 List 存最近 N 条弹幕新用户进入直播间时直接把最近几十条推给他避免一进来就冷场。第三限流计数用 INCR 加 EXPIRE 做固定窗口计数防止某个房间被恶意刷屏或刷请求。第四分布式锁虽然单机部署时本地锁也行但为了后面扩展到多节点我用 Redis 的 SETNX 做了简单互斥用于在线人数批量写库这类操作。为什么这些状态要放 Redis 而不是放 Java 内存因为在线人数、弹幕历史这些数据是跨连接、跨实例共享的。如果放在单台机器的内存里将来做水平扩展加一台节点两台机器各自维护自己的内存状态立刻就错乱了。把状态收敛到 Redis所有实例共享同一份数据水平扩展时不用改业务代码这个收益越到后面越明显。2.4 数据库只做“收尾”不做“并发主力”我在设计阶段就反复提醒自己别让 MySQL 出现在请求热点路径上。比如用户进直播间我只需要把“进入记录”异步写进 MySQL观众弹幕则是先写 Redis 再异步批量刷库。这个“异步落库”的过程我用了一个非常朴素的方案定时任务每 3 秒把 Redis 中攒下的弹幕批量 INSERT 到 MySQL。批量插入比单条插入效率高得多也把数据库的写压力降到了几乎没有的程度。有人可能会问为什么不直接用消息队列我当时考虑过 RabbitMQ 和 Kafka但最后还是决定先用定时任务过渡。原因是这套环境里写库量并不大一个热门直播间一天弹幕也就几十万条3 秒一批完全顶得住。真到了每秒钟几千条消息的写入量级再引入消息队列才是合理的否则只是给系统增加维护成本。技术选型永远是看量级和收益的不为了用而用。3. 从零开始搭建实操配置和踩坑记录3.1 准备一台可以随便折腾的服务器我用的是云服务器8核16G操作系统是 Ubuntu 22.04。为什么强调“可以随便折腾”因为搭建过程中肯定要改各种配置、装各种依赖甚至可能把系统环境弄坏重来。如果用本地虚拟机也行性能差一点但一样能练。域名我用的是测试域名验证 Nginx 的 Server 配置时直接改 hosts 指向即可。基础依赖按顺序执行安装apt update apt install -y build-essential git libpcre3-dev libssl-dev zlib1g-dev然后装 Nginx 和 RTMP 模块。我图省事直接编译安装了带 rtmp 模块的 Nginx因为发行版自带的 Nginx 默认不带这个模块。下载 Nginx 源码和 nginx-rtmp-module 源码后一起编译wget http://nginx.org/download/nginx-1.24.0.tar.gz git clone https://github.com/arut/nginx-rtmp-module.git tar zxvf nginx-1.24.0.tar.gz cd nginx-1.24.0 ./configure --add-module../nginx-rtmp-module make -j$(nproc) make install这里有个细节值得单独说configure 的时候一定要加上 --add-module 指向 nginx-rtmp-module 的路径否则后面配置 rtmp 指令时 Nginx 会直接报错不认识。我第一次编译就漏了这一步启动时提示 unknown directive rtmp排查了半天才发现是编译参数的问题。编译安装完成后Nginx 默认装在 /usr/local/nginx启动命令是 /usr/local/nginx/sbin/nginx注意不是系统 apt 装的 /usr/sbin/nginx路径错了会容易产生“明明装了却启动不了”的错觉。3.2 Nginx 配置直播流和 API 请求分开处理在 nginx.conf 里我同时配置了 HTTP 服务和 RTMP 服务。HTTP 部分主要做三件事托管前端静态资源、把 /api 反向代理到 Spring Boot、把 /live 拉流地址交给 HTTP-FLV 处理。RTMP 部分负责接收 OBS 的推流。关键配置片段如下worker_processes auto; events { worker_connections 65535; use epoll; } rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; # 测试阶段限制推流来源避免公网被随意推流 allow publish 127.0.0.1; allow publish 服务器内网IP; deny publish all; } } } http { upstream backend { least_conn; server 127.0.0.1:8080 max_fails3 fail_timeout30s; } server { listen 80; server_name test.local; root /data/www/live-frontend; index index.html; location /api/ { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /live/ { types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; video/x-flv flv; } alias /data/hls/; } } }worker_processes 设成 autoworker_connections 设成 65535这是当时踩了连接数瓶颈的坑之后改的。use epoll 在高并发长连接场景下比默认的 select 高效得多这个后面压测时会再讲。RTMP 部分我限制了 publish 来源只允许本机和内网 IP 推流不然公网上任何人都能往你的服务器推流很快就会被拿流量刷爆带宽。这里再补充一个经验不要把 RTMP 的 chunk_size 调得过大4096 是兼顾延迟和稳定性的常用值太大会增加首帧延迟太小会放大 CPU 开销。3.3 Spring Boot 侧线程池、连接池、WebSocket 一个都不能少Spring Boot 默认内嵌 Tomcat默认最大线程数 200默认最大连接数 8192。这两个默认值看起来不小但在长连接场景下很快就会暴露问题。我在 application.yml 里把线程池调大了server: port: 8080 tomcat: threads: max: 500 min-spare: 50 accept-count: 1000 max-connections: 10000连接池这块我引入的是 JedisPool 连接 Redis最大连接数设置成 200并且打开了空闲连接检测避免连接堆积后大量失效。数据库连接池用的是 HikariCPmaximum-pool-size 设成 80minimum-idle 设成 10。WebSocket 接入用了 Spring 自带的 WebSocketHandler直播间推送消息的核心结构是“频道加订阅者”每个直播间一个 channel客户端订阅后服务端把消息广播给该 channel 下的所有会话。为了避免广播时把所有人循环遍历一遍我维护了一个 ConcurrentHashMap 存储 session 集合每来一条弹幕就直接遍历这个集合发送。直播间数量多了以后可以再做拆片但当前这个规模下结构完全够用而且逻辑很清晰。3.4 Redis 缓存设计并发场景下的三个防护姿势既然是做高并发环境Redis 就不能只是“存数据”这么简单。在线状态和弹幕的读写频率非常高我花了很大精力在缓存设计上。先说缓存穿透如果查询一个不存在的直播间 ID每次都直接打到 MySQL几百个恶意请求就能把数据库拖垮。我处理的办法是缓存空值对不存在的 ID 也缓存一个空结果过期时间短一点比如 30 秒同时在入口处做参数校验。高并发场景下入口校验一定要做不只是安全问题更是性能问题。第二是缓存击穿某个热点直播间的信息缓存过期瞬间大量请求同时打到数据库。我用的办法是对热点数据做互斥重建也就是缓存过期后只有一个线程去查库并回填缓存其他线程短暂等待后直接拿缓存里的结果。第三是缓存雪崩大量 key 同时失效会导致瞬时数据库压力飙升。我处理时给缓存时间加了随机偏移量比如基础过期时间一分半再加 0 到 30 秒的随机值避免同时过期。这招看起来简单但比设置统一的过期时间稳得多。3.5 前端接入与前后端交互打通前端我用了 Vue3项目结构上做了前后端分离。页面组件里我通过 fetch 调用 /api 获取直播间基础信息通过 new WebSocket 连接弹幕推送服务。这里有个注意点WebSocket 的服务端地址要和 HTTP 的域名保持一致否则浏览器会因为跨域问题直接拦截。我在开发环境用 Nginx 做了反向代理把 /ws 路径的升级请求也代理到后端保证了同源策略下的 WebSocket 连接是通的。这个细节非常关键很多人本地联调时“CORS 问题查了半天”就是因为没有走同源代理。前端部署也直接放在 Nginx 的静态目录里和后端 API 同域这样生产环境就不会有跨域烦恼也便于后续做 CDN 加速。4. JMeter 压测实录把高并发问题一个接一个打出来4.1 压测前必须想清楚的三件事压测不是拿工具乱点一通我是按三步来的。先定指标关注三个数——接口平均响应时间、错误率、吞吐量 TPS。当时我给自己定的目标是2000 并发以内接口 RT 小于 500ms错误率小于 0.1%。再定场景分别压静态资源、REST API、WebSocket 长连接以及完整的直播拉流场景。静态资源和 API 的并发特征完全不同分开测才能定位瓶颈。最后定策略用阶梯上升的方式从 100 并发开始每轮增加 100 或 200每轮持续 5 分钟观察系统在不同压力下的表现而不是一上来就扔 5000 并发把服务器打死。JMeter 方面我准备了两个脚本一个用 HTTP Request 压 REST API另一个用 WebSocket Sampler 压弹幕连接。线程数逐步增加配合聚合报告查看 RT、错误率和吞吐量。这里要提醒一句启动 JMeter 的机器和被测服务器最好不在同一台机器上否则 JMeter 本身也会吃掉资源结果失真。我当时用另一台 4 核 8G 的机器专门跑压测数据才相对干净。4.2 第一轮压测Nginx 的 worker_connections 直接被打穿第一轮我先压静态页面和 API当线程数加到 1024 左右时大量请求开始报 499 错误错误率直接飙到 15%。我第一反应是 Spring Boot 扛不住了赶紧去看 Tomcat 日志结果发现 Tomcat 那边非常平静反而是 Nginx 的 error log 里全是 worker_connections are not enough 的报错。这时候才意识到问题是 Nginx 的 worker_connections 默认只有 1024。虽然我启动时有 4 个 worker 进程但每个 worker 能同时处理的连接数只有 1024一旦某个 worker 上的连接超过这个数新的连接就会被排队或直接丢弃。我把 worker_connections 改到 65535并把 use epoll 打开后499 错误立刻消失同样压力下错误率降到了 0。这个坑太典型了因为是“Nginx 扛不住”而不是“后端扛不住”只盯后端根本发现不了问题。4.3 第二轮压测Tomcat 线程池和数据库连接池一起爆修正 Nginx 之后我把并发加到 1500又出了问题接口 RT 从几百毫秒涨到 3 秒多错误率开始回升Tomcat 日志里出现 Connection is not available, request timed out。查 JMeter 聚合报告发现吞吐量卡在 800 TPS 上不去说明瓶颈转移到了后端。第一刀先看 Tomcat。默认最大线程数 200意味着同时只能有 200 个请求在处理中其余的请求全部在 accept-queue 里排队。我把 max-threads 调到 500 后吞吐量立刻从 800 涨到 1400 左右。第二刀看数据库连接池。当时 HikariCP 的 maximum-pool-size 是 20而接口里有一个查询直播间信息的操作直接打到 MySQL并发一上来连接池被占满请求只能等待连接释放。我把池子调到 80并且把“查询直播间信息”这个热点操作加入了 Redis 缓存读写数据库的次数大幅下降压制住之后 RT 才稳定下来。做完这一轮2000 并发下接口 RT 稳定在 300ms 左右。4.4 第三轮压测长连接比想象的更吃内存最后用多节点验证扩展REST API 稳定后我开压 WebSocket 弹幕长连接。这里有个很直观的感受长连接根本没有高并发请求但每个连接都要占用线程和内存。单机 8G 内存、Tomcat 500 线程的情况下压到 3000 个 WebSocket 连接时内存已经吃掉了 6G 多GC 频繁弹幕广播出现明显卡顿。这说明长连接场景下瓶颈更多在于内存和线程模型而不是 CPU。所以我把最大线程数继续调到 800并限制 WebSocket 的最大连接数超过后给出“直播间人数已满”的提示而不是无限制地接受新连接。为了验证水平扩展时这套架构能否承受更大的并发我又复刻了一台同样的节点在 Nginx 的 upstream 里加了一个 server配置为 least_conn 模式。由于在线人数、弹幕等状态全部在 Redis 里新增节点后系统状态没有冲突压测时流量能被两个节点分摊开。实测下来同样 2000 并发两节点时 RT 比单节点下降了一大截吞吐量也接近翻倍。这也是我在前文强调“状态尽量往 Redis 收敛”的原因扩展时业务代码零改动真的能省太多事。5. 直播高并发环境中的常见坑与排查思路5.1 连接数、线程数、队列深度之间的关系要先画出约束线很多新手配置高并发时喜欢“参数越大越好”实际情况并不是这样。Nginx 的 worker_connections、Tomcat 的 max-threads、操作系统的文件描述符上限、Redis 的连接池上限这些参数之间存在约束关系。比如你用 JMeter 开 5000 个连接但操作系统的 ulimit -n 只有 1024那么连接在 TCP 层就被拒绝了根本不是应用的问题。我当时调整完应用参数后还是报错最后用 ulimit -n 65535 才解决。这些参数关系可以简单理解成一条供应链客户端连接数受限于系统文件描述符总数超出的连接会被拒绝Nginx 接收的连接受限于 worker_connections 总和超出的部分排队等待转发到后端的请求受限于 Tomcat 线程池线程不够就排队线程执行时要拿 Redis 连接和数据库连接池子太小就会拿不到连接。每一层都有自己的容量上限只有把每一层都调到匹配整个链路的高并发能力才能真正发挥出来。5.2 TIME_WAIT 与连接回收压测后必须检查的“隐形杀手”压测结束之后我用 netstat 看服务器端口状态发现大量 TIME_WAIT 状态的连接。TIME_WAIT 是 TCP 连接正常关闭后为保证最后一个 ACK 能被对端收到而保留的状态默认会持续 60 秒。压测时每秒建立和断开大量连接TIME_WAIT 连接越积越多最终会占满本地端口或文件描述符导致新连接无法建立。排查方法是执行netstat -ant | awk {print $6} | sort | uniq -c | sort -rn如果 TIME_WAIT 数量特别大优先检查服务端和客户端是否都在主动关闭连接。HTTP/1.1 默认开启了 keep-alive如果前端和后端之间连接复用做得好TIME_WAIT 的数量会明显下降。当时我把 Nginx 到后端的 proxy_http_version 设为 1.1并设置了合理的 keepalive 参数情况立刻改善。注意网上有些文章建议直接调整内核参数来减少 TIME_WAIT但在我这边实测下来优先做好连接复用才是最稳的做法。关于连接复用Nginx 的 upstream 里可以加 keepaliveupstream backend { least_conn; server 127.0.0.1:8080; keepalive 32; }然后 location 里设置proxy_http_version 1.1; proxy_set_header Connection ;如果不设置 Connection 为空Nginx 默认向后端转发的是 Connection: close等价于每次转发完就断开连接长连接完全失效TIME_WAIT 会大量堆积。这个配置细节我也是排查 TIME_WAIT 时才发现的关键之前一直以为 keepalive 是浏览器的事没想到 Nginx 和后端之间的复用同样重要。5.3 WebSocket 高并发下的三个典型问题WebSocket 连接多了之后有几个问题集中出现。心跳超时导致连接被误判断开我在后端设置了 60 秒空闲超时但客户端如果长时间不发送消息连接会被服务端主动关闭。后来我把心跳机制找整齐了客户端每 30 秒发送一次 ping服务端收到后返回 pong两边都能感知到连接是否存活。第二是广播风暴热门房间同时在线几千人时一条弹幕广播给所有订阅者一个不小心就会把 Tomcat 的 worker 线程全部占住。我的处理是在 WebSocketHandler 里对发送操作做限流单房间每秒最多广播多少条消息超出部分丢弃或合并推送。直播场景下用户对弹幕的实时性有要求但对“每一条都不能丢”没有要求所以合并推送是合理的取舍。第三是连接泄漏客户端异常断开时onClose 可能不触发导致服务端 session 一直保留在内存里越积越多。我的解决方法是定时任务扫描 session 的 open 状态和最后活跃时间超过阈值就主动关闭并清理。聊天场景里连接管理的健壮性比业务逻辑更值得关注因为业务逻辑出错最多报个错连接泄漏会直接耗尽服务器资源。5.4 常见问题速查表现象可能原因排查方向解决方案Nginx 报 worker_connections 不足单 worker 连接数超上限查看 error.log调大 worker_connections开启 epoll大批 499/502 错误后端线程池打满或服务假死看 Tomcat 活跃线程、堆内存调线程池参数优化慢接口接口 RT 突然变慢数据库连接池耗尽看连接池活跃数、慢 SQL扩大池子、热点查询加缓存大量 TIME_WAIT 连接连接复用没生效netstat 统计连接状态启用 keepalive设置 Connection 为空WebSocket 频繁断开心跳或超时配置不对抓包看断连原因客户端定时 ping服务端合理设超时Redis 连接超时连接池太小或未释放看 JedisPool 活跃连接数调整池大小检查归还逻辑内存持续上涨长连接 session 未清理查看堆内存和 GC 日志定时清理无效 session限制最大连接数这张表是我在整个搭建过程中反复对着排查的总结不一定覆盖所有场景但覆盖了直播高并发环境里最容易出现的几条主线。遇到问题先别改配置先定位是哪个环节报错再对症处理。我自己的习惯是每次压测前把监控命令开好包括 nginx error.log、Tomcat 日志、Redis 的 INFO 输出出现问题第一件事不是拍脑袋改参数而是先看报错在哪个日志里再沿着请求链路往后追。6. 一点复盘与个人体会整套环境从搭建到现在已经稳定跑了快两个月。复盘下来我最庆幸的是没有一上来就追求微服务和 K8s而是先把 Nginx、Spring Boot、Redis、WebSocket 这一条主链路一点点调通。高并发这个东西说到底不是靠某个组件或者某个参数实现的而是靠对整个请求生命周期每一层的理解。对于和我一样从零开始的后端小白我的建议是这样的先在本机或一台云服务器上把单节点的连接数、线程池、连接池、缓存、长连接管理这些基础内容彻底搞明白再用压测工具把系统打出一个又一个瓶颈逐个解决。这个过程比看一百篇“高并发架构设计”的文章都值钱。等到单机已经摸透再考虑把系统迁移到 K8s 集群、接入消息队列、做服务拆分那时候每一步都有明确的目的不会再是被名词牵着走。最后再说一个值得养成的习惯每次改动配置后保留改动前后的压测数据。比如同样是 1000 并发调整 Nginx worker_connections 前后错误率从 15% 降到 0调整 WebSocket 心跳后断连次数从每分钟几十次降到 0。这些前后对比才是高并发调优中最直观的成就感来源也是下次再遇到类似问题时的最好参考。