jstack那些事

2022-07-27 19:28:07 浏览数 (1)

前言

工作多年,经常遇到灵异事件,比如说: "任务为什么不跑了" , "没什么复杂业务呀,怎么cpu这么高" ,"用了缓存,怎么查询还是这么慢" , 一些工作一两年的同学遇到这类问题可能会手足无措 ,所以我想写一些文字来和大家分享下 。文笔有限,主要是交流。 这篇是先和大家交流 jstack的用法,以及编程方面的一些建议。

1 经典场景

  • 场景一 :任务(线程)突然不跑了 程序是一段简单的quartz 任务 ,伪代码类似:
代码语言:javascript复制
public class DataJobs {
   public void run() {
    log.info("任务开始运行");
    //准备数据 
    Object prepareData = prepare();
    //调用http远程接口获取
    Result result = getHttpRemoteData(prepareData);
    //存储
    store(result);
    log.info("任务结束运行");
   }
}

查看日志可用查看到 “任务开始运行” 但后续没有打印“任务结束运行”

业务也没有正常处理 , 也没有异常 无奈之下 只好重启服务

  • 场景二 cpu load 特别高 业务也不复杂

如下图:

业务场景是比分直播。

页面的逻辑是每隔几秒从memached 获取实时比赛信息 ,根据彩种类别获取数据,然后前端渲染。

页面访问 伪代码类似:

代码语言:javascript复制
 @RequestMapping("/getLiveData")
 @ResponseBody
 public String getLiveData(String lotteryCode) {
     //从memcache中获取比分直播数据  仅仅是一个 get key from memcahed
     String result = getDataFromMemcache(lotteryCode); 
     return result;
 }

异常问题:页面非常卡顿 ,服务器上 cpu 利用率 和 load 非常高 。

2 jstack分析

jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。

执行命令类似:

代码语言:javascript复制
jstack -l pid >> statck.txt

有时 也可以采用 kill -3 命令 实现打印堆栈的效果

代码语言:javascript复制
kill  -3  pid

堆栈格式:

代码语言:javascript复制
"redisson-netty-2-4" #37 prio=5 os_prio=0 tid=0x00007f91ae738000 nid=0x3bf1 runnable [0x00007f91a5b4d000]
  java.lang.Thread.State: RUNNABLE
      at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
      at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
      at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
      at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
      - locked <0x00000000ee106580> (a io.netty.channel.nio.SelectedSelectionKeySet)
      - locked <0x00000000ee1065b0> (a java.util.Collections$UnmodifiableSet)
      - locked <0x00000000edff8448> (a sun.nio.ch.EPollSelectorImpl)
      at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
      at io.netty.channel.nio.SelectedSelectionKeySetSelector.select(SelectedSelectionKeySetSelector.java:62)
      at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:752)
       at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:408)
       at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
       at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:138)
       at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

dump格式说明:

属性

说明

redisson-netty-2-4

线程名称

prio=5

该线程jvm中优先级 ‍

os_prio=0

该线程在os中的优先级 ‍

tid

该jvm内的thread id ‍

nid

Native thread id 本地操作系统相关 (高度依赖操作系统平台)

在jstack输出的第二行为线程的状态,在JVM中线程状态使用枚举 java.lang.Thread.State 来表示,State的定义如下:

代码语言:javascript复制
 public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

在线程转换图:

jstack关键字分析

关键字

说明

deadlock

死锁

waiting on condition

等待某个资源或条件发生来唤醒自己。具体需要结合jstacktrace来分析,比如线程正在sleep,网络读写繁忙而等待

Blocked

阻塞

waiting on monitor entry

等待获取锁 ‍

in Object.wait()

获取锁后又执行obj.wait()

3 解决问题

3.1 线程阻塞问题

回到场景一 依靠上面的命令 我们打印出模拟的jstack堆栈

代码语言:javascript复制
"Quartz-2" #1 prio=5 os_prio=31 tid=0x00007feb6c008000 nid=0x2603 runnable [0x000070000771a000]
   java.lang.Thread.State: RUNNABLE
  at java.net.SocketInputStream.socketRead0(Native Method)
  at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
  at java.net.SocketInputStream.read(SocketInputStream.java:170)
  at java.net.SocketInputStream.read(SocketInputStream.java:141)
  at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
  at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
  at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
  - locked <0x00000007959e1830> (a java.io.BufferedInputStream)
  at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704)
  at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647)
  at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1535)
  - locked <0x00000007959b1848> (a sun.net.www.protocol.http.HttpURLConnection)
  at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1440)
  - locked <0x00000007959b1848> (a sun.net.www.protocol.http.HttpURLConnection)
  at sun.net.www.protocol.http.HttpURLConnection.getHeaderField(HttpURLConnection.java:2942)
  at java.net.URLConnection.getContentType(URLConnection.java:512)
  at com.courage.platform.sms.client.util.SmsHttpClientUtils.getResponseAsString(SmsHttpClientUtils.java:345)
  at com.courage.platform.sms.client.util.SmsHttpClientUtils.doGet(SmsHttpClientUtils.java:241)

由上可见,当前线程阻塞在从http链接中获取响应数据。 可能对方httpserver耗时长,或者对方server hang住了 。针对这种情况,我适当的调整了 http client 的 connect 和read timeout。

3.2 cpu高问题

回到场景二,

我们先用ps命令找到对应进程的pid(如果你有好几个目标进程,可以先用top看一下哪个占用比较高)。 接着用

代码语言:javascript复制
top -H -p pid

来找到cpu使用率比较高的一些线程 (下图是模拟图)

然后将占用最高的pid转换为16进制printf '%xn' pid得到nid

接着直接在jstack中找到相应的堆栈信息jstack pid | grep 'nid' -C5 –color

