Java cpu 監控 分析

Abstract

在這篇文章中我們會綜合性的介紹如何監控JVM cpu,  thread 級別cpu, 以及如何通過JFR技術來分析JVM的CPU 問題.

 

如何獲取CPU

這裏我們會先介紹如何在進程內部獲取JVM的CPU. 這裏我們主要採用JVM 自帶的JMX來實現對自己的監控.

獲取整個系統的JVM cpu

可以通過調用mbean中的getProcessCpuTime方法來得到中的cputime. 

簡單點來說就是:

(cputime2 - cputime1)/1000000/elapseTimeInMs/processorCount

完整代碼如下:

package cpu;

import com.sun.management.OperatingSystemMXBean;

import java.lang.management.ManagementFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class CpuTest {

    public static void main(String[] args) {
        final AtomicInteger seq = new AtomicInteger(0);

        ScheduledExecutorService es = Executors.newScheduledThreadPool(20, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread th = new Thread(r);
                th.setName("consumingthreads-" + seq.incrementAndGet());
                return th;
            }
        });

        for (int i = 0; i < 200; i++) {
            es.scheduleAtFixedRate(new ConsumingCpuTask(), 0, 10, TimeUnit.MILLISECONDS);
        }

        // not terminate the es



        // another thread to print host cpu
        ScheduledExecutorService printer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread th = new Thread(r);
                th.setName("printer");
                return th;
            }
        });
        // print every 10 seconds
        printer.scheduleAtFixedRate(new PrintCurrentProcessCpuTask(), 0, 10, TimeUnit.SECONDS);
    }

    static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors();
    // notice here is com.sun.management.OperatingSystemMXBean and it's not java.lang.management.OperatingSystemMXBean
    static final OperatingSystemMXBean bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();

    /**
     * get process cpu in nanoseconds
     */
    static double getProcessCpuTime() {
        return bean.getProcessCpuTime();
    }


    /**
     * A task to simulate consuming cpu
     */
    static class ConsumingCpuTask implements Runnable {

        @Override
        public void run() {
            AtomicInteger integer = new AtomicInteger(0);
            for (int i = 0; i < 10000; i++) {
                integer.incrementAndGet();
            }
        }
    }

    static class PrintCurrentProcessCpuTask implements Runnable {

        double cpuTime = 0;
        long collectTime = 0;

        @Override
        public void run() {
            if (cpuTime == 0) {
                cpuTime = getProcessCpuTime();
                collectTime = System.currentTimeMillis();
            }
            else {
                double newCpuTime = getProcessCpuTime();
                long newCollectTime = System.currentTimeMillis();
                double cpu = (newCpuTime - cpuTime) / (newCollectTime - collectTime) / 1000_000 / PROCESSOR_COUNT;
                cpuTime = newCpuTime;
                collectTime = newCollectTime;
                System.out.println(String.format("Process cpu is: %.2f %%", cpu * 100));
            }
        }
    }
}

當你運行這個代碼就可以看到定時打出的cpu指標,比如在我的機器上是17%左右(本身有16核): 

而這個值跟系統顯示也是一致的(mac端的Activity Monitor/top命令):

或者用Java自帶的Jconsole 可以看(是JDK自帶的工具, 在bin目錄),運行jconsole

選擇關注的進程:

 

獲取系統中各個線程的CPU

這裏我們會展示如何獲取系統中各個線程的CPU 這個也很好統計.

