点击上方“后端技术精选”,选择“置顶公众号”

技术文章第一时间送达!

来源:牛哥的博客

cnblogs.com/nxlhero/p/11660854.html

在生产上我们经常会碰到一些不好排查的问题,例如线程安全问题,用最简单的threaddump或者heapdump不好查到问题原因。为了排查这些问题,有时我们会临时加一些日志,比如在一些关键的函数里打印出入参,然后重新打包发布,如果打了日志还是没找到问题,继续加日志,重新打包发布。对于上线流程复杂而且审核比较严的公司,从改代码到上线需要层层的流转,会大大影响问题排查的进度。

这个时候我们可以使用能够在线调试的工具帮助我们查找问题,例如btrace,可以动态的插入代码,极大提高我们查找问题的效率。本文通过生产案例介绍如何用这一类工具快速定位传统方法不好定位的问题。

问题描述

服务器在高并发的情况下cpu消耗很高,响应速度变慢,打threaddump,发现创建了大量的Timer-xxx线程,状态都是runnable,但是没有函数堆栈,也就是刚创建了还没执行。打heapdump看,还是不好发现问题,也只能看到一堆没有堆栈的线程。

查了代码,发现近期改的地方没有主动创建Timer的地方,所以大概率是第三方jar包创建的,怎么才能找到是谁创建的线程。用这种静态的分析方法已经不太好用了,因为不管是ThreadDump还是HeapDump,抓住的都是一个快照,ThreadDump主要用来看线程的状态,HeapDump主要是看内存里的东西,还可以看线程里调用链上的栈的内容(仅限于对象,基本类型看不到)。

我们要知道谁创建的线程,只要知道谁调用了Thread类的start方法即可。假如我们在ThreadDump里看到如下的stacktrace:

java.lang.Thread.start()
com.xxx.SomeClass.startNewThread();
…

那我们就知道是谁创建了新线程,但是遗憾的是start方法太快,而ThreadDump很难抓到,start调用结束后就有一个新的stacktrace了。大部分情况下ThreadDump只会抓到比较慢的函数,最常见的是socket.read()这样的,还有一些需要多次循环的。

必须借助功能更强大的工具进行分析,比如Btrace,可以动态的插入字节码,可以使我们动态的在线查找问题。Btrace是比较基础的工具,需要自己写代码插入,原理就是字节码增强,或者叫Instrumentation,类似于AOP,通过插入一些代码进行调试分析,包括很多性能监控工具,也是通过这个原理进行无侵入的埋点。

Btrace和Arthas简介及使用示例

BTrace是Java的安全可靠的动态跟踪工具。它的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。使用Btrace时需要写btrace脚本,也就是Java代码,这些代码会被植入运行的JVM中,如果写的不好,可能会把JVM搞崩溃。

Arthas是阿里开源的一款工具,跟Btrace的原理一样,它用起来更简单,把最常用的功能都做了封装,不需要自己写代码了,使用起来比较简单,但是灵活性不如BTrace。

用Btrace举两个例子,没法用threaddump和heapdump快速定位问题的情况。

1.用btrace查看stacktrace

假如我们有以下代码,f函数会一直不停的创建Timer,而Timer会启动新线程,大量的线程导致OOM,我们需要查到是谁创建的线程。

public class Test {public static void main(String[] args) throws InterruptedException {f();}      public static void f() throws InterruptedException {     for(int i = 0; i < 10000; i++) {new Timer(true);Thread.sleep(1000);}}
}

打一个threaddump,没法发现f与新线程的关系。

