☰
JMeter UDP并发发送频率测试:自定义Sampler实战与踩坑记录
2026/10/14 15:09:53 网站建设 项目流程

性能测试里,JMeter做HTTP、TCP、JDBC这些协议早就轻车熟路了,但一碰到UDP并发发送频率这种需求,事情就会变得很微妙——JMeter官方一直没有一个像样的UDP采样器,网上流传的插件也普遍年久失修。我之前刚好负责一个并发UDP上报模块的性能摸底工作,业务场景不复杂:边缘节点通过UDP往中心服务发心跳和指标数据,需要验证在并发场景下发送端的发送频率能不能稳定达到预期值。过程中绕了不少弯路,也踩了几个只有UDP才会遇到的坑,这篇把完整的流程和思考写下来,给后面要做类似测试的同学一个可以直接上手的参考。

1. UDP测试的特殊性:为什么直接套TCP压测思路会翻车

1.1 无连接、无应答,让“成功/失败”的定义变得很微妙

做性能测试时,HTTP和TCP的开局思路通常是:建连、发请求、等响应、统计响应时间和错误率。这套模型到了UDP这里直接失效,因为UDP是面向无连接的协议,发送端调用send之后,内核只要把数据交给网卡,send就返回成功了,它根本不会等待对端回复。

这个差异带来的直接影响是:采样器无法通过ACK来判断“对端是否收到”,只能确认“本机发送调用没有异常”。换句话说,JMeter里看到的成功率和响应时间,在UDP场景下只是一个局部视角,只能说明客户端这一侧的行为,不能说明消息真的送达了。

而且,UDP没有TCP那样的拥塞控制机制。TCP在网络拥堵时会自动降速、重传,UDP完全不管这些,发多少就是多少,丢不丢全靠网络和接收端。所以测试UDP时,核心关注点不是“响应有多快”,而是“发送端能不能按照设计频率把数据稳定地发出去”。这个思路转变非常重要,后面的测试计划和指标解读全部围绕它来设计。

1.2 “发送频率”到底指什么:两个指标别混淆

并发UDP发送频率里,“频率”这个词其实包着两个完全不同维度的指标,很多人不提前看清楚,后面读数据时就会发懵。

第一个维度是包速率(pps,packets per second),强调的是单位时间内发出多少个UDP数据报。这个指标在网关转发、防火墙吞吐、心跳上报这类场景里最常用。第二个维度是吞吐量(MB/s或Mbps),强调的是单位时间内传输了多少字节,日志传输、视频流、文件分发这类场景更关心它。

这两者在测试设计上差异很大。比如目标是从一个节点持续向中心发送2000pps,每包100字节,那么带宽其实只有 2000 × (100 + 42) × 8 ≈ 2.27Mbps,千兆网卡下绰绰有余,瓶颈显然不在带宽上,而在应用层自己的发送节奏、系统调用开销和Socket缓冲区配置上。如果只看吞吐量而忽略pps,很可能会因为窗口太小测不出问题;反过来,如果只看pps而不看字节数,又可能完全忽略大包分片带来的隐性故障。

这里还有一个坑:JMeter的“吞吐量”默认单位是请求数/分钟,不是请求数/秒。用常数吞吐量定时器按“每分钟发送24000次”配置时,稍微算错一位,实际发出去的就是每秒400次,整个测试节奏全偏了。在我自己的实践里,最稳妥的做法是先把目标换算成“每线程每秒发多少包”,再由线程数和循环次数去拆解,具体计算方式放在第4章详细说。

1.3 JMeter原生能力边界:没有官方UDP采样器,插件也基本不可用

在JMeter 5.x的采样器列表里,官方提供了HTTP、TCP、FTP、JMS、JDBC、Java Request等,但确实没有UDP。这意味着开箱即用是做不到的,必须走扩展路线。我调研了一圈,归纳下来只有三种路:

方案实现方式优点缺点适合场景
JSR223脚本Groovy脚本里直接new DatagramSocket发UDP写起来快,不用编译打包高并发下脚本引擎开销大;代码逻辑难维护简单冒烟、临时验证
第三方UDP插件加载网上现成的插件jar省事,看起来能直接用年久失修,依赖旧JMeter版本;遇到bug基本得自己改源码环境封闭、无法造轮子时兜底
自定义Java Sampler继承AbstractJavaSamplerClient,编译成jar放进lib/ext官方扩展点,稳定高效;代码可控,逻辑可复现需要写Java代码和打包需要长期复用、并发量较大的正式性能测试

