日志打印产生的相关问题

日志打印过多,会对磁盘IO和CPU性能造成比较大的压力,所以可以通过日志分级去控制日志的输出。

日志级别

日志通常使用到ERROR、WARN、INFO、DEBUG级别,日志虽然可以提示更多的信息,但是生产打印大量日志消耗性能

  1. DEBUG级别:一般是调试程序的时候开启。
  2. INFO级别:消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志。
  3. WARNING:没有发生错误或者错误不太重要,但需要给出一些警告信息。
  4. ERROR:指出虽然发生错误事件,但仍然不影响系统的继续运行。打印错误和异常信息,如果不想输出太多的日志,可以使用这个级别。

日志打印影响性能的原因

  1. 同步打印日志磁盘 I/O 成为瓶颈,导致大量线程 Block,多个线程输出到同一个文件中,需要加锁去完成线程安全,其他没获取到锁的线程阻塞等待,造成卡顿。
  2. 高并发下日志打印异常堆栈造成线程 Block,qps过高,上下游出现异常,导致打印日志堆栈信息。

优化手段

  1. 选择合适的日志打印级别
  2. 采用异步打印的方式,AsyncLogger是Log4j2 官方推荐的异步方式。
  3. 对日志设置打印格式,不要打印堆栈信息。

Log4j2异步日志配置原理

Log4j2 异步日志基于高性能数据结构 Disruptor,Disruptor 是一个环形 buffer,通过环形无阻塞队列作为缓冲,多生产者多线程的竞争是通过CAS(compare and swap)无锁化实现,可以极大的提高日志输出效率。

记一次日志打印引发的问题

Blocked线程堆栈中可以看出,线程阻塞在类加载流程上,查看WebAppClassLoader相关代码片段如下图11所示,发现加载类时确实会根据类名来加synchronized同步块,因此初步猜测是类加载导致线程Block。

继续从Blocked线程堆栈着手分析,查看堆栈中的ThrowableProxy相关代码,发现其构造函数会遍历整个异常堆栈中的所有堆栈元素,最终获取所有堆栈元素类所在的JAR名称和版本信息。具体流程如下:

  1. 首先获取堆栈元素的类名称。
  2. 再通过loadClass的方式获取对应的Class对象。
  3. 进一步获取该类所在的JAR信息,从CodeSource中获取JAR名称,从Package中获取JAR版本
// org.apache.logging.log4j.core.impl.ThrowableProxy
  
private ThrowableProxy(final Throwable throwable, final Set<Throwable> visited) {
    this.throwable = throwable;
    this.name = throwable.getClass().getName();
    this.message = throwable.getMessage();
    this.localizedMessage = throwable.getLocalizedMessage();
    final Map<String, CacheEntry> map = new HashMap<>();
    final Stack<Class<?>> stack = ReflectionUtil.getCurrentStackTrace();
   // 获取堆栈扩展信息
    this.extendedStackTrace = this.toExtendedStackTrace(stack, map, null, throwable.getStackTrace());
    final Throwable throwableCause = throwable.getCause();
    final Set<Throwable> causeVisited = new HashSet<>(1);
    this.causeProxy = throwableCause == null ? null : new ThrowableProxy(throwable, stack, map, throwableCause,
        visited, causeVisited);
    this.suppressedProxies = this.toSuppressedProxies(throwable, visited);
}

ExtendedStackTraceElement[] toExtendedStackTrace(final Stack<Class<?>> stack, final Map<String, CacheEntry> map,
                                                 final StackTraceElement[] rootTrace,
                                                 final StackTraceElement[] stackTrace) {
    int stackLength;
    if (rootTrace != null) {
        int rootIndex = rootTrace.length - 1;
        int stackIndex = stackTrace.length - 1;
        while (rootIndex >= 0 && stackIndex >= 0 && rootTrace[rootIndex].equals(stackTrace[stackIndex])) {
            --rootIndex;
            --stackIndex;
        }
        this.commonElementCount = stackTrace.length - 1 - stackIndex;
        stackLength = stackIndex + 1;
    } else {
        this.commonElementCount = 0;
        stackLength = stackTrace.length;
    }
    final ExtendedStackTraceElement[] extStackTrace = new ExtendedStackTraceElement[stackLength];
    Class<?> clazz = stack.isEmpty() ? null : stack.peek();
    ClassLoader lastLoader = null;
    for (int i = stackLength - 1; i >= 0; --i) {
       // 遍历 StackTraceElement
        final StackTraceElement stackTraceElement = stackTrace[i];
       // 获取堆栈元素对应的类名称
        final String className = stackTraceElement.getClassName();
        // The stack returned from getCurrentStack may be missing entries for java.lang.reflect.Method.invoke()
        // and its implementation. The Throwable might also contain stack entries that are no longer
        // present as those methods have returned.
        ExtendedClassInfo extClassInfo;
        if (clazz != null && className.equals(clazz.getName())) {
            final CacheEntry entry = this.toCacheEntry(stackTraceElement, clazz, true);
            extClassInfo = entry.element;
            lastLoader = entry.loader;
            stack.pop();
            clazz = stack.isEmpty() ? null : stack.peek();
        } else {
           // 对加载过的 className 进行缓存,避免重复加载
            final CacheEntry cacheEntry = map.get(className);
            if (cacheEntry != null) {
                final CacheEntry entry = cacheEntry;
                extClassInfo = entry.element;
                if (entry.loader != null) {
                    lastLoader = entry.loader;
                }
            } else {
               // 通过加载类来获取类的扩展信息,如 location 和 version 等
                final CacheEntry entry = this.toCacheEntry(stackTraceElement,
                    // 获取 Class 对象
                    this.loadClass(lastLoader, className), false);
                extClassInfo = entry.element;
                map.put(stackTraceElement.toString(), entry);
                if (entry.loader != null) {
                    lastLoader = entry.loader;
                }
            }
        }
        extStackTrace[i] = new ExtendedStackTraceElement(stackTraceElement, extClassInfo);
    }
    return extStackTrace;
}