"Timer-31" daemon prio=5 tid=0x00007fc74a894800 nid=0x6407 in Object.wait() [0x00007000017db000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x00000007d596be10> (a java.util.TaskQueue)at java.lang.Object.wait(Object.java:503)at java.util.TimerThread.mainLoop(Timer.java:526)- locked <0x00000007d596be10> (a java.util.TaskQueue)at java.util.TimerThread.run(Timer.java:505)Locked ownable synchronizers:- None
"main" prio=5 tid=0x00007fc74a80b000 nid=0x1703 waiting on condition [0x0000700000219000]java.lang.Thread.State: TIMED_WAITING (sleeping)at java.lang.Thread.sleep(Native Method)at test.Test.f(Test.java:13)at test.Test.main(Test.java:8)Locked ownable synchronizers:- None

可以用Btrace查找是谁创建的。用Btrace需要写脚本,以下脚本就是在java.lang.Thread类的start方法被调用的时候执行onnewThread方法,打印5行堆栈。

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;@BTrace public class ThreadStart {@OnMethod(clazz="java.lang.Thread",method="start")public static void onnewThread(@Self Thread t) {println("");Threads.jstack(5);}
}

然后在指定的进程号上执行脚本,得到下面的输出。

localhost:btrace-bin-1.3.11.3 $ ./bin/btrace 95428 ThreadStart.java
/Users/Downloads/btrace-bin-1.3.11.3java.lang.Thread.start(Thread.java)
java.util.Timer.<init>(Timer.java:176)
java.util.Timer.<init>(Timer.java:146)
test.Test.f(Test.java:12)
test.Test.main(Test.java:8)java.lang.Thread.start(Thread.java)
java.util.Timer.<init>(Timer.java:176)
java.util.Timer.<init>(Timer.java:146)
test.Test.f(Test.java:12)
test.Test.main(Test.java:8)java.lang.Thread.start(Thread.java)
java.util.Timer.<init>(Timer.java:176)
java.util.Timer.<init>(Timer.java:146)
test.Test.f(Test.java:12)
test.Test.main(Test.java:8)

通过调用堆栈,就能找到是谁创建的线程了。

2.用btrace查看函数参数

下面的代码,xx是一个业务逻辑函数,它的入参经过了严格的校验,如果传入参数过大,可能会处理时间特别长,我们现在怀疑参数检查出了问题,导致有些时候函数时间特别长。

public class Test {public static void main(String[] args) throws InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(10);for(int i = 0; i < 100000; i++) {final int x = new Random().nextInt(5000);pool.execute(new Runnable() {@Overridepublic void run() {xx(x,String.valueOf(x%5));}           });}}   public static void xx(int x , String y) {try {Thread.sleep(x);} catch (InterruptedException e) {}}
}

打一个heapdump,可以看到函数堆栈以及入参,查看一下慢线程的入参:

在heapdump里,我们只能看到xx的一个入参,但是代码里明明有两个。这是因为heapdump只能看到对象类型,而不能看到基本类型,因为是heapdump而不是stackdump,是从gc root对象开始把所有可达对象给抓出来了,所以int类型的参数被忽略了,如果我们传的是Integer,就能在heapdump里看到。