我最终选了第三种。原因很现实:UDP发送操作本身非常轻量,毫秒级甚至微秒级就能完成一次,一旦并发线程数上去,JSR223脚本的解释执行开销会被放大到不可接受。而自定义Java Sampler是JMeter官方预留的扩展机制,类加载路径清晰,运行时性能和JMeter自己内置的采样器是一个量级,跑高频发送场景最稳妥。

至于第三方插件,我并非完全排斥,但性能测试工具最重要的属性是可复现。插件版本和JMeter版本强绑定,一旦JMeter升级,插件很容易失效,测试结果就无法回溯了。自己写Sampler虽然前期多花一两个小时,但后续换环境、调参数、加功能都非常顺手。

2. 环境准备:JDK、JMeter与自定义Sampler的装载

2.1 部署时最容易翻车的三个环节

JMeter本质上是一个Java程序,环境准备绕不开JDK。第一步是确认版本匹配:JMeter 5.x要求Java 8以上,低于这个版本启动会直接报Class版本错误。Linux服务器上经常预装的就是OpenJDK 11或17,跑JMeter 5.6基本没问题,但如果用的JMeter版本比较老,比如5.0之前的,还是要老老实实装JDK8。

第二个容易翻车的地方是堆内存。JMeter默认的启动堆内存只有512MB,跑HTTP测试可能勉强够用,测UDP高并发时,每个采样结果、每个线程的执行上下文都会占内存,再加上聚合报告和结果树监听器,几分钟就能把堆撑爆。我一般会在JMeter的bin目录下找到setenv.sh,把HEAP参数改成:

export HEAP="-Xms2g -Xmx4g"

记得-Xms和-Xmx设成一样的值,避免运行中频繁扩容和回收导致GC抖动。注意不要用GUI模式跑正式测试。JMeter的图形界面本身就要占很多CPU和内存,而且GUI模式下监听器实时刷新数据会进一步干扰测试节奏。正确做法是用GUI搭好jmx脚本,保存后用命令行执行:

./jmeter.sh -n -t udp-push.jmx -l result.jtl -j jmeter.log

第三个环节是文件描述符限制。UDP测试虽然不建连接,但每个线程的DatagramSocket会占用一个文件描述符,加上JMeter自身的各类文件句柄,高并发时很容易碰到ulimit -n的限制。服务器上最好先执行ulimit -n 65535,或者直接写到当前用户的profile里,否则跑到一半报“Too many open files”会让人摸不着头脑。

2.2 自定义Sampler的装载机制

写好的Java Sampler会打成一个jar包,放到JMeter安装目录的lib/ext下。这里有个细节:如果Sampler依赖了第三方库,依赖的jar要放到lib目录而不是lib/ext目录。因为lib/ext下的jar全部会被JMeter当作扩展扫描,放错了可能导致依赖加载顺序异常。

包路径也要特别留意。JMeter通过完整类名来识别Sampler,配置Java Request时填的Classname必须和jar里实际的包路径完全一致,大小写敏感。比如我下面的示例代码类名是com.demo.udp.UdpSampler,那么在jmx脚本里填的就必须一字不差,否则运行时会直接报ClassNotFoundException。

修改完lib/ext目录后,必须完全重启JMeter进程,扩展不会被热加载。如果是在远程Linux机上临时追加jar,可以在重启前先看一眼jmeter.log,确认没有加载异常再继续,不然排查半天可能是环境没生效。

2.3 快速验证Sampler是否被加载

验证环境是否就绪,最直接的方法是打开JMeter GUI,添加一个“Java Request”采样器,然后点击Classname下拉框。如果能从下拉列表里看到com.demo.udp.UdpSampler,说明jar已经被正确加载。

如果看不到,优先级最高的检查是确认jar文件本身的内部结构。用jar tf命令看一眼:

jar tf /opt/jmeter/lib/ext/udp-sampler.jar

正常输出应该包含com/demo/udp/UdpSampler.class。如果class文件路径和这里不一致,比如编译时没有带包路径直接生成了根目录的class文件,那么在JMeter里填包名类名就永远找不到。这个问题我遇到过不止一次,多数是因为编译时-d参数指定错了输出目录。

3. 自研UDP发送Sampler:代码逻辑与参数设计

