HTTPS 双向证书场景,如何在 JMeter 与 Locust 中配置并验证握手耗时

解读

国内金融、政务、IoT 等系统普遍采用“双向 TLS(mTLS)”做身份强校验,面试时考官不仅想看“能不能跑通”,更关注“如何量化握手成本、如何隔离握手耗时、如何在高并发下验证证书链与 OCSP 对性能的影响”。回答必须体现:

  1. 证书链完整且符合国密/国际双栈要求;
  2. 握手耗时指标精确到 TCP 完成后的第一个 ApplicationData 之前;
  3. 工具配置与代码级埋点并重,能落地到持续集成流水线;
  4. 压测结果可回溯,满足国内等保、央行 237 号文对“关键页面可溯源”的要求。

知识点

  1. mTLS 握手阶段:ClientHello → ServerHello → ServerCertificate → CertificateRequest → ServerHelloDone → ClientCertificate → ClientKeyExchange → CertificateVerify → ChangeCipherSpec → Finished → ApplicationData。
  2. 耗时定义:tcp.connect.time + ssl.handshake.time;JMeter 5.5 起通过 JMeter Property https.default.protocol=TLSv1.3jsse.enableSNIExtension=true 控制;Locust 侧依赖 gevent 的 ssl.SSLContexttime_to_first_byte 差值计算。
  3. 国密双证书:签名证书 + 加密证书,需在 JDK 装入 bcprov-jdk18on、加载 SM2/SM3/SM4 算法提供者;JMeter 通过自定义 Sampler 调用 GMJSSE 的 GMSSLContext,Locust 通过 ssl.PROTOCOL_TLS 加载 gmssl 库。
  4. 证书固定:JMeter 的 Keystore Configuration 元件支持 PKCS12 与 JKS,但默认不校验 CRL/OCSP,需打开系统属性 com.sun.net.ssl.checkRevocation=true 并在 <JAVA_HOME>/lib/security/java.security 中配置 ocsp.enable=true
  5. 耗时隔离:JMeter 用 Connect TimeSSL Handshake Time 两个独立字段写入 jtl,Granfana 插件可直接绘图;Locust 需在 on_start 里复写 HttpSessionsend 方法,在 socket.do_handshake() 前后打时间戳。
  6. 高并发下证书开销:ECC 256 位单核 QPS 损耗约 8%,RSA 2048 约 18%;国密 SM2 单核损耗约 12%,但硬件加速卡(海光、兆芯)可把损耗压到 3% 以内。
  7. 等保测评要求:测试报告需包含“证书链深度”“吊销状态”“密钥强度”“随机数熵值”四项截图,JMeter 可打开 -Djavax.net.debug=ssl:handshake 重定向到日志,Locust 用 ssl.SSLSocket.shared_ciphers() 打印协商套件。

答案

一、JMeter 侧

  1. 准备客户端证书:
    a. 导出用户证书+私钥到 user.p12,指定别名 client,密码 secret;
    b. 把 CA 证书导入 JMeter 所在 JDK 的 truststore(cacerts),避免 UNKNOWN_CA 告警。
  2. 配置元件:
    a. 测试计划→添加“Keystore Configuration”,路径指向 user.p12,密码 secret,别名变量 ${certAlias};
    b. HTTP Request Defaults→Protocol 选 https,端口 443;
    c. 在 system.properties 追加:
    https.use.cached.ssl.context=false
    https.default.protocol=TLSv1.3
    javax.net.ssl.keyStore=user.p12
    javax.net.ssl.keyStorePassword=secret
    javax.net.ssl.keyStoreType=PKCS12
    com.sun.net.ssl.checkRevocation=true
    d. 如需国密,把 https.default.protocol=GMSSLv1.1 并替换 JSSE 提供者为 GMJSSE。
  3. 提取握手耗时:
    a. 修改 user.properties:
    sampleresult.timestamp.start=true
    sampleresult.useNanoTime=true
    b. 结果树中自动输出 ConnectTimeSSLHandshakeTime
    c. 非 GUI 运行追加 -Jjmeter.save.saveservice.ssl=true,jtl 文件会多两列,可直接喂给 Grafana。
  4. 验证:
    a. 单线程跑一次,查看 jtl 中 SSLHandshakeTime 应≈TCP 建立后 2-RTT(TLS1.3)或 4-RTT(TLS1.2);
    b. 开 200 线程阶梯加载,观察 SSLHandshakeTime 99 线突增即证书链或 OCSP 阻塞,需引入 stapling 或本地缓存。

