ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JMeter压测ETL任务全流程:脚本设计、瓶颈定位与性能分析实践

JMeter压测ETL任务全流程:脚本设计、瓶颈定位与性能分析实践 接到一个大数据集成平台的压测任务要求我用JMeter对ETL任务做一轮压力测试把性能瓶颈找出来。说实话JMeter大家都不陌生一般拿来做接口压测、Web性能测试比较多但拿它压ETL很多人第一反应是“这能压吗怎么压”。实际上完全可行关键是想清楚ETL任务的负载入口是什么。这篇文章就把我这次压测的完整思路、脚本设计、压测执行和瓶颈定位过程记录下来适合正在做数据集成平台性能验证或者刚接手ETL压测的测试开发同学参考。我当时遇到的场景是这样的平台上有几十个ETL任务每天定时跑批偶尔会出现凌晨任务积压到了早上报表出不来。老板要求搞清楚这些任务到底能撑多少并发瓶颈是在调度层、数据库还是计算引擎。于是我用JMeter设计了一套针对ETL触发接口和任务状态轮询接口的压力测试脚本配合数据库监控和日志分析最终定位到了两个主要瓶颈。整个过程算不上复杂但有不少坑下面按思路展开。1. 压测ETL之前先理清这三件事1.1 ETL任务的真实负载入口在哪很多人一上来就打开JMeter创建线程组添加一个HTTP请求随便填一个URL以为这就是压测ETL了。这么做大概率压的不是ETL而是某个前端页面或网关。ETL任务不像普通查询接口那样一次请求返回结果它通常是一个异步过程平台提交任务调度系统下发底层计算引擎执行执行完写回目标库。所以压测前必须先搞清楚这台平台对外暴露的“任务提交入口”到底在哪。常见入口有几种如果平台是自研的一般有REST API类似POST /api/etl/run前端点击“运行任务”就是调这个接口如果是开源工具比如Kettle、DataX、Sqoop这类可能会通过命令行走或者通过Java SDK调用还可能存在消息队列入口上游系统发一条消息ETL任务被触发。我这次压的是自研平台入口是HTTP接口所以JMeter最顺手。如果你的场景是命令行触发也可以考虑用JMeter的OS Process Sampler但可控性不如HTTP方式建议优先推动平台暴露API这本身也是平台化建设的需要。还有一个容易忽略的点任务提交接口往往是异步返回的你提交成功不等于任务跑完。所以压测脚本不能只打提交接口还要轮询任务状态接口直到任务真正完成。否则你测出的只是调度系统的接收能力不是ETL的实际处理能力瓶颈会被完全掩盖。1.2 压测目标与指标怎么定压测前一定要把目标写清楚不然别人问起来你只能回答“我觉得挺卡的”。我一般会把指标分成三层吞吐量、响应时间、资源消耗。对于ETL场景吞吐量通常不是每秒请求数而是每分钟或每半小时能稳定跑完的任务个数更准确叫“任务完成率”和“任务排队耗时”。响应时间方面重点关注两个值提交接口的返回耗时以及从提交到任务状态变成成功的整体耗时。另外强烈建议单独记录P95和P99不要只盯着平均值。ETL任务有个特点偶发性慢任务往往才是积压的元凶。比如平均耗时3分钟但P99跑到15分钟说明系统在极端负载下抖得非常厉害批处理调度就会崩溃。我用聚合报告和自定义Sampler记录耗时分布再结合平台日志里的任务开始和结束时间能比较准确地还原出排队情况。成功率的定义也要提前说清楚。HTTP 200不算成功任务状态为“SUCCESS”才算成功。如果任务提交了但执行失败或者产生了重复数据、丢失数据这属于更严重的质量问题。所以我会在脚本里加断言既校验接口返回码也校验任务状态和最终数据量确保压测过程中没有产生脏数据。1.3 环境与数据准备要做足ETL压测对环境的要求比普通接口压测高得多。首先是数据量如果源表只有几千行ETL秒级跑完根本压不出瓶颈。需要准备和线上量级一致甚至更大的数据至少让单次任务耗时在几十秒到分钟级这样并发增加时才看得出排队效果。其次要准备多组数据不要所有压力线程都拿同一个主键去跑否则大量请求会盯着同一行数据性能结果会失真。我这次的做法是准备了一个独立的压力测试Schema从生产数据脱敏后灌入再按不同日期范围、不同分区字段拆成多份用CSV参数化让每个线程组各自读不同数据。这样既不会污染线上数据也能模拟真实的数据倾斜场景。压测前还做了一次单任务冒烟测试确认数据能正常跑到目标表并且记录一条最简单的时间基线。别小看这一步很多问题比如权限、连接串错误、目标表锁冲突在单任务环节就能提前暴露能省不少排错时间。2. JMeter做ETL压测的工具选型与脚本设计2.1 为什么选JMeter而不是其他工具压测工具的选择直接影响项目进度。LoadRunner功能确实强但授权贵安装部署也重Gatling性能和逼格都高但写Scala脚本的学习成本摆在那里团队大多数人不会自研压测脚本倒是灵活但开发维护成本高等脚本写完项目都上线了。JMeter最大的优势是开源、协议覆盖广、社区资料多团队里只要有人会Java或Groovy脚本能力几乎不受限制。更关键的是JMeter的线程模型和插件生态非常适合“从零起步做容量摸底”。通过线程组可以精确控制并发数、Ramp-Up时间、持续时长配合CSV参数化能模拟多用户多数据请求配合JMeter插件可以做出阶梯加压的曲线。对于ETL这种需要长时间压测的场景非GUI命令行模式足够稳定可以在服务器上挂着跑几小时中间不容易崩。可以说用JMeter做数据集成性能测试是性价比很高的选择。如果项目对实时监控要求更高可以考虑JMeter的Backend Listener把指标直接发送到InfluxDB和Grafana做可视化看板。这个我在第三节详细说。工具选型的原则很简单够用、可维护、团队能上手JMeter在这三点上都比较均衡。2.2 三种常见施压方式HTTP接口、JSR223、JDBC根据ETL任务入口的不同JMeter脚本有三种典型写法我逐个说一下适用场景。第一种是HTTP接口方式也是最常见的。ETL平台一般会提供任务提交接口、任务状态查询接口有时候还有任务停止接口。JMeter里就是普通的HTTP Sampler通过POST JSON体触发任务再循环轮询状态。这种方式测的是整条链路Nginx、网关、调度服务、任务队列、计算引擎甚至数据库全都会被压到定位问题最方便因为每一层的日志时间戳都能对上。第二种是JSR223 Sampler方式也就是用Groovy脚本去调用内部Client或SDK。这种方式适合平台没有开放HTTP接口或者需要在压测前构造复杂作业配置的场合。比如我可以直接在Groovy里new一个Client对象把任务配置拼好然后调提交方法。好处是绕过了网关能更准确地测试底层ETL引擎本身坏处是脱离了真实链路测出来的结果不能代表用户侧的感受。我一般在需要对比“是网关瓶颈还是引擎瓶颈”时才会单独跑一轮JSR223压测。第三种是JDBC方式也就是JMeter自带的JDBC Connection Configuration加JDBC Request直接对源库或目标库施压。这个方式主要用来测数据库层的极限能力模拟上游并发写入或下游并发读取。很多人问为什么ETL压测要压数据库因为很多ETL瓶颈就出在SQL语句、索引、锁等待上。用JDBC方式单独把数据库压透一次能快速确认是不是SQL层的问题。不过要注意JDBC Request走的不是平台的任务调度所以它只是辅助手段不是完整的ETL压测。我这次先做了HTTP方式全链路压测发现任务积压后再用JDBC方式单独压了数据库从而把瓶颈定位到了数据库连接池参数上。三种方式配合起来比单打一种要有效得多。2.3 线程组与参数化设计细节线程组设计是压测脚本的骨架。对于ETL任务我强烈不建议一开始就设100个并发猛压而应该分级加压。我用的是“5、10、20、40、80”五级阶梯每级持续5分钟观察系统在哪一级开始出现任务排队或失败。如果直接压100并发系统可能瞬间被打到假死日志一大堆反而不好定位。Ramp-Up Period的设置也很有讲究。假如线程组是20个线程如果Ramp-Up设成0秒就是瞬间发20个并发这对ETL调度系统来说太突兀真实场景下任务大多是错峰提交的。我会根据一次任务的平均耗时来设置Ramp-Up比如线程数20、单任务平均耗时1分钟就让Ramp-Up设成60秒相当于每分钟均匀提起20个任务更接近实际批处理节奏。参数化是保证结果准确的关键。ETL任务往往需要区分任务ID、源表、日期分区。推荐用CSV Data Set Config读取参数文件格式如下taskId,sourceTable,pt etl_task_001,ods_order_20240101,20240101 etl_task_002,ods_order_20240102,20240102CSV配置里要注意Encoding设置为UTF-8分隔符用英文逗号避免中文乱码。线程共享模式建议选择“Current thread group”保证每个线程从头到尾读取自己那一行不会出现两个线程抢同一任务的情况。这样压测数据才是均匀的不然热点全堆在同一个任务上结果没有参考意义。3. 实操全过程从零压测一个ETL同步任务3.1 搭建JMeter压测环境JMeter本身不需要安装下载对应平台的二进制压缩包解压就能用。前提是机器上要有JDK推荐JDK 8或JDK 11新版JMeter对JDK版本有最低要求我这次用的是JDK 8加JMeter 5.6.3跑得很稳。解压后把JAVA_HOME环境变量指到JDK目录再进bin目录执行jmeterLinux/macOS或jmeter.batWindows启动即可。命令行可以用jmeter -v验证版本。不过压测ETL任务我一般不在GUI里跑最终压力。GUI模式本身会消耗不少内存和CPU还会因为界面实时绘图影响压测结果。我通常的做法是先用GUI模式调试脚本看脚本逻辑和断言是否正确确认无误后再保存jmx脚本改用命令行执行压测jmeter -n -t etl_stress.jmx -l result.jtl -e -o ./report其中-n是非GUI模式-l输出原始结果文件-e和-o生成HTML报告。执行期间可以用-j参数指定日志文件方便观察压测进度和异常。压测机的性能也要注意。不要小看JMeter客户端本身如果线程数开到几百客户端CPU内存不够JMeter自身的GC就会拖慢请求发送速度导致结果偏乐观或偏悲观。我习惯把压测客户端放在独立机器上至少4核8G压测时用top或任务管理器盯着JMeter进程的资源占用。3.2 编写第一个ETL压测脚本我把脚本拆成两步提交任务和轮询状态。可以建一个“循环控制器”把轮询逻辑放在里面最多轮询20次每次间隔3秒防止任务还没跑完就结束采样。结构大概是这样的简单列一下步骤新建线程组设置线程数为10Ramp-Up为30秒循环次数为1勾选“调度器”并设置持续时间为600秒。添加“HTTP Header Manager”配置Content-Type为application/json鉴权Token放在里面。添加“HTTP Request”作为任务提交Sampler方法POST路径填/api/etl/runBody Data写JSON例如{ taskId: ${taskId}, sourceTable: ${sourceTable}, pt: ${pt} }在提交请求下面添加“JSON Extractor”表达式写$.data.runId变量名取runId方便后续接口引用。添加“循环控制器”设置循环次数20内部放一个HTTP Request路径是/api/etl/status/${runId}。在循环控制器里的HTTP请求下面添加“响应断言”检查返回JSON里的data.state字段是否等于SUCCESS。再添加一个“JSR223 Assertion”用Groovy做更复杂的数据校验下面给一段示例。// JSR223 Assertion推荐用Groovy而不是BeanShell性能更好 def response prev.getResponseDataAsString(); def json new groovy.json.JsonSlurper().parseText(response); def state json.data.state; if (state ! SUCCESS) { AssertionResult.setFailure(true); AssertionResult.setFailureMessage(任务状态未成功当前状态: state); }这里我特意说一下JMeter老版本里很多人用Beanshell断言但Beanshell的性能确实不好每个断言都要解释执行一遍Groovy或者Beanshell脚本。在高并发压测时如果断言脚本本身跑得慢反而成了瓶颈。我现在统一用JSR223加Groovy因为JSR223有缓存机制脚本执行效率比Beanshell高很多。当然如果你只是简单比对响应码直接用“响应断言”就够了别动不动就上脚本。3.3 监听器与指标采集配置调试阶段我习惯在脚本里加“查看结果树”和“聚合报告”两个监听器。查看结果树用来确认每个Sampler的请求响应内容和断言结果聚合报告用来粗略看看平均响应时间和错误率。但是到正式压测阶段不建议再加这两个监听器尤其是查看结果树它会把每个响应都存进内存压测过程中极易导致JMeter内存溢出。更推荐的做法是正式压测时去掉结果树只保留聚合报告或者直接通过-l结果文件来统计后续用命令行生成HTML报告更规范。如果项目有监控看板需求再配置一个“Backend Listener”。这个监听器能把JMeter测到的TPS、响应时间、错误率等数据发送到InfluxDB然后在Grafana上实时画曲线。配置参数不复杂老版本常用的是InfluxDB的HTTP写入地址例如http://localhost:8086/write?dbjmeter新版本还会有Token和Organization这些字段按实际InfluxDB版本填就行。我这里还遇到过一个问题JMeter生成的HTML报告里中文任务名显示为乱码。解决方法是把bin/jmeter.properties里的sampleresult.default.encoding改成UTF-8再重启JMeter。如果配置了上传文件文件名乱码通常也是编码问题尽量保证文件名的编码和JMeter一致。3.4 逐步加压与数据记录压测执行不是跑一遍就完事而是连续加压、记录每一段的数据。我是这样安排的第一轮单线程跑一次完整任务验证脚本和数据没问题。第二轮5并发持续5分钟观察基线指标。第三轮10并发持续5分钟。第四轮20并发持续5分钟。第五轮40并发持续10分钟。第六轮80并发持续10分钟。每轮结束后把聚合报告的TPS、平均响应时间、P95、P99、错误率记录下来。如果发现某一段的TPS不再上涨甚至下降或者错误率突然超过1%就可以标记为疑似瓶颈点。我不建议直接一次把并发拉满因为ETL任务并发数通常不会像普通秒杀系统那么高可能10个并发就已经把调度队列打爆了。阶梯加压能明显看出拐点这是最直观的方式。记得在每轮压测之间把目标表的数据清空或者换一个分区避免上一轮数据还没跑完下一轮又重复跑导致数据量叠加最终任务越来越慢。流程图就不画了实际按这个步骤执行就行。4. 结果分析怎么快速定位性能瓶颈4.1 从聚合报告看整体趋势压测完成以后第一件事是打开聚合报告或生成的HTML报告不要急着看某一个采样点的数据而是看整体趋势。聚合报告里有几个关键字段Samples样本数、Average平均响应时间、Error%错误率、Throughput吞吐量。如果随着并发增加Throughput同步线性上升说明系统还有余量如果Throughput上升变缓甚至掉头向下说明系统已经接近饱和。错误率的变化也要注意。ETL任务刚压下去时可能一切正常等到某个并发值错误率从0突然变成20%这时候基本可以确定碰到了明确瓶颈可能是连接池满了可能是数据库锁超时也可能是调度线程池拒绝新任务。我习惯把每轮的数据整理成一个简单表格一眼就能看出哪一级开始恶化。另外别只看平均值一定要看P99。平均值容易被大量快速任务拉低。如果P99明显高于Average好几倍就说明有部分任务被阻塞了很长时间这在批处理场景里就是积压隐患。通过JMeter的HTML报告Response Time Percentiles这一页能直接看到不同百分位的耗时曲线很方便。4.2 分层定位ETL引擎、数据库、中间件还是网络有一次我压测发现提交接口响应越来越慢但任务实际执行时间并不长。后来排查发现瓶颈在网关层并发一高等待网关协程处理的任务排了很长的队。这就提醒我们压测结果异常时要分清楚是哪个环节慢。通常我会按四层来排查第一层是接入层包括Nginx、网关、认证中心。重点看响应码、连接数、请求排队时间。如果接口整体变慢但后端日志显示任务很快大概率是网关转发能力不够。第二层是调度层也就是ETL任务的调度中心。这里关注线程池大小、调度队列深度、任务分发耗时。如果任务提交后长时间不走调度队列可能满了。第三层是计算引擎比如Spark、Flink或纯Java任务。这里关注CPU使用率、内存GC、Executor数量、任务并发度。瓶颈通常是资源不够或参数配置不合理。第四层是数据库包括源库和目标库。关注慢SQL、锁等待、连接池活跃数、磁盘IO。把这四层的监控指标在时间线上对齐就能定位到具体瓶颈。比如压力测试期间监控曲线显示某数据库连接池活跃数在某个并发值到顶然后JMeter错误率同步上升那问题基本就锁定了。我还会同时打开慢查询日志看是不是某个SQL在数据量增大后出现了全表扫描。4.3 用Beanshell断言辅助校验数据质量性能测试不能只关心快不快还得确认压测过程中没有把数据跑坏。ETL任务跑完以后目标表里的数据量是否符合预期这个校验一定要进脚本。一种常见做法是在轮询状态接口返回成功后再发一个“数据核对接口”或者直接查JDBC请求查目标表的行数。如果你只需要简单判断返回内容可以继续用响应断言。如果要做稍微复杂的逻辑比如从响应里提取数量再跟期望值比较就需要写脚本。这里有人会用Beanshell断言我个人的建议是脚本尽量用JSR223 Groovy。拿一段实际用过的代码来说// 在任务状态成功后调用数据校验接口把返回的数量和期望数量做比较 def response prev.getResponseDataAsString(); def json new groovy.json.JsonSlurper().parseText(response); def actualCount json.data.count; def expectedCount vars.get(expectedCount).toInteger(); if (actualCount ! expectedCount) { AssertionResult.setFailure(true); AssertionResult.setFailureMessage(数据量校验失败预期: expectedCount 实际: actualCount); }在压测脚本里加入数据校验能避免一种尴尬情况测试报告写着系统能支撑20个并发任务结果每个任务都在偷偷丢数那这个结论没有任何意义。但要注意校验动作也会增加请求量最好用单独的后置处理器或者独立脚本去核对不直接计入主要吞吐量指标。或者只在每个线程的最末尾校验一次不要对每个轮询请求都做全量校验。5. 实战中的坑与排查技巧5.1 常见问题速查表我把这次压测过程中遇到和身边同事遇到的几个高频问题整理成一个速查表遇到类似情况可以直接对照问题现象常见原因解决方法启动JMeter时报could not delete existing file c:\windows\system32临时目录权限受限或杀毒软件拦截通过-Djava.io.tmpdirD:/tmp指定用户有权限的临时目录或用管理员权限启动压测HTTPS接口报证书错误JMeter未信任目标证书或证书过期用keytool把证书导入JDK的cacerts或临时在JMeter配置中关闭SSL验证上传文件名中文乱码JMeter默认编码不是UTF-8jmeter.properties中设置sampleresult.default.encodingUTF-8并在HTTP请求中显式设置编码JDBC压测连不上数据库缺少驱动程序jar包或Classname配置错误把MySQL/Oracle驱动jar放到lib/ext目录JDBC Connection Configuration中选对Driver class压测客户端CPU跑到100%结果失真JMeter自身资源不够减小监听器数量用命令行模式或启用JMeter分布式压测任务提交成功但一直轮询不到SUCCESS任务在引擎侧排队或失败查看调度队列和引擎日志确认提交参数是否合法这些坑都很典型尤其是第一条网上问的人特别多。其实它很多情况下就是临时目录权限问题把java.io.tmpdir换个目录就行。碰到类似报错时不要慌先看日志上下文。5.2 避免压测结果失真的一些习惯压测结果要能真实反映生产情况除了脚本正确还取决于测试习惯。第一个习惯是预热。ETL任务涉及大量的连接池、线程池、JIT编译刚开始跑的前几个任务往往比较慢直接取这段数据会拉低指标。我的做法是每个并发级别开跑后先忽略前1-2分钟的数据等稳定了再记录。第二个习惯是不要在压测机上开着一堆图形界面。GUI模式本身就有内存开销在压测机上开着实时结果树几百个线程一跑很可能先把自己压垮。要记录指标就保存成jtl等压测结束后再解析。第三个习惯是控制可变因素。比如压测时源表数据量要保持一致不能这边压着那边还往源表灌数目标表也要及时清理或分区隔离。后台任务比如定时备份、凌晨跑批都要停掉否则混在一起瓶颈无法归因。我还会在每一轮压测后记录一下目标表行数和最终状态防止数据堆叠导致任务越跑越慢。第四个习惯是多次重复。单轮压测的偶然性很大尤其是ETL这种依赖资源调度的任务。同样的并发第一轮跑得好第二轮可能因为上一轮残留任务没清干净结果差别很大。我一般会每个关键并发点至少压两轮如果两轮指标差超过15%就检查环境是否被污染。5.3 给高并发压测场景的优化建议如果ETL并发要求很高比如在数仓初始化或补数场景下可能要同时跑几十个甚至上百个任务单台JMeter客户端就会扛不住。这时可以考虑JMeter分布式压测一个Controller负责调度多台Worker来分发压力。分布式模式下要注意Controller和Worker的JMeter版本保持一致并且在Worker机器上关闭GUI只需启动jmeter-server。高并发压测ETL还有一个容易被忽略的点任务幂等性。ETL任务被压力脚本反复提交如果任务逻辑没有做幂等处理比如重复跑同一分区会导致数据翻倍那脚本压出来的结果只能算“垃圾数据产生速度”。所以在压测之前务必确认ETL任务是否支持覆盖写入或者让测试数据使用独立日期分区跑完一轮换一个分区。如果你需要更精细的压力曲线我推荐用JMeter插件中的Ultimate Thread Group它可以配置多个阶段的启动和停止比自带线程组更适合做阶梯压测。插件通过JMeter Plugin Manager安装即可非常方便。不过也要注意插件版本和JMeter版本要匹配否则可能出现菜单里找不到的情况。最后再补充一句个人体会ETL压测和普通Web压测最大的不同是ETL任务往往牵涉大量资源调度和数据一致性测出来不好看并不一定是系统差也有可能是压力模型没有贴近真实场景。把入口、数据量、调度模型摸透再用JMeter一步一步加压找到的那个瓶颈才是需要真正优化的问题点。以上就是一个能直接复用的思路希望对你有用。
返回列表