3.1 为什么不用JSR223脚本

既然JMeter支持JSR223 Groovy,很多人会直接在脚本里写DatagramSocket发送,我的建议是:临时验证没问题,正式测试不要用。

原因有两层。第一层是性能,Groovy脚本虽然会被JMeter缓存编译,但脚本体只要稍微复杂一点,解释执行的开销就上来了。UDP发送本身就是一次几十微秒的系统调用,脚本引擎的CPU开销很容易超过实际发送的开销,最终测出来的pps掉得厉害。第二层是可控性,脚本里的网络错误、参数传递、结果记录全部混在一起,出问题后调试成本高。自定义Java Sampler把发送逻辑和JMeter采样逻辑分离,后续加报文模板、加编号、加统计都很自然。

3.2 核心代码:一个可直接运行的UDP Sampler

下面这个类是我在实际项目中用过的版本,去掉了业务字段,保留了核心逻辑。类继承AbstractJavaSamplerClient,JMeter会通过getDefaultParameters读取参数表、通过setupTest在每个线程开始时初始化资源、通过runTest执行每次采样发送。

package com.demo.udp; import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; public class UdpSampler extends AbstractJavaSamplerClient { private static final long serialVersionUID = 1L; private DatagramSocket socket; private InetSocketAddress targetAddress; private int sendInterval; private byte[] payload; @Override public Arguments getDefaultParameters() { Arguments args = new Arguments(); args.addArgument("host", "127.0.0.1"); args.addArgument("port", "9000"); args.addArgument("message", "hello udp"); args.addArgument("intervalMs", "0"); args.addArgument("packetSize", "0"); return args; } @Override public void setupTest(JavaSamplerContext context) { String host = context.getParameter("host"); int port = Integer.parseInt(context.getParameter("port")); sendInterval = Integer.parseInt(context.getParameter("intervalMs")); String msg = context.getParameter("message"); int packetSize = Integer.parseInt(context.getParameter("packetSize")); payload = msg.getBytes(StandardCharsets.UTF_8); if (packetSize > 0 && packetSize != payload.length) { byte[] fixed = new byte[packetSize]; System.arraycopy(payload, 0, fixed, 0, Math.min(payload.length, packetSize)); for (int i = payload.length; i < packetSize; i++) { fixed[i] = (byte) 0x00; } payload = fixed; } try { socket = new DatagramSocket(); targetAddress = new InetSocketAddress(host, port); } catch (Exception e) { throw new IllegalStateException("init udp socket failed", e); } } @Override public SampleResult runTest(JavaSamplerContext context) { SampleResult result = new SampleResult(); result.sampleStart(); try { if (sendInterval > 0) { Thread.sleep(sendInterval); } DatagramPacket packet = new DatagramPacket(payload, payload.length, targetAddress); socket.send(packet); result.setSuccessful(true); result.setResponseCode("0"); result.setResponseMessage("udp send ok"); result.sampleEnd(); result.setBytes(payload.length); } catch (Exception e) { result.sampleEnd(); result.setSuccessful(false); result.setResponseCode("ERR"); result.setResponseMessage(e.getMessage()); } return result; } @Override public void teardownTest(JavaSamplerContext context) { if (socket != null) { socket.close(); } } }

编译和打包的命令我一般这样写。注意classpath需要包含JMeter核心jar,最省事的方式是把整个lib目录下的jar都拼到classpath里:

CP=$(find /opt/jmeter/lib -name '*.jar' | tr '\n' ':') javac -cp "$CP" -source 1.8 -target 1.8 -d /tmp/udp-class com/demo/udp/UdpSampler.java jar cvf /tmp/udp-sampler.jar -C /tmp/udp-class . cp /tmp/udp-sampler.jar /opt/jmeter/lib/ext/

编译时加-source 1.8 -target 1.8是为了让class文件版本保持兼容,避免服务器上JDK版本比编译环境低时出现UnsupportedClassVersionError。

3.3 参数设计的几个关键点

代码里的参数看着简单,实际使用时有几个细节值得展开。

intervalMs这个参数控制每次发送前的等待时间,它是整个频率控制的核心。设置为0时,线程会以“跑满”的状态疯狂发送,适合摸底极限pps;设置为10时,单线程每秒约发100包,适合作平稳频率测试。

