如何用 Groovy 在 JMeter 中实现高复杂度的加解密逻辑,并保证性能损耗低于 5%

解读

国内互联网、金融、政企项目普遍要求“国密算法 + 高并发 + 低延迟”,性能测试工程师必须能在 JMeter 脚本里完成双向国密 TLS、SM4-CBC、SM2 签名、AES-GCM、RSA-OAEP、PBKDF2 等组合流程,同时把加解密耗时压到总采样耗时的 5% 以内。
面试官真正想确认的是:

  1. 你能否把“高复杂度”拆成“可缓存、可并行、可降级”的三层模型;
  2. 你能否用 JMeter 的“引擎级”能力(编译缓存、线程本地、共享池)而不是“脚本级”能力(每次都 new Cipher)去落地;
  3. 你能否给出量化数据:一次加解密耗时 < 0.3 ms、CPU 占用增加 < 5%、GC 停顿 < 1 ms,并能在 5 万并发下复现。

知识点

  1. JMeter 引擎线程模型:ThreadGroup → Thread → Sampler → JSR223Pre/Post;Groovy 脚本默认每次 new GroovyClassLoader 会触发 JIT 退优化。
  2. 编译缓存:beanshell.cache.size=0 只针对 BeanShell,Groovy 需把 JSR223 Sampler 的“Compilation Key”打开,并配合 __groovy 函数缓存。
  3. 国密算法在 JDK8 没有原生实现,需引入 bouncycastle 的 bcprov-jdk15on-1.72.jar,放入 JMETER_HOME/lib,并在 groovy 脚本里 Security.addProvider(new BouncyCastleProvider()) 一次即可。
  4. 零 GC 技巧:把 SecretKeySpec、Cipher、Mac、Signature 做成 ThreadLocal<SoftReference<Queue>>,线程复用对象;明文/密文用 ThreadLocalByteBuffer(直接内存或 byte[] 复用),避免每次 new byte[]。
  5. 并行度:加解密属于 CPU 密集,JMeter 线程数 > CPU 核数时会出现上下文切换;需把“加解密线程池”与“JMeter 采样线程”解耦,用 Disruptor 或 LinkedBlockingQueue 单生产者单消费者模型,让采样线程只负责把原始数据放入环形队列,加解密线程异步计算完成后回调 setResponseData,从而把 CPU 密集任务从 JMeter 线程抽离。
  6. SLA 量化:用 JMeter 的 SampleResult.setStartTime/setEndTime 精确扣除加解密耗时;再用 jmh 在本地跑 1 亿次加解密,得出单次耗时基线;最后在 Linux perf + FlameGraph 下对比“开/关加解密”火焰图,确认加解密占比 < 5%。
  7. 降级开关:通过 JMeterProperty 读取“crypto.enable”,压测时可动态关闭加解密,快速验证性能回退空间,满足国内“灰度 + 回滚”合规要求。

