原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。

简介

我们组有一个流量较大的Java服务,每次发代码时,服务都会有一小波接口超时,之前简单分析过,发现这些超时的case仅发生在服务刚启动时,少量请求会耗时好几秒,但之后又马上恢复正常。

问题发生

如下,是我们服务的一次上线,可以看到,上线期间(21:10左右)会有一小波499超时。

而从我们全链路日志平台查看这些超时的调用,会发现外部网络操作(如:rpc调用、查询数据库等)耗时不高,所以耗时来源于执行java代码而非外部调用。

但为啥就刚启动完成那会比较耗时,之后又正常了呢,有点经验的话,肯定会想到这里面估计发生了什么隐式操作,那Java代码执行时会有哪些隐式操作可能导致耗时高呢?

我想到了如下几种情况:

  1. 懒加载操作,如连接池初始化、缓存加载?

经过检查,发现这些都已在启动时加载,不会延迟到请求时。

  1. 发生了GC?

经过检查,启动时GC正常,耗时不高。

  1. JIT即时编译功能导致?

java代码默认是解释执行的,当某些代码被多次执行后,会被JIT编译成原生指令执行,执行性能相应提升,但我通过JVM参数-Xint关闭了JIT后,发现问题依然存在,故排除了此原因。

  1. 执行过程中有锁?

经过检查代码,未发现锁的存在。

  1. 操作系统相关隐式操作,上下文切换、缺页中断、文件io慢?

经初步检查,CPU、内存、磁盘使用率都正常,这部分深入排查比较费力,且有权限限制,暂先跳过。

那会是什么原因导致的?

问题排查

暂时没啥头绪,我打算先用arthas的profile命令,收集一些CPU火焰图看看。

由于超时仅发生在刚启动完成后的部分请求,之后又恢复正常,故我计划在启动完成后开始收集火焰图,每次收集10s的火焰图,收集3次,然后对比前后的火焰图,看看它们有什么不同,收集脚本如下:

function flamegraph_sample(){
# 不断检测服务直到它启动完成
while sleep 1; do curl -sS --connect-timeout 3 -m3 http://127.0.0.1:8080/health | grep ok && break; done
pid=`pgrep -n java`
for i in {1..3}; do
java -jar arthas-boot.jar -c "profiler start --alluser" "$pid";
sleep 10s;
java -jar arthas-boot.jar -c "profiler stop --file /tmp/flamegraph_cpu_%t.html " "$pid";
done
java -jar arthas-boot.jar -c "stop" "$pid";
}

生成的前2个火焰图如下:





乍一看,火焰图中没有明显的瓶颈点,但经过仔细查看,在第一张火焰图中搜索ClassLoader,可以搜到不少类加载操作(红色部分),而第二张则基本没有!

难道是类加载导致的?目前我有80%信心怀疑就是它导致的,但类加载有那么慢?

为此,我计划使用profile命令的-e wall模式收集刚启动完成时的调用栈,并使用jfr格式保存数据,其中wall模式适合诊断高耗时问题,而jfr格式数据会保存时间戳与线程名称,适合case by case分析,命令如下:

profiler start -e wall --file /tmp/result.jfr

收集到jfr文件后,使用jmc工具打开,然后我在日志平台上找到一个慢调用日志,它显示http-nio-8080-exec-28线程在21:14:1021:14:18时间段是一次耗时近8s的慢调用,所以我用此条件在jmc里过滤出此case的调用栈数据,如下:



可以发现,确实绝大多数耗时发生在类加载上,类加载之所以慢是因为加载类有锁竞争,而我们接口由于查表较多,确实会触发非常多类的加载,所以问题比较明显。

问题解决

知道原因后,解决起来就简单了,把类提前加载到JVM即可,为了简单,我直接使用了spring中的工具方法,如下:

private static final String[] CLASS_PREFIX_ARR = new String[] {
"org.apache", "com.thoughtworks", "io.netty", "com.google", "io.grpc",
"com.alibaba", "org.springframework", "cn.hutool", "com.fasterxml", "org.hibernate",
"io.opencensus", "org.redisson", "io.micrometer", "io.prometheus",
}; PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
for (String classPrefix : CLASS_PREFIX_ARR) {
Resource[] resources;
try {
resources = resolver.getResources(
"classpath*:" + StringUtils.replaceChars(classPrefix, '.', '/') + "/**/*.class");
} catch (IOException e) {
ExceptionUtils.rethrow(e);
return;
}
for (Resource resource : resources) {
String className = null;
try (InputStream is = resource.getInputStream()) {
ClassReader cr = new ClassReader(is);
className = StringUtils.replaceChars(cr.getClassName(), '/', '.');
Class<?> clz = Class.forName(className);
log.info("preLoadClass success: " + className + ", classLoader: " + clz.getClassLoader());
} catch (Throwable e) {
log.warn("preLoadClass failed: " + className);
}
}
}

类预加载上线后,后面又进行过多次代码发布,发布过程中几乎不会再产生超时情况,问题确认已解决。

总结