packetSize参数用来固定报文大小。真实业务报文长度往往有波动,但性能测试最好用一个固定值,这样pps、带宽和系统调用次数都是确定的,方便和接收端对账。我的做法是把payload长度不够的字节用0x00补齐,超过的则只取前N个字节,这样无论测试报文怎么变化,网络层看到的数据报大小都一样。

setupTest和runTest的分工也要说清楚。setupTest在每个线程首次执行采样前只会调用一次,DatagramSocket在这里创建,可以复用整个线程的整个生命周期。如果把new DatagramSocket()放进runTest,每个线程每次迭代都会创建新Socket、绑定新端口,高并发下会迅速耗尽临时端口,后期就会报Cannot assign requested address。

4. 测试计划设计:从“每秒多少包”倒推线程与循环

4.1 先定频率指标,再反推线程与循环次数

做并发UDP发送频率测试,最容易犯的错是一上来就拍脑袋定线程数,比如“并发100个线程”,好像并发数上来了频率自然就达标了。但实际上并发数和发送频率是两个层面的东西,必须从目标pps反推所有参数。

假设业务目标是端到端稳定达到2000pps。如果单线程每发一包后等待10ms,也就是每线程每秒约100包,那么需要的线程数是:

线程数 = 目标pps / 每线程每秒发包数 线程数 = 2000 / 100 = 20

线程组里再设置“循环次数”为每秒发包数 × 持续秒数。比如持续运行60秒,每个线程的循环次数就是 100 × 60 = 6000次。总样本量就是 20 × 6000 = 120000,正好和2000pps × 60秒对得上。

如果只想测指定时间,更稳妥的方式是在线程组里勾选“持续时间(Duration)”,并设置一个足够大的循环次数。运行时JMeter会在时间到了之后主动停止,不需要精确计算循环次数。

Ramp-Up Period的设置同样会影响曲线。如果20个线程瞬间同时启动,前几秒会有一个明显的发包洪峰,然后才会回落到设定频率。Ramp-Up设为5秒,让线程错峰启动,曲线的上升过程会平滑很多。因为UDP没有连接建立过程,Ramp-Up的唯一作用就是错峰,避免瞬时洪峰把接收端socket缓冲区直接打满。

4.2 Java Request采样器的配置细节

测试计划里添加“线程组→Java Request”之后,Classname填写com.demo.udp.UdpSampler,参数表里按getDefaultParameters里声明的参数填入值。JMeter的参数表支持函数和变量引用,所以我一般会把host、port独立成JMeter用户自定义变量,切换环境时只改一处。

需要随机报文内容时,很多人会直接在message参数里用${__RandomString(...)}生成随机字符串。这个做法可行,但函数解析发生在采样器执行过程中,高ppm下会有额外开销。更高效的做法是在Sampler的getDefaultParameters里加一个randomSuffixLength参数,在setupTest中生成一次随机字节数组,作为报文的后缀,这样每个线程使用的报文形态固定,但线程之间各不相同,既模拟了真实业务的报文多样性,又不增加运行时的解析开销。

如果希望每个包的报文都不同,比如要测试接收端对乱序、重复、缺失的处理,建议在runTest里维护一个AtomicLong计数器,把序号拼到payload开头。这个逻辑在JSR223脚本里不是不能做,只是并发时的原子性和性能都不如Java类里直接用AtomicLong。

4.3 监听器的取舍:别让统计本身拖垮测试

测试计划里加监听器不是越多越好。查看结果树(View Results Tree)在调试阶段很有用,能直接在GUI里看到每个采样的发送结果和异常堆栈,但正式跑大规模测试时一定要关掉它。

我之前用100线程、每线程每秒50包做验证时,开着结果树跑了两分钟,JMeter的CPU直接飙到300%,发送频率曲线明显变形,测出来的pps比预期低了近三分之一。原因就是结果树要为每个sample保存完整的响应数据,这个成本在高频UDP场景下比测试业务本身还高。

正确做法是:正式测试只用两种数据收集方式。第一种是聚合报告,在非GUI模式下通过-l result.jtl输出原始样本文件,再用-e -o report-dir生成HTML报告;第二种是Simple Data Writer,只记录时间戳、线程名、成功标志、发送字节数这几个字段,落盘文件很小,对执行端的干扰也小。跑完后再看聚合报告里的“吞吐量”和“样本数”,结合前面提到的公式核验实际pps是否达标。

