在互联网设计架构过程中,日志异步落库,俨然已经是高并发环节中不可缺少的一环。为什么说是高并发环节中不可缺少的呢? 原因在于,如果直接用mq进行日志落库的时候,低并发下,生产端生产数据,然后由消费端异步落库,是没有什么问题的,而且性能也都是异常的好,估计tp99应该都在1ms以内。
但是一旦并发增长起来,慢慢的你就发现生产端的tp99一直在增长,从1ms,变为2ms,4ms,直至send timeout。尤其在大促的时候,我司的系统就经历过这个情况,当时mq的发送耗时超过200ms,甚至一度有不少timeout产生。
考虑到这种情况在高并发的情况下才出现,所以今天我们就来探索更加可靠的方法来进行异步日志落库,保证所使用的方式不会因为过高的并发而出现接口ops持续下降甚至到不可用的情况。
方案一: 基于log4j的异步appender实现
此种方案,依赖于log4j。在log4j的异步appender中,通过mq进行生产消费入库。相当于在接口和mq之间建立了一个缓冲区,使得接口和mq的依赖分离,从而不让mq的操作影响接口的ops。
此种方案由于使用了异步方式,且由于异步的discard policy策略,当大量数据过来,缓冲区满了之后,会抛弃部分数据。此种方案适用于能够容忍数据丢失的业务场景,不适用于对数据完整有严格要求的业务场景。
来看看具体的实现方式:
首先,我们需要自定义一个Appender,继承自log4j的AppenderSkeleton类,实现方式如下:
public class AsyncJmqAppender extends AppenderSkeleton {
@Resource(name = "messageProducer")
private MessageProducer messageProducer;
@Override
protected void append(LoggingEvent loggingEvent) {
asyncPushMessage(loggingEvent.getMessage());
}
/**
* 异步调用jmq输出日志
* @param message
*/
private void asyncPushMessage(Object message) {
CompletableFuture.runAsync(() -> {
Message messageConverted = (Message) message;
try {
messageProducer.send(messageConverted);
} catch (JMQException e) {
e.printStackTrace();
}
});
}
@Override
public boolean requiresLayout() {
return false;
}
@Override
public void close() {
}
}
然后在log4j.xml中,为此类进行配置:
<!--异步JMQ appender-->
<appender name="async_mq_appender" class="com.jd.limitbuy.common.util.AsyncJmqAppender">
<!-- 设置File参数:日志输出文件名 -->
<param name="File" value="D:/export/Instances/order/server1/logs/order.async.jmq" />
<!-- 设置是否在重新启动服务时,在原有日志的基础添加新日志 -->
<param name="Append" value="true" />
<!-- 设置文件大小 -->
<param name="MaxFileSize" value="10KB" />
<!-- 设置文件备份 -->
<param name="MaxBackupIndex" value="10000" />
<!-- 设置输出文件项目和格式 -->
<layo