二、Locust 侧

  1. 准备:
    a. 把客户端证书转为 PEM 格式:user.pem + key.pem;
    b. 安装 geventhttpclient>=2.0cryptography>=41,国密再加 gmssl>=3.2.0
  2. 代码模板:
import ssl, time, locust
from geventhttpclient import HTTPClient
from geventhttpclient.url import URL

class MutualTLSLocust(locust.HttpUser):
    abstract = True
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
        ctx.load_cert_chain('user.pem', 'key.pem')
        ctx.check_hostname = False   # 如域名与证书 CN 不一致
        ctx.verify_mode  = ssl.CERT_REQUIRED
        ctx.load_verify_locations('ca.pem')
        self.client = HTTPClient.from_url(URL(self.host), ssl_context=ctx, concurrency=200)

class ApiUser(MutualTLSLocust):
    @locust.task
    def t(self):
        t0 = time.perf_counter()
        resp = self.client.get('/api')
        t1 = time.perf_counter()
        # 首次连接会触发 handshake,后续复用
        locust.events.request.fire(
            request_type='mTLS',
            name='handshake' if resp._connection.sock else 'reuse',
            response_time=int((t1-t0)*1000),
            response_length=len(resp.content)
        )
  1. 隔离首次握手:
    a. 在 on_start 里主动预热:
    self.client.get('/health')
    events.request 把第一次耗时单独标记为 ssl_handshake
    b. 并发场景下,Locust 的 FastHttpUser 默认连接池复用,如需强制全握手,把 ctx.options |= ssl.OP_NO_TICKET 并设置 client.connection_pool_size = 0
  2. 校验:
    a. 本地抓包:tshark 过滤 ssl.handshake.type == 1 and ip.src == 客户端IP,看 ClientHello 到 ChangeCipherSpec 时间差;
    b. 对比 Locust 日志与抓包,误差应 <5 ms,确保脚本级指标可信;
    c. 输出到 InfluxDB,配置 Grafana 面板:维度为 certificate_type(RSA/SM2)、 concurrency,指标为 p99_ssl_handshake

三、通过准则

  1. 单线程握手耗时:TLS1.3+ECC ≤ 90 ms,TLS1.2+RSA ≤ 250 ms,SM2 ≤ 110 ms;
  2. 2000 并发、持续 5 min,握手失败率 0%,99 线涨幅不超过基线 30%;
  3. 服务器端 CPU 增加 ≤ 5 个百分点,网络包重传率 ≤ 0.1%;
  4. 报告含证书链深度、OCSP 状态、协商套件、随机数熵值截图,满足央行 237 号文与等保 2.0 测评要求。

拓展思考

  1. 硬件加速:海光 C86 板卡对 SM2 签名可达 18 k ops/core,JMeter 通过 JNI 调用 libhsm.so,把 SSLContext 的私钥操作 offload 到卡内,可将握手 QPS 提升 4.5 倍;面试可提“如何在 JMeter 自定义 Sampler 里调用 JNI”。
  2. 会话复用 vs 全握手:在微服务网格中,Sidecar 默认启用 Session Ticket,若压测目标为“新建连接风暴”,需关闭 ticket 并随机化 ClientHello 填充,防止网关聚合欺骗结果。
  3. 0-RTT 风险:TLS1.3 0-RTT 存在重放攻击,金融交易类系统禁止开启,压测时可故意重放同一 Early-Data 观察服务器是否拒绝,验证安全策略。
  4. 证书热轮换:K8s 场景下,Cert-Manager 可做到 30 秒级滚动更新,压测脚本需支持动态加载新证书而不重启线程,可在 Locust 中监听文件事件,调用 ctx.load_cert_chain() 热更新,考查候选人对“长连接无损轮换”的理解。
  5. 合规审计:等保 2.0 要求“关键操作可追溯到人”,可把 JMeter 的 SampleEvent 写入区块链存证平台(如蚂蚁链 BaaS),每条握手记录带上证书指纹与操作员工号,面试中提及即可体现“性能+合规”双重意识。