5. 跑测实操与结果解读:哪些数据能信,哪些不能信

5.1 一次完整的跑测过程记录

用一个虚构的数据上报服务来演示完整流程。服务端在某台Linux主机上监听UDP 9000端口,客户端需要持续上报100字节左右的业务心跳,目标验证2000pps下客户端发送模块是否稳定。

环境是两台同网段的8C16G服务器,千兆网卡,JMeter部署在发送端,接收端是一个简单的UDP收包程序,会统计每秒实际收到的包数和字节数。

测试计划参数如下:

  • 线程数:20
  • Ramp-Up:5秒
  • 循环次数:6000
  • Java Request参数:host=接收端IP,port=9000,message=100字节固定报文,intervalMs=10
  • 集成:聚合报告 + Simple Data Writer

执行命令:

./jmeter.sh -n -t udp-push.jmx -l result.jtl -e -o report

跑完后聚合报告显示样本数119820,运行时长约60秒,换算实际pps约1997,成功率100%,平均响应时间约1.4ms。单看这个结果,似乎频率达标了。

5.2 从发送端指标能确认什么,不能确认什么

聚合报告里这几个数字,在UDP场景下要逐个拆开看:

指标能说明什么不能说明什么
样本数/运行时长实际发送频率的近似值不包含对端实际收包情况
错误率本机socket send是否抛异常不反映网络层和接收端丢包
平均响应时间包含intervalMs等待+send系统调用耗时不是端到端RTT,不能当作网络延迟
吞吐量(KB/s)本机发送数据的速率不反映接收端实际消费速率

这里要重点提醒一句:当runTest里设置了sendInterval时,SampleResult响应时间包含Thread.sleep的时间。也就是说,intervalMs=10时,哪怕send调用本身只耗时0.05ms,采样器记录的平均响应时间也会接近10ms,这个数字和网络延迟没有关系,千万别拿去当UDP RTT分析。

真正能说明问题是样本数:120000个样本中实际记录了119820个,误差大概率来自测试收尾阶段线程组Graceful Shutdown时的线程停止过程,这个偏差在可接受范围内。发送端能确认的是“客户端这一侧已经把流量按设定节奏发出去了”,仅此而已。

5.3 接收端对账:丢包率怎么补充验证

UDP性能测试一定要做发送端和接收端的对账。我在这个项目里最深刻的体会就是:发送端pps达标不等于业务端到端可达,中间的网卡、交换机、接收端socket缓冲区都可能丢包,而且UDP丢包静默无声,不主动对账就永远不知道。

接收端程序需要在每个数据报里提取序号字段,按秒统计收到的包数,再和发送端记录的每秒发出包数对比。如果收包数持续低于发包数,优先怀疑三个位置:

一是接收端socket缓冲区太小。Linux默认的UDP接收缓冲区通常只有几百KB,突发流量一来,内核在socket缓冲区满的时候直接丢掉后续包。排查方法是看接收端有没有报ENOBUFS,同时用ss -u查看接收端的接收队列是否溢出。临时调大可以执行:

sysctl -w net.core.rmem_max=8388608 sysctl -w net.core.rmem_default=8388608

应用代码里再调用setReceiveBufferSize设置更大的接收缓冲区,注意要设置在socket监听之前。

二是发送端瞬时突发。即使平均频率是2000pps,线程组同时触发的瞬间也可能出现短期3000pps的突发,接收端来不及消费就丢。此时调整Ramp-Up分散启动,或者在Sampler里加一个小的发送抖动,让发包间隔更均匀。

三是链路层丢包。如果接收端pps持续偏低,且没有任何socket buffer错误,就要考虑链路丢包。千兆网卡上跑2000pps、每包100字节的流量远不到瓶颈,一般不会出现这种问题,但在多层交换网络中确实遇到过。

6. 高频踩坑与排查实录

6.1 数据包超过1472字节之后,发送成功并不代表对端能收到

以太网MTU是1500字节,IPv4头部20字节、UDP头部8字节,留给应用层payload的空间是1472字节。发超过1472字节的UDP包,IP层会自动分片,接收端收到后再重组。

实测里最诡异的一次,是把业务报文模拟成3000字节大小,发送端看着一切正常,pps稳定、成功率100%,接收端却疯狂丢包。接收端程序没有任何报错,抓包才发现是分片重组失败:某个分片在网络中丢失,整个数据报在接收端重组不完成,被内核直接丢弃。而分片丢失的瞬间往往没有明显规律,看起来就像随机丢包。

