ARTICLE DETAIL

资讯详情

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

千人并发到底算不算大事?从压测到数据库锁全解析

千人并发到底算不算大事?从压测到数据库锁全解析 千人并发这四个字放在一起在技术群里能吵出一篇长文。有人说“我单机JVM配个线程池就扛2000”有人说“一到1000系统直接雪崩CPU飙到99%”。我观察了很久发现两边大概率说的根本不是同一件事。有的人测的是“1000个用户在线”有的人测的是“1000个请求同时打到数据库”还有的人其实是“1000个线程在JMeter里跑完了”这三者的技术含量天差地别。这篇文章我就从“千人并发算不算大事”这个问题出发把并发数、压测方法、数据库锁、流量治理这些事彻底捋一遍看看1000个并发到底意味着什么压测要怎么看数据系统会在哪一层先顶不住。1. 先把“千人并发”四个字拆开你测的到底是谁的并发很多争论吵到最后发现大家连“并发”的定义都没对齐。这个问题不解决后面所有压测、调优、架构设计都是空中楼阁。1.1 在线人数并发数容易混这是百分之九十争论的根源我在很多项目的技术方案评审里见过同一个场景产品经理过来说“我们要支持1000个用户同时在线技术能做到吗”开发拍着胸脯说“没问题1000并发小意思”。结果上线后一到高峰期接口响应从20毫秒涨到2秒数据库连接池爆掉链路监控里全是超时警告。问题出在哪1000个用户“在线”和1000个“并发请求”是两个完全不同的概念。在线用户指的是当前登录在系统里、处于可用状态的人数这1000个人不可能每时每刻都在点击请求。按照二八原则倒推1000个在线用户里同一秒内真正发起请求的可能只有十几到几十个。那什么是真正的并发数严格来说是同一时刻系统正在处理中的请求数量也就是 in-flight requests。一个用户打开一个App页面前端可能一次性并行发出5个请求这5个请求如果在服务端同时被处理就算5个并发。所以并发数是一个关于“瞬时压力”的数据跟在线人数之间有换算关系但绝不是等号。1.2 千人并发对应的QPS要按业务形态分别看待搞清楚定义之后还得看业务形态。同样是“千人并发”在不同场景下对系统的冲击力完全不一样。我把常见的业务分成三类第一类是低延迟的读场景比如商品详情页、文章浏览。这种请求平均响应时间RT一般在100到300毫秒。根据 Littles Law并发数等于QPS乘以平均响应时间也就是 QPS 并发数 / 平均响应时间。按1000并发、RT200毫秒算QPS大概是5000左右。这个量级对一台配置合理的服务器来说撑住问题不大只要缓存命中率够、代码没有明显热点。第二类是写场景比如下单、支付回调、库存扣减。RT通常要500到1000毫秒因为涉及数据库事务、分布式锁、外部系统调用。按1000并发、RT1秒算QPS1000。这个量级下压力会直接传导到数据库和中间件尤其是同一行数据被并发更新的时候数据库层面的锁冲突会放大RT而RT变长又会让并发数继续堆积形成恶性循环。第三类是计算型或长连接场景比如报表导出、AI推理、WebSocket推送。单请求RT可能达到5到10秒QPS只有100到200但线程被长时间占用线程池、内存、连接数全部告急。这种“千人并发”比前两类难缠得多因为卡脖子资源是线程数和内存加机器都未必好使。1.3 “扛得住”和“响应快”是两个层面的事还有一个经常被忽略的问题“千人并发”下系统不崩和千人并发下每个用户都能快速拿到结果这是两码事。很多系统在压测的时候确实能处理完1000个并发请求但看响应时间分布会发现大量请求的响应时间已经超过3秒。如果你不设超时时间这些请求最后也会完成从“系统处理能力”角度看确实抗住了但从用户体验看这已经是不可用的状态了。所以我个人在判断一个系统能不能扛住千人并发时从来不看“没崩”这个下限而是看“响应时间达标率”这个上限。就像面试官问“你能独立负责一个系统吗”你说“系统不炸就算成功”这跟“我能在业务高峰期保证核心接口P99小于500毫秒”是完全不一样的回答。后面这句话里隐含了缓存设计、连接池规划、熔断降级、全链路压测一整套功夫。2. 用JMeter压测千人并发线程数1000不代表并发1000聊完定义就到了实操环节。很多人用JMeter压测千人并发踩进来第一个坑就是在JMeter里设置线程数为1000线程组一启动瞬间把所有线程都跑满然后盯着聚合报告看结果。这种做法得到的数据基本没有参考价值。2.1 压测前必须想清楚的线程模型先搞明白JMeter的线程组参数在模拟什么。线程数就是模拟的虚拟用户数Ramp-Up Period是所有线程从0开始到全部启动完毕的时长循环次数是每个线程跑几轮请求。拿“千人并发”举例比较接近真实的做法有这两种第一种模拟长期的持续压力。线程数设成1000Ramp-Up Period设为60秒勾选调度器持续时间设为300秒。这样1000个虚拟用户在60秒内陆续登场然后持续施压5分钟。这更贴近真实场景因为真实用户不会在同一毫秒集体按下按钮。第二种模拟瞬时的集中爆发。比如秒杀场景就是要在几秒内把所有用户放进来那可以把Ramp-Up Period设成5秒甚至1秒模拟流量瞬间冲顶。但这里有个关键认知JMeter里线程数1000不代表系统实际并发就是1000。JMeter线程发出请求后会等响应返回或者超时才会继续下一次请求或结束。如果服务端处理得很慢RT是2秒而线程发完请求后处于等待状态那JMeter端的“活跃线程数”是1000但服务端同一时刻真正在处理的请求数可能只有几百剩下的都在等待响应。要确认服务端的真实并发得去服务端看监控看同时有多少请求在处理中也就是活跃线程数和连接数。2.2 聚合报告里真正要看的三个指标压测跑完很多人第一时间看聚合报告的Average和Throughput。但我建议先看Error%再看TP99最后才看Average和TPS。这三个指标要联动着看缺一个都可能得出错误结论。先看一个我压测时经常遇到的场景模拟数据如下指标数值判断Samples50万采样量足够Average RT180 ms平均数很好看90% Line350 ms还算正常99% Line920 ms已经接近1秒Max RT4800 ms有严重长尾Error %0.15%750个请求失败Throughput2100/s吞吐量数据这个报告单看平均RT180毫秒挺优秀单看TPS2100也不差但Error%是0.15%换算下来50万个请求里失败了750个。在千人并发级别的压测里0.1%的失败率意味着有用户真实感知到了报错。如果这是下单接口750个订单失败业务上根本扛不住。所以我的习惯是压测结果里Error%必须为0或者无限接近0。TP99超过1秒的接口就要开始排查了。2.3 怎么判断系统的真实并发上限压测的目的不只是验证“能不能扛住1000并发”更重要的是找出“最多能扛多少并发”。这里分享一个我自己常用的阶梯压测法从系统当前预估并发的一半起步比如目标是1000就先跑300观察各项指标稳定后提升到500再观察再到800、1000、1200。每提升一档记录下TPS、RT、Error%、系统资源使用情况。判断是否触及上限要同时看三个信号第一TPS是否不再随并发数增长。如果并发从800加到1000TPS基本持平甚至下降说明系统处理能力到顶了。第二RT是否出现拐点。并发数增长初期RT可能只涨一点点一旦超过某个临界点RT会急剧上升这是因为队列堆积了。第三Error%开始从0蹦出数值说明某层组件开始抛异常或超时。这么压一圈下来你才能真正回答“千人并发算不算大事”——如果系统在800并发时TPS就开始下降那对于你这个具体系统来说1000并发就是一场灾难千真万确的大事。3. 千人并发最先顶不住的环节数据库并发锁CPU、内存不够了可以加配置线程池不够了可以调参数但数据库层的并发问题往往不是加机器就能解决的。千人并发的请求如果穿透了应用层、打到数据库最容易出事的就两个地方一个是锁冲突一个是连接池耗尽。3.1 一条库存扣减SQL引发的连锁反应先看一个特别经典的电商场景扣减库存。最朴素的写法是这样的-- 伪代码逻辑 SELECT stock FROM product WHERE id 100; -- 程序里判断 stock 0 UPDATE product SET stock stock - 1 WHERE id 100;这种写法在低并发下没问题一旦出现1000个并发请求同时去扣同一件商品的库存就会出大问题。两个事务可能同时读到stock1然后都判断“还有库存”都执行扣减最后库存变成-1。很多人第一反应是给查询加锁于是改成这样SELECT stock FROM product WHERE id 100 FOR UPDATE; UPDATE product SET stock stock - 1 WHERE id 100;这个方案确实解决了超卖但引入了新的问题同一行数据的FOR UPDATE锁会让1000个并发请求排成一队。如果每个事务执行要50毫秒1000个请求串行处理最后一个请求要等将近50秒这结果等于系统直接不可用。更合理的方式是使用原子性的条件更新把判断和扣减合并成一条SQLUPDATE product SET stock stock - 1 WHERE id 100 AND stock 0;执行后判断影响行数如果是1说明扣减成功是0说明库存不足。这种写法让数据库在行锁内完成“检查库存和扣减”两个动作把锁持有时间压缩到最短。但注意就算这样1000个请求仍然要排队去更新同一行只是每个请求占用行锁的时间极短整体吞吐能上去不少。现实中的秒杀系统连这一层行锁都不愿意承受会把库存预先加载到Redis里做前置扣减真正落库时再异步对账。千人并发的库存扣减纯靠数据库扛已经非常吃力这是业务场景决定了必须上额外组件。3.2 更隐蔽的杀手数据库连接池耗尽比起行锁冲突连接池耗尽在千人并发下更常见也更隐蔽。因为锁冲突会直接反映在慢SQL上比较容易发现而连接池耗尽的表现多种多样可能是应用日志里出现“Connection is not available, request timed out”、可能是某个接口突然变慢、也可能是数据库负载看着不高但应用整体像死了一样。线程池和连接池的关系经常被混淆。一个经典配置错误是Tomcat的max-threads设成500但HikariCP的maximum-pool-size才20。当500个请求线程同时进来只有20个能拿到数据库连接剩下480个全堵在“获取连接”的等待队列里。这些等待线程占着Tomcat线程池不释放新请求进不来应用表现为整体不可用但数据库其实闲得很。所以配置连接池之前先理解这个等式最大并发数据库请求数 应用最大线程数 × 单线程串行请求数。如果应用线程池是500连接池至少得能覆盖大部分线程的并发数据库访问量。我一般建议先粗调为Tomcat线程数的四分之一到三分之一比如Tomcat线程数是200HikariCP的maximum-pool-size可以先设成50然后通过压测观察活跃连接数再微调。参数上HikariCP有一个minimum-idle和maximum-pool-size。压力场景下建议把minimum-idle和maximum-pool-size设为相同值省去连接动态创建的延迟。连接池不是越大越好因为数据库的CPU核数和内存决定了它真正能同时执行的SQL数量连接太多反而增加上下文切换成本。MySQL一般建议连接数控制在500以内HikariCP默认值10对这个场景来说太小要根据压力测试结果上调。3.3 热点行更新的对冲思路千人并发下如果都盯着同一行更新不管怎么调优单行数据库更新的天花板就摆在那里。这里有两个思路可以借鉴一是拆分热点。比如库存扣减不要只在一个字段上扣可以按ID取模拆成多个库存子记录每个子记录独立扣减汇总时再加总。这本质上是把单行的行锁竞争分摊到多行上。二是把热点请求串行化。用Redis的去重队列或分布式锁让同一商品的扣减请求在应用层排成一个队列依次处理。1000个并发请求在Redis队列里排队进入应用应用每次只处理一个既避免了数据库行锁又因为Redis单线程特性天然有序逻辑还更简单。代价是你引入了新的组件、新的故障点。千人并发可以把数据库层打穿这些事情如果不提前设计压测的时候就会直接暴露出来。早点通过压测把瓶颈炸出来比上线后被用户炸要好得多。4. 流量治理不是摆设Sentinel在千人并发下的真实角色千人并发的流量一旦进来最怕的不是请求多而是流量没有节制——系统本来能扛500并发突然来了2000结果就是雪崩。这个时候流量治理的那套东西就开始起作用了。很多人把Sentinel仅仅理解成“限流工具”有点低估它。4.1 流控、熔断、系统保护三件事各有各的职责Sentinel的三大核心能力分别是流量控制、熔断降级和系统自适应保护。我在这几年的实践中确实体会到了这三者分工的不同。流量控制解决的是“入口流量太大”的问题。比如接口只支持1000 QPS超过的就直接返回“系统繁忙”。这不是为了拒绝用户而是为了保住系统的其他部分不至于全挂。熔断降级解决的是“下游已经坏了别再往里打流量”的问题。比如一个接口依赖了第三方慢接口正常RT是200毫秒现在变成10秒了。如果继续放流量进来应用线程会被全部占满。熔断器在连续失败率达到阈值后打开直接短路请求给下游恢复的时间。系统自适应保护解决的是“别把整台机器干死”的问题。Sentinel会根据系统当前的Load、CPU使用率、平均RT这些指标动态地调整入口流量。喝咖啡类比就是杯子就那么大咖啡倒得太快会溢出来系统保护就是那个自动放慢倒水速度的装置。这三种能力在千人并发场景下的配合是流量控制管入口熔断降级管调用链系统保护兜底保命。4.2 一个能直接照抄的QPS限流规则配置以Spring Cloud Alibaba Sentinel为例最简单的限流规则可以这样定义[ { resource: POST:/api/order/create, count: 1000, grade: 1, limitApp: default, strategy: 0, controlBehavior: 0 } ]这段配置的含义是针对/api/order/create这个资源QPS超过1000的时候开始限流超过的请求直接拒绝。grade1代表按QPS维度限流strategy0表示直接拒绝controlBehavior0是默认的快速失败策略。但这里有个很容易踩的坑count设成1000不代表你系统就能扛1000 QPS。这个数字应该来自压测结果而不是拍脑袋。如果你的压测数据显示这个接口在600 QPS的时候RT已经开始快速上升那count就设600留出30%到50%的余量放在生产环境。线上流量是突发的不像压测那样线性增长所以阈值留余量是保命的习惯。另外一个被很多人忽略的点是控制行为。默认的快速失败对突发流量并不友好突刺流量过了之后系统会有一段时间的“空窗期”因为大量请求被拒了。更平滑的做法是用冷启动或匀速排队模式。匀速排队适合削峰填谷的场景比如每个请求在队列里等最多10毫秒超过就拒绝。对秒杀这类场景非常有用。4.3 我对Sentinel的观察限流拦得住流量拦不住烂代码聊到这里想说一句实践中的体会Sentinel这类流量治理组件是“保险丝”不是“发动机”。它能在流量超载时保护系统不被打死但如果你系统本身在500 QPS时RT就飙到3秒上了Sentinel只是让它在500 QPS时开始报错而不是让系统能扛住1000并发。我见过不少团队的做法是系统压测不过就加限流。把阈值调低压测就过了。但业务指标摆在那里用户量增长一点限流就开始拒绝用户了。这其实是把问题往后推并没有解决系统本身的性能瓶颈。所以正确姿势是先通过压测和排查把系统本身的性能做到位再用Sentinel做最后的流量兜底。千人并发下系统能不能扛6成靠代码质量和缓存设计3成靠连接池和线程池的配置最后1成才是Sentinel这种保命装置。顺序反了治理组件反而会成为你掩盖问题的工具。5. 并发和并行、线程数调优Java并发里容易翻车的细节千人并发的另一个侧面是把请求“并发地”处理。很多Java后端项目的并发问题根源不在框架而在对并发编程基础的理解偏差。5.1 并发与并行不是一回事它在高并发场景下的意义并发是指系统能够同时处理多件事并行是指系统在同一时刻真的同时执行多件事。这里的区别我用喝酒来说一个人在四张桌子之间来回倒酒一个时间点只能处理一张桌子这叫并发四个服务员同时各服务一张桌子这叫并行。在单核CPU机器上程序本质上没法真正并行只能通过时间片切换制造“并发”的假象。多核机器上多线程才能真并行。这也解释了为什么很多并发问题在研发环境的4核机器上测不出来一上生产8核甚至16核的机器又出现数据错乱因为并行度高并发竞争变严重了。Java并发里最经典的误区就是遇到性能问题就加大线程池。但线程不是越多越好因为CPU时间片就那么多线程超过核心数之后大部分时间都花在线程切换上。操作系统在线程切换时要保存和恢复上下文这个操作的CPU开销不小。当一个线程池里有上千个活跃线程时光是上下文切换就能吃掉几十个核心的算力。5.2 线程池大小怎么估别套公式先分场景网上流传一个线程池大小公式线程数 CPU核数 × (1 等待时间 / 计算时间)。这个公式有一定参考价值但很多人用错了场景。公式背后的逻辑是如果线程大部分时间在IO等待那在同一时刻可以多放一些线程来“填补”等待空隙。比如计算时间50毫秒等待时间950毫秒每个线程有95%的时间在等那理论上可以多加近20倍的线程来压满CPU。但真实场景里等待时间不是固定值而是随并发数变化。因为数据库连接池排队、第三方接口响应变慢都会让等待时间变长。所以公式只能作为起点最终值还是要压测来定。我最近的实操习惯是对IO密集型的业务大部分后端接口都算线程数先设为 (CPU核数 × 2) 然后用压测结果校准。而不是一上来就设1000。一个1000线程的Tomcat如果数据库连接池只有50大部分线程都在傻等反而把内存和CPU耗在无用的调度上。5.3 锁、原子类、线程封闭各自处理不同层面的并发问题Java并发领域的三板斧synchronized/ReentrantLock管互斥CAS原子类管单变量更新ThreadLocal管线程封闭。它们的使用边界很清晰。synchronized适合临界区代码块较长的场景JVM会做偏向锁、轻量级锁的自动优化如果只是对一个计数做递增用synchronized就太重了LongAdder或AtomicInteger更合适CAS在低竞争下性能很好如果希望每个线程有自己的副本、互不干扰就用ThreadLocal。千人并发场景下最容易出的问题是用错了工具。比如乐观锁CAS适合冲突低的场景一旦1000个线程同时对一个AtomicLong做incrementAndGetCAS会大量自旋重试性能可能反而比synchronized差。这就是为什么LongAdder在并发度高时性能更好——它把计数分散到多个槽位减少冲突。线程池的拒绝策略也是一样ThreadPoolExecutor默认的AbortPolicy在任务满了之后直接抛异常这在千人并发下会把错误堆栈打满。实际生产里我更推荐CallerRunsPolicy任务满了让提交任务的那个线程自己执行把压力逆向传导给调用方相当于一个自然的背压机制。6. 从一千到一万容量估算和预警水位不能靠拍脑袋很多系统一开始的目标就是千人并发但业务发展起来后就会变成五千人、一万人。如果从第一天起就把容量估算的模型建好后面扩容和改造都有据可依。6.1 从业务指标反推技术指标一个可以抄的计算过程容量估算最忌讳张嘴就说“我们要支持100万用户”。技术指标必须从业务指标一步步倒推出来中间每一步的假设都要明确记录后续才能根据真实数据去校准。我拿一个典型的B端管理后台来演算。目标支持10万注册用户日活按照10%估算就是1万人。假设每个活跃用户一天触发50个请求那一整天的请求总量就是50万。除以86400秒得到平均QPS大约是5.8峰值按平均值的5倍估算峰值QPS大约30——这量级压根不叫压力。但换个场景同样是10万用户如果是课件抢购秒杀系统10万人同时点抢购按钮这个瞬时流量就不能按平均值算了。按“10万用户中10%的人挤在同一秒”来估算瞬时QPS就有1万。对接口的要求就完全变了。千人并发在普通业务系统算中高压力在秒杀场景里只能算一个数据点。这是估算的第一步先明确业务模型再谈QPS。第二步是用Littles Law把QPS转成并发数。假设你的接口平均RT是200毫秒峰值QPS是5000那平均并发数就是5000×0.21000。正好是题目里的1000并发。也就是说如果RT不变5000 QPS和1000并发是一回事。这串换算在你做容量规划的时候非常有用。6.2 千人并发场景下的预警水位建议系统上线前一定要把监控指标和预警水位提前配好不然等线上出问题再查监控黄花菜都凉了。千人并发场景下我建议重点盯这几个指标和水位监控对象预警水位说明CPU使用率连续5分钟超过70%如果长期75%以上压测能力会明显下降内存使用率持续超过80%排查GC频率和内存泄漏Tomcat活跃线程数接近max-threads的80%说明请求在排队RT会快速恶化数据库连接池活跃数达到maximum-pool-size的80%大概率有连接泄漏或者慢SQL数据库慢SQL数单分钟超过10条慢SQL会拖垮CPU和连接池接口P99 RT超过500毫秒P99比平均值更敏感地反映用户体验这些水位不是死的但可以作为初始模板每个项目根据压测结果去调整。重点是预警水位必须在压测阶段就确定下来而不是上线后再猜。6.3 一千并发到一万并发架构演进的关键分岔路最后聊聊演进。1000并发的时候你可能觉得只要加机器就行但到了5000并发很多问题会因为规模而质变。第一步是先做单机极致优化缓存热点数据、优化SQL索引、调整连接池和线程池参数、开启HTTP压缩。这一步能让单机能力从200并发提到800甚至1000。第二步才是加机器做水平扩展。但水平扩展有一堆前置条件无状态应用、会话外置、缓存集群化、数据库从库拆分。很多系统在1000并发时能跑是因为应用状态都藏在本地内存里一旦水平扩展到多台Redis、分布式锁、消息队列这些基础设施就成了新的短板。第三步是异步化和削峰填谷。很多同步处理逻辑在1000并发下还能硬扛但到5000并发就扛不住了。把核心链路里的非关键步骤比如消息通知、积分结算、日志落库扔到MQ里异步处理系统的同步压力会小很多。每次架构演进都是一次新的压测循环估算、压测、优化、升级。这么一轮轮跑下来你才会慢慢形成对“千人并发”这种问题的手感。7. 聊聊我自己的体感做了这么久后端和架构设计我对“千人并发算不算个大事”这个问题已经有了一个自己的答案不说算也不算不算。说它不算是因为这确实是一个可以量化、可以通过压测验证、可以通过架构设计解决的问题。它不是那种悬而未决的科学难题1000并发就是1000并发测出来的数据不会骗人。说它算是因为很多团队在真正面对1000并发的过程中才发现问题根本不在“1000”这个数字而在于你花了多久时间发现系统在哪一层先扛不住又花了多久去优化那一层。我见过太多项目死在“觉得1000并发不难”这件事上。代码写得随意、SQL没有explain过、线程池参数保持默认、缓存策略靠猜——等到压测的时候问题的复杂度才开始爆发。反过来如果你能把压测和监控当成日常习惯1000并发确实又是一道不算太难的坎。最后分享一个我的个人偏好这种问题不要去网上争也不要去听别人吹牛。自己搭个环境压一遍把TPS、RT、Error%、系统资源这几个数字打出来你看一眼就明白自己的系统离1000并发还差多远。数字不会骗人比任何经验之谈都管用。
返回列表