我们用btrace在返回的地方植入代码,如果函数时间超过3s,就打印一下参数,看看是不是参数不正常导致的。代码如下:

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class Params {@OnMethod(clazz = "test.Test", method = "xx", location = @Location(Kind.RETURN))public static void argPrint(int param1 ,String param2, @Duration long duration) {if(duration > 3000000000L) { //单位是nsprintln(param1);}}
}

运行结果如下:

localhost:btrace-bin-1.3.11.3 $ ./bin/btrace 53936 Params.java
/Users/Downloads/btrace-bin-1.3.11.3
4247
3734
…

可以看到btrace的功能很强大,但是我们每次查找问题,都需要写一些代码,这样的方式比较灵活,但是也麻烦,而且更危险,因为插入字节码本身就有风险。我们可以提前把一些通用的脚本写好,例如查找执行时间过长的函数,或者用其他整合了多种功能的工具,例如阿里的arthas,跟btrace原理是一样的,但是功能更多,不需要再写代码,不过对于特别不常见的需求还是用btrace写代码比较好。

3.用Arthas替代btrace

上述两个问题都可以用arthas解决,调用堆栈的方法在下面Timer问题分析里面会介绍。

用arthas实现上面的参数查看,连接这个进程,然后用下面标红的watch指令打印test.Test函数耗时大于3000ms时的参数。

localhost:Downloads $ java -jar arthas-boot.jar 76420
[INFO] arthas-boot version: 3.1.1
[INFO] arthas home: /Users/.arthas/lib/3.1.1/arthas
[INFO] Try to attach process 76420
[INFO] Attach process 76420 success.
[INFO] arthas-client connect 127.0.0.1 3658,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          wiki      https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version   3.1.1
pid       76420
time      2019-08-19 17:54:14                                                   $ watch test.Test xx params '#cost>3000'
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 55 ms.
ts=2019-08-19 17:54:29; [cost=3049.163ms] result=@Object[][@Integer[3045],@String[0],
]
ts=2019-08-19 17:54:30; [cost=3287.747ms] result=@Object[][@Integer[3287],@String[2],

虽然arthas比btrace方便,但是两个都不安全,能不用尽量不用,用了的话最好重启应用。

大量Timer线程问题分析

1.用arthas找到是谁创建的线程

找到jvm进程的进程号,然后使用arthas连到这个线程上,注意最好用同一个jdk版本,否则可能出问题。

weblogic@host:~/arthas> /usr/java/jdk1.7.0_80/bin/java -jar arthas-boot.jar 22316
[INFO] arthas-boot version: 3.1.1
[INFO] arthas home: /home/weblogic/arthas
[INFO] Try to attach process 22316
[INFO] Attach process 22316 success.
[INFO] arthas-client connect 127.0.0.1 3658,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          wiki      https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version   3.1.1
pid       22316
time      2019-08-02 17:43:18                                                   $

然后用stack命令查看Thread.start的调用堆栈。

$ stack java.lang.Thread start
No class or method is affected, try:
1. sm CLASS_NAME METHOD_NAME to make sure the method you are tracing actually exists (it might be in your parent class).
2. reset CLASS_NAME and try again, your method body might be too large.
3. check arthas log: /home/weblogic/logs/arthas/arthas.log
4. visit https://github.com/alibaba/arthas/issues/47 for more details.

提示没找到这个类或者方法,这是因为java.lang.Thread属于基础类,不建议修改它的字节码,如果确实需要,需要修改一个选项,把unsafe选项设为true。

$ options unsafe trueNAME    BEFORE-VALUE  AFTER-VALUE
-----------------------------------                                                                                                            unsafe  false         true
$stack java.lang.Thread start
.....
ts=2019-08-02 18:13:11;thread_name=[ACTIVE] ExecuteThread: '17' for queue: 'weblogic.kernel.Default (self-tuning)';id=2e;is_daemon=true;priority=5;TCCL=weblogic.utils.classloaders.ChangeAwareClassLoader@40d0d36f@java.util.Timer.<init>()at java.util.Timer.<init>(Timer.java:146)at com.ibm.db2.jcc.am.io.a(io.java:4953)at com.ibm.db2.jcc.am.io.Zb(io.java:5019)at com.ibm.db2.jcc.am.jo.b(jo.java:4058)at com.ibm.db2.jcc.am.jo.hc(jo.java:2732)at com.ibm.db2.jcc.am.jo.execute(jo.java:2715)at weblogic.jdbc.wrapper.PreparedStatement.execute(PreparedStatement.java:105)at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:57)at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:70)at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:57)at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:267)at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:141)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:98)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:73)at org.apache.ibatis.executor.keygen.SelectKeyGenerator.processGeneratedKeys(SelectKeyGenerator.java:65)at org.apache.ibatis.executor.keygen.SelectKeyGenerator.processBefore(SelectKeyGenerator.java:41)at org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize(PreparedStatementHandler.java:80)at org.apache.ibatis.executor.statement.RoutingStatementHandler.parameterize(RoutingStatementHandler.java:58)at org.apache.ibatis.executor.BatchExecutor.doUpdate(BatchExecutor.java:68)at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:108)
......

2.问题分析与验证

看代码是db2的驱动创建的线程,反编译看一下代码。应用通过weblogic提供的jndi访问数据库,数据源是weblogic创建的,在weblogic的lib目录里,有两个驱动,db2jcc4.jar和db2jcc.jar,到网上查,这两者的区别在于:

Question

IBM Data Server driver for JDBC and SQLJ(JCC) has both db2jcc.jar and db2jcc4.jar. What is the difference between those 2 files, db2jcc.jar and db2jcc4.jar?

Answer

Both of them are DB2 JDBC driver jar files and are Type 4 drivers.

db2jcc.jar includes functions in the JDBC 3.0 and earlier specifications. If you plan to use those functions, include the db2jcc.jar in the application CLASSPATH.

db2jcc4.jar includes functions in JDBC 4.0 and later, as well as JDBC 3.0 and earlier specifications. If you plan to use those functions, include the db2jcc4.jar in the application CLASSPATH.

通过stacktrace里的代码行数,确定加载的是db2jcc.jar这个jar包。创建线程的代码如下:

网上搜索这个参数,这个选项配成1的时候,每次查询都要启动一个探测超时的Timer,如果选项是2的话,就每个连接一个Timer。

timerLevelForQueryTimeout can be disabled by setting it to a value

DB2的jar包都经过了混淆,查看起来比较麻烦,最终确定了这个选项在DB2BaseDataSource类里,实际使用的是DB2SimpleDataSource 。

我们用jdbc写一段代码进行验证,使用DB2的数据源

