接到内部代号“999999999”的压测专项时我刚进项目群群里只有一张截图某个网关节点在压力机上瞬时跑出的QPS曲线末尾一长串数字几乎把监控面板撑满。负责人顺手把项目代号改成了九个9说这就是本次活动的流量峰值模型。一开始我都觉得这数字是键盘乱按出来的直到完整跟完整个专项才发现比“能不能抗住峰值”更难的是搞明白系统到底在什么条件下崩溃、崩溃之前有什么征兆、恢复需要多久。这篇文章就围绕这个代号“999999999”的极限压测专项展开。它本质上不是一次简单的压测而是一次面向极端流量场景的全链路容量验证从压测工具选型、流量模型设计到内核参数调整、网关线程模型优化再到踩过的坑和排查实录我都会拆开讲。如果你正在做网关、微服务或者中间件相关的工作或者马上要备战大促、活动的系统验收这篇文章可以作为一份可复用的压测实战参考。1. 项目背景与目标拆解9个9到底在测什么1.1 极限压测不是拍脑袋是业务峰值倒逼出来的很多团队对压测的理解停留在“拿工具打一下接口看看有没有报错”。但真实场景里流量峰值从来不是一个均匀的曲线而是瞬间涌进来的尖刺。拿这次“999999999”专项来说业务侧给到的预测是零点过后第一波流量会呈脉冲式到达持续几十秒到几分钟之后快速回落到常态。这种形态最危险的地方在于系统扩容通常跟不上流量爬升速度等监控发现CPU已经打满用户早就开始报错了。所以我们做这个专项的第一目标不是“压出9亿QPS”而是验证在极端脉冲流量下网关集群、下游服务、缓存、数据库连接池、日志链路每一个环节还能不能各司其职。编号“999999999”更像是一个心理暗示所有保守估计都要去掉所有“应该没问题”都得用数据证明。另外还有一个现实原因线上不可能随便把流量导到真实集群里去试。一次错误的全量上线影响的可能就是核心链路。专用压测环境、隔离数据、可控施压这些只有专项压测能做到。1.2 项目范围和验收标准要提前定死这种专项最容易翻车的就是范围不清。开发觉得只要测网关运维觉得要测全链路业务又觉得响应时间不能超过某个阈值。我们这次在启动前就和各方拉齐了下面几个模块全部纳入压测范围模块压测目标主要关注指标接入网关验证单节点QPS上限、连接数上限CPU、内存、P99延迟、错误率业务后端服务验证线程池、数据库连接池是否够用线程池活跃数、连接池等待时间缓存层验证热点Key下的读写性能命中率、RT、慢查询消息队列验证积压后能否快速恢复消费 lag、堆积量日志链路验证全量日志写入是否拖垮业务磁盘IO、写入失败率验收标准也写成了表格避免事后扯皮指标目标值说明整体成功率不低于99.99%错误率里包含超时和5xx不包含主动限流P99延迟不超过300ms从网关入口开始计算含下游调用限流触发比例不超过总请求的1%允许限流保护但不能直接拖垮故障恢复时间不超过5分钟压测结束后恢复常态不需要重启集群这些标准看起来简单实际执行里每条都够喝一壶。比如P99延迟很多人只看平均值结果平均响应时间100msP99已经飙到2秒用户真实体验完全是另一回事。后面我会专门讲指标怎么盯。2. 压测方案设计工具选型比压测本身更重要2.1 为什么开源压测工具差点翻车压测一开始我们直接用常见的开源压测工具。单机模式跑小流量没问题一旦把并发拉高问题全出在压测端自身端口不够用短连接模式下源端口耗尽压测机报错Cannot assign requested address。单进程性能上限有些工具是阻塞IO模型并发上来之后工具自己先CPU打满发出去的请求还没到网关就排队了。分布式协调麻烦用多台压测机虽然能解决单机瓶颈但汇总数据、同步时间、控制执行节奏都比较费事尤其是跑长时长稳定性验证的时候经常出现某台压测机掉线导致整轮数据作废。后来我们用了一段时间自研压测客户端核心思路是连接复用加异步发送。在Go语言里用fasthttp或者net/http的Transport把MaxConnsPerHost调高开启keep-alive压测机本身就能轻松跑出几十万QPS。下面是一个简化版的压测代码骨架package main import ( fmt net/http sync/atomic time ) var ( totalRequests int64 totalErrors int64 ) func worker(client *http.Client, url string, wg *sync.WaitGroup, duration time.Duration) { defer wg.Done() end : time.Now().Add(duration) for time.Now().Before(end) { resp, err : client.Get(url) if err ! nil { atomic.AddInt64(totalErrors, 1) continue } resp.Body.Close() atomic.AddInt64(totalRequests, 1) } } func main() { client : http.Client{ Transport: http.Transport{ MaxIdleConns: 1000, MaxIdleConnsPerHost: 1000, MaxConnsPerHost: 10000, }, Timeout: 10 * time.Second, } var wg sync.WaitGroup for i : 0; i 64; i { wg.Add(1) go worker(client, http://gateway.example.com/test, wg, 60*time.Second) } wg.Wait() fmt.Printf(requests: %d, errors: %d\n, totalRequests, totalErrors) }这段代码的关键在于MaxConnsPerHost和MaxIdleConnsPerHost如果这两个值不调大底层连接池会频繁创建新连接压测机自身会出现大量TIME_WAIT直接影响压测结果真实性。2.2 流量模型怎么搭才像真实业务压测不能只按固定QPS匀速打真实业务的流量是上下起伏的。我们根据业务预测把压测模型分成了四类场景脉冲峰值从低到高在30秒内快速拉升模拟活动开始瞬间的流量洪峰最考验系统的自动扩容和限流预案。持续高压把QPS维持在上限的80%左右持续跑20到30分钟重点观察内存泄漏、连接泄漏、GC频率、日志积压。双重高峰一段时间内出现两次脉冲中间短暂回落。这种场景最容易暴露缓存穿透问题——第一次高峰后热点Key过期第二次高峰直接把DB打崩。混合请求不只是打同一个URL还要按业务比例混合多种接口不同接口的超时时间、依赖资源都不一样单接口压测通过不代表全链路能扛住。流量模型不是压测脚本里随便写个循环就完事。还要控制请求体大小、是否携带Cookie、URL路径分布因为我们发现单个大Body请求会把网关的缓冲区打满和真实的小请求完全不同。2.3 压测环境隔离与数据构造“999999999”专项用的是一套独立压测集群和线上物理隔离但网络拓扑保持一致。为什么强调网络拓扑有一次我们在压测环境里省略了某个缓存节点结果网关直连数据库压测数据一出数据库连接池立刻被打满。后来虽然补上了缓存节点数据结果已经失真。数据构造同样有讲究。不能只构造几条缓存数据然后高并发读要把冷热数据比例构造出来同时保留一部分过期Key专门验证缓存穿透保护逻辑。压测库里的数据量级也尽量贴近线上量级太小会导致索引走偏QPS看起来很高实际线上根本达不到。3. 动手压测前先把系统和网关参数调到不出幺蛾子3.1 内核层参数文件句柄、端口范围和连接队列压测最尴尬的场面不是目标系统崩了而是压测机先弹出一堆连接失败大家还以为是网关挂了。排查一圈发现是压测机的文件句柄限制太低或者端口范围不够。我们执行前会在压测机上做一套标准配置下面这段是通用的可以直接保存成脚本cat /etc/sysctl.conf EOF net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_timestamps 1 net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 net.core.netdev_max_backlog 65535 fs.file-max 1000000 EOF sysctl -p ulimit -n 1048576这里要解释几个关键参数ip_local_port_range本地端口范围使用短连接测试时源端口不够就会报错。tcp_tw_reuse开启TIME_WAIT复用配合tcp_timestamps使用减少四元组重用时的等待时间。somaxconn和tcp_max_syn_backlog如果压测目标服务器接受新连接的能力不足即使程序没问题连接也会在TCP握手阶段被丢掉。但注意tcp_tw_reuse只能在连接发起方配合时间戳使用它改变的是TIME_WAIT超时内的端口重用行为不是所有场景都适用。我们压测机开线上服务器保持默认避免影响真实连接的语义。3.2 网关线程模型与连接管理不要一上来就调线程数网关节点我们用的是常见的多worker进程模型最开始遇到的问题是连接数上去了但worker进程的CPU使用率不均衡。压测发现单workerCPU跑到80%另一个Worker只有20%。后来确认是连接分配不均匀部分长连接集中在同一个worker上。解决办法不是盲目加worker进程而是先看每个worker的连接数分布调整连接分配策略和keep-alive超时时间。下面是调整后的nginx关键配置片段worker_processes auto; worker_rlimit_nofile 1048576; events { worker_connections 40960; accept_mutex off; multi_accept on; } http { keepalive_timeout 30s; keepalive_requests 1000; upstream backend { server 10.0.0.11:8080; server 10.0.0.12:8080; keepalive 1000; } }这些参数里最容易忽略的是upstream keepalive。如果不配网关和后端服务之间每次请求都会新建TCP连接压力一大后端机器上全是SYN_RECV业务超时直接飙起来。keepalive不是越大越好它决定每个worker进程里空闲连接的上限要根据后端服务能承载的连接数反推。后端如果是Java服务同样要查Tomcat或者undertow的连接器配置。我们这次压测发现网关层扛住了但Java服务的acceptCount设置太小大量连接在操作系统层面排队最后表现为客户端超时。Tomcat下相对合理的配置类似server.tomcat.threads.max400 server.tomcat.accept-count2000 server.tomcat.max-connections20000 server.tomcat.connection-timeout5000ms这里的坑在于线程数不是越高越好。线程太多会导致上下文切换频繁GC压力变大。我们一边压测一边观察线程池活跃线程数和等待队列长度发现活跃线程基本稳定在250到300之间于是把max定在400留出一定余量。3.3 下游Mock服务不能先崩全链路压测离不开下游服务但我们不可能把真实业务DB、真实MQ都拿出来压。常见做法是用Mock服务模拟下游。Mock服务看起来简单实际上是最容易出问题的环节。很多压测项目失败并不是目标系统扛不住而是Mock服务自己先崩了导致目标系统出现大量依赖超时结果全被记在目标系统头上。Mock服务的核心要求有两点一是延迟分布要贴近真实不能所有请求都立即返回二是要能扛住目标系统转发过来的全部流量。我们直接用类似前面提到的异步HTTP服务来做Mock把随机延迟控制在1到20毫秒之间CPU跑满也不怕因为Mock服务器不存储状态纯粹回包。4. 阶梯加压与容量评估怎么从曲线里读出上限4.1 阶梯加压先找到稳定区再试探崩溃区压测执行不是一上来就打到目标压力。我们把整个加压过程分成多个阶梯每秒500 QPS起步稳定3分钟观察指标然后跳到每秒2000再稳定再5000再稳定。一直加下去直到找到某个临界点QPS不再上升反而下降或者错误率开始快速增加。这个阶梯的过程本质是给系统一个自适应的机会。很多系统在流量平缓上升时能够自动扩容但如果压力瞬间拉满扩容还没触发系统就已经崩了。阶梯加压可以观察到自动化策略的生效时间。当时跑出来的曲线大致是这样单节点在每秒8000 QPS以内表现稳定CPU在55%左右P99在120毫秒以内到每秒1.2万QPS时CPU升到75%P99开始上升到250毫秒到每秒1.5万QPS时错误率突然窜到5%P99超过1秒。这个现象说明1.2万到1.5万之间就是单节点的性能拐点。我们并没有继续把单节点压到极限而是在1万QPS附近停下来因为生产环境还需要留出至少30%的冗余。4.2 关键指标怎么盯平均值是骗人的压测过程中监控面板上指标特别多但我们真正盯死的只有几个错误率包括连接错误、超时、5xx任何一个超过0.1%就要看原因。P99/P95延迟比平均值重要得多P99上涨往往意味着线程池或连接池开始排队。GC频率和耗时Java服务重点看Full GC年轻代GC频繁说明对象分配速率很高。TCP连接状态重点关注TIME_WAIT、CLOSE_WAIT、SYN_RECV的数量。CLOSE_WAIT堆积基本是应用没有正确关闭连接属于代码问题SYN_RECV堆积则需要看半连接队列是否溢出。日志写入速率如果磁盘IO超过80%日志大概率已经成为瓶颈。这些指标要放在同一个时间轴上对比单独拿出来看都说明不了问题。有一次我们只看到P99上涨以为代码有锁竞争查了半天结果发现是同时刻GC日志在写一个大文件磁盘IO被日志线程吃掉了。指标之间没有相互印证排查方向就会跑偏。4.3 容量评估用压测数据反推节点数压测结束后的容量评估是“999999999”专项最重要的交付物。核心公式其实很简单所需节点数 预估峰值QPS / (单节点实测QPS x 冗余系数)举例单节点在P99稳定、错误率达标的前提下实测QPS上限是1万目标峰值是100万冗余系数取0.7。那么节点数就是100 / (1 x 0.7) 约等于143个节点。这里冗余系数0.7意味着每个节点只用到70%的能力剩下30%留给突发流量和单点故障场景。冗余系数不是拍脑袋定的主要看业务容忍度。如果系统具备秒级自动扩容能力系数可以到0.8如果扩容需要人工介入建议保守一点取0.6。我们最后给出的建议是145个节点并且用这组数据重新压了一轮整体P99稳定在200毫秒以内才算真正验收通过。5. 压测实录五个把团队折磨到深夜的问题5.1 压测机先崩了你以为在压目标其实在压实测机第一次全链路压测我们压了不到5分钟监控显示目标系统压力没上来压测端倒是先报了一堆端口分配错误。排查过程其实不复杂先看压测机的连接数发现大量短连接处于TIME_WAIT状态。因为压测脚本最初没有复用连接每个请求都新建TCP连接测试结束后四元组进入TIME_WAIT端口都被占住了。后来把客户端改成强制连接复用同时调大了端口范围问题立刻消失。这个坑的教训是压测结果如果出现大面积“连接失败”优先检查压测机自己的网络状态不要第一个怀疑目标服务器。压测工具产生的异常连接状态会直接污染整个测试数据。5.2 连接风暴网关CLOSE_WAIT堆积第二个问题出在网关到后端服务这一层。压测中我们看到网关节点上的TIME_WAIT不多但CLOSE_WAIT从几十涨到几万随后接口超时开始蔓延。CLOSE_WAIT表示对端后端服务已经关闭连接但本端网关还没有完成关闭流程。最常见的触发原因是网关把请求转发给后端后没有及时读取或关闭响应流。当时我们排查了后端服务发现它在高并发下主动关闭了一部分连接但网关侧由于连接池回收逻辑不完善没能及时处理这些半关闭连接。解决方案有两步一是网关到后端的连接池配置加上了idle超时和异常连接清理逻辑二是后端服务调大了连接接受队列减小主动断开连接的概率。这类问题在低并发时很难暴露只有连接数到达一定量级才会触发。5.3 下游服务雪崩线程池队列爆了第三轮压测我们把流量拉到峰值附近。目标网关指标还算平稳但下游某个核心服务的线程池队列突然开始迅速增长紧接着该服务出现大面积超时网关的重试机制又把流量重新打过来越滚越大最终变成雪崩。查看线程池监控才发现活跃线程数其实没有到max但是队列长度已经超过10万。因为该服务的线程池配置合理但拒绝策略是CallerRunsPolicy也就是说线程池满了之后调用线程自己来执行任务。调用线程本身是网关的worker线程这样一来网关的响应也被拖住了整体延迟被拉爆。修复方式是调整线程池队列容量以及拒绝策略。更重要的是我们在网关层加上了快速失败机制当下游超时比例超过阈值时网关不再无限重试而是直接返回降级结果给下游留出恢复时间。这一改动在下一轮压测中起到了关键作用雪崩没有再发生。5.4 日志把磁盘写满全量日志在高压下是灾难压测进行到第15分钟告警突然弹出压测环境中的两个节点磁盘使用率超过90%。一开始还以为是压测生成的临时数据登录上去才发现是应用的全量访问日志。为了快速定位问题压测期间我们把日志级别从INFO调整到了DEBUG加上请求体比较大每分钟产生几个GB的日志磁盘很快就满了。这类问题在真实业务里非常常见。日志系统在高并发下看似微不足道的写入量会被请求量放大几百倍。我们后来把压测期间的日志策略改成INFO级别只记录关键链路RequestBody默认不打印日志输出改成异步写盘并且给日志文件单独挂载了一块磁盘和业务数据物理隔离。这样即使日志量爆炸也不会把系统盘占满。5.5 监控延迟导致误判数据没回来大家都慌了第五个问题不算故障但比故障更影响判断。有一轮压测结束监控大屏上的错误率还在持续上升团队差点把已经跑完的压测当成失败案例。后来发现监控系统采用定时聚合数据采集存在一分钟左右的延迟压测已经结束了后台还在处理堆积的指标。这件事提醒我压测时的监控数据延迟和业务延迟一样重要。我们为专项压测单独搭了一套实时性更高的监控通道数据采集周期调整到5秒关键指标直接写入独立的时序库。之后每一次加压都能在十几秒内看到真实反馈压测效率提升非常明显。问题现象直接原因解决方法压测机端口耗尽短连接没有复用客户端开启keep-alive调大端口范围网关CLOSE_WAIT堆积连接池回收不完善调优连接复用和空闲清理下游线程池队列暴涨队列策略导致调用线程阻塞调整拒绝策略加快速失败磁盘写满DEBUG日志输出过多异步日志、关键链路采样、独立磁盘监控数据滞后聚合周期过长专项压测使用短周期监控通道6. 可直接复用的压测配置与命令速查6.1 压测脚本骨架如果你不想从零写压测客户端可以直接参考下面这个思路外部传入并发数、压测时长、目标URL内部用协程池和连接复用发起请求每隔一段时间打印QPS和错误率。核心代码在2.1节已经给出实际使用时只要把worker数量和压测时长参数化就行。我单独提一个易错点压测脚本的输出频率不要太高。每10秒输出一批QPS数据足够输出太密集会在压测机上产生大量小文件写入影响施压端性能。6.2 内核与连接层配置速查下面是整个专项下来沉淀的一套配置适用于大多数基于TCP的网关和Web服务压测文件句柄ulimit -n 至少到1048576。内核参数参考3.1节注意tcp_tw_reuse只配置在施压端。网关keep-alive连接数上限、空闲时间、单连接请求数三个参数要联动调整。后端线程池线程数、队列大小、拒绝策略必须做组合验证不能单看线程数。监控通道压测期间缩短聚合周期关键指标实时刷新。6.3 最后一个小建议压测不只是测上限还要测恢复“999999999”专项做完了我印象最深的不是那根漂亮的QPS曲线而是压测结束后的一次额外演练突然把压测流量撤掉观察系统能不能自动恢复。很多系统能扛住压力但撤掉压力后连接池和线程池不会立刻回收大量连接处于半开状态导致恢复期依然有错误请求。如果恢复时间过长下一次流量高峰再打过来系统会带着一身问题重新接入结果只会更糟。所以我建议每一个压测专项都加上“止血和恢复”验证。压测结束后至少留出10分钟的观察窗口看CPU是不是回落到正常水位、线程池活跃数是否回落、错误率是否归零。这个数据和压测峰值数据一样值得写进报告。回头再看“999999999”这个代号它从一开始就不只是一个数字。九亿QPS当然不是真实目标但每一次压测执行我们都是在问同一个问题当所有东西都按最坏情况发生的时候系统还能不能稳定地给出回答。做了这么多轮压测我最大的体会是极限压测的核心价值从来不是证明系统有多强而是在真正的流量洪峰到来之前把系统最脆弱的那一面找出来然后亲手修好它。