重新认识日志系统

324 阅读3分钟
原文链接: runningegg.cn

近期,考虑到Job系统需要打很多的日志(调用别人的API真的是坑呀,一不留神别人就把你限流了,还是别人内部服务器的问题,所以还是要多打印些日志)。笔者想优化下日志系统,然后成功又把自己拐坑里了。不过也是因为自己对于日志系统不够充分了解,借此机会更好的了解日志系统。

优化的手段比较简单:

  1. 生产环境去除打印控制台日志
  2. 将RollingFile改为RollingRandomAccessFile
  3. 添加Disruptor的Jar,开启Log4j2的异步化

ps:优化2是借用RollingRandomAccessFile中的buffer来提升IO效率,在开启Log4j2的异步化之后,笔者选择将RollingRandomAccessFile改回RollingFile。另外,异步化也不是十全十美的,在停止服务的时候会损失一部分日志的。如果能在停止服务的时候Sleep一段时间那是最好的,本项目对于一部分日志不在意所以也就没加Sleep。

步骤1:剔除Log4j1

既然需要用到Log4J2,那么就需要将所有的Log4j1的Jar给剔除。

mvn dependency:tree --> tree.txt

[INFO] |  +- org.apache.hadoop:hadoop-hdfs:jar:2.5.1:compile
[INFO] |  |  +- log4j:log4j:jar:1.2.17:compile

把依赖树打印出来,发现hdfs的Jar下有Log41,找准Log4j1,使用:

<exclusion>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
</exclusion>

结果发现没有生效……哔了狗了。这个Jar是包含在Biz层的pom中,索性我直接把Biz的Log4j1给剔除了。
世界应该安静了吧,结果报错了。

Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Level
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 66 more

不应该呀网上都说日志系统都是使用common-log或者slf4j,除非是直接使用Log41的代码~ 看代码hdfs也是用的common-log呀。查了两天发现hdfs使用了hdfs的INFO级别的常量…………所以,Log4j1不能删除,坑爹呀。

步骤2:隔离Log4j1

行吧,既然Hdfs你是大爷,你要用就用呗。但是打印日志如何嫁接到Log4j2上面,这个就是我要做的工作了。万能的google告诉我添加jcl-over-slf4j的Jar就可以解决问题,jcl-over-slf4j能够将commom-log嫁接到slf4j上,然后添加log4j-slf4j-impl:jar:2.7的Jar就能使用Log4j2。

这就陷入了我的知识盲区,为何添加Jar就能成功切换日志系统?
解决方案:万事不懂先google看别人原理讲解,然后在自己乖乖看源码。

Commom-log原理

commom-log:

  1. 从System.properties中查找LogFactory
  2. 从SPI来找
  3. 使用默认的LogFactoryImpl

为了方便大家理解,笔者把代码给抠了出来,还把常量给替换了方便看,可自行在common-log 1.2中搜索找到。

第一步,从System.properties中查找LogFactory

String factoryClass = getSystemProperty("org.apache.commons.logging.LogFactory", null);
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

第二步,从SPI来找

final InputStream is = getResourceAsStream(contextClassLoader, "META-INF/services/org.apache.commons.logging.LogFactory");
...
...
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
public InputStream getResourceAsStream(String name) {
    URL url = getResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

ps:使用Java 1.6提供的java.util.ServiceLoader也是不错的选择,dubbo即其他很多框架都是SPI的最好实践者。

第三步,使用默认的LogFactoryImpl

factory = newFactory("org.apache.commons.logging.impl.LogFactoryImpl", thisClassLoader, contextClassLoader);

以下是每个会去寻找的Logger:

private static final String[] classesToDiscover = {
        LOGGING_IMPL_LOG4J_LOGGER,
        "org.apache.commons.logging.impl.Jdk14Logger",
        "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
        "org.apache.commons.logging.impl.SimpleLog"
};

Slf4j原理

从classPath中查找,将StaticLoggerBinder绑定到 Enumeration paths 上。

ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
    paths = ClassLoader.getSystemResources("org/slf4j/impl/StaticLoggerBinder.class");
} else {
    paths = loggerFactoryClassLoader.getResources("org/slf4j/impl/StaticLoggerBinder.class");
}
while (paths.hasMoreElements()) {
    URL path = paths.nextElement();
    staticLoggerBinderPathSet.add(path);
}

可以看到log4j-slf4j-impl:jar:2.7中就有StaticLoggerBinder.class这个类。

总结

可以看到Commom-log和Slf4j为我们提供了两种做插件化服务很好的思路。Common-log使用接口(SPI的模式)或者是Slf4j使用门面模式(StaticLoggerBinder.class)来实现插件化,都是不错的选择。

最后,推荐一波Log4j2的异步化。
性能测试可看:

www.jianshu.com/p/570b406bd…