ARTICLE DETAIL

资讯详情

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

压测实战指南:从场景设计到MySQL性能调优

压测实战指南:从场景设计到MySQL性能调优 做压测这行当最怕的不是测出来的数据难看而是测完不知道下一步该干什么。最近手头正好在推进一个代号为[特殊字符]的内部接口服务压测与性能调优项目从压测方案设计、脚本编写、施压执行到数据库层的慢SQL治理和参数调整前前后后把整条链路完整走了一遍。这篇就把完整过程拆开来讲从场景设计到MySQL调优再到压测报告怎么写适合刚接触压力测试的后端开发、测试工程师也适合那些被线上接口性能问题折磨得想直接改行的运维同学。先说一个基本判断压力测试不是拿工具把请求量怼上去就完事。它本质上是给系统做一次可控的“极限试探”通过逐步增加负载来观察吞吐量、响应时间、错误率和资源水位的变化找到系统的性能拐点和瓶颈位置。[特殊字符]这个项目之所以要专门做一轮压测是因为上线前只做过功能回归对并发能力完全没底。与其等线上用户来当测试员不如在压测环境里先把问题暴露出来。1. 压测前先想清楚这个项目到底要压什么1.1 明确测试目标和业务模型动手压测之前第一件事不是打开JMeter而是坐下来把“业务模型”理清楚。什么叫业务模型就是你要模拟的流量到底是什么样子是用户登录后疯狂刷新列表页还是下单接口的大促峰值或者是定时任务凌晨批量扫表的场景不同业务模型对应的压测重点完全不一样。[特殊字符]这个项目是一个偏读多写少的查询服务核心接口是列表查询和详情查询占比大约7:3。这就决定了压测重心要放在查询链路上包括Nginx入口、应用线程池、数据库连接池、缓存命中率这几个环节。如果一上来就均匀地压所有接口反而会把真实瓶颈淹没在噪声里。另一个关键是确定目标水位。怎么定一般看两个依据一是现有线上流量的峰值和增长预期二是业务方给出的SLA要求。当时业务给的底线是核心接口在高峰期单机QPS不低于800平均RT控制在200ms以内P95不超过500ms错误率低于0.1%。有了这三个数字压测和目标调优就有明确的收敛条件了不然测到什么时候算结束都说不清楚。1.2 场景设计从单接口到全链路场景设计建议分三层来做每一层的意义不一样别想一口吃成胖子。第一层是单接口压测目的是摸清每个核心接口自己的极限。这个阶段最干净可以排除接口之间的互相干扰快速定位某个接口本身的性能缺陷比如SQL没走索引、JSON序列化太慢、Redis连接池不够等。通常我会挑流量占比最高的两三个接口每个单独跑一轮。第二层是混合链路压测模拟真实用户在一个会话里的连续操作。比如用户先查询列表再点进详情中间可能触发一次写入。混合链路要按真实比例分配流量这样才能暴露接口之间的资源竞争比如应用线程池被慢查询占满、数据库连接被长事务耗尽。第三层是全链路压测把缓存、消息队列、第三方依赖全部纳入压力范围。这层最接近真实线上但实施成本也最高通常需要独立的压测环境和挡板系统来隔离外部依赖。[特殊字符]项目这次做了前两层第三层因依赖系统改造进度问题这轮没做但我在报告里明确标注了遗留风险。1.3 关键指标定义QPS、RT、错误率、资源水位压测必须有一套共同语言不然开发说“还行”测试说“扛不住”双方根本聊不到一块去。QPS每秒查询数是吞吐量指标体现系统处理能力。但单看QPS没意义必须和RT、错误率放在一起看。QPS上去了、RT飙到好几秒这种高吞吐是不健康的。RT响应时间要重点关注平均值和百分位值。平均值很容易骗人100个请求里99个是10ms1个是5s平均值还是59ms。所以压测报告里一定要看P95、P99这两个数字代表绝大多数用户的真实体验。错误率是压测的“红线”指标超过阈值就要立刻停止加压排查原因。错误类型也要区分是超时、连接拒绝、5xx还是业务异常码不同错误指向不同问题。资源水位包括CPU、内存、磁盘IO、网络带宽以及更细的线程池活跃数、数据库连接数、GC频率。压测时如果CPU没到80%系统就不行了那是应用或数据库有问题如果CPU已经打满那你先想的是扩容还是优化。2. 工具选型与压测脚本设计2.1 为什么我选了这套工具组合压测工具五花八门有开源的JMeter、Locust、wrk、k6也有各种云上PTS这类信息压力测试网页版平台。[特殊字符]项目为什么最终选了JMeter主要原因是团队已有一定的JMeter基础脚本维护成本低而且它能覆盖从单接口到混合链路的全部场景不需要在多个工具之间来回切换。具体组合是JMeter负责施压生成、Prometheus加Grafana负责系统和应用层监控、mysqld_exporter负责数据库指标采集再加上Arthas在需要时做JVM层面的在线诊断。为什么不用纯wrk这类轻量工具因为它只能做最简单的HTTP压测没法做复杂的参数关联和断言遇到登录态、签名这类场景就抓瞎了。云压测平台虽然省事但我们压测环境是内网隔离的数据出不去所以最后还是本地化部署。有一点要提醒JMeter是Java写的施压机本身也会消耗CPU和内存。如果压测目标本身吞吐很高而你的施压机性能不够结果会先打到施压机的瓶颈测出来的数据完全失真。后面我会专门讲施压机怎么算。2.2 脚本参数化与数据准备避开压测数据坑压测脚本里最容易踩的坑就是数据问题。最典型的表现是线上并发1000没毛病压测环境一压就报错查了半天发现是压测数据只有几条数据库里所有请求都在抢同一行记录的行锁。所以参数化和数据准备必须放在脚本设计阶段一起考虑。参数化方面[特殊字符]项目用JMeter的CSV Data Set Config来驱动用户ID和商品ID准备了20万条独立的业务数据保证每个虚拟用户拿到的数据都不一样。同时用正则表达式或者JSON Extractor做了接口间的关联比如登录接口返回的token要动态提取出来传给后面的查询请求不然所有请求都用同一个token根本模拟不了真实场景。数据量还有一个容易被忽略的点压测环境的数据分布要和线上对齐。如果线上这张表有5000万行你压测环境只有500万行索引效果和SQL执行计划可能完全不同最后测出来的性能参考价值会大打折扣。[特殊字符]项目因为隐私合规限制不能直接拉线上数据当时用了数据脱敏工具按线上分布重新生成了一份测试链路和SQL执行计划才算对得上。2.3 施压机的资源规划和并发模型计算施压机数量怎么算有个粗略公式并发线程数乘以单请求平均带宽再乘以目标QPS可以估算出需要的网络带宽。更实际的做法是由目标QPS反推假设目标QPS是2000单请求响应时间平均150msJMeter单线程或者说单循环每秒大约能完成6-7个请求那至少需要300左右的并发线程数。而JMeter一个线程本身占用一定的CPU和内存300个线程压测实测下来4核8G的机器能勉强支撑但如果目标QPS再翻一倍就得考虑分布到多台施压机。并发模型这块[特殊字符]项目用的是JMeter的线程组加Constant Throughput Timer来控制吞吐量而不是简单地把线程数调到最大。因为压测的目的不是“有多少线程就发多少请求”而是按照预定的QPS曲线去施压。比如想要从100 QPS起步每5分钟涨一倍加到2000 QPS封顶就在不同线程组里配置不同的吞吐目标让压力呈阶梯状上升。注意施压机自身的内存配置要留意。JMeter跑大规模压测时GUI模式会吃掉大量内存生成图表建议压测时用jmeter -n -t脚本的非GUI模式执行压力结果通过-l参数输出为jtl文件最后再用GUI模式打开分析。3. 压测执行与指标观测3.1 从小并发到大并发的阶梯式加压策略压测执行最忌讳一上来就直接拉到目标并发数。如果系统直接被打崩你根本不知道它是在哪个负载级别开始出现问题的。所以压测执行要按阶梯式加压来每一档保持一定时间观察指标确认平稳后再往上加。[特殊字符]这轮的加压节奏是100并发持续5分钟然后200并发5分钟500并发5分钟800并发10分钟1000并发10分钟最后顶到1200并发并保持15分钟观察崩溃点。每档之间都记录当前QPS、平均RT、P95 RT、错误率和CPU/内存水位。当加压到1200并发的时候接口P95从220ms一路涨到960ms错误率开始出现0.05%左右的超时说明系统已经接近性能拐点。这里要特别强调一下“持续观察”这个动作。很多新手压测是并发数到了、跑完一分钟就收工这远远不够。系统的性能劣化往往是慢慢积累的比如连接池在持续压力下被慢请求占满、内存逐渐被撑大触发频繁GC、日志文件膨胀拖慢磁盘IO。短时间压测根本发现不了这些问题所以每一档至少要稳定5分钟以上顶峰值那档建议跑15-30分钟。3.2 监控体系搭建应用、中间件、数据库三层观测压测过程中如果只看JMeter的结果聚合报告就像开车只盯仪表盘上的速度表引擎水温、机油压力全都不看等于盲开。我当时搭了三层监控第一层是应用层重点看接口RT、线程池活跃度、Tomcat队列堆积、JVM堆内存和GC日志。用Prometheus的JMX exporter把JVM指标接进来Grafana里直接看堆内存曲线。第二层是中间件层包括Redis的命中率、慢命令数以及Nginx入口的请求量和upstream响应时间。第三层是数据库层看连接数、活跃会话数、慢查询数量、InnoDB缓冲池命中率、redo log写入频率。这三层指标不是孤立看的要放在同一时间轴上对照。比如某段时间JMeter显示RT从200ms涨到800ms光看应用线程池发现没满但数据库监控显示慢查询数在同步猛增同时InnoDB的行锁等待时间变长那就很容易定位到是数据库侧的问题而不是应用线程不够。3.3 瓶颈定位的常用三板斧定位性能瓶颈时我有一个习惯性的排查顺序基本三板斧就能覆盖大多数场景。第一板斧看CPU。用top -Hp查看应用进程内每个线程的CPU占用如果某个线程CPU很高就用jstack导出线程栈看看它到底在忙什么。如果是用户线程在执行计算密集任务那是业务代码问题如果大量线程处于RUNNABLE但都在等待同一个锁那就是锁竞争问题如果CPU不高但RT飙高基本可以排除CPU瓶颈往下看IO和锁。第二板斧看数据库慢查询。把慢查询日志打开long_query_time设置成1秒压测过程中实时抓慢SQL。哪个SQL出现频率最高基本就是它拖垮了接口。定位到慢SQL之后用EXPLAIN分析执行计划重点看type字段是ALL全表扫描还是range/ref索引扫描以及rows扫描行数和Extra里有没有Using filesort或Using temporary。第三板斧看GC。如果压测过程中CPU忽高忽低、RT出现周期性尖刺大概率是GC问题。用jstat -gcutil观察Young GC和Full GC频率如果Full GC频繁基本上是堆内存设置不合理或者内存里塞进了大量大对象。[特殊字符]项目在压测中发现过每分钟四五次Full GC后来排查是因为一个接口把全量配置一次load到内存做匹配虽然缓存了结果但是匹配用的临时Map在每轮请求都会重复构建改掉之后GC立刻恢复正常。4. MySQL性能调优压测中暴露出来的真问题4.1 从慢查询日志入手定位SQL问题MySQL性能调优是压测项目里最耗时也最见效的部分。[特殊字符]项目压测到500并发时慢查询数量开始明显增多于是直接开启慢查询日志抓现场SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_output TABLE;为什么建议log_output用TABLE因为线上场景用文件日志还要去服务器上翻用mysql.slow_log表之后可以直接SQL查询排名靠前的慢语句配合mysqldumpslow工具聚合分析更方便mysqldumpslow -s c -t 10 /var/lib/mysql/*-slow.log抓出来的慢SQL集中在核心列表查询上。那条SQL本身看起来不长但执行计划里type是ALL扫描行数达到百万级更致命的是排序字段和WHERE条件里的字段不在同一个索引上导致Extra里出现了Using filesort。文件和排序直接在临时表上做并发一高性能立刻崩。4.2 通过EXPLAIN看懂执行计划定位到慢SQL之后我用EXPLAIN把执行计划完整看了一遍这里直接贴当时的关键分析思路EXPLAIN SELECT id, name, status, create_time FROM info_table WHERE status 1 AND category_id 52 ORDER BY create_time DESC LIMIT 20;这个执行计划有几个关键信号typeALL意味着全表扫描rows预估扫描120万行Extra里有Using filesort。也就是说数据库需要先把满足条件的120万行都捞出来再在内存里做排序然后才取前20条。压测并发一高磁盘IO和临时表空间双双告急。正常的优化路径是建联合索引。因为查询条件是status加category_id两个等值条件排序字段是create_time要消除filesort索引设计要遵循“等值字段在前排序字段在后”的顺序ALTER TABLE info_table ADD INDEX idx_status_cat_time (status, category_id, create_time);建完索引后执行计划里type变成了refrows降到几百行Extra里不再出现Using filesort这条SQL的单次执行时间从300ms级别降到10ms以内。压测数据对比也很直接同样的800并发下接口平均RT从680ms降到了155ms。4.3 索引、缓冲池、连接池参数调整的实操经验SQL层面的问题处理完之后MySQL实例层面的参数也要过一遍。重点是三个参数第一个是innodb_buffer_pool_size。这个参数决定InnoDB把多少数据页缓存在内存里。如果命中率高大部分查询直接走内存根本不会碰磁盘。[特殊字符]项目的库大概60GB实例内存64GB最初buffer_pool只有8GB命中率不到80%。后来调整到40GB命中率升到99.2%。调整依据有一个经验值buffer pool至少要能装下你的“工作集”也就是高频访问的那部分数据和索引而不是把整个库都塞进去。第二个是innodb_buffer_pool_instances。buffer pool调大之后如果实例数不跟着调并发高的时候会有缓冲池的锁竞争。修改原则是每个instance不小于1GB当时40GB的buffer pool分了8个instance。第三个是数据库连接池。连接池不是越大越好连接数越多上下文切换开销越大。我当时根据应用的总并发和单连接处理时间把连接池上限从300调低到100配合应用侧线程池限流反而让性能更平稳了。计算方式很简单连接池大小 应用节点数 × 每节点线程池峰值并发 × (单请求数据库耗时 / 单请求总耗时)。如果单请求总耗时200ms数据库耗时20ms比例是0.1那3000个应用线程并发时数据库连接池只需要300个左右就够了而不是盲目配到1000。5. 压测后的优化验证与回归5.1 优化效果量化对比怎么证明改动有效调优做完必须有数据证明改动有效不能嘴上说“好像变快了”。我习惯的做法是固定压测参数用同样的并发曲线、同样的压测时长在优化前后各跑一轮然后把关键指标列成对比表。[特殊字符]项目这轮的优化效果对比是这样的指标优化前优化后变化核心接口QPS8202150162%平均RT380ms135ms-64%P95 RT920ms270ms-70%错误率1.2%0.03%明显下降数据库慢查询数/分钟462大幅减少这里有一个细节要强调前后对比时压测机的配置、并发梯度、脚本必须保持一致否则对比没有任何意义。我当时连JVM参数和数据库配置变更都做了记录确保变量只有“优化”本身。5.2 稳定性测试与长尾效应不只盯平均值性能调优常见误区是只盯平均RT变快了然后就说“搞定”。但很多系统在短时压测下表现不错长跑之后就出问题典型的例子是内存缓慢泄漏和连接池逐渐耗尽。所以优化验证之后我又补了一轮稳定性测试用800并发持续压测4小时重点观察QPS曲线是否平稳、RT是否出现周期性尖刺、内存曲线是否逐步上涨、数据库连接数是否持续累积。这轮跑下来还真发现了一个问题一个定时任务会周期性加载大量数据导致堆内存每两小时出现一次陡增触发一次Full GCRT同步出现一个约600ms的毛刺。后来靠调整任务执行时间和分批处理把问题解决了。长尾效应也值得单独说。优化后平均RT确实到了135ms但细看P99还是偏高达到450ms。这说明仍然有少量请求被卡在某些环节可能是偶尔的GC停顿、偶发的锁等待或者网络抖动。如果能接受这个长尾可以不管但如果是那种强一致、对时延极敏感的业务就得继续往底层挖。5.3 性能基线与常态化回归机制压测和调优做完一次不代表一劳永逸。接口代码每改一次、数据库表结构每动一次、第三方依赖每升一次版本性能都可能悄悄劣化。所以我在这轮压测之后顺手做了两件事一是把压测脚本和场景配置纳入代码仓库统一用JMeter脚本管理关键接口的压测场景可以随时一键执行。二是建立了一份简单的性能基线表里面记录每个核心接口在特定并发下的QPS、平均RT、P95、错误率和资源水位。以后每次发版前如果涉及核心接口改动就会跑一次冒烟压测拿结果和基线对比超过10%的劣化就要拦截评审。这套机制投入不大但价值很高。它让性能问题不再靠“上线后被用户发现”而是在发布前就能拦下来。6. 常见问题与排查技巧实录6.1 压测中我踩过的5个典型坑这轮压测下来印象深刻的问题有这么几个第一个坑是压测数据不够导致的锁等待。最初只准备了1000条测试数据压到500并发时数据库行锁等待时间飙升大量请求堆积在UPDATE操作上。后来准备了20万条数据问题直接消失。第二个坑是JMeter施压机先于目标系统崩溃。跑到1200并发时施压机CPU被打满导致压测结果出现大量超时但目标系统其实还没到极限。换了4台施压机做分布式施压才解决。第三个坑是线程池满导致的雪崩。应用侧默认Tomcat最大线程数是200压到一定量后请求全在队列里排队RT飙升。后来根据接口耗时和期望QPS重新计算了线程池大小并配置了合理的拒绝策略。第四个坑是Redis缓存穿透。压测用的查询条件大量命中不存在的数据缓存没有生效请求全部压到数据库。解决办法是缓存空值并加了布隆过滤。第五个坑是日志拖垮性能。压测时应用开启了DEBUG级别日志每请求输出几十行日志文件写盘成了新的瓶颈。压测环境下日志级别调成WARN性能立刻回升。6.2 问题排查速查表压测问题排查时很容易慌下面这张速查表是按优先级排序的遇到问题先从第一行开始对照症状优先排查对象常用手段RT飙升但应用CPU不高数据库慢查询、缓存命中率、网络IO慢查询日志、EXPLAIN、Redis监控应用CPU打满业务线程死循环、序列化开销、正则回溯top -Hpjstack定位线程栈错误率突然上升连接池耗尽、线程池拒绝、依赖超时查连接池监控、Tomcat线程状态周期性RT尖刺GC频率、定时任务、备份任务jstat -gcutil、查看定时任务执行日志内存持续上涨内存泄漏、大对象缓存、静态集合Heap dump后用MAT分析数据库连接耗尽连接池配置过大或过小、长事务show processlist、调整连接池参数这张表的价值在于给团队一个共同的排查起点不至于每次都是从头查一遍。6.3 一份可以直接参考的压测报告结构最后说说压测报告。报告不需要做得多花哨但结构必须完整。我会包含几个固定板块测试背景与目标、测试环境说明硬件、软件版本、网络拓扑、测试数据和脚本设计说明、压测执行记录每档并发、时长、观测到的指标、瓶颈分析与优化过程、优化前后对比数据、稳定性测试结果、遗留风险与后续计划。其中“遗留风险”这块特别重要。压测不是表演测出来问题不丢人藏着掖着才要命。比如[特殊字符]项目这轮因为时间限制没有做全链路压测第三方依赖系统的联合压测往后排了这些遗留项在报告里都明确标了优先级和负责人方便后续跟进。压测和调优这件事我个人做了这么多轮之后最大的体会是别迷信工具也别迷信参数。工具只是手段参数调整必须回到业务模型和实际观察数据上来。每一轮压测结束真正有价值的产出不是那份“系统扛住了多少并发”的结论而是你对系统的理解又深了一层知道它会在什么条件下变慢、为什么会变慢、怎么才能让它平稳扛住更高的流量。[特殊字符]这个项目后续如果再改核心接口或者要做容量预估这一轮留下的脚本、基线和排查方法可以直接复用。如果你们团队正准备做压测希望这份完整指南能帮你少踩几个坑。
返回列表