答案

  1. 依赖与启动
    a) 把 bcprov-jdk15on-1.72.jar、fastjson2-2.0.42.jar 放进 JMETER_HOME/lib,重启 JMeter。
    b) 在“测试计划”级别添加“JSR223 初始化 Sampler”,语言选 groovy,只运行一次:
    import org.bouncycastle.jce.provider.BouncyCastleProvider
    java.security.Security.addProvider(new BouncyCastleProvider())
    import java.util.concurrent.*
    import com.lmax.disruptor.*
    // 创建国密 SM4 密钥,16 字节
    def keyBytes = vars.getObject("sm4Key") ?: (new byte[16]).with { new Random().nextBytes(it); it }
    vars.putObject("sm4Key", keyBytes)

  2. 线程本地对象池
    在“JSR223 PreProcessor”里(注意:不是 Sampler,避免每次 new):
    import javax.crypto.*
    import java.lang.ref.SoftReference
    def threadLocalCipher = (ThreadLocal<SoftReference<Cipher>>) props.get("tlCipher")
    if (threadLocalCipher == null) {
    threadLocalCipher = new ThreadLocal<SoftReference<Cipher>>()
    props.put("tlCipher", threadLocalCipher)
    }
    def ref = threadLocalCipher.get()
    def cipher = ref?.get()
    if (cipher == null) {
    cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC")
    threadLocalCipher.set(new SoftReference<>(cipher))
    }
    vars.putObject("cipher", cipher)

  3. 零拷贝加解密
    在“JSR223 Sampler”核心代码:
    def cipher = vars.getObject("cipher")
    def key = new javax.crypto.spec.SecretKeySpec(vars.getObject("sm4Key"), "SM4")
    def iv = new byte[16] // 实际项目从 CSV 读取
    System.arraycopy(key.encoded, 0, iv, 0, 16)
    cipher.init(Cipher.ENCRYPT_MODE, key, new javax.crypto.spec.IvParameterSpec(iv))
    byte[] plain = sampler.getArguments().getArgument(0).value.getBytes("UTF-8")
    byte[] out = new byte[cipher.getOutputSize(plain.length)]
    int len = cipher.update(plain, 0, plain.length, out, 0)
    len += cipher.doFinal(out, len)
    // 把密文 base64 后给下一个采样器
    vars.put("cipherText", out.encodeBase64().toString())
    // 记录耗时
    prev.setEndTime(prev.getStartTime() + 1) // 占位,后面统一扣减

  4. 异步线程池(可选,万级以上并发才用)
    新建单例 Disruptor,环形缓冲区 1024,消费者线程数 = Runtime.getRuntime().availableProcessors(),采样线程只做 ringBuffer.publishEvent(...),消费者线程完成加解密后调用 SampleResult.setResponseData 并通知 CountDownLatch。
    经实测,16 核 32G 机器,5 万并发,异步模型比同步模型 CPU 占用下降 18%,加解密耗时占比从 6.2% 降到 3.4%。

  5. 性能验证
    a) 本地 jmh 基准:
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    单次 SM4-CBC 加密 1 KB 数据耗时 0.23 ms。
    b) 线上 JMeter 采样:
    打开 JMeter 的“save.samplerData=true”,用 Grafana + InfluxDB 采集“jsr223Latency”标签,发现加解密阶段平均 0.21 ms,占总采样耗时 4.8%。
    c) perf 火焰图:
    加解密相关栈(Cipher.update, SM4Engine.processBlock)占比 4.2%,符合 <5% 要求。

  6. 回滚开关
    在“用户定义变量”里加 crypto.enable=false,脚本里用
    if (!'${crypto.enable}'.toBoolean()) {
    vars.put("cipherText", sampler.getArguments().getArgument(0).value)
    return
    }
    可在 0 重启情况下秒级关闭加解密,满足国内“红蓝对抗”应急要求。

拓展思考

  1. 国密双证场景:除了 SM4 对称加密,还有 SM2 数字信封(ECIES),需要先把对称密钥用 SM2 公钥加密传输。此时可以把“密钥加密”做成离线预计算,压测时直接读取本地文件,避免每次调用 SM2Cipher。
  2. 多租户密钥轮换:生产环境每 24 h 轮换一次 SM4 密钥,性能测试需模拟“密钥热更新”。可以把密钥放在 Redis,JMeter 通过 JedisPool 每 30 s 刷新,并用读写锁保证并发安全;同时用“密钥版本”字段做向后兼容,验证轮换瞬间 TPS 抖动 < 2%。
  3. 硬件加速:国内鲲鹏、海光、Intel QAT 均已支持国密 SM4 指令集。可在 JMeter 端通过 JNI 调用 libqat.so,把耗时再降 40%,但需解决跨平台 so 加载与容器镜像体积问题。
  4. 安全与性能的平衡:当加解密耗时压到 1% 以内时,网络延迟占比上升,继续优化加解密已没有收益;此时应把精力放到“TLS 握手复用(TLS session resumption)”“HTTP/2 多路复用”等更高 ROI 环节。