除日誌外,還有沒有別的方式跟蹤線上服務問題呢?或者,跟蹤並排除日誌裏沒法發現的問題?
方法固然是有的,就是經過現場快照定位並發現問題。咱們所說的現場,主要指這兩方面:git
今天,咱們從 Java 線程角度,研究下基於線程棧若是排除問題。redis
在正式介紹線程棧以前,有必要先了解下 Java 線程的相關狀態。
Java 線程就是在這幾個狀態間完成本身的整個生命週期。微信
狀態 | 是夠消耗 CPU | 描述 |
---|---|---|
RUNNABLE | 不肯定 | 運行中 或者 就緒 |
WAITING | 不消耗 | 1. object monitor 2. unpark |
TIME_WAITING | 不消耗 | 1. object monitor 2. unpark 3. sleep |
BLOCKED | 不消耗 | object monitor |
線程棧是問題的第一現場,從線程棧中能夠得到不少日誌之外的瞬時信息。
強烈建議使用 jstack 命令!一者,方便重定向;兩者,最大限度的避免 kill 這種高危命令的使用。
核心信息:併發
從線程中獲取信息,有兩個視角。
單次線程棧app
屢次線程棧ide
通常會導出多份線程棧,共 10 份,每一個 2s 打一份。
線程棧不一樣於日誌,是程序運行時的快照,能夠定位不少詭異問題。
死鎖是程序最爲嚴重的問題,致使進程 hold 在那,沒法處理正常請求。
死鎖發生存在幾個條件:oop
主要指使用 synchronized 關鍵字,經過對象鎖保護資源,致使的死鎖。
測試代碼以下:學習
private final Object objectA = new Object(); private final Object objectB = new Object(); private String objectMethod1(){ synchronized (this.objectA){ sleepForMs(10); synchronized (this.objectB){ return getCurrentTime(); } } } private String objectMethod2(){ synchronized (this.objectB){ sleepForMs(10); synchronized (this.objectA){ return getCurrentTime(); } } } private ExecutorService deadlockExecutor = Executors.newFixedThreadPool(20, new BasicThreadFactory .Builder() .namingPattern("DeadLock-Thread-%d") .build() ); @RequestMapping("object") public DeferredResult<String> object(){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String> r1 = CompletableFuture.supplyAsync(this::objectMethod1, this.deadlockExecutor); CompletableFuture<String> r2 = CompletableFuture.supplyAsync(this::objectMethod2, this.deadlockExecutor); CompletableFuture.allOf(r1, r2).thenRun( ()-> result.setResult("SUCCESS")); return result; }
請求 /deadlock/object 返回超時,打印 Java 棧信息,發生死鎖:
測試
從棧信息中,咱們能夠得到如下信息:ui
主要指使用 Lock 對象進行資源保護,從而致使的死鎖。
測試代碼以下:
private final Lock lockA = new ReentrantLock(); private final Lock lockB = new ReentrantLock(); private String lockMethod1(){ try { this.lockA.lock(); sleepForMs(10); try { this.lockB.lock(); return getCurrentTime(); }finally { this.lockB.unlock();; } }finally { this.lockA.unlock(); } } private String lockMethod2(){ try { this.lockB.lock(); sleepForMs(10); try { this.lockA.lock(); return getCurrentTime(); }finally { this.lockA.unlock();; } }finally { this.lockB.unlock(); } } @RequestMapping("lock") public DeferredResult<String> lock(){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String> r1 = CompletableFuture.supplyAsync(this::lockMethod1, this.deadlockExecutor); CompletableFuture<String> r2 = CompletableFuture.supplyAsync(this::lockMethod2, this.deadlockExecutor); CompletableFuture.allOf(r1, r2).thenRun( ()-> result.setResult("SUCCESS")); return result; }
請求 /deadlock/lock 返回超時,打印 Java 棧信息,發生死鎖:
和上個棧信息很是類似,發生了死鎖現象。但,丟失了很重要的一個信息「線程得到哪一個鎖,又在申請哪一個鎖」,這可能就是 JVM 內置鎖和 AQS 家族的區別。
線程數太高,主要因爲線程池的不合理使用,好比沒有設置最大線程數。
測試代碼:
@RestController @RequestMapping("many-thread") public class ManyThreadController { private ExecutorService executorService = Executors.newCachedThreadPool(new BasicThreadFactory .Builder() .namingPattern("Many-Thread-%d") .build() ); @RequestMapping("/{tasks}") public DeferredResult<String> manyThreads(@PathVariable int tasks){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String>[] futures = new CompletableFuture[tasks]; for (int i=0;i<tasks;i++){ futures[i] = CompletableFuture.supplyAsync(this::getValue, executorService); } CompletableFuture.allOf(futures).thenRun( ()-> result.setResult("SUCCESS")); return result; } private String getValue() { sleepForMs(50); return getCurrentTime(); } }
請求 /many-thread/2000 ,查看棧信息:
存在 1729 個類似線程,若是在次加大 loop ,還可能會出現異常信息,有興趣能夠自行測試。
通常是大集合處理或死循環致使。
測試代碼以下:
@RestController @RequestMapping("high-cpu") public class HighCPUController { private ExecutorService executorService = Executors.newFixedThreadPool(1, new BasicThreadFactory .Builder() .namingPattern("High-CPU-%d") .build() ); @RequestMapping("/{loop}") public DeferredResult<String> highCpu(@PathVariable long loop){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture.supplyAsync(()->{ for (int i=0;i<loop;i++){ try { Math.cos(i + 10); }catch (Exception e){ } } return getCurrentTime(); }, executorService).thenAccept(r->result.setResult(r)); return result; } }
請求 /high-cpu/100000000000, CPU 會飆升。
屢次獲取線程棧,特定線程長期停留在一個運行代碼。
先獲得高 CPU 線程,在經過 nid 與線程棧線程對應,從而定位問題線程。
各類 pool 最多見問題。
Pool 工做原理:
測試代碼以下:
@RestController @RequestMapping("low-resource") public class LowResourceController { private ExecutorService executorService = Executors.newCachedThreadPool(new BasicThreadFactory .Builder() .namingPattern("Low-Resource-%d") .build() ); @Autowired private StringRedisTemplate redisTemplate; @RequestMapping("/{batch}") public DeferredResult<String> lowReource(@PathVariable int batch){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String>[] futures = new CompletableFuture[batch]; for (int i=0;i<batch;i++){ futures[i] = CompletableFuture.supplyAsync(this::getValue, executorService); } CompletableFuture.allOf(futures).thenRun( ()-> result.setResult("SUCCESS")); return result; } private String getValue() { try { return redisTemplate.execute((RedisCallback<String>)(redisConnection -> { sleepForMs(5000); return getCurrentTime() + redisConnection; })); }catch (Exception e){ e.printStackTrace(); } return "ERROR"; } }
請求 /low-resource/1000 超時後,查看堆棧信息:
可見,存在 998 個線程在等待 Jedis 資源。
線程能夠造成本身的依賴鏈條,增長問題排查的難度。
代碼以下:
@RequestMapping("future") public DeferredResult<String> future(){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); Future<String> future = this.executorService.submit(()->{ sleepForMs(5000); return getCurrentTime(); }); CompletableFuture<String>[] futures = new CompletableFuture[CONCURRENT_COUNT]; for (int i=0;i<CONCURRENT_COUNT;i++){ futures[i] = CompletableFuture.supplyAsync(()->{ try { return future.get(); }catch (Exception e){ } return "ERROR"; }, executorService); } CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS")); return result; }
訪問 /wait-chain/future 後,查看線程棧信息:
共有 100 個線程在 future.get 處進行等待。
Guava Cache 是最經常使用的 Local Cache,其內部作了併發處理,讓多個線程請求同一個 Key,會發生什麼事情呢?
測試代碼以下:
private final LoadingCache<String, String> cache; public WaitChainController(){ cache = CacheBuilder.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String s) throws Exception { sleepForMs(5000); return getCurrentTime(); } }); } @RequestMapping("guava-cache") public DeferredResult<String> guavaCache(){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String>[] futures = new CompletableFuture[CONCURRENT_COUNT]; for (int i=0;i<CONCURRENT_COUNT;i++){ futures[i] = CompletableFuture.supplyAsync(this::loadFromGuava, executorService); } CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS")); return result; }
訪問 /wait-chain/guava-cache 後,查看線程棧信息:
可見有 98 個線程在 Sync.get 處等待,整個現象和 Future 很是類似。
日誌是最經常使用的組件,也是最容易忽略的組件,若是多個線程同時訪問日誌的寫操做,會產生什麼精緻的?
測試代碼以下:
@RequestMapping("logger") public DeferredResult<String> logger(){ DeferredResult<String> result = new DeferredResult<>(10 * 1000L, "TimeOut"); CompletableFuture<String>[] futures = new CompletableFuture[CONCURRENT_COUNT]; for (int i=0;i<CONCURRENT_COUNT;i++){ futures[i] = CompletableFuture.supplyAsync(this::writeLogger, executorService); } CompletableFuture.allOf(futures).thenRun(()->result.setResult("SUCCESS")); return result; } private String writeLogger(){ for (int i = 0;i<10000;i++){ LOGGER.info("{}", i); } return getCurrentTime(); }
訪問 /wait-chain/logger 後,查看線程棧信息.
寫堆棧:
從日誌中可見,Wait-Chain-Thread-52 線程正在執行文件寫操做。
等待棧:
而有 98 個線程處於等待鎖的狀態。
Java 線程棧是線程運行時快照,能夠幫助咱們定位不少問題。掌握這一技能會讓咱們在平常工做中駕輕就熟。
最後附上 項目源碼
掃一掃,添加個人微信,一塊兒交流共同成長(備註爲技術學習)