獲取線程的cpu主要通過ThreadMXBean獲取:

    // it's com.sun.management.ThreadMXBean
    static ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();

    static class PrintThreadCpuTask implements Runnable {
        // not consider thread safe here
        Map<Long, Long> threadId2CpuTime = null;

        Map<Long, String> threadId2Name = null;
        private long collectTime = 0;

        @Override
        public void run() {
            if (threadId2CpuTime == null) {
                threadId2CpuTime = new HashMap<>();
                threadId2Name = new HashMap<>();
                long threads[] = threadMXBean.getAllThreadIds();
                long cpuTimes[] = threadMXBean.getThreadCpuTime(threads);
                for (int i = 0; i < threads.length; i++) {
                    threadId2CpuTime.put(threads[i], cpuTimes[i]);
                    // get the thread name
                    // maybe null, if not exists any more
                    ThreadInfo info = threadMXBean.getThreadInfo(threads[i]);
                    if (info != null) {
                        threadId2Name.put(threads[i], info.getThreadName());
                    }
                }
                collectTime = System.currentTimeMillis();
            }
            else {
                long threads[] = threadMXBean.getAllThreadIds();
                long cpuTimes[] = threadMXBean.getThreadCpuTime(threads);
                Map<Long, Long> newthreadId2CpuTime = new HashMap<>();
                for (int i = 0; i < threads.length; i++) {
                    newthreadId2CpuTime.put(threads[i], cpuTimes[i]);
                }

                long newCollectTime = System.currentTimeMillis();
                threadId2CpuTime.entrySet().forEach(en -> {
                    long threadId = en.getKey();
                    Long time = en.getValue();
                    Long newTime = newthreadId2CpuTime.get(threadId);
                    if (newTime != null) {
                        double cpu = (newTime - time) * 1.0d / (newCollectTime - collectTime) / 1000000L / PROCESSOR_COUNT;
                        System.out.println(String.format("\t\tThread %s cpu is: %.2f %%", threadId2Name.get(threadId), cpu * 100));
                    }
                    threadId2CpuTime.put(threadId, newTime);

                });
            }
        }
    }

輸出如下:

如何分析

什麼JFR

JFR(Java flying recorder), 是java內置的一個性能數據採集器. 可以詳細的獲取JVM內部的狀態和事件.

JFR分析示例

通過如下的命令激活JFR:

java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder cpu.CpuTest

運行後通過內置的JCMD就可以進行採集等操作(JCMD 還有其他命令比如JFR.check 等.)

這個文件可以通過JMC(Java  mission control)打開, 直接運行jmc即可 (也在jdk/bin下面):

在這裏面就可以看到熱點方法:

JFR本身還支持很多事件的採集包括GC/IO等.

Reference:https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm

一個實際例子

在產品環境中發現的一個問題, 客戶會週期的執行一些snmp數據採集任務集合A, 然後每隔一段時間又會執行另外的任務集合B(此時A也在同步執行), 發現此時B執行時, CPU比較高,  這是正常的因爲任務B本身比較耗CPU, 但是發現任務B完成後, CPU依然很高, 並沒有下降的趨勢.

CPU圖形如下:

1. 分析了任務A的成功率並沒有變化.

2. 我們對比了前後的線程是否有增減, 找到了一個Windows selector 但是一查CPU 也沒有明顯的變化(~3%).

3. 最後我們用前面的腳本查詢了前後兩次的所有線程的CPU 發現了, snmp相關的線程cpu每個都從1.5% 增加到了 7%.

59 lm-collector-snmp-transport--4-1=1.61
60 lm-collector-snmp-transport--4-2=1.48
61 lm-collector-snmp-transport--4-3=1.56
62 lm-collector-snmp-transport--4-4=1.80
64 lm-collector-snmp-transport--4-5=1.64
65 snmp-selector=1.22
66 lm-collector-snmp-transport--4-6=1.41
67 lm-collector-snmp-transport--4-7=1.46
68 lm-collector-snmp-transport--4-10=1.77
69 lm-collector-snmp-transport--4-9=1.51
70 lm-collector-snmp-transport--4-8=1.88

4. 然後突然想起來了 好像我們底層是共享的snmp 發送線程, 然後又因爲有流控. 所以總共10組線程會每隔10ms 做一個host的發送任務. 但是在任務B中我們新加進來了很多任務(每個host新加了一個任務,然後在這10個線程中),相當於以前有900個host, 我們的線程組就會: 每10ms 執行900 次任務了, 然後B任務執行時, 又加了900個任務進來就是每10ms執行1800個任務.

這個對ScheduleExecutorService來說可能是個不小的性能問題.

5. 驗證猜想. 我們dump 2次JFR也發現了:

B任務執行前:

B任務執行後:

注意這個Context Switch count 被Double了, 還有ScheduledThreadPool中的siftDown方法也被調用更加頻繁了.

這個我本來最開始發現這個每個snmp線程的samplecount變多了, 但是感覺沒有多很多, 就沒去管.

實際上發現有還是多了很多的. 

解決辦法嘛, 還在思考之中~~~~

博客代碼:https://github.com/gaoxingliang/goodutils/blob/master/src/cpu/CpuTest.java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章