建议测试前用ping探测路径MTU。在Linux上可以执行:

ping -M do -s 1472 <目标IP>

如果这条命令能通,说明1472字节payload不需要分片;如果报错或无法正常返回,说明路径MTU小于1500,需要相应减小payload。性能测试场景中如果业务报文超过1472字节,最好在测试报告里明确记录分片风险,避免把应用层问题误判成网络问题。

6.2 源端口耗尽:一个被忽略的坑

前面提到过,把new DatagramSocket()放在runTest里会造成每次迭代都绑定新的本地端口。Linux临时端口的默认范围通常是32768到60999,大约28000多个端口。一旦JMeter线程频繁创建Socket,端口被占满,新的Socket绑定就会失败,报错信息是java.net.BindException: Cannot assign requested address。

这个问题的修复不复杂:把Socket创建挪到setupTest里,每个线程只创建一个、全程复用,稳定后系统调用次数也大幅下降。如果确实需要多Socket模型,可以考虑调大临时端口范围:

echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range

另外注意JVM进程本身的文件描述符上限。20线程的常规测试可能看不出来,一旦线程数涨到200以上,每个线程一个Socket加上JMeter自身的socket、文件和日志句柄,很容易顶到ulimit -n默认值,症状是各种莫名其妙的IOException。

6.3 JMeter内存溢出与高并发GC抖动

高频UDP测试场景下,JMeter自身的内存管理会成为影响测试结果的因素。我遇到过两次比较典型的情况。

第一次是开着聚合报告的同时又把结果树开着,跑了几分钟之后开始疯狂GC,界面卡顿,发送频率曲线呈锯齿状。第二次是长时间持续测试,比如跑30分钟以上,往往前几分钟一切正常,后面突然爆java.lang.OutOfMemoryError,查看jmeter.log才发现堆已经满了。

这类问题的处理经验是:正式测试时堆内存设大,并保证-Xms和-Xmx一致;监听器只保留必要的,关闭结果树;采样数据只记录必填字段。如果条件允许,可以在setenv.sh里调整新生代大小,比如加上-XX:NewSize=2g -XX:MaxNewSize=2g,减少短命对象在老年代频繁晋升带来的GC开销。

其实JMeter在大几千甚至上万pps时仍有适用边界。我做过对比,用JMeter测几千pps非常稳定,但到了几万pps时,发送线程调度的误差开始变大,这时候可能更适合直接用底层工具。JMeter的优势在于脚本化、有报告、易复现,这个定位要想清楚。

6.4 Sampler加载失败/类找不到的排查链路

Java Request的下拉框里看不到自定义Sampler,是老生常谈的问题,但每次排查的路径其实很固定。

第一步,确认jar包已经在lib/ext目录,并且JMeter是重启后的新进程。修改lib/ext目录后不重启,加载器不会感知新jar。

第二步,用jar tf确认class路径是否正确。如果class文件不在com/demo/udp/这个包结构下,填类名时无从下手。

第三步,看jmeter.log里是否有异常。很多时候JMeter启动时就已经加载失败了,但GUI界面不会弹窗,所有错误都沉默在日志文件里。

第四步,确认JDK版本匹配。JDK17编译的class文件扔到JDK8环境里运行,会报UnsupportedClassVersionError。反过来JDK8编译的class在JDK17下通常没问题,但为了稳妥,编译时统一加-source 1.8 -target 1.8。

还有一种比较隐蔽的情况:jar包里带了META-INF里的重复清单文件,或者打包时把整个目录结构打错了。保持一个最小jar包是最省心的,只包含.class文件,任何多余的内容都不要带进去。

跑完这一轮UDP并发发送频率测试,我最深的感受是:用JMeter测UDP,本质上不是在测网络,也不是在测接收端,而是把发送端这个“发射器”的节奏精确对齐到我们想要的pps上。发送端指标永远只是半个答案,另一半需要接收端对账。而Sampler代码里Socket复用、intervalMs控制、固定包大小这几个设计点,看起来不起眼,其实是整个测试能不能平滑跑完的关键。如果后面有时间,我打算把接收端统计埋点也做成一个配套的JMeter插件,让一套jmx文件能同时看发送和接收两边的数据,那样UDP测试的完整度就更高了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询