可以看到我们已经找到了nid为0x42的堆栈信息。

比分直播 我们查询到占用cpu最高的是 GC 线程。那我们怀疑是由于GC 频率过高导致的。

然后使用 jstat 每隔1s查看内存使用情况

代码语言:javascript复制
jstat -gcutil pid 1000    

发现新生代每隔两秒就基本占满了 , 然后查看每个key对应的数据大小,发现每个value大小是500k左右 。

解决方式也比较简单 :

  • 减少缓存数据大小
  • 适当调整新生代的大小
  • 首次查询全量, 后续通过websocket推送增量给前端

4 编码习惯引导

4.1 设置线程名

线程名设置主要是为了在jstack堆栈中便于查询。在logback日志中最好也标注下线程名。当遇到问题也有一个查询源。

线程名有如下两种方式:

  • 手工设置线程名
代码语言:javascript复制
Thread t = new Thread(new Runnable() {
     @Override
     public void run() {
       //something process 
      }
});
t.setName("mytestThread");
t.start();
  • 线程工厂设置 (源码来自rocketmq 4.4)
代码语言:javascript复制
public class ThreadFactoryImpl implements ThreadFactory {
    private final AtomicLong threadIndex = new AtomicLong(0);
    private final String threadNamePrefix;
    private final boolean daemon;
    public ThreadFactoryImpl(final String threadNamePrefix) {
      this(threadNamePrefix, false);
    }
    public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon) {
      this.threadNamePrefix = threadNamePrefix;
      this.daemon = daemon;
    }
    @Override
    public Thread newThread(Runnable r) {
     Thread thread = new Thread(r, threadNamePrefix   this.threadIndex.incrementAndGet());
      thread.setDaemon(daemon);
      return thread;
    }
}

使用方式:

代码语言:javascript复制
this.traceExecutor = new ThreadPoolExecutor(
  10, 
  20, 
  1000 * 60, 
  TimeUnit.MILLISECONDS, 
  this.appenderQueue,
  new ThreadFactoryImpl("MQTraceSendThread_"));
traceProducer = getAndCreateTraceProducer(rpcHook);

4.2 线程池隔离

线程隔离主要有线程池隔离,在实际使用时我们会把请求分类,然后交给不同的线程池处理,当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池,从而保证其他服务可用。

以开源代码rocketmq举例:

我们来看下broker启动时 ,发送消息 & pull 消息 都是不同的线程池处理请求。

分为

  • 发送消息线程池
  • pull消息线程池
  • 查询消息线程池
  • 回复消息线程池

代码如下:

代码语言:javascript复制
//发送消息 处理器
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);

//pull消息 处理器
this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor);
this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList);

//消费管理处理器
ConsumerManageProcessor consumerManageProcessor = new ConsumerManageProcessor(this);
this.remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor);

//事务处理器
this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor);
this.fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor);

4.3 超时设置

为了避免线程池大规模阻塞, 合理的设置超时时间非常重要。 常见超时有如下

  • http 读写超时 (jdk httpurlconnection)
代码语言:javascript复制
cononnection(new URL(n = getCurl), METHOD_POST, ctype);
fillHeaders(conn, headers);
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
out = conn.getOutputStream();
  • 加锁时间(分布式场景下 redission)
代码语言:javascript复制
RLock lock = redisson.getLock("myLock");
// traditional lock method
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
  • 网络通讯 (通过countdownlatch实现)
代码语言:javascript复制
 public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
        final long timeoutMillis)
   throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
    final int opaque = request.getOpaque();
    try {
       final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
       this.responseTable.put(opaque, responseFuture);
       final SocketAddress addr = channel.remoteAddress();
       channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture f) throws Exception {
           if (f.isSuccess()) {
              responseFuture.setSendRequestOK(true);
             return;
          } else {
             responseFuture.setSendRequestOK(false);
          }
          responseTable.remove(opaque);
          responseFuture.setCause(f.cause());
          responseFuture.putResponse(null);
           log.warn("send a request command to channel <"   addr   "> failed.");
       }
     });
   RemotingCommand responseCommand =
       responseFuture.waitResponse(timeoutMillis);
       if (null == responseCommand) {
          if (responseFuture.isSendRequestOK()) {
            throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, responseFuture.getCause());
          } else {
            throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
          }
        }
         return responseCommand;
    } finally {
        this.responseTable.remove(opaque);
     }
}

rocketmq封装了 responseFuture 对象超时设置通过countdown来实现超时效果

代码语言:javascript复制
public RemotingCommand waitResponse(final long timeoutMillis) 
throws InterruptedException {
    this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
    return this.responseCommand;
}

5 专业产品推荐

5.1. 阿里Arthas

Arthas 是Alibaba开源的Java诊断工具 。我们从下图看到 一个简单得命令就可以查看当进程阻塞的线程

5.2 PerfMa 笨马网络

笨马网络是一家致力于打造一站式IT系统稳定性保障解决方案,专注于性能评测与调优、故障根因定位与解决的公司。

我们来看看社区版本如何查看堆栈情况 。

  1. 首先上传dump文件
  2. 通过控制台即可从 线程池 栈 锁 多个层面展示当前jvm线程信息

6 总结

玩过金庸群侠传的朋友可能听过“野球拳” ,这在游戏初期是非常基础的招式,威力也弱 。修炼也很耗时 ,很多玩家都放弃了 , 没有想到的是,野球拳练到10级,玩家就像打通任督二脉, 基本一击必杀 。

我认为学习技术也是这样 。

  1. 坚持,never give up 不断磨练自己的基本功
  2. 勇于实践 ,当出现问题 ,不要害怕 ,这是一个千载难逢学习的机会
  3. 思考,向别人学习

第一次写 , 希望对大家有帮助。

0 人点赞