public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException {Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;String name=ManagementFactory.getRuntimeMXBean().getName();System.out.println(name); //把线程号打出来让arthas使用Thread.sleep(40000);System.out.println(name);DB2SimpleDataSource ds = new DB2SimpleDataSource();ds.setUser("xxx");ds.setPassword("xxx");ds.setServerName("xxx");ds.setPortNumber(60000);ds.setDatabaseName("xxx");ds.setDriverType(4);//ds.setTimerLevelForQueryTimeOut(1); //改变选项//ds.setCommandTimeout(5000); //相当于设置了全局queryTimeOutds.setConnectionTimeout(5000);//ds.timerLevelForQueryTimeOut = 1;System.out.println(ds.timerLevelForQueryTimeOut);conn = ds.getConnection();String sql = "select * from test";ps = conn.prepareStatement(sql);ps.setQueryTimeout(5000); //为单条语句设置queryTimeOutps.executeQuery();ps.executeQuery();//Thread.sleep(100000);}

用以上代码验证,发现只有设置了setCommandTimeout或者对于statement设置了setQueryTimeout的时候才会每次都启动一个Timer。setCommandTimeout相当于设置了全局的queryTimeout。

用arthas验证结果如下:

Weblogic上部署了多个应用,但是只有一个应用有这个问题,通过分析数据源的配置,发现在mybatis的参数里有一个

这个配置的单位是秒,也不知道开发人员怎么想的,设置了25000秒,如果sql变慢了,就会导致大量的Timer线程出现。

3.深入研究与解决方案

这个queryTimeOut到底是干啥用的呢?

一般网络连接会有两个超时时间,connectionTimeout和readTimeout,这两个超时都是由操作系统实现。connectionTimeout是在建立连接的时候的超时事件,这个比较容易理解。

而readTimeout,是指的等待对方数据到达的超时时间。

byte[] buf = new byte[128];while(true) {int byteRead = socket.read(buf); //socket会阻塞在这个地方,如果过了readtimeout还没有接到数据,就超时了if(byteRead < 0) {Break;}
}

假如我们的应用所有的sql都应该在1s内返回,如果超了1s可能是有问题了,这个时候超过1s有两种可能:

(1)网络问题导致读超时

(2)Sql出了问题,例如被sql注入了,where条件里出现了1=1,导致原来只用读取1k数据,现在要读取3G数据,而这3G数据不可能1s内返回,它们不停的read,也就是上述的死循环会执行很久,但是不会出现readTimeout。

第二种情况是没法通过网络层的超时来实现的,解决的方法就是单起一个线程,在一定时间后打断读取数据的线程。伪代码如下:

byte[] buf = new byte[128];
//创建一个timer,在queryTimeout到的时候执行
Timer timer = new Timer();
timer.schedule(new TimerThread(), queryTimeout); //TimerThread里面的实现就是打断主线程
while(true) {//超时,被计时器打断if(Thread.isInterrupted()) {break;System.out.println(“query Time out”);}int byteRead = socket.read(buf); //socket会阻塞在这个地方,如果过了readtimeout还没有接到数据,就超时了if(byteRead < 0) {break;}
}
timer.cancel(); //正常返回,中断计时器

queryTimeout是jdbc的一个参数,而各种数据库驱动可以有自己的实现,大部分都是起一个线程来控制,当然也可以选择不使用这个机制,只靠网络层的超时机制。

Db2驱动在默认情况下,如果设置了queryTimeout,会为每个statement起一个timer,这个对资源的消耗很大,可以把选项设置为2,这样就是每个connection一个timer。因为一个connection同一时间只能给一个线程用,所以这样是没有问题的。

在weblogic里加入下面的参数就可以了。

这个参数到底要不要用,怎么用,还得根据场景不同来区分,mysql也有这方面的坑,

https://blog.csdn.net/aa283818932/article/details/40379211

这一篇文章讲了jdbc的各种超时,讲的比较好。

https://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration

推荐阅读(点击即可跳转阅读)

1.

2. 

3. 

4. 

5. 

利用JVM在线调试工具排查线上问题(超实用)相关推荐

  1. 阿里技术:如何快速排查线上故障?

    阿里技术:如何快速排查线上故障? 以下文章来源于阿里技术 ,作者小峯 阿里技术 阿里巴巴官方技术号,关于阿里的技术创新均呈现于此. 有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从 ...

  2. JVM内存管理机制线上问题排查

    本文主要基于"深入java虚拟机"这本书总结JVM的内存管理机制,并总结了常见的线上问题分析思路.文章最后面是我对线上故障思考的ppt总结. Java内存区域 虚拟机运行时数据区如 ...

  3. docker容器cpu高问题排查_干货详解:一文教你如何利用阿里开源工具,排查线上CPU居高问题...

    前言 在我们开发过程中,无法避免的会出现所谓的垃圾代码,导致服务器的CPU一直处于100%.但我们应用已经上线,导致服务器CPU居高,但又不知道哪边出现的问题,我们应该怎么去找出哪边的代码出现问题呢? ...

  4. 马士兵—JVM—内存溢出—1.线上OOM(内存溢出)问题排查——亲测

    1. 模拟线上oom问题 1.1 代码 @GetMapping("/addList")public void addList(){List list = new ArrayList ...

  5. php线上问题排查,线上问题排查神器 Arthas

    摘要: rController|kite.springcloud.jxm.service.MonitorDashboardServiceoverviewstack输出当前方法被调用的调用路径stack ...

  6. 通过btrace排查线上频繁Full GC的case

    概述 又是一次因为线上报警机制开启的排查问题之旅.某日,钉钉机器人疯狂报警: 接着就是申请机器权限去排查问题,既然是频繁Full GC,那我们排查问题的思路就应该是找到引起Full GC的原因.引起频 ...

  7. 通过btrace排查线上频繁Full GC的case 1

    概述 又是一次因为线上报警机制开启的排查问题之旅.某日,钉钉机器人疯狂报警: 接着就是申请机器权限去排查问题,既然是频繁Full GC,那我们排查问题的思路就应该是找到引起Full GC的原因.引起频 ...

  8. 排查线上问题的9种方式

    德国科技管理专家斯坦门茨早年移居美国,他以非凡的才能成为美国企业界的佼佼者.一次,美国著名的福特公司的一组电机发生故障,在束手无策之时,公司请斯坦门茨出马解决问题. 斯坦门茨在电机旁仔细观察,经过计算 ...

  9. 没有打印日志,如何排查线上问题?——arthas(阿尔萨斯)

    背景 实际工作中,有时候漏打了日志或者线上环境才会出现问题,不得不到线上去复现,导致问题无法及时处理或者线上的包版本与本地的包版本不一样,导致各种种样的问题缺少实际一手的数据进行分析排查问题. Art ...

最新文章

  1. 二叉查找树(binary search tree)详解
  2. Python学习day5作业
  3. Jar/War/Ear等包的作用与区别详解
  4. html清除溢出,深入理解CSS overflow:hidden——溢出,坍塌,清除浮动
  5. *【CodeForces - 1150D】Three Religions(dp,预处理,思维)
  6. 老派程序员——徒手实现伟大成就
  7. 人生苦短,Python之父要解开这个困惑
  8. CSS基础必备知识点05
  9. 【学习OpenCV4】图像通道的分离、合并与混合方法(C++)
  10. python qq聊天机器人_Python QQBot库的QQ聊天机器人
  11. USB接口定义详解(图)
  12. masm5 0的使用介绍
  13. 微信登录显示网页无法连接服务器地址,手机微信登陆不了?
  14. 郑强上任后,太原理工大学不断引援!
  15. pandas读取csv文件UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xca in position 37: invalid contin
  16. JAVA(第六版)——期末复习2
  17. java 检测表情符号_关于Java:检查字母是否为表情符号
  18. 小白要搭建电商系统,看看这个开源项目!
  19. Ubuntu20.04启动paraFoam失败
  20. java电商秒杀深度优化_【B0796】Java性能优化亿级流量秒杀方案及电商项目秒杀实操2020视频教程...

热门文章

  1. 时序约束优先级_Vivado工程经验与各种时序约束技巧分享
  2. 2022年全球市场汽车用打气筒总体规模及应用细分研究报告
  3. Rviz中如何导入自定义障碍物 | 从建模到导出urdf到导入rviz | Ros中如何导入障碍物 | sw2urdf的下载和使用 | MeshLa的下载和使用
  4. RAK7258 end node 配置相关
  5. Excel公式向导:文本数字转为日期格式的操作
  6. 逆向分析工具有哪些?
  7. 单页应用 vs 多页应用
  8. 背部训练(一):背部肌肉解剖
  9. 硅钢铁损测量 | 铁损仪用户样片及相关问题的答
  10. AI人工智能标记数据的技术:类型、方法、质量控制、应用