性能常识 从一次性能 “问题”,来看看 jmeter 的临界控制器原理

varqiao · 2025年01月14日 · 最后由 varqiao 回复于 2025年01月16日 · 4650 次阅读

“线程数加了,TPS 却没增?” 同事这一反馈把我整个人都弄懵了。增加线程数本来是提升 TPS 的基本操作,怎么会无效呢?我赶紧打开用户面板一看,线程数已经增加到 20 了,但 TPS 却还停留在 6 左右。查看接口的响应时间,居然也没特别高,最高才 1 秒左右。那我就更疑惑了——理论上,如果有 20 个线程,每个接口的 RT 是 1 秒,TPS 至少应该能到 20 才对。可是,为什么就是达不到?我一时间都懵了,心里想:怎么回事?

一开始,我以为是压测容器资源达到了瓶颈。于是,我迅速打开容器的 top 命令一看,结果发现总 CPU 使用率才 20% 左右,JMeter 进程的 CPU 也只用到了 100%,这说明其实只用了一个核。然而,我的压测 Pod 配置是 request: 2c 4g, limit: 4c 8g,资源并没有达到瓶颈。那么问题到底出在哪里呢?我心里一紧,突然想到,进程只用了一个核,而线程是 CPU 执行的最小单位,难道是只有一个线程在跑?于是我赶紧安装了 arthas,检查一下(不过遗憾的是,压测容器没有加上线程监控,要是有就好了)。

具体的执行步骤可以看下面

https://arthas.aliyun.com/doc/install-detail.html //官网

curl -O https://arthas.aliyun.com/arthas-boot.jar //下载
java -jar arthas-boot.jar //执行
dashboard //实时数据面板
thread

可以看到下面只有一个线程在同时执行,好像印证我我的想法了,同时只有一个线程是 runnable 的状态,其他的都是 waiting,看起来是有什么锁。

然后通过 thread 命令,打印线程对堆栈出来,最后看到 reentrantLock,顿时 java 的八股文涌上心头,丸辣,确实有个锁,但是这个锁是干嘛用的呢?其实在排查的时候打印堆栈的时候,就能看到 CriticalSelectController 好像调用了 reentrantLock。

但是为了进一步判断问题,我还是把 jmx 脚本拉到了本地去运行,然后用 jdk 自带的工具做了个线程的 dump,结果如下,在这里我就关注到了好像是 CriticalSelectController 好像调用了 reentrantLock。初步判断是 CriticalSelectController 的问题,CriticalSelectController 不就是临界控制器么?

下面是 window 平台下 jmeter 线程的运行时间序列图,绿色的是正在执行,我们可以看到压测线程只有一个在运行。

一看 jmx 脚本,果然存在 CriticalSelectController 控制器,破案了,然后将 CriticalSelectController 改成事务控制器就好了~

下面是 jmeter 官方对 Critical Section Controller 的解释,其实意思就是说配置这个之后,同时只能有一个线程在执行这个临界控制器。

这里是 Critical Section Controller 源码,很明显看到这里有个 currentlock 负责锁住临界资源。

那么问题来了,我其实很好奇,jmeter 是如何加载各类的 controller 的呢?于是乎,我尝试去扒了一下 jmeter 的源码,画出了以下的流程图(比较粗,也是按照我的想法来画的,可能有理解不准确的地方,各位大佬轻喷)

下面就是我扒 jmeter 源码的过程,只节选了部分关键的代码,并没有把整条链路给扒下来~

//加载xml的节点
public void traverse(HashTreeTraverser visitor) {
    for (Object item : list()) {
        visitor.addNode(item, getTree(item)); 
        getTree(item).traverseInto(visitor);
    }

}

//遍历所有的节点
private void traverseInto(HashTreeTraverser visitor) {
    if (list().isEmpty()) {
        visitor.processPath();
    } else {
        for (Object item : list()) {
            final HashTree treeItem = getTree(item);
            visitor.addNode(item, treeItem);
            treeItem.traverseInto(visitor);
        }
    }
    visitor.subtractNode();
}

@Override
public void subtractNode() {
    if (log.isDebugEnabled()) {
        log.debug("Subtracting node, stack size = {}", stack.size());
    }
    TestElement child = stack.getLast();
    trackIterationListeners(stack);
    if (child instanceof Sampler) {
        saveSamplerConfigs((Sampler) child);
    }
    else if(child instanceof TransactionController) {
        saveTransactionControllerConfigs((TransactionController) child);
    }
    stack.removeLast();
    if (!stack.isEmpty()) {
        TestElement parent = stack.getLast();
        boolean duplicate = false;
        // Bug 53750: this condition used to be in ObjectPair#addTestElements()
        if (parent instanceof Controller && (child instanceof Sampler || child instanceof Controller)) {
            if (parent instanceof TestCompilerHelper) {
                TestCompilerHelper te = (TestCompilerHelper) parent;
                duplicate = !te.addTestElementOnce(child); //加载节点的object进来
            } else { // this is only possible for 3rd party controllers by default
                ObjectPair pair = new ObjectPair(child, parent);
                synchronized (PAIRING) {// Called from multiple threads
                    if (!PAIRING.contains(pair)) {
                        parent.addTestElement(child);
                        PAIRING.add(pair);
                    } else {
                        duplicate = true;
                    }
                }
            }
        }
        if (duplicate) {
            if (log.isWarnEnabled()) {
                log.warn("Unexpected duplicate for {} and {}", parent.getClass(), child.getClass());
            }
        }
    }
}
//这里是children的定义
private transient ConcurrentMap<TestElement, Object> children = new ConcurrentHashMap<>();

//add object进来
@Override
public final boolean addTestElementOnce(TestElement child){
    if (children.putIfAbsent(child, DUMMY) == null) {
        addTestElement(child);
        return true;
    }
    return false;
}

//这里是每个控制器都要实现的统一方法,前面的thread group通过调用next方法来调用后面的实现。    
@Override
public Sampler next() {
    if (StringUtils.isEmpty(getLockName())) {
        if (log.isWarnEnabled()) {
            log.warn("Empty lock name in Critical Section Controller: {}", getName());
        }
        return super.next();
    }
    if (isFirst()) {
        // Take the lock for first child element
        long startTime = System.currentTimeMillis();
        if (this.currentLock == null) {
            this.currentLock = getOrCreateLock();
        }
        this.currentLock.lock();
        long endTime = System.currentTimeMillis();
        if (log.isDebugEnabled()) {
            log.debug("Thread ('{}') acquired lock: '{}' in Critical Section Controller {}  in: {} ms",
                    Thread.currentThread(), getLockName(), getName(), endTime - startTime);
        }
    }
    return super.next();
}

最后,欢迎各位大佬批评指正~

共收到 8 条回复 时间 点赞

为什么要加这个临界控制器呢,求指教

我去催饭 回复

这个是是平台自动转换的,测试同学选错控制器了😂

😀 jemter 这么难用想不通为啥那么多人研究

回复

哈哈,jmeter 的设计我觉得还是不错的,就是太重了而已

大佬!大佬!大佬!果然, testerhome 能学到知识!
请教一下,原文中 “下面是 window 平台下 jmeter 线程的运行时间序列图。。。。。” 下面的图是如何获得的吖,是 Jmeter 的某个插件? 还是某个软件?膜拜 Orz~

Duke 回复

这个是 jdk 自带的包 jvisualvm,可以在你 java 的安装目录下找到,注入你的线程就好了。可以看看官方文档:https://visualvm.github.io/

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册