排查springboot启动Jar很慢的问题

475 阅读2分钟

排查springboot启动Jar很慢的问题

最近在一台服务器遇到了jar启动很慢的问题。其他机器都是24s,就那台机器需要240s+。这很奇怪 于是写了一个javassist。排查一下启动的时候那些方法运行很慢。

public class PreMainTraceAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
            String javaClassName = className.replaceAll("/", ".");
                try {
                    CtClass ctClass = BuildClass.methodAddExecuteTime(loader, javaClassName);
                    if (Objects.isNull(ctClass)) {
                        return null;
                    }
                    return ctClass.toBytecode();
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            return null;
        }, true);
    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}
public class BuildClass {


    public static CtClass methodPrintSql(ClassLoader loader, String className) throws NotFoundException, CannotCompileException {
        ClassPool classPool = new ClassPool();
        classPool.appendClassPath(new LoaderClassPath(loader));
        //热更新替换
        //加载系统classpath
        classPool.appendSystemPath();
        CtClass ctClass = classPool.getOrNull(className);
        if (Objects.isNull(ctClass)) {
            return null;
        }
        for (CtMethod method : ctClass.getDeclaredMethods()) {
            if (!Modifier.isAbstract(method.getModifiers()) && !Modifier.isInterface(method.getModifiers())
                    && !Modifier.isNative(method.getModifiers()) && Objects.nonNull(method)) {
                if (method.getName().equals("execute")) {
                    methodPrintSql(ctClass, method);
                }
            }
        }
        return ctClass;
    }

    public static CtClass methodAddExecuteTime(ClassLoader loader, String className) throws NotFoundException, CannotCompileException {
        ClassPool classPool = new ClassPool();
        classPool.appendClassPath(new LoaderClassPath(loader));
        //热更新替换
        //加载系统classpath
        classPool.appendSystemPath();
        CtClass ctClass = classPool.getOrNull(className);
        if (Objects.isNull(ctClass)) {
            return null;
        }
        for (CtMethod method : ctClass.getDeclaredMethods()) {
            if (!Modifier.isAbstract(method.getModifiers()) && !Modifier.isInterface(method.getModifiers())
                    && !Modifier.isNative(method.getModifiers()) && Objects.nonNull(method)) {
                methodAddExecuteTime(ctClass, method);
            }
        }
        return ctClass;
    }

    public static void methodAddExecuteTime(CtClass ctClass, CtMethod method) throws CannotCompileException, NotFoundException {
        method.addLocalVariable("begin", CtClass.longType);
        method.addLocalVariable("end", CtClass.longType);
        method.insertBefore("{begin = System.currentTimeMillis();}");
        method.insertAfter("{end = System.currentTimeMillis();\n" +
                "        System.out.println("" + ctClass.getName() +"#"+ method.getName() + " method execute time:" + (end - begin) + "ms");}");
    }

    public static void methodPrintSql(CtClass ctClass, CtMethod method) throws CannotCompileException, NotFoundException {
//        method.insertBefore("{System.out.println(this.toString());}");
        method.insertBefore("{System.out.println("cfb sql "+this.toString());}");
//        method.addLocalVariable("begin", CtClass.longType);
//        method.addLocalVariable("end", CtClass.longType);
//        method.insertBefore("{begin = System.currentTimeMillis();}");
//        method.insertAfter("{end = System.currentTimeMillis();\n" +
//                "        System.out.println("" + method.getName() + " method execute time:" + (end - begin) + "ms");}");
    }
}

打包成jar包。在业务jar包启动的时候加入当前jar包的启动。 接下来会打印出来很多的日志。类似: sun.usagetracker.UsageTrackerClient#getPropertyPrivileged method execute time:0ms sun.usagetracker.UsageTrackerClient#getPropertyPrivileged method execute time:0ms sun.usagetracker.UsageTrackerClient#getOSSpecificConfigFilePath method execute time:1ms

接下来就是写代码聚合:

public class Test2 {
    public static void main(String[] args) {
        Map<String, Long> map = new HashMap<>();
        try (BufferedReader br = Files.newBufferedReader(Paths.get("xxx.log"))) {
            String line;
            while ((line = br.readLine()) != null) {
                try {
                    String key = line.split(" ")[0];
                    String number = line.split(" ")[3].replaceAll("time:", "").replaceAll("ms", "").trim();
                    if (map.containsKey(key)) {
                        Long data = map.get(key);
                        map.put(key, data + Long.parseLong(number));
                    } else {
                        map.put(key, Long.parseLong(number));
                    }
                }catch (Exception e){
                    System.out.println(e);
                }
            }
            System.out.println(sortMap(map));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Map<String, Long> sortMap(Map<String, Long> map) {
        //利用Map的entrySet方法,转化为list进行排序
        List<Map.Entry<String, Long>> entryList = new ArrayList<>(map.entrySet());
        //利用Collections的sort方法对list排序
        entryList.sort((Comparator<Map.Entry<String, Long>>) (o1, o2) -> {
            //正序排列,倒序反过来
            return Math.toIntExact(o2.getValue() - o1.getValue());
        });
        //遍历排序好的list,一定要放进LinkedHashMap,因为只有LinkedHashMap是根据插入顺序进行存储
        LinkedHashMap<String, Long> linkedHashMap = new LinkedHashMap<String, Long>();
        for (Map.Entry<String,Long> e : entryList
        ) {
            linkedHashMap.put(e.getKey(),e.getValue());
        }
        return linkedHashMap;
    }
}

获取到了一些基本的信息 {org.springframework.boot.loader.archive.JarFileArchive$NestedArchiveIterator#adapt=177710, org.springframework.boot.loader.jar.JarFile#getNestedJarFile=177445,.....} 从这里可以看到是springboot的jar包加载很慢。但是换一台机器就没问题。问题大概率是磁盘吧。 接下来怼磁盘进行性能测试。

本人使用sysbench进行测试。

  • sysbench --threads=32 --report-interval=4 --time=60 --test=fileio --file-num=10 --file-total-size=1G --file-test-mode=rndrw prepare
  • sysbench --threads=32 --report-interval=4 --time=60 --test=fileio --file-num=10 --file-total-size=1G --file-test-mode=rndrw run

有问题的机器:

File operations: reads/s: 1646.66 writes/s: 1098.01 fsyncs/s: 279.67

Throughput: read, MiB/s: 25.73 written, MiB/s: 17.16

General statistics: total time: 60.1176s total number of events: 181571

Latency (ms): min: 0.00 avg: 10.56 max: 283.46 95th percentile: 44.17 sum: 1917327.68

Threads fairness: events (avg/stddev): 5674.0938/109.15 execution time (avg/stddev): 59.9165/0.04

没问题的机器:

File operations: reads/s: 15729.82 writes/s: 10486.58 fsyncs/s: 2626.94

Throughput: read, MiB/s: 245.76 written, MiB/s: 163.84

General statistics: total time: 60.0193s total number of events: 1730958

Latency (ms): min: 0.00 avg: 1.11 max: 90.98 95th percentile: 5.99 sum: 1917612.56

Threads fairness: events (avg/stddev): 54092.4375/440.21 execution time (avg/stddev): 59.9254/0.00

破案了。辣鸡运维给了辣鸡磁盘。甩锅成功。