/**
 * Construct the CacheEntry from the Class's information.
 *
 * @param stackTraceElement The stack trace element
 * @param callerClass       The Class.
 * @param exact             True if the class was obtained via Reflection.getCallerClass.
 * @return The CacheEntry.
 */
private CacheEntry toCacheEntry(final StackTraceElement stackTraceElement, final Class<?> callerClass,
                                final boolean exact) {
    String location = "?";
    String version = "?";
    ClassLoader lastLoader = null;
    if (callerClass != null) {
        try {
            // 获取 jar 文件信息
            final CodeSource source = callerClass.getProtectionDomain().getCodeSource();
            if (source != null) {
                final URL locationURL = source.getLocation();
                if (locationURL != null) {
                    final String str = locationURL.toString().replace('\\', '/');
                    int index = str.lastIndexOf("/");
                    if (index >= 0 && index == str.length() - 1) {
                        index = str.lastIndexOf("/", index - 1);
                        location = str.substring(index + 1);
                    } else {
                        location = str.substring(index + 1);
                    }
                }
            }
        } catch (final Exception ex) {
            // Ignore the exception.
        }
      // 获取类所在 jar 版本信息
        final Package pkg = callerClass.getPackage();
        if (pkg != null) {
            final String ver = pkg.getImplementationVersion();
            if (ver != null) {
                version = ver;
            }
        }
        lastLoader = callerClass.getClassLoader();
    }
    return new CacheEntry(new ExtendedClassInfo(exact, location, version), lastLoader);
}

ThrowableProxy#toExtendedStackTrace方法通过Map<String, CacheEntry>缓存当前堆栈元素类对应的CacheEntry,来避免重复解析CacheEntry。

因此正常情况下,一个类加载器对于一种类只会加载一次,类加载器保存有类缓存,无需重复加载,因此是有一个类一直重复加载不了,导致的问题。

android Log打印日志展示不完整 打印日志会影响性能吗_开发语言


第一阶段 Tomcat + Spring Boot

这个阶段是 Spring Boot 接收到一个 Http 请求到进入业务代码之前。Spring Boot 把收到的 Http 请求转发到业务代码,这个过程使用Method.invoke反射。在sun.reflect包中Method.invoke使用代理类,代理类有如下 2 种方式实现:

  • NativeMethodAccessorImpl
  • GeneratedMethodAccessorXX

这 2 个代理类都包含invoke方法,都能完成对反射类方法的调用。不同点是:1、NativeMethodAccessorImpl 通过new方法生成,invoke方法底层调用native方法;2、GeneratedMethodAccessorXX 通过bytecode动态生成。前 16 次调用使用NativeMethodAccessorImpl,从 17 次调用开始使用GeneratedMethodAccessorXX,这种方式称为Inflation机制。

  1. 默认条件使用native方法进行反射操作
  2. 一定条件下会生成字节码进行反射操作,即生成sun.reflect.GeneratedMethodAccessor类,它是一个反射调用方法的包装类,代理不同的方法,类后缀序号递增。

从MethodAccessorGenerator#generateName方法可以看到,字节码生成的类名称规则是sun.reflect.GeneratedConstructorAccessor,其中N是从0开始的递增数字,且生成类是由DelegatingClassLoader类加载器定义,所以其他类加载器无法加载该类,也就无法生成类缓存数据,从而导致每次加载类时都需要遍历JarFile,极大地降低了类查找速度,且类加载过程是synchronized同步调用,在高并发情况下会更加恶化,从而导致线程Block。

其实这是JVM对反射调用的一种优化手段,在sun.reflect.ReflectionFactory的文档注释里对此做了解释,这是一种“Inflation”机制,加载字节码的调用方式在第一次调用时会比Native调用的速度要慢3~4倍,但是在后续调用时会比Native调用速度快20多倍
Log4j2打印异常日志时,AsyncAppender会先创建日志事件快照,并进一步触发解析、加载异常堆栈类。JVM通过生成字节码的方式优化反射调用性能,但该动态生成的类无法被WebAppClassLoader类加载器加载,因此当大量包含反射调用的异常堆栈被输出到日志时,会频繁地触发类加载,由于类加载过程是synchronized同步加锁的,且每次加载都需要读取文件,速度较慢,从而导致线程Block。

通过以上分析,总结如下:

1、日志Layout默认使用%xEx打印异常栈的类信息,而异常栈中包含动态类GeneratedMethodAccessorXX的加载,需要类加载;

2、Log4j2 最终调用TomcatEmbeddedWebappClassLoader.loadclass进行类加载,由于是动态类和类加载器使用的原因,导致无法加载。每次日志打印都会进行类加载过程,而在上面的方法中有synchronized发生同步等待,线程阻塞后,并发能力下降。

解决方法

1、在Layout中增加标识符%ex{n},这个不会打印类信息,不会因为类加载导致线程阻塞;n 表示打印多少行栈信息,一般只需要把业务调用栈信息打印出来即可,框架栈信息不需要。经过压测效果有提升,但还会发生其他方面的线程阻塞;