從一次效能 “問題”,來看看 jmeter 的臨界控制器原理

varqiao發表於2025-01-14

“執行緒數加了,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();
}

最後,歡迎各位大佬批評指正~

相關文章