
做压测最怕的不是TPS上不去而是脚本跑到一半查看结果树里突然冒出一片红。你在JMeter里看到java.io.IOException: Error writing to server又或者几十个请求全部卡在同一个Sampler上报Connection reset这种时候大多数人第一反应是“服务器挂了”或者“网络有问题”但真相等你排查完之后往往会发现问题藏在脚本配置、参数化策略、协议细节这些不起眼的地方。这篇内容是我自己从一次次失败里攒下来的JMeter性能测试异常排查记录。我把压测中最常遇到的问题——网络层报错、数据参数化踩坑、HTTPS证书和令牌校验、文件冲突与断言脚本——按“现象 → 排查链路 → 根因 → 解决方案”的顺序整理出来。无论你是刚装好JMeter准备跑第一个压测脚本的新手还是已经在用JMeter做接口测试、数据库参数化、高并发压测的老手这篇文章都能帮你少走几趟弯路。1. 压测现场高频异常全景先对号入座再动手排查很多人一看到报错就急着改JMeter配置或者直接转头去问开发“服务器是不是挂了”这样效率很低。异常分析的第一步不是修而是分类。不同的异常类别排查路径完全不同处理优先级也有先后。1.1 按报错位置把异常切分成三层我习惯把JMeter中的异常分成三个层面来看请求发起层JMeter客户端自己报出来的错比如Error writing to server、Connection reset、文件已经存在、Software caused connection abort。这类问题通常出在线程数配置、网络连接复用、结果文件管理上。服务端响应层HTTP状态码异常400、401、403、500、502以及响应体里的业务错误。这类问题一般需要结合后置处理器、断言、服务端日志一起定位。数据处理层参数化取不到值、CSV数据错位、JDBC查询结果为空、数据库连接失败、防伪标记token未提供。这类问题最隐蔽因为JMeter不会直接红一片但你的压测数据是错的结果自然没有参考价值。1.2 异常处理的优先级怎么排三层异常不是平均用力我一般按这个顺序处理优先级异常类别典型报错影响范围第一梯队网络连接层Error writing to server、Connection reset请求发不出去压测直接中断第二梯队数据参数化层CSV取值错位、JDBC传参失败、数据库密码错误请求数据不准确测试结论失真第三梯队协议校验层HTTPS证书无效、防伪标记缺失请求被服务端拒绝但报错形式很多样第四梯队文件与脚本层文件已存在、Beanshell断言失败脚本中断或误判结果报告不可信网络层必须先确认因为基础连通性都不稳后面排查全是白搭。协议校验和数据层的错误往往交织在一起比如__RequestVerificationToken未提供看起来是协议问题实际是数据提取的前置步骤没做。文件与脚本层的坑则最容易被人忽略你压测跑了半小时结果因为追加写文件失败整段数据作废这种事我遇到过不止一次。提示压测中遇到异常先截图保留现场再去看报错详情。结果树和日志文件是排查的第一手资料很多人在慌乱中直接清空重跑反而丢掉了最关键的定位线索。2. “Error writing to server”排查链路从抓包到配置逐层找根因这个报错在JMeter压测中出现频率极高尤其当并发线程数拉起来之后。java.io.IOException: Error writing to server看起来像客户端的问题但根因往往在TCP连接链路的某个环节。2.1 我先说清楚这个报错的真实含义JMeter向服务器发送HTTP请求时底层是通过Socket写数据。当JMeter尝试把请求数据写到Socket缓冲区却发现连接已经不可用时就会抛出这个IOException。换句话说请求还没发到服务器或者发到一半时连接断了。常见误判有两个一是觉得是服务器压力过大导致拒绝服务二是觉得是JMeter本身有bug。实际上我在压测中遇到这个报错真正原因大多数是下面三类之一。2.2 排查链路按顺序走不要跳步我把自己排查这个问题的完整路径写出来你可以照着做先复现场景单线程跑一次看是否复现。单线程也报错多半是网络路径或服务端直接拒绝只有并发时出问题则优先怀疑连接池和端口资源。看服务端日志Tomcat的localhost.log、Nginx的error.log或者云上负载均衡的访问日志。如果服务端根本没收到请求那就是中间链路断了如果收到了但返回异常那是服务端处理问题。抓包确认在JMeter所在的机器上用Wireshark或tcpdump抓包看TCP连接是否出现RST包。出现RST说明连接被中间设备或服务端主动重置。检查JMeter自身配置HTTP请求默认的超时设置、Keep-Alive配置、本机可用端口数。2.3 三类根因和对应的解决方案根因一连接被服务端或中间设备主动断开很多Web应用服务器、负载均衡器都有空闲连接超时设置比如Nginx默认的keepalive_timeout可能是65秒而JMeter在长时间压测中如果某次请求间隔超过这个时间连接就会被服务端关闭。JMeter请求配置里如果勾选了Use KeepAlive会尝试复用旧连接但连接其实已经被服务端回收了就会报错。解决方案在HTTP Request Advanced选项卡里把Timeouts中的Connect和Response都设置成合理值比如Connect3000ms、Response60000ms同时确认JMeter与服务器之间的任何中间组件负载均衡、防火墙、云安全组空闲超时都大于压测的请求间隔。根因二并发过高导致本机端口耗尽压测机在高并发下会大量占用本地端口。Linux系统默认的临时端口范围是32768到60999大约28000个端口。如果脚本里没有开启连接复用每个请求都新建TCP连接那么当并发线程数乘以每秒请求数超过端口可用范围时新的连接就无法建立报错就来了。解决方案压测前检查并调整本地端口范围sysctl -w net.ipv4.ip_local_port_range10240 65000 sysctl -w net.ipv4.tcp_tw_reuse1同时缩短TIME_WAIT状态的连接回收时间或者开启连接复用。根因三JMeter脚本里没设置合理的超时很多人跑压测时不给HTTP请求设置任何超时结果服务器端响应稍慢JMeter的Socket就卡住了。等到服务器腾出手来处理连接可能已经被判定超时写入失败就会报这个异常。解决方案不复杂在HTTP Request的Advanced选项卡中配置Connect Timeout建立连接的超时时间建议3000msResponse Timeout等待响应的超时时间建议30000ms注意如果压测的是长事务接口比如导出报表、批量计算Response Timeout要适当调大否则会出现服务端还在处理客户端已经超时断连的误判。2.4 分布式压测时特别容易踩的端口坑用多台机器做分布式压测Agent方式时除了每台压测机的端口范围还要检查JMeter Agent到Master之间的RMI通信端口。默认RMI端口是随机分配的如果防火墙只开了1099端口Agent会报Connection refused to host表面看起来也是连接类异常。解决方案是在jmeter.properties中配置固定的RMI端口server.rmi.localport4000 server.rmi.port1099这个坑我压测第一次没注意后来排查了很久才发现是两台机器之间的防火墙把RMI回连端口挡了不是业务接口的问题。3. 数据层连环坑CSV分块取值、JDBC传参和数据库密码网络层异常解决之后下一个高频灾区是数据准备层。很多压测脚本跑起来不报错但请求参数全是错的要么是并发用户都用了同一份数据要么是数据库查询结果没传到下一个接口里。3.1 同一个CSV参数化文件怎么让每个线程分块取值这是一个经常被问到的问题压测时有100个并发用户每个用户需要不同的登录名和密码但数据都在同一个CSV文件里。默认情况下CSV Data Set Config的Sharing mode是All threads所有线程共享指针轮流读取下一行数据。这样做的结果是100个用户会快速把文件里的数据游标走完后面的请求要么读到EOF要么不断重复循环和“每个用户取各自独立一段数据”的目标相差很远。要让每个线程在同一个CSV文件中分块取值核心是修改Sharing mode共享模式行为说明适用场景All threads所有线程共享一个游标依次读取不需要区分用户身份的场景Current thread group同一线程组内的线程共享游标多个线程组需要各自独立取数时Current thread每个线程独立游标各自从第一行开始每个用户固定使用自己的数据段使用__CSVRead函数通过函数参数指定文件列配合计数器手动控制行号需要精确按行号分块取值时如果你需要“每个线程固定获取CSV中某个区间的数据”更可控的方案是结合Counter配置元素和__CSVRead函数手动指定行号偏移。比如每个线程负责连续20行数据用计数器从0递增每次读取__CSVRead(file.csv, 列号)时行号用${__intSum(${threadNum}, ${counter},)}动态计算。我在实践中得到的建议是能用Current thread模式解决的就不要用函数硬算。函数方式灵活度高但脚本可读性差多人协作时很容易改错。3.2 JDBC Request查询出的数据如何传给下一个接口做参数“jmeter将jdbc request查询出的数据作为下一个接口的参数”这个场景在压测有前置数据准备的接口时非常常见。比如每个用户下单前需要先从订单表查到自己的订单号再拿订单号去查询支付状态。实现这个需求的标准链路如下配置JDBC Connection Configuration线程组下添加配置元件输入数据库连接的URL、JDBC驱动类、用户名、密码并设置一个Variable Name比如dbConn。这里最容易踩的坑是数据库驱动没放对位置需要把MySQL或PostgreSQL的JDBC驱动JAR拷贝到JMeter的lib目录下不是lib/ext。添加JDBC Request Sampler在Variable Name中填写上面配置的名称在SQL Query中写查询语句。关键步骤是Variable Names字段比如填orderId查询结果的第一列就会被保存到变量orderId中多列数据用逗号分隔变量名。在下一个接口中引用直接用${orderId}引用。但注意如果查询返回多行结果${orderId}只能拿到第一行。需要处理多行数据时要配合JSR223 PostProcessor或者ForEach Controller。用JSR223 PostProcessor把ResultSet转成参数如果你用的是MySQL查询结果要动态传给后续接口推荐写Groovy脚本import java.sql.ResultSet; import java.sql.ResultSetMetaData; def rs vars.getObject(resultSet); def ids []; while (rs.next()) { ids.add(rs.getString(order_id)); } vars.put(orderIds, ids.join(,));脚本里vars.getObject(resultSet)能拿到JDBC Request中Variable Names定义的ResultSet对象循环提取后拼成字符串方便后续用${orderIds}一次性传入。提示JDBC Request中ResultSet variable name字段设置后变量类型是ResultSet对象不能直接用${变量名}拿值。很多新手在这里卡住不是脚本写法问题而是对变量类型理解有偏差。3.3 数据库密码连接报错不止是密码输错这么简单“jmeter的mysql密码”这个问题看起来很简单实际排查时能延伸出好几个分支。最常见的报错是Communications link failure Public Key Retrieval is not allowed The server time zone value xxx is unrecognized这三个报错分别对应三种情况Communications link failureJDBC驱动版本和MySQL版本不兼容或者MySQL端口没开。JMeter 5.x自带MySQL驱动版本较老连接MySQL 8.x时建议使用官方最新版Connector/J。Public Key Retrieval is not allowedMySQL 8.x默认使用caching_sha2_password认证方式JDBC URL需要加上allowPublicKeyRetrievaltrue。The server time zone valueJDBC URL缺少时区参数。我自己的JDBC URL一般这样写jdbc:mysql://127.0.0.1:3306/testdb?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8另外如果用的是云数据库安全组或防火墙白名单没放行压测机的外网IP也会报连接超时这类问题在JMeter配置里永远查不出来要直接去看数据库的网络访问控制。4. 协议层校验失败HTTPS证书录制和防伪标记token协议层异常的特点是不是每次请求都失败而是某些请求成功、某些请求失败或者只在特定环境本地可以、压测环境不行下失败。这类问题排查起来容易让人烦躁因为报错信息往往不直接。4.1 JMeter录制HTTPS脚本时的证书导入流程想用JMeter自带的HTTP(S) Test Script Recorder录制HTTPS请求很多人第一步就卡住了。浏览器访问HTTPS页面时会校验证书而JMeter代理生成的是自签名根证书浏览器默认不信任就会提示连接不安全或者直接拦截。正确流程是在JMeter中创建HTTP(S) Test Script Recorder并配置一个代理端口默认8888。点击StartJMeter会在bin目录下生成ApacheJMeterTemporaryRootCA.crt。把这个证书导入浏览器的“受信任的根证书颁发机构”。Windows上双击证书文件选择“安装证书”存储位置选“本地计算机”证书存储选“受信任的根证书颁发机构”。配置浏览器代理指向本机8888端口后录制。我在实际录制中踩过两个坑系统时间不对导致证书失效如果压测机系统时间和实际时间偏差太大浏览器会认为证书无效即使是刚生成的根证书也一样。先校准系统时间再录。证书导错存储区如果导入到“个人”而不是“受信任的根证书颁发机构”HTTPS请求依然会被拦截。另外压测环境如果用的是自签证书的HTTPS服务JMeter请求该服务时需要在HTTP Request的Implementation处选HttpClient4并在bin/system.properties里配置信任所有证书javax.net.ssl.trustStore/path/to/your/truststore.jks javax.net.ssl.trustStorePasswordchangeit更省事的办法是用HTTP Request的Use multipart/form-data下一行的Advanced里设置Implementation并关闭SSL验证但生产环境我不建议彻底关闭校验。4.2 NYC框架防伪标记缺了token请求永远失败“jmeter 测试 mvc3项目提示__requestverificationtoken 未提供必要的防伪标记”这个问题本质是ASP.NET MVC的CSRF防护。服务端通过ValidateAntiForgeryToken校验请求中的__RequestVerificationToken如果你录制的脚本里没有这个参数或者参数是一次性的没有更新请求就会被拦。处理方式分两步第一步先获取token。在JMeter脚本里增加一个GET请求访问包含表单的页面然后用正则表达式提取器从响应中提取__RequestVerificationToken的value值。正则可以这样写name__RequestVerificationToken[^]*typehidden[^]*value([^])第二步在后续POST请求中带上token。把提取到的值通过${token}引用放进POST请求的Body参数里。同时还需要注意ASP.NET MVC的防伪标记会同时产生一个Cookie必须启用HTTP Cookie Manager让Cookie自动跟随请求。这里有个细节容易被忽略token和用户会话是绑定的。如果你用同一份token去跑100个并发用户服务端会校验失败。正确做法是让每个线程用户单独走一遍“先GET获取token再POST提交”的链路或者使用CSV Data Set Config给每个用户准备不同的会话Cookie。提示如果脚本中使用了HTTP Cache Manager要注意缓存可能导致GET请求返回304从缓存里拿不到最新的token。录制脚本后清理掉冗余的缓存控制头或者对获取token的请求禁用缓存。5. 文件冲突、断言脚本与结果解读隐性失败往往最难察觉最后一类异常在结果树里看起来不是红色但影响非常严重。它会让你的压测数据失真甚至作废而你如果不主动去翻日志可能还觉得自己压测跑得很顺利。5.1 “文件已经存在”的三类触发场景Jmeter的“文件已经存在”报错一般有三个触发场景命令行压测时指定了已存在的结果文件用-l result.jtl指定输出文件如果文件已经存在JMeter在某些配置下会直接报错。解决方法是换文件名或者在命令中加上-f强制删除已有文件注意版本差异5.x支持。CSV文件被占用Windows系统下CSV文件被Excel打开或者被上一个未正常关闭的JMeter进程占用JMeter读取时会报File .... must be kept in same directory或者权限错误。上传文件请求的路径和文件名冲突压测上传接口时HTTP Request的Files Upload选项卡中指定的文件路径不存在或文件名被JMeter自动改名后冲突。前两类问题好解决第三类问题我多说一句。JMeter上传文件时如果同一个线程组里的多个请求上传相同文件名服务端可能因为重名覆盖而报错。我在压测图片上传接口时会通过参数化方式给每个线程生成唯一文件名${__time(yyyyMMddHHmmss)}_${__threadNum}_${__counter(,)}.jpg这样既能避免重名冲突也方便在服务端日志里追踪是哪条线程上传的文件。5.2 Beanshell断言脚本能跑不等于断言正确“jmeter beanshell断言”很多人在用但Beanshell在JMeter里有两个天生的缺陷一是性能较差高并发下脚本执行会拖慢请求二是变量类型处理容易出问题。举个最常见的错误写法String response prev.getResponseDataAsString(); if (response.contains(success) false) { Failure true; }这段代码的问题在于它只判断了响应里没有“success”就失败但没有处理response为null的情况也没有设置FailureMessage断言失败时结果树里只有红叉没有具体原因。更稳妥的写法是String response prev.getResponseDataAsString(); if (response null || response.indexOf(success) -1) { Failure true; FailureMessage Response does not contain success, actual: response; }不过要提醒一句JMeter官方对Beanshell的态度是“尽量迁移到JSR223”尤其在做性能测试时Beanshell的脚本引擎比Groovy慢不少。我自己已经全面转向JSR223 Groovy脚本性能高一个量级排查问题也更容易。如果压测时发现大量断言失败先用查看结果树里的“Response Data”标签确认实际返回值再对照断言逻辑。很多断言失败不是服务端问题而是响应内容里有动态字段比如时间戳、token导致字符串匹配失败。5.3 用graphlib分析异常源头从一片红里找根节点压测脚本大了之后Sampler有成百上千个异常只在一小部分请求中出现而且多个接口的异常看起来互不相关。“graphlib分析异常原因”的思路其实是用依赖关系图谱的方法把所有Sampler、前置处理器、后置处理器、断言看成节点把它们之间的执行顺序和数据流转看成边利用拓扑排序找到异常传播的源头。JMeter的测试计划天然可以抽象成一张图同一线程组内的Sampler按顺序执行如果某个Sampler失败但脚本配置了“继续执行”后续Sampler的数据依赖就会断裂导致一连串的异常。用graphlib做这个分析并不复杂Python标准库里有现成实现import graphlib # 以sampler名称为节点模拟依赖关系 graph { login: {setup}, create_order: {login}, pay_order: {create_order}, query_order: {create_order}, } ts graphlib.TopologicalSorter(graph) print(list(ts.static_order()))在真实项目中我会把JMeter脚本导出为JMX文件解析XML结构后构建依赖图找出哪个前置节点是异常链路的根因。比如所有报错都集中在pay_order之后的query_order上但根因可能在login的token没有正确传递。这个方法不需要额外安装JMeter插件只用Python的标准库而且JMX文件本质是XML解析起来很简单。同时也适合分析同一个CSV文件在多个接口间被不同线程引用后产生的数据错位问题——把数据流画成图一眼就能看出哪个环节最先断了。6. 一点建立排查方法论的体会做了这么多年压测我最大的感受是JMeter本身的报错信息其实非常友好真正不友好的地方在于压测中的异常往往是连锁反应一个根因会引发雪崩式的报错。所以看到结果树一片红时不要只看数量而是先找时间线上最早出现的那个异常。大多数情况下最前面的那一个才是真正的病根后面的都是它引发的连锁崩溃。另外一个值得养成的习惯是任何压测脚本都要做“最小可行性验证”。先用1个线程、1次循环跑通全链路再拉高并发。很多人在并发环境下排查半天的问题其实单线程跑一遍就能发现是参数化或者断言写错了。希望这篇异常分析记录能给你节省一些时间。如果你在压测中也遇到过什么奇怪的报错欢迎在评论区把报错贴出来一起交流。