JMeter分布式压测负载均衡改造:轮询与最少连接数算法实战

JMeter分布式压测负载均衡改造:轮询与最少连接数算法实战
1. 项目概述当分布式压测遇上“旱的旱死涝的涝死”做性能测试的朋友尤其是用过JMeter做分布式压测的大概率都遇到过这个让人头疼的场景你信心满满地启动了十几台压力机脚本一跑监控大盘一看好家伙有的机器CPU飙到90%以上风扇呼呼转眼看就要冒烟有的机器却闲得发慌CPU使用率不到10%仿佛在度假。这就是典型的“负载不均”它不仅浪费了宝贵的硬件资源更致命的是它会让你的压测结果严重失真——瓶颈可能不是被测系统而是你那台“过劳”的压力机本身。这个项目要解决的就是JMeter分布式压测中这个老大难问题。很多人以为在JMeter Master-Slave架构里Master节点把任务分下去就完事了负载自然会均匀。但现实很骨感默认的分配策略往往很“傻”尤其是在处理动态请求、不同响应时间的接口或者使用了像CSV Data Set Config这类数据驱动组件时问题会暴露得更加明显。所以我们今天不聊怎么搭建分布式环境那太基础了我们直接切入核心如何改造JMeter让它从“平均分配”进化到“智能分配”。核心思路就是引入两种经典的负载均衡算法——轮询和最少连接数并手把手带你实现它们。这不仅仅是改个配置而是深入到JMeter的RPC通信层RemoteJMeterEngineImpl去动手术。通过这个实战你不仅能解决眼前的问题更能深刻理解JMeter分布式运行的内核机制以后遇到其他定制化需求也能举一反三。2. 负载不均的根源与JMeter默认机制剖析要解决问题得先挖出根子。JMeter分布式压测负载不均绝不是偶然而是其默认设计在特定场景下的必然结果。2.1 默认的任务分配机制简单粗暴的“静态分割”JMeter的Master-Slave模式其任务分配发生在压测启动的瞬间。Master节点会做以下几件事脚本与资源分发将测试计划jmx文件、依赖的jar包、CSV数据文件等一次性发送给所有已连接的Slave节点。线程组分割这是关键。假设你的线程组设置了1000个线程循环10次总共有10000个样本。如果你有5个SlaveMaster默认的算法是总线程数 / Slave数量。每个Slave会分配到200个线程1000/5去执行每个线程依然运行10次循环。请注意这个分配是静态的、一次性的。启动与收集Master发出启动命令各个Slave开始独立执行分配给自己的那部分线程并将结果实时回传。2.2 导致不均的三大“元凶”在这种静态分割下以下几种情况会直接导致负载倾斜CSV数据读取不同步这是最常见的原因。如果脚本使用了CSV Data Set Config并且设置为All threads共享模式。每个Slave都会获得一份完整的数据文件副本。但由于线程是静态分配的Slave A的线程可能很快读到了文件末尾后续循环只能重复最后一行或报错而Slave B的线程可能才读到文件中部。这导致Slave A的请求可能更简单例如一直在重复登录失败用例而Slave B的请求更复杂执行各种业务操作响应时间自然不同负载也就产生了差异。请求本身的异构性你的测试脚本里不可能全是相同的接口。可能混合了登录、查询、下单等不同业务。这些接口的响应时间RT天生差异巨大。静态分配线程无法保证每个Slave上“快请求”和“慢请求”的比例是均衡的。可能某个Slave“运气好”分配到的都是RT短的查询接口它很快就完成了自己的样本数然后闲置而另一个Slave“倒霉”全是RT长的下单接口忙得不可开交。系统资源与网络波动即使请求完全一样不同Slave节点所在的物理机或虚拟机性能可能存在细微差异或者在执行过程中遭遇网络波动、宿主机资源竞争等导致相同请求的处理速度不同。静态分配无法感知和应对这种动态变化。注意很多人会想到用Random或RandomOrder控制器来打乱请求顺序但这只在单个JMeter实例内有效。在分布式环境下每个Slave执行的都是Master分发的、已经确定好的线程组和逻辑控制器序列这个“打乱”动作在各个Slave内部是独立的无法解决跨节点的负载均衡问题。所以JMeter默认的机制本质上是“任务分割”而非“负载均衡”。它假设每个任务单元线程的执行成本是相同的但这在复杂的真实业务场景中几乎不成立。我们需要一种能动态感知Slave当前负载状态并将新任务智能调度到最“闲”节点上的机制。3. 核心算法选型为什么是轮询与最少连接数面对负载均衡算法有很多如轮询、加权轮询、最少连接、最快响应、哈希等。针对JMeter分布式压测这个特定场景我们优先考虑实现轮询和最少连接数这是基于以下考量3.1 轮询算法的适用场景与实现价值轮询是一种无状态调度算法它简单地按顺序将新任务对我们来说可以理解为“新启动的虚拟用户线程”或“下一批请求”分配给Slave列表中的下一个节点。优点实现极其简单完全公平。在理想情况下如果所有请求的类型和消耗资源完全一致轮询能实现完美的负载均衡。在JMeter中的价值即使请求不完全同质轮询也比默认的静态分割有优势。因为它是在“任务执行过程中”进行调度。例如我们可以改造为Master不再一次性分配所有线程而是控制线程的启动节奏。每当需要启动一个新线程或一批请求时就使用轮询算法选择一个Slave来执行。这样即使每个请求耗时不同从长远统计来看每个Slave接收到的任务数量是均等的可以缓解因一次性分配不均带来的长期倾斜。缺点它无法感知节点的实时负载。如果一个Slave因为某个慢请求被卡住轮询依然会继续给它派发新任务可能导致该节点任务堆积。3.2 最少连接数算法的核心优势最少连接数是一种动态的、感知负载的算法。它维护每个Slave节点当前正在处理的连接数或任务数总是将新任务分配给当前连接数最少的节点。优点能非常好地应对我们前面提到的“请求异构性”和“节点性能差异”问题。处理快请求的Slave会更快释放连接从而接到更多新任务处理慢请求的Slave连接释放慢接收到的新任务就会减少。这实现了基于节点实时处理能力的动态负载均衡是解决JMeter负载不均的更优解。在JMeter中的映射我们需要在JMeter的语境里定义“连接”。最直接的映射就是每个Slave上当前活跃的线程数即正在执行采样器的线程。Master需要能够从每个Slave实时获取这个指标。挑战实现比轮询复杂需要Master和Slave之间建立一个轻量级的、持续的心跳或状态汇报机制。3.3 方案取舍与组合策略对于JMeter压测我们的目标是尽可能模拟真实用户的行为。真实用户的到达通常是随机的并且系统后端会采用负载均衡器。因此最少连接数算法更贴近真实场景效果通常也更好。但在实际改造中我们可以采用分阶段策略先实现轮询作为基础框架验证改动相对较小主要验证动态任务调度的可行性。再升级为最少连接数在轮询的调度框架上增加状态收集模块实现更智能的分配。本项目将涵盖这两个阶段的实战。我们会先剖析JMeter源码中任务分配的关键类然后分别实现两种算法。4. 深入JMeter分布式内核RemoteJMeterEngineImpl解密要实现自定义调度必须深入JMeter分布式通信的核心。关键入口点是org.apache.jmeter.engine.DistributedRunner和org.apache.jmeter.engine.RemoteJMeterEngineImpl。4.1 默认流程的代码级透视当你在Master机器上点击“远程启动所有”时大致会发生以下调用链DistributedRunner.start()被调用。它为每个Slave创建一个RemoteJMeterEngineImpl实例。RemoteJMeterEngineImpl通过RMIRemote Method Invocation调用远端Slave上的RemoteJMeterEngine接口方法。关键方法RemoteJMeterEngineImpl.start()会调用createClient()进而通过getEngine()获取到Slave端的StandardJMeterEngine。在启动引擎前Master会通过configureTest()方法将测试计划、属性文件等发送给Slave。核心所在在StandardJMeterEngine的运行过程中线程的创建和管理是由ThreadGroup及其调度器在Slave本地完成的。Master在启动命令中并没有传递“每个Slave运行多少线程”的精细指令而是依赖初始的静态分割。4.2 我们的改造切入点RemoteJMeterEngineImpl要变静态为动态我们必须介入任务分配的过程。一个可行的思路是不在启动时分配所有线程而是由Master作为一个中央调度器按需向Slave发送“启动一个虚拟用户线程”的指令。这意味着我们需要重构Master端的启动逻辑不让Slave的ThreadGroup自己控制线程的生命周期而是由Master的一个调度器来控制。定义新的RPC方法在RemoteJMeterEngine接口中增加类似startNewThread(threadGroupName)或startThreads(threadGroupName, count)的方法让Master可以命令某个Slave启动指定线程组下的一个或几个线程。实现调度器在Master端实现一个Scheduler类它根据轮询或最少连接数算法决定下一个任务发给哪个Slave然后调用对应的RPC方法。4.3 实操准备源码获取与编译环境提示修改JMeter源码需要一定的Java开发基础。请确保已安装JDK 8或11与你的JMeter版本匹配、Maven或Gradle以及一个IDE如IntelliJ IDEA。获取源码git clone https://github.com/apache/jmeter.git cd jmeter # 查看与你本地JMeter版本对应的tag例如 git checkout v5.6.2导入项目用IDE打开项目根目录下的pom.xml等待Maven下载依赖。定位关键类在项目中找到RemoteJMeterEngineImpl.java通常路径为jmeter/src/core/org/apache/jmeter/engine/RemoteJMeterEngineImpl.java。同时找到其接口RemoteJMeterEngine.java。这是我们的“手术台”。接下来我们将分步实施改造。5. 实战一实现轮询调度算法轮询算法是动态调度的基础。我们的目标是Master按顺序、循环地向Slave发出“启动单个线程”的指令直到达到总线程数。5.1 修改RPC接口与实现首先需要在RemoteJMeterEngine接口中添加一个新的远程方法。修改RemoteJMeterEngine.java:import org.apache.jmeter.threads.RemoteThreadGroupListener; // ... 在接口内添加以下方法 /** * 启动指定线程组下的一个线程 * param threadGroupName 线程组名称 * param listener 用于回调的监听器可选用于状态同步 * return 返回启动线程的标识符可用于后续停止 */ String startThread(String threadGroupName, RemoteThreadGroupListener listener) throws RemoteException;修改RemoteJMeterEngineImpl.java: 实现这个接口方法。这里的关键是我们不能直接操作Slave上的ThreadGroup需要通过Slave的StandardJMeterEngine来触发。// 在类中添加方法 Override public synchronized String startThread(String threadGroupName, RemoteThreadGroupListener listener) throws RemoteException { try { StandardJMeterEngine engine getEngine(); HashTree testTree engine.getTestTree(); // 获取测试计划树 // 遍历找到指定名称的ThreadGroup ThreadGroup threadGroup findThreadGroupByName(testTree, threadGroupName); if (threadGroup null) { throw new RemoteException(ThreadGroup not found: threadGroupName); } // 创建一个只包含该线程组单个线程的测试树副本 HashTree singleThreadTree createSingleThreadTree(testTree, threadGroup); // 这里需要巧妙的操作实际上更可行的方法是在Slave端预先准备好线程组 // 但暂停所有线程。然后Master通过RPC通知Slave“激活”其中一个线程。 // 由于直接动态添加线程到运行中的引擎非常复杂一种更实际的方案是 // 1. 在Slave端线程组启动后立即暂停设置线程数为0或通过属性控制。 // 2. Master通过RPC调用Slave端的一个“线程池”从中取出一个暂停的线程并启动它。 // 这涉及对ThreadGroup和Thread模型的深度修改超出了简单示例范围。 // 因此为了简化演示我们采用一种替代思路不修改Slave端线程模型 // 而是由Master在启动时就为每个Slave分配一个“任务队列”。 // 轮询算法分配的是“任务单元”比如一定数量的循环次数而不是物理线程。 // 我们在下一节“最少连接数”中会采用更贴近这种思想的实现。 // 对于轮询我们先实现一个简化的版本Master控制每个Slave运行的循环次数。 // 假设每个“任务”是执行N次循环。 // Master维护一个任务计数器每次轮询到一个Slave就通过RPC告诉它“执行下一个任务即下一组循环”。 // 这需要Slave端脚本配合使用__counter或${__threadNum}等函数来分割数据。 logger.info(Received command to start a thread for group: threadGroupName); // 返回一个模拟的任务ID return TASK- System.currentTimeMillis(); } catch (Exception e) { logger.error(Error starting single thread, e); throw new RemoteException(Failed to start thread, e); } } // 辅助方法根据名称查找线程组 private ThreadGroup findThreadGroupByName(HashTree tree, String name) { // 实现树遍历逻辑这里省略具体代码 for (Object key : tree.keySet()) { if (key instanceof ThreadGroup ((ThreadGroup) key).getName().equals(name)) { return (ThreadGroup) key; } HashTree subTree tree.get(key); ThreadGroup found findThreadGroupByName(subTree, name); if (found ! null) return found; } return null; }重要说明直接动态增删Slave上的JMeter线程是极其复杂的因为JMeter的线程模型并非为这种热插拔设计。上面的代码展示了接口层的修改但真正的难点在于Slave端的线程生命周期管理。因此在生产实践中更常见的折中方案是控制每个Slave的负载权重或者将一个大任务拆分成许多小任务由Master进行任务级调度。下面的实战我们将转向更可行的“任务调度”思路。5.2 实现Master端轮询调度器既然直接调度线程困难我们采用“任务调度”的思路。假设我们的压测场景是总共要发送10000个请求。我们可以把这10000个请求分成1000个任务包每个包10个请求。创建轮询调度器RoundRobinScheduler.java:package com.custom.jmeter.scheduler; import org.apache.jmeter.engine.RemoteJMeterEngine; import java.rmi.RemoteException; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class RoundRobinScheduler { private final ListRemoteJMeterEngine slaves; private final AtomicInteger currentIndex new AtomicInteger(0); public RoundRobinScheduler(ListRemoteJMeterEngine slaves) { this.slaves slaves; } /** * 获取下一个Slave轮询方式 */ public RemoteJMeterEngine getNextSlave() { if (slaves null || slaves.isEmpty()) { return null; } int index currentIndex.getAndUpdate(i - (i 1) % slaves.size()); return slaves.get(index); } /** * 分发一个任务单元 * param task 任务描述例如包含起始索引和请求数量 */ public void dispatchTask(TaskUnit task) throws RemoteException { RemoteJMeterEngine slave getNextSlave(); if (slave ! null) { // 这里调用一个我们自定义的RPC方法例如 executeTask(TaskUnit task) // slave.executeTask(task); // 由于我们尚未在RemoteJMeterEngine中定义此方法此处为示意 System.out.println(Dispatching task task to slave at index: currentIndex.get()); } } // 任务单元定义 public static class TaskUnit { int startIndex; int requestCount; // ... 构造器、getter/setter } }5.3 修改启动类与脚本配合为了让这个调度器工作我们需要修改Master启动逻辑在DistributedRunner中不再直接调用engine.start()而是初始化我们的RoundRobinScheduler并按照总任务数循环调用dispatchTask。改造JMX脚本脚本不能再用固定的循环次数。我们需要使用变量来控制循环。例如在“用户定义的变量”中设置${__P(task.start, 1)}和${__P(task.count, 100)}。线程组的循环次数设为${__P(task.count)}并在采样器中使用__counter函数起始值为${__P(task.start)}。这样Master在分发任务时通过RMI设置Slave的JMeter属性JMeterEngine.setProperty就能控制该Slave执行哪一段数据范围的请求。实现Slave端的任务接收器在Slave端需要有一个RemoteJMeterEngineImpl中的方法接收Master传来的task.start和task.count属性并在引擎启动前将其设置到JMeter的上下文属性中。这种“属性注入变量化脚本”的方式是实现分布式动态调度相对可行且侵入性较小的方案。它实现了数据层面的轮询分割虽然不如线程级调度精细但能有效解决因CSV数据读取不均导致的负载问题。6. 实战二实现最少连接数调度算法最少连接数算法需要感知Slave的实时负载。我们需要建立一个简单的状态上报机制。6.1 定义负载指标与状态上报在JMeter中最合适的负载指标是当前活跃线程数。我们可以在每个Slave上启动一个轻量的后台线程定期向Master汇报此指标。在Slave端创建状态上报器StatusReporter.java:package com.custom.jmeter.agent; import org.apache.jmeter.threads.JMeterContextService; import java.rmi.RemoteException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class StatusReporter { private final MasterStatusService masterService; // Master服务的RMI存根 private final String slaveId; private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); public StatusReporter(String slaveId, MasterStatusService masterService) { this.slaveId slaveId; this.masterService masterService; } public void startReporting() { scheduler.scheduleAtFixedRate(() - { try { int activeThreads JMeterContextService.getNumberOfThreads(); masterService.reportStatus(slaveId, activeThreads); } catch (RemoteException e) { e.printStackTrace(); // 上报失败处理如重连 } }, 1, 2, TimeUnit.SECONDS); // 延迟1秒启动每2秒上报一次 } public void stopReporting() { scheduler.shutdown(); } }同时需要在Master端定义一个RMI接口MasterStatusService供Slave回调上报状态。6.2 实现Master端最少连接数调度器Master端需要维护一个从slaveId到当前活跃线程数的映射并提供一个方法供Slave上报。创建LeastConnectionsScheduler.java:package com.custom.jmeter.scheduler; import org.apache.jmeter.engine.RemoteJMeterEngine; import java.rmi.RemoteException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; public class LeastConnectionsScheduler implements MasterStatusService { private final MapString, SlaveNode slaveMap new ConcurrentHashMap(); private final MapString, RemoteJMeterEngine engineMap new ConcurrentHashMap(); Override public void reportStatus(String slaveId, int activeConnections) { SlaveNode node slaveMap.get(slaveId); if (node ! null) { node.setActiveConnections(activeConnections); node.setLastReportTime(System.currentTimeMillis()); } } public void registerSlave(String slaveId, RemoteJMeterEngine engine) { slaveMap.put(slaveId, new SlaveNode(slaveId)); engineMap.put(slaveId, engine); } public RemoteJMeterEngine getOptimalSlave() { return slaveMap.values().stream() .min((a, b) - Integer.compare(a.getActiveConnections(), b.getActiveConnections())) .map(node - engineMap.get(node.getSlaveId())) .orElse(null); } public void dispatchTask(TaskUnit task) throws RemoteException { RemoteJMeterEngine slave getOptimalSlave(); if (slave ! null) { // 同样调用自定义的 executeTask 方法 // slave.executeTask(task); System.out.println(Dispatching task task to least loaded slave: slaveMap.get(getSlaveIdByEngine(slave)).getSlaveId()); // 假设任务开始执行立即更新连接数1 String sid getSlaveIdByEngine(slave); slaveMap.get(sid).incrementConnections(); } } // ... 其他辅助方法 private String getSlaveIdByEngine(RemoteJMeterEngine engine) { for (Map.EntryString, RemoteJMeterEngine entry : engineMap.entrySet()) { if (entry.getValue().equals(engine)) { return entry.getKey(); } } return null; } static class SlaveNode { private final String slaveId; private final AtomicInteger activeConnections new AtomicInteger(0); private volatile long lastReportTime; // ... 构造器、getter/setter public void incrementConnections() { activeConnections.incrementAndGet(); } public void decrementConnections() { activeConnections.decrementAndGet(); } } }6.3 整合调度器与JMeter生命周期整合是关键且复杂的一步。我们需要启动时注册在Slave启动后立即初始化StatusReporter并向Master注册自己。任务与连接数绑定当Master通过dispatchTask分发一个任务给Slave时立即增加该Slave的“连接数”。当Slave上的任务执行完毕如何感知需要通知Master减少连接数。这可以通过在任务结束时Slave主动调用Master的taskCompleted(slaveId)方法来实现。改造JMeter引擎最彻底的整合方式是修改StandardJMeterEngine或ThreadGroup在线程启动和线程结束时触发事件通过这些事件来增加/减少StatusReporter中的计数并通知Master。这需要对JMeter线程生命周期有深入理解。一个更轻量级的替代方案是基于样本计数的近似估计。假设每个任务单元如100次循环的平均执行时间是T。Master在分发任务时启动一个定时器在T时间后自动将该Slave的连接数减1。这种方法不精确但实现简单在长时间运行的压测中误差可以接受。7. 部署、测试与效果验证完成代码改造后必须经过严格的测试。7.1 编译与打包自定义JMeter在项目根目录执行Maven命令进行编译和打包mvn clean package -DskipTests -P jmeter-dev这会在jmeter/bin目录下生成新的ApacheJMeter.jar和ApacheJMeter_core.jar等。将生成的核心jar包替换你本地JMeter安装目录下lib/ext中的对应jar包。注意所有Slave节点也必须使用相同的自定义jar包。7.2 设计验证测试计划为了验证负载均衡效果需要设计一个能凸显负载差异的测试计划脚本设计创建两个HTTP请求采样器一个指向一个快速响应的接口如一个简单的查询RT~50ms另一个指向一个慢速接口如一个复杂的计算或加了Constant Timer模拟等待RT~2000ms。将它们放在一个线程组内使用Random Controller或Throughput Controller控制比例比如快慢请求比例7:3。监控准备在每台Slave上使用ServerAgent和PerfMon Metrics Collector监听器监控CPU、内存和网络IO。同时在JMeter中为每个Slave添加一个Backend Listener如配置到InfluxDB收集每个Slave的每秒事务数TPS和活跃线程数。对比基准首先用原生JMeter分布式模式运行记录各Slave的TPS和资源使用率曲线。然后用改造后的轮询和最少连接数模式分别运行进行对比。7.3 效果评估指标资源使用均衡度观察各Slave节点的CPU使用率曲线。理想情况下曲线应该高度重合波动一致。计算一段时间内各节点CPU使用率的方差方差越小越均衡。吞吐量一致性比较各Slave节点产生的TPS。在原生模式下各Slave的TPS可能会有显著差异。在改造后各Slave的TPS应趋于接近。总吞吐量在总资源不变的情况下更均衡的负载往往能带来更高的总吞吐量QPS。因为避免了部分节点过载成为瓶颈。响应时间分布观察聚合报告中的响应时间百分比如90% Line, 95% Line。负载均衡后由于没有单个节点过载整体响应时间应该更稳定高百分位的延迟可能会降低。7.4 可能遇到的问题与排查RMI连接失败确保Master和Slave防火墙端口默认1099开放且主机名能正确解析。可以在Master的jmeter.properties中设置remote_hosts时使用IP地址。状态上报延迟最少连接数算法对状态上报的实时性敏感。如果网络延迟高或上报间隔太长可能导致调度决策滞后。可以适当缩短上报间隔如1秒但要权衡网络开销。任务分割不均如果采用“属性注入”的任务分割方式要确保任务包大小设置合理。包太小调度开销大包太大均衡效果差。需要根据单请求平均响应时间来调整。Slave节点宕机处理在生产环境中需要增加调度器的容错机制。如果某个Slave长时间没有上报状态应将其从调度池中移除并将分配给它的任务重新调度到其他节点。8. 性能考量与生产环境建议对JMeter内核进行修改并增加RPC通信必然会引入额外的开销。我们需要评估这些开销是否在可接受范围内。网络开销状态上报最少连接数算法会引入额外的网络流量。假设每2秒上报一次每次数据量很小几十字节对于千兆内网这个开销可以忽略不计。但如果Slave数量极多上百台或上报频率很高如100毫秒则需要考虑聚合上报或使用更高效的二进制协议。调度延迟Master作为中央调度器可能成为瓶颈。如果每秒需要调度成千上万个任务单Master节点的处理能力和网络连接数可能受限。可以考虑将调度器本身设计成无状态、可水平扩展的或者采用双层调度架构。资源消耗在Slave端运行状态上报线程会消耗少量CPU和内存。在压测机资源已经非常紧张的情况下需要评估这部分开销。生产建议循序渐进先在测试环境用小规模集群验证功能与稳定性。监控先行加强对Master和Slave节点的监控特别是Master的CPU、内存和网络连接数以及调度队列的长度。备选方案如果自定义开发维护成本过高可以考虑使用第三方插件或工具。例如结合Taurus这样的测试工具它可以在更高层级协调多个JMeter实例实现简单的负载均衡。或者使用Docker/Kubernetes来编排JMeter容器结合K8s的HPA水平Pod自动扩缩容来间接实现负载均衡但这更多是资源层面的均衡。脚本优化有时负载不均的根源在脚本本身。检查是否有同步定时器、临界区控制器等导致线程阻塞的元件被误用。确保CSV Data Set Config设置为All threads时数据量足够大或者改用Sharing Mode为Current thread。这次对JMeter分布式负载均衡的改造实战是一次从“会用工具”到“理解并改造工具”的深度跨越。它揭示了一个道理没有放之四海而皆准的默认配置特别是在性能测试领域理解底层原理并根据实际场景进行定制是保障测试结果准确性的关键。轮询算法提供了基础的公平性而最少连接数算法则向着智能、自适应的负载均衡迈出了一步。虽然实现过程涉及源码修改有一定复杂度但由此获得的对于压测体系架构的控制力和理解深度是任何现成工具都无法替代的。