此次问题的排查过程,还是用到了不少排查技巧的,总结一下:

  1. 当看起来不应该慢的代码执行慢时,可以想想有哪些可能的隐式操作存在,此次case的隐式操作就是类加载。
  2. 当诊断问题没有头绪时,可考虑使用arthas的profile命令来绘制火焰图,看从火焰图中能不能找到线索,尽管不会总是有效。
  3. 当从CPU火焰图中看不出明显问题时,可通过对比问题前后的火焰图来找不同点。
  4. 理解profile的-e cpu(默认)与-e wall选项的差异,一般-e cpu诊断高cpu问题,而-e wall诊断高耗时问题,但如果是偶尔慢一下,需要case by case分析,可考虑使用jfr格式保存诊断数据。

Java服务刚启动时,一小波接口超时排查全过程的更多相关文章

  1. 服务刚启动就 Old GC,要闹哪样?

    1.背景 最近有个同学说他的服务刚启动就收到两次 Full GC 告警, 按道理来说刚启动,对象应该不会太多,为啥会触发 Full GC 呢? 带着疑问,我们还是先看看日志吧,毕竟日志的信息更多. 2 ...

  2. java中服务器启动时,执行定时任务

    package com.ripsoft.util; import java.util.Calendar; import java.util.Timer; import javax.servlet.Se ...

  3. java web项目启动时自动加载自定义properties文件

    首先创建一个类 public class ContextInitListener implements ServletContextListener 使得该类成为一个监听器.用于监听整个容器生命周期的 ...

  4. Java 命令行启动时指定配置文件目录

    java -jar -Xbootclasspath/a:/home/tms/conf    /home/tms/bin/S17-tms.jar 先指定配置文件目录: 再指定jar包路径: 运行clas ...

  5. Debian GNU Linux服务列表的获取、服务的关闭/开启、服务在启动时是否自己主动执行的生效/失效

    /*********************************************************************  * Author  : Samson  * Date   ...

  6. java web项目启动时浏览器路径不用输入项目名称方法

    http://blog.csdn.net/qq542045215/article/details/44923851

  7. dubbo启动时检查服务

    Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true". 可以通过 che ...

  8. 使用JavaService.exe(amd64)发布java服务(jdk x64)

    最近项目中需要使用java服务,但是java服务已经写好了,就等待部署到windows服务中,遇到了种种困难------在x64服务器中部署jdk x64编译的jar时,遇到了各种纠结. 本文找到了一 ...

  9. Dubbo启动时检查

    Dubbo在启动时会检查服务提供者所提供的服务是否可用,默认为True. (1).单个服务关闭启动时检查(check属性置为false) 1).基于xml文件配置方式 <!--3.声明需要调用的 ...

  10. Dubbo -- 系统学习 笔记 -- 示例 -- 启动时检查

    示例 想完整的运行起来,请参见:快速启动,这里只列出各种场景的配置方式 启动时检查 Dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止Spring初始化完成,以便上线时,能及早发 ...

随机推荐

  1. jQuery 在图片和文字中插入内容(多种情况考虑)

    昨天接到一个新的需要,在后台文章编辑器中,每一个文章的正文前面,可以单独添加一个电头字段,但是如果在富文本编辑器中最上面就添加图片的话,图片就会把电头和正文中的文字给隔开.需要做的是获取到电头字段,然 ...

  2. Xxl-job安装部署以及SpringBoot集成Xxl-job使用

    1.安装Xxl-job: 可以使用docker拉取镜像部署和源码编译两种方式,这里选择源码编译安装. 代码拉取地址: https://github.com/xuxueli/xxl-job/tree/2 ...

  3. 【总结】从++i思考计算机原子性和线程安全

    在C++中,++i被认为是一种原子性操作,即不可分割的.不可中断的整体.它能够确保对变量的修改完整且正确,从而避免了数据竞争等问题,提高了程序的并发性和可靠性.然而,有些人可能会将原子性和线程安全混淆 ...

  4. Pwn系列之Protostar靶场 Stack6题解

    源码如下: #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <stri ...

  5. ChatGPT不算新技术革命,带不来什么新机会(转载)

    吴军,1967年出生,毕业于清华大学和约翰霍普金斯大学,计算机专业博士,前Google高级资深研究员.原腾讯副总裁.硅谷风险投资人. 近日,计算机科学家.自然语言模型专家吴军,就人工智能和ChatGP ...

  6. 2021-09-01:三数之和。给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

    2021-09-01:三数之和.给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组. ...

  7. 常用设计模式之.Net示例代码合集

    每一次初学者粉丝朋友,在后台向我咨询编程问题,我除了给他们指导学习路线,我都会建议他们学完基础知识后,一定要要注重编程规范,学习设计模式,修炼内功. 虽然说很多程序员,他们日常主要工作是CRUD,但是 ...

  8. 认识CPU底层原理(1)——MOSFET

    本文为B站UP主硬件茶谈制作的系列科普<[硬件科普]带你认识CPU>系列的学习笔记,仅作个人学习记录使用,如有侵权,请联系博主删除 近年来,由于国内外各种因素影响,半导体行业逐渐被推向风口 ...

  9. 为什么有了 HTTP 还要 RPC

    哈喽大家好,我是咸鱼 随着互联网技术的发展,分布式架构越来越被人们所采用.在分布式架构下,为了实现复杂的业务逻辑,应用程序需要分布式通信实现远程调用 而这时候就需要一种协议来支持远程过程调用,以便实现 ...

  10. AcWing 1215. 小朋友排队

    n个小朋友站成一排. 现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友. 每个小朋友都有一个不高兴的程度. 开始的时候,所有小朋友的不高兴程度都是 0. 如果某个小朋友第一次 ...