直播高并发架构实战:从连接池崩溃到5000+QPS的调优全记录 开播前十分钟数据库连接池先炸了。凌晨一点我蹲在工位上盯着监控大屏实时QPS冲到三千多MySQL连接池被打满WebSocket连接一断一片弹幕没人收到连进房接口都开始超时。那是我第一次以主力后端身份碰直播项目峰值在线刚过两万人高并发的第一个“高”字就这样给了我一个结实的下马威。那次之后我把自己关在房间里补了两周课从架构选型、环境搭建、缓存设计到消息队列最后用一台4C8G的服务器把整套直播互动环境从0到1手搓了出来再用JMeter做了完整的压测验证。这篇文章就是我那两周的完整记录。我尽量不写虚的直接讲我踩过的坑、我选过的型、我最后跑出来的数据。如果你是后端小白正准备接触高并发或者正在搭第一个直播、IM类项目这篇应该能给你省下不少时间。1. 直播高并发到底高在哪四个链路拆解很多人一提高并发就想加服务器、上Kubernetes但动手之前得先搞明白一件事直播场景的流量模型跟普通网站的高并发完全不一样。普通网站的高并发是“访问量大”但请求之间相对独立用户点击一下页面后端返回HTML或者JSON一次请求就结束了。直播不同直播间是一个强实时、强互动的空间所有在线用户几乎同时在刷弹幕、点赞、送礼消息还要广播给房间里的所有人。更难受的是流量不是均匀铺开的主播说一句“上链接”、抽个奖、唱首歌瞬时流量就能涨好几倍来得猛、去得也快。我把直播后端的压力拆成四条链路后面所有的架构设计、缓存策略、接口优化都是围绕这几条链路展开的。1.1 四个核心链路进房、看播、互动、交易进房链路用户点进直播间后端要查房间信息、主播信息、在线人数、公告、禁言状态、房管列表。看似简单但上百万人同时点进同一个直播间时这就是一场读请求的洪峰。这个链路最怕缓存穿透一旦有人恶意用不存在的房间号刷请求DB直接被打穿。看播链路视频流本身走CDN不归后端管但用户进房后要维持一条WebSocket长连接用于收发弹幕、点赞、礼物、房间状态变更。峰值两万在线至少维持两万条长连接加上Nginx反向代理的连接后端要能扛住海量Socket。互动链路弹幕、点赞、礼物、关注这些都是写操作而且一条弹幕要广播给全房间所有人。点赞更是极端场景一千个人在十秒内连续点几万次不可能每条都落库。交易链路送礼、打赏这类涉及余额、订单的操作一致性要求最高不能和弹幕一个处理逻辑需要独立的队列削峰、异步落库、幂等处理。四条链路压力方向不同对后端的挑战也不同。表格整理如下链路并发特征后端最大压力点进房高读、热点集中房间信息缓存、避免缓存击穿看播长连接数量巨大WebSocket集群、Nginx连接数互动读写混合、广播扩散弹幕广播、点赞削峰交易写可靠性要求高并发扣减、幂等、异步落库1.2 为什么单机能跑通一上线就挂很多人刚做完一个单体项目的时候都有个错觉本地跑得好好的接口秒开数据库也没压力那是不是直接部署就行了我这次就是被这个错觉坑了。本地开发时同时在线就你自己一个人Tomcat默认线程池200线程绰绰有余Redis连接池哪怕默认只有几个连接也完全够用MySQL一条SQL查几百毫秒也无所谓因为请求量上不去根本暴露不了瓶颈。但一旦真实流量进来连接一多所有问题同时爆发Tomcat线程被慢查询占满新的请求全部阻塞数据库连接池被耗尽Redis连接排队超时WebSocket因为超时大规模断开。整个服务就像一条小路突然涌进十万人谁也别想过去。理解了这一点再说高并发才有意义。所谓的“搭建高并发环境”本质上就是把压力分摊到各个组件让每一层都只处理自己擅长的事Nginx负责连接接入和流量控制Redis负责扛热点读和计数消息队列负责削峰和异步解耦MySQL只做最终落库。我的整套搭建思路都是围绕这句话展开的。2. 组件选型为什么我选了 Spring Boot Redis Kafka Nginx 这套组合选型这一步我被问得最多为什么用Kafka不用RabbitMQ为什么网关不用Spring Cloud Gateway为什么不用Netty答案其实很简单我是在手搓一套能让小白复现的高并发环境不是在大厂搭生产级微服务架构。同样能用不是选型的标准遇到问题能不能快速定位、资料多不多、社区坑多不多才是。2.1 选型前先定原则我先给自己定了三条原则后面所有组件都是按这个原则选的开源免费搭建成本低一个人用Docker能在半小时内跑起来生态成熟遇到问题能搜到大量案例和技术文档足够覆盖直播场景的真实压力点不要为了炫技引入用不上的组件。有了这三条选型范围一下就小了网关层先上Nginx不走微服务网关应用层用最熟悉的Java Spring Boot不碰分布式RPC缓存用Redis消息队列在Kafka和RabbitMQ之间二选一数据库是MySQL不引入其他存储。2.2 每个组件的选取理由Nginx反向代理、负载均衡、WebSocket代理、限流四个需求它一个软件全搞定。Nginx处理高并发连接的能力极强一个worker进程可以撑几万并发连接放在最前面挡流量最合适。Spring BootJava生态里对小白最友好的框架内置Tomcat就能跑HTTP服务加几个注解就能搭WebSocket配合Spring Data Redis、Spring Kafka开发效率很高。社区资料多到你想查什么都有。Redis直播场景里在线人数计数、房间信息缓存、分布式锁、热点数据缓存、限流计数全都是Redis的强项。它的单线程模型让操作都是原子的天然适合做计数器和分布式限流。Kafka消息队列的核心价值是削峰填谷和异步解耦。弹幕、点赞、礼物这些高频写操作先写进Kafka再由消费者异步落库下游DB就不会被瞬时流量打死。Kafka的吞吐量比RabbitMQ高一个量级而且消息堆积能力强直播这种大量小消息的场景它比RabbitMQ更合适。MySQL最终数据都落到MySQL但它绝不承担高并发压力。通过缓存前置、队列削峰MySQL的QPS能控制在很低的水平。表格对比一下当时纠结的几个选择组件我的选择备选项为什么这么选网关NginxSpring Cloud GatewayNginx更轻量、连接处理能力更强配置简单应用框架Spring BootNettyBoot生态全、上手快Netty需要自己处理太多东西缓存RedisMemcachedRedis数据类型丰富不止能做缓存还能做计数和限流消息队列KafkaRabbitMQ吞吐量高削峰能力更强适合直播高频消息数据库MySQLPostgreSQL团队更熟MySQL分表示例也多出了问题好排查2.3 我不选哪些组件为什么有段时间流行“微服务高并发”的说法好像不上一套微服务就算不上高并发一样。我这次明确不上注册中心、不搞网关服务、不拆十几个微服务模块。原因很简单直播互动环境的核心瓶颈根本不在服务拆分的粒度上而在缓存设计、连接管理和队列削峰上。服务拆得再细Redis缓存没设计好、DB被穿透打穿照样崩。如果你是从若依这类快速开发框架起步的前端Vue Spring Boot前后端分离的骨架完全可以直接用我下面说的缓存、队列、限流方案也都能嵌进去。微服务是团队协作和组织架构的产物不是一个后端小白第一次搭建高并发环境时应该优先考虑的东西。3. 从零铺环境Docker Compose 先把底子搭好搭建环境是整个过程中最枯燥但又最容易出错的环节。我第一次搭的时候是一步一步手动装JDK、MySQL、Redis、Kafka装完还因为版本兼容问题折腾了半天。后来换成了Docker Compose十几个组件的环境一条命令起好干净利落换一台服务器也能秒级复现。3.1 Docker Compose 编排文件示例我最后用的编排文件长这样MySQL、Redis、Kafka、Spring Boot应用和Nginx五个关键服务全在同一个Compose文件里定义version: 3.8 services: mysql: image: mysql:8.0 container_name: live-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: live_room ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7 container_name: live-redis command: redis-server --requirepass redispwd --maxmemory 512mb --appendonly yes ports: - 6379:6379 kafka: image: bitnami/kafka:3.4 container_name: live-kafka environment: KAFKA_CFG_NODE_ID: 0 KAFKA_CFG_PROCESS_ROLES: controller,broker KAFKA_CFG_LISTENERS: PLAINTEXT://:9092 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0kafka:9093 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER ports: - 9092:9092 app: build: ./app container_name: live-app depends_on: - mysql - redis - kafka environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/live_room?useUnicodetruecharacterEncodingutf8 SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PASSWORD: redispwd SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092 ports: - 8080:8080 nginx: image: nginx:1.24 container_name: live-nginx ports: - 80:80 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf depends_on: - app关键点有几个MySQL和Redis的数据目录都是挂载到宿主机上的容器删了数据还在Kafka用的advertised.listeners指向kafka:9092这是给容器内应用访问用的外部工具如果要从宿主机连需要额外配置一个映射地址Spring Boot应用的数据库地址和Redis地址都用了服务名这是Compose网络内的通信方式不能写localhost。3.2 启动后的验证清单环境起完不能直接开写先做一次快速验证确认每个组件都活着# 查看所有服务状态 docker-compose ps # 验证Redis可连接带密码 docker exec -it live-redis redis-cli -a redispwd ping # 验证MySQL可连接 docker exec -it live-mysql mysql -uroot -proot123 -e select 1; # 验证Kafka broker是否注册成功 docker exec -it live-kafka kafka-topics.sh --bootstrap-server kafka:9092 --list这套验证清单看起来基础但真的能救你命。我在第一次搭建时就是在Kafka上栽了跟头容器起来了但broker一直没注册成功应用连不上排查了很久才发现是Kafka的listen配置写错了。先把基础组件全部验证通过再去写业务代码后面出问题才能确定是业务问题还是环境问题。3.3 容器网络与服务名小白在这里掉过坑这里必须专门说一个坑容器里的localhost和宿主机根本不是一回事。刚开始我把Spring Boot的数据库地址配成localhost:3306结果应用永远连不上MySQL一直报连接拒绝。后来才明白在Compose网络里应用要访问MySQL容器的服务必须写成mysql:3306写localhost:3306其实是去找容器自己。反过来宿主机上的工具比如Navicat、JMeter要访问容器里的服务就要用宿主机IP加映射端口比如192.168.x.x:3306。这个“容器内外地址”的区别对第一次用Docker的新手来说几乎是必踩的坑提前写出来希望你们别在这个地方耗一晚上。4. Redis 缓存上线第一天就踩了穿透坑我是怎么调整的Redis在我这套架构里的角色极其关键。房间信息、在线人数、热点排行、分布式锁、限流计数全部依赖它。但缓存设计不是简单的“先查缓存查不到查DB”就算完直播这种高并发场景缓存设计不好Redis不仅挡不住压力还会成为新的故障点。4.1 我最初的缓存方案刚开始我设计的方案很朴素用Hash结构存房间信息key是room:info:{roomId}在线人数用room:online:{roomId}这个key做INCR和DECR礼物榜用ZSet按金额排序。思路都对但漏了一个致命场景。4.2 穿透不存在的房间号把 MySQL 打穿了压测第一天我模拟了一批并发请求去查一批不存在的房间号。因为房间里查不到缓存也存不下每个请求都直接穿透到MySQL一个select * from room where id ?查出来空结果然后下一个请求再来一遍。MySQL的QPS瞬间飙升到几千CPU爆表最后连接池被打满。解决穿透有几个常见方案空值缓存查出结果为null时往Redis里存一个空串或特殊标识设置一个很短的TTL比如60秒。下一次同样的请求就会命中缓存不会再去打DB。布隆过滤器把所有存在的房间ID预先加载到布隆过滤器里请求来了先过滤一遍不存在的房间号直接返回。我最终选了空值缓存原因很简单布隆过滤器虽然省内存但实现、维护、处理误判都要额外代码空值缓存三行代码就能搞定而且对当时压测规模完全够用。关键代码如下public RoomInfo getRoomInfo(Long roomId) { String cacheKey room:info: roomId; RoomInfo roomInfo redisTemplate.opsForValue().get(cacheKey); if (roomInfo ! null) { // 空值缓存的标识直接返回null表示房间不存在 if (EMPTY.equals(roomInfo.getRoomName())) { return null; } return roomInfo; } RoomInfo info roomMapper.selectById(roomId); if (info null) { // 空值缓存TTL设短一点比如60秒 RoomInfo empty new RoomInfo(); empty.setRoomName(EMPTY); redisTemplate.opsForValue().set(cacheKey, empty, 60, TimeUnit.SECONDS); return null; } // 加随机过期时间防止雪崩 int expire 300 ThreadLocalRandom.current().nextInt(60); redisTemplate.opsForValue().set(cacheKey, info, expire, TimeUnit.SECONDS); return info; }4.3 击穿和雪崩同一个坑的组合拳解决了穿透接着又遇到了缓存击穿某个大主播的房间信息缓存过期的一瞬间恰好大量用户同时进房所有请求一起跑到MySQL把刚刚缓过来的数据库又打趴了一次。击穿的核心问题是热点Key过期瞬间没有缓存兜底。我用了互斥锁的思路在重建缓存前先抢一把Redis分布式锁抢到锁的请求去查DB并写缓存其他请求短暂等待后重试读缓存。伪代码如下public RoomInfo getRoomInfoWithLock(Long roomId) throws InterruptedException { String cacheKey room:info: roomId; RoomInfo roomInfo redisTemplate.opsForValue().get(cacheKey); if (roomInfo ! null) { return roomInfo; } String lockKey room:lock: roomId; // 用SETNX抢锁5秒自动过期防止死锁 boolean lock redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS); if (!lock) { // 没抢到锁睡50毫秒再回查缓存 Thread.sleep(50); return redisTemplate.opsForValue().get(cacheKey); } try { roomInfo roomMapper.selectById(roomId); int expire 300 ThreadLocalRandom.current().nextInt(60); redisTemplate.opsForValue().set(cacheKey, roomInfo, expire, TimeUnit.SECONDS); return roomInfo; } finally { redisTemplate.delete(lockKey); } }雪崩的应对就简单了不要让大量Key在同一时刻过期给每个Key的TTL都加一个随机偏移量。直播场景里房间信息、排行榜、公告这种Key特别多如果全部设置成固定300秒到点那一刻就是一次集体失效压力瞬间全部打到DB上。加随机值能把这个集中的失效时间摊开效果立竿见影。4.4 热点房间 Key 的本地缓存兜底空值缓存、互斥锁解决的是“少量请求打到DB”的小问题但如果某个主播有几百万粉丝开播瞬间几万人同时请求同一个房间的KeyRedis自己都可能成为瓶颈。这时候我上了第三层防线本地进程缓存。我的做法是在Spring Boot应用里引入Caffeine作为本地缓存热点房间信息在本地先缓存30秒Redis作为二级缓存。查询的时候先查Caffeine没命中再查Redis最后才查DB。这样一个热点房间的请求大部分会在本地内存直接返回Redis的压力也能降下来。这个方案的注意点是数据一致性本地缓存有延迟房间信息改了之后用户可以接受最多30秒的同步延迟。直播场景里房间标题、封面这种低频修改数据这个延迟几乎无感。5. 弹幕推送的集群化难点WebSocket 广播与消息削峰互动链路是直播后端最有意思的部分。弹幕怎么推给房间里的所有人点赞这种超高频率操作怎么处理服务从单机变成多实例之后消息广播怎么保证每个用户都能收到这一章我逐一聊聊。5.1 为什么直播要用 WebSocket 而不是轮询开发直播弹幕之前我先对比过轮询和长连接两种方案。轮询就是前端每隔几秒拉一次接口实现简单但延迟高、无效请求多。两万人在线五秒轮询一次每秒就是四千次HTTP请求其中大部分请求还没有新消息纯属浪费。WebSocket是全双工长连接建立一次TCP连接后服务端可以主动往客户端推消息。弹幕发出来到其他用户看到延迟可以压到毫秒级。而且WebSocket是长连接不需要像HTTP那样每次握手、每次带Header连接维护成本也低。直播这种消息量大的场景WebSocket几乎是唯一合理的选择。5.2 单机 Session 管理没问题一上集群就失效最开始我的WebSocket实现很直白在内存里维护一个ConcurrentHashMapString, Sessionkey是userIdvalue是WebSocket session。用户发弹幕时按房间ID遍历所有在线用户逐个发送消息。单机测试完美延迟低、消息不错发。但当我尝试把Spring Boot应用扩到两个实例时问题来了用户A连的是实例1用户B连的是实例2A发了条弹幕B收不到。原因很简单A的弹幕到达实例1后实例1只遍历自己内存里的session列表B的session在实例2上实例1根本不知道它的存在。这个问题的本质是WebSocket session是有状态的存在哪个实例上只能哪个实例用。集群环境下必须把消息广播能力抽出来不能依赖单实例内存。5.3 Redis Pub/Sub 做实时广播Kafka 做削峰落库我的解决方案分两层弹幕的实时广播走Redis Pub/Sub业务数据的异步落库走Kafka。先看广播链路。所有Spring Boot实例启动时都订阅同一个Redis频道danmu:channel。用户A发的弹幕先到达它连接的实例1实例1不仅要把消息发给本机的session还要把消息发布到Redis频道。实例2订阅了这个频道收到消息后遍历自己机器上的session列表把消息推给房间里的用户。这样不管用户连的是哪个实例消息都能到达。微信里类似的场景都用“发布订阅”模式解决跨实例推送问题。Kafka则承担另一个职责削峰和落库。直播间的点赞量非常高一条条直接写MySQL肯定不行。我的做法是点赞请求先写Kafka的like-topic消费者异步从Kafka拉数据攒一批再批量写到MySQL。数据库的写入压力从每秒几千次降到每秒几十次批量写入完全不是一个量级。5.4 消息积压了怎么办Kafka用起来之后还会有新问题消费速度跟不上生产速度消息就积压了。我在压测时遇到过lag几百万条的情况查了一下原因是消费者线程太少默认配置只有一个线程拉取消息。解决办法有两个方向一是增加消费者实例数量让多个实例并行消费同一个topic下的不同分区二是把消费线程池调大在Spring Kafka里配置concurrency参数。我最后是把消费者并发从1调到了8lag迅速降了下来。这里要提醒一点消费并发放大之后下游MySQL的写入压力也会跟着涨一定要配合批量处理来控制节奏。6. 让 MySQL 在高压下不先死限流、读写分离与降级直播高并发环境里MySQL是整个系统最脆弱的环节没有之一。它一挂订单、礼物记录、弹幕历史全部写不进去整个直播间的状态同步都会出问题。所以我对MySQL的态度非常明确能不进DB就不进DB必须进DB的就异步进异步也顶不住的时候就限流降级。6.1 Nginx 限流最外层先挡一刀限流最外一层是Nginx。Nginx有个limit_req模块可以按IP或按请求参数做漏桶限流。我在直播间的进房接口和弹幕接口上都做了接口限流比如进房接口限制单个IP每秒最多20个请求limit_req_zone $binary_remote_addr zoneroom_limit:10m rate20r/s; server { listen 80; location /api/room/enter { limit_req zoneroom_limit burst40 nodelay; proxy_pass http://live_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws { proxy_pass http://live_app; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }burst40的意思是允许瞬时突发40个请求超过的直接返回503。这个配置让我那些压测脚本里忘加延迟的请求在Nginx这一层就被拦住了后端应用的压力一下子小了很多。6.2 应用层 RedisLua 限流更精细的控制Nginx限流只能覆盖单个IP维度但对付分散的机器人攻击或某些接口的全局控制需要在应用层做更精细的限流。我用了RedisLua实现了一个简单的令牌桶限流每秒补充固定数量的令牌请求来了消耗一个令牌没有令牌就拒绝。Lua脚本local key KEYS[1] local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local last tonumber(redis.call(GET, key .. :last) or 0) local tokens tonumber(redis.call(GET, key .. :tokens) or capacity) if last 0 then last now end tokens math.min(capacity, tokens (now - last) * rate) if tokens 1 then redis.call(SET, key .. :tokens, tokens - 1) redis.call(SET, key .. :last, now) return 1 else redis.call(SET, key .. :tokens, tokens) redis.call(SET, key .. :last, now) return 0 endRedis单线程执行Lua脚本是原子性的所以这段脚本可以在高并发下安全使用。我把它用在送礼和秒杀接口上每个用户每秒最多一次请求超出直接返回“操作太快了”。这里有个小经验限流值不要拍脑袋定要用JMeter先打一轮全链路压测测出单机能承受的QPS上限再留30%的余量作为限流阈值。我最初把限流值定得太高导致压测时Redis先被打满后面把阈值调低效果立竿见影。6.3 读写分离与消息表分表直播数据里弹幕历史、礼物记录、点赞流水都是超高写入量的表。如果不做任何优化单表数据量几个月就能到千万级查询和插入都会变慢。我的做法是读写分离MySQL一主一从主库负责写从库负责查。进房接口查房间公告、查历史弹幕都走从库礼物订单、用户消费记录走主库。按月分表礼物流水和弹幕历史按月份分表比如gift_log_202504、danmu_log_202504。查询时通过路由字段确定查哪张表避免单表数据量过大。分表之后还有一个连带好处冷数据可以归档到更低成本的存储里热表保持在可接受的数据量级查询性能也能稳住。6.4 降级策略保证主流程能用再怎么设计极端情况下系统的某个环节还是会扛不住。这时候必须有降级预案。我的原则是次要功能可以让核心功能不能瘫。弹幕历史查不到返回空列表但实时弹幕推送不能断。礼物排行榜暂时拉不到返回缓存里的旧榜不影响送礼操作。送礼接口超时返回失败但绝不允许因为订单处理失败导致用户账户扣款不确定。每个接口都要设计降级方案压测时专门模拟过Redis宕机的场景验证弹幕功能挂了但进房、看播流程不受影响用户体验只是少了互动功能不至于整个直播间黑屏。7. 小白翻车现场五个必须提前知道的大坑这一章是实战中最容易踩的坑几乎每个我都真实经历过有些甚至同一个坑踩了两次。列出来你们可以直接避。7.1 坑一容器间通信用了 localhost前面已经提过一次但值得单独标记。Spring Boot应用在Docker里访问MySQL和Redis时如果还在用localhost:3306、localhost:6379这些地址会报连接超时。因为容器内的localhost指向它自己不是宿主机。在Docker Compose网络里服务名就相当于主机名访问MySQL直接写mysql访问Redis写redis。如果你非要在宿主机上调试应用那才需要连宿主机IP:映射端口。这个方向搞反了排查两小时起步。7.2 坑二Redis 连接池默认值太小Spring Boot 3.0默认用Lettuce作为Redis客户端连接池默认上限很小具体看版本有些版本默认只有8个连接。压测一上来几百个线程同时去拿Redis连接连接池瞬间被打满后面的请求全部排队排队超时直接抛异常。解决方法是把连接池调大并且设置合理的超时时间spring: data: redis: host: redis port: 6379 password: redispwd lettuce: pool: max-active: 100 max-idle: 20 min-idle: 10 max-wait: 3000msmax-active100可以根据压测情况继续调大但别盲目加到几千因为每个连接都要占用文件描述符和内存太大反而拖垮Redis。7.3 坑三WebSocket Session 没做心跳检测WebSocket长连接很优雅但很多小白会忽略一个现实问题TCP连接断开后服务端可能很久都感知不到。用户直接关掉浏览器、切换网络、手机锁屏连接就是半开状态session对象还在内存里但消息已经推不出去了。日积月累内存里堆满了无效session广播消息时白白遍历一遍性能越来越差。解决方案是心跳机制。客户端每30秒发一个Ping服务端收到后更新在线时间服务端每60秒扫一次超过90秒没收到心跳的连接直接关掉并从session管理器里移除。这个机制不仅要自己实现还要放在压测里一起测否则线上运行几天后内存溢出是必然的。7.4 坑四JVM 堆内存默认值根本不够Spring Boot应用在Docker容器里如果不设置JVM参数默认堆内存是容器内存的四分之一左右。我那个4G内存的测试服务器容器里Spring Boot的堆可能只有不到1G。压测一开始频繁Full GC接口响应时间蹭蹭往上涨从几百毫秒直接跳到几秒。我后来在Dockerfile里显式指定了JVM参数FROM openjdk:17 COPY target/live-app.jar /app.jar CMD [java, -Xms2g, -Xmx2g, -jar, /app.jar]堆内存固定成2G后GC频率明显降低。如果你的服务器内存更大可以按实际负载继续调但记得留给操作系统和其他中间件足够的内存。7.5 坑五日志刷屏把磁盘IO打满调试的时候习惯性把所有日志都打成DEBUG级别上线时忘了改回来。压测一开每秒几万条日志往磁盘写日志文件一小时好几个G最后磁盘满了服务直接挂掉。这个问题太隐蔽了我当时排查了很久才发现是日志在搞事情。现在的做法是生产环境日志级别默认WARN专门用一个logback-spring.xml配置性能日志的采样率接口慢查询单独打成WARN日志其他没用的Debug信息全部关掉。日志服务要开滚动切割按大小或按天切割保留最近几天的即可。这个坑排查起来极其费时间强烈建议做环境时就处理好。8. JMeter 压测实录从 300 QPS 到 5000 QPS 我做了什么最后也是我最想分享的部分压测。没有压测验证的“高并发环境”都是自我感动。我用JMeter脚本模拟真实用户行为对每个接口、每条链路做了多轮压测每次调整都记录数据最后得到了一套可复现的优化验证流程。8.1 JMeter 脚本怎么设计压测脚本不是每个接口单独压一遍那么简单直播场景的用户行为是混合的先进房再发几条弹幕中间穿插点赞偶尔送礼。我建了一个线程组用200个线程并发每个线程循环100次代表200个虚拟用户反复进出直播间并执行互动操作。核心配置HTTP请求默认值指向Nginx的80端口第一个请求是进房接口拿到roomId然后用WebSocket Sampler建立长连接发送弹幕和点赞用JSON Extractor提取接口返回的token模拟登录态聚合报告统计吞吐量、错误率、平均响应时间、P99耗时。压测机建议不要和后端服务在同一台机器上否则JMeter自己会抢CPU和内存测出来的数据不准。我一开始就是在同一台机器上压数据忽高忽低后来分开才稳定下来。8.2 四轮压测的调整过程第一轮压测我直接用默认配置跑结果惨不忍睹吞吐量只有300多QPS错误率接近40%大量请求超时。打开后台日志一看Tomcat线程全部被慢查询和Redis连接等待占满连接池早就爆了。第二轮调整了Tomcat线程池和Redis连接池把server.tomcat.threads.max调成500Redis连接池从默认调到100错误率立刻降到5%吞吐量跑到1500左右。这时候慢查询开始成为新的瓶颈DB的CPU使用率居高不下。第三轮优化集中在缓存和限流上给房间信息查询加本地缓存、给弹幕发送加应用层限流、给进房接口加Nginx层限流。同时把历史弹幕查询改成只查最近100条不再全表扫描。这轮跑下来吞吐量到了3200平均响应时间降到300毫秒左右。第四轮针对互动链路做优化点赞请求全部异步进Kafka由消费者批量落库弹幕广播改成Redis Pub/Sub方式再加上WebSocket心跳检测防止压测时产生无效session导致广播性能下降。最终压测结果轮次关键调整吞吐量QPS错误率平均响应时间第一轮默认配置直连30038%3200ms第二轮Tomcat Redis连接池调优15005%900ms第三轮缓存 限流 降级32000.5%300ms第四轮广播优化 Kafka削峰53000.1%180ms8.3 压测要注意的副作用压测虽然爽但副作用也很大。第一次压测时我没用独立环境直接在开发环境的同一套服务器上跑结果把开发用的Redis打挂了一堆同事在旁边骂人。之后我所有的压测都在独立的Docker Compose环境里跑压完直接销毁。另外压测结果和服务器本身性能强相关我这边的数据是4C8G单机跑出来的你用2C4G可能只有一半8C16G可能会更高。重要的是看每一轮优化的相对提升不是绝对值。如果要用JMeter做更大规模的压测比如上万并发单台JMeter机器自己可能先扛不住了需要做分布式压测用多台施压机分担压力。这是后面量级上来之后再考虑的事。回看这次搭建过程我自己最大的收获不是学会配置某个组件而是建立了一套在高并发场景下做技术决策的思维方式先拆解流量特征再针对每个特征选择最合适的组件和策略最后用压测数据验证每个决定。这套思路从直播场景迁移到IM、秒杀、抢课底层逻辑都是通用的。如果你也在走这条路希望这篇笔记能让你少熬两个通宵。