除日志外,还有没有别的方式跟踪线上服务问题呢?或者,跟踪并排除日志里无法发现的问题?
方法当然是有的,就是通过现场快照定位并发现问题。我们所说的现场,主要指这两方面:
今天,我们从 Java 线程角度,研究下基于线程栈如果排除问题。
在正式介绍线程栈之前,有必要先了解下 Java 线程的相关状态。
Java 线程就是在这几个状态间完成自己的整个生命周期。
状态 | 是够消耗 CPU | 描述 |
---|---|---|
RUNNABLE | 不确定 | 运行中 或者 就绪 |
WAITING | 不消耗 | 1. object monitor 2. unpark |
TIME_WAITING | 不消耗 | 1. object monitor 2. unpark 3. sleep |
BLOCKED | 不消耗 | object monitor |
线程栈是问题的第一现场,从线程栈中可以获得很多日志以外的瞬时信息。
强烈建议使用 jstack 命令!一者,方便重定向;二者,最大限度的避免 kill 这种高危命令的使用。
核心信息:
从线程中获取信息,有两个视角。
一般会导出多份线程栈,共 10 份,每个 2s 打一份。
线程栈不同于日志,是程序运行时的快照,可以定位很多诡异问题。
死锁是程序最为严重的问题,导致进程 hold 在那,无法处理正常请求。
死锁发生存在几个条件:
主要指使用 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 栈信息,发生死锁:
从栈信息中,我们可以获得以下信息:
主要指使用 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 线程栈是线程运行时快照,可以帮助我们定位很多问题。掌握这一技能会让我们在日常工作中得心应手。