在互联网设计架构过程中日志异步落库俨然已经是高并发环节中不可缺少的一环。为什么说是高并发环节中不可缺少的呢 原因在于如果直接用mq进行日志落库的时候低并发下生产端生产数据然后由消费端异步落库是没有什么问题的而且性能也都是异常的好估计tp99应该都在1ms以内。但是一旦并发增长起来慢慢的你就发现生产端的tp99一直在增长从1ms变为2ms4ms直至send timeout。尤其在大促的时候我司的系统就经历过这个情况当时mq的发送耗时超过200ms甚至一度有不少timeout产生。考虑到这种情况在高并发的情况下才出现所以今天我们就来探索更加可靠的方法来进行异步日志落库保证所使用的方式不会因为过高的并发而出现接口ops持续下降甚至到不可用的情况。方案一 基于log4j的异步appender实现此种方案依赖于log4j。在log4j的异步appender中通过mq进行生产消费入库。相当于在接口和mq之间建立了一个缓冲区使得接口和mq的依赖分离从而不让mq的操作影响接口的ops。此种方案由于使用了异步方式且由于异步的discard policy策略当大量数据过来缓冲区满了之后会抛弃部分数据。此种方案适用于能够容忍数据丢失的业务场景不适用于对数据完整有严格要求的业务场景。来看看具体的实现方式首先我们需要自定义一个Appender继承自log4j的AppenderSkeleton类实现方式如下12345678910111213141516171819202122232425262728293031323334353637383940publicclassAsyncJmqAppender extends AppenderSkeleton {Resource(name messageProducer)privateMessageProducer messageProducer;Overrideprotectedvoidappend(LoggingEvent loggingEvent) {asyncPushMessage(loggingEvent.getMessage());}/*** 异步调用jmq输出日志* param message*/privatevoidasyncPushMessage(Object message) {CompletableFuture.runAsync(() - {Message messageConverted (Message) message;try{messageProducer.send(messageConverted);}catch(JMQException e) {e.printStackTrace();}});}Overridepublicboolean requiresLayout() {returnfalse;}Overridepublicvoidclose() {}}然后在log4j.xml中为此类进行配置123456789101112131415161718!--异步JMQ appender--appendernameasync_mq_appender classcom.jd.limitbuy.common.util.AsyncJmqAppender!-- 设置File参数日志输出文件名 --paramnameFile valueD:/export/Instances/order/server1/logs/order.async.jmq /!-- 设置是否在重新启动服务时在原有日志的基础添加新日志 --paramnameAppend valuetrue /!-- 设置文件大小 --paramnameMaxFileSize value10KB /!-- 设置文件备份 --paramnameMaxBackupIndex value10000 /!-- 设置输出文件项目和格式 --layoutclassorg.apache.log4j.PatternLayoutparamnameConversionPattern value%m%n //layout/appenderloggernameasync_mq_appender_loggerappender-refrefasync_mq_appender//logger最后就可以按照如下的方式进行正常使用了1privatestaticLogger logger LoggerFactory.getLogger(filelog_appender_logger);注意 此处需要注意log4j的一个性能问题。在log4j的conversionPattern中匹配符最好不要出现 C% L%通配符压测实践表明这两个通配符会导致log4j打日志的效率降低10倍。方案一很简便且剥离了接口直接依赖mq导致的性能问题。但是无法解决数据丢失的问题但是我们其实可以在本地搞个策略落盘来不及处理的数据可以大大的减少数据丢失的几率。但是很多的业务场景是需要数据不丢失的所以这就衍生出我们的另一套方案来。方案二增量消费log4j日志此种方式是开启worker在后台增量消费log4j的日志信息和接口完全脱离。此种方式相比方案一可以保证数据的不丢失且可以做到完全不影响接口的ops。但是此种方式由于是后台worker在后台启动进行扫描会导致落库的数据慢一些比如一分钟之后才落库完毕。所以适用于对落库数据实时性不高的场景。具体的实现步骤如下首先将需要进行增量消费的日志统一打到一个文件夹以天为单位每天生成一个带时间戳日志文件。由于log4j不支持直接带时间戳的日志文件生成所以这里需要引入log4j.extras组件然后配置log4j.xml如下​编辑之后在代码中的申明方式如下1privatestaticLogger businessLogger LoggerFactory.getLogger(file_rolling_logger);最后在需要记录日志的地方使用方式如下1businessLogger.error(JsonUtils.toJSONString(myMessage))这样就可以将日志打印到一个单独的文件中且按照日期每天生成一个。然后当日志文件生成完毕后我们就可以开启我们的worker进行增量消费了这里的增量消费方式我们选择RandomAccessFile这个类来进行由于其独特的位点读取方式可以使得我们非常方便的根据位点的位置来消费增量文件从而避免了逐行读取这种低效率的实现方式。注意为每个日志文件都单独创建了一个位点文件里面存储了对应的文件的位点读取信息。当worker扫描开始的时候会首先读取位点文件里面的位点信息然后找到相应的日志文件从位点信息位置开始进行消费。这就是整个增量消费worker的核心。具体代码实现如下(代码太长做了折叠) View Code此种方式由于worker扫描是每隔一段时间启动一次进行消费所以导致数据从产生到入库可能经历时间超过一分钟以上但是在一些对数据延迟要求比较高的业务场景比如库存扣减是不能容忍的所以这里我们就引申出第三种做法基于内存文件队列的异步日志消费。方案三基于内存文件队列的异步日志消费由于方案一和方案二都严重依赖log4j且方案本身都存在着要么丢数据要么入库时间长的缺点所以都并不是那么尽如人意。但是本方案的做法既解决了数据丢失的问题又解决了数据入库时间被拉长的尴尬所以是终极解决之道。而且在大促销过程中此种方式经历了实战检验可以大面积的推广使用。此方案中提到的内存文件队列是我司自研的一款基于RandomAccessFile和MappedByteBuffer实现的内存文件队列。队列核心使用了ArrayBlockingQueue并提供了produce方法进行数据入管道操作提供了consume方法进行数据出管道操作。而且后台有一个worker一直启动着每隔5ms或者遍历了100条数据之后就将数据落盘一次以防数据丢失。具体的设计就这么多感兴趣的可以根据我提供的信息自己实践一下。由于有此中间件的加持数据生产的时候只需要入压入管道然后消费端进行消费即可。未被消费的数据会进行落盘操作谨防数据丢失。当大促的时候大量数据涌来的时候管道满了的情况下会阻塞接口数据不会被抛弃。虽然可能会导致接口在那一瞬间无响应但是由于有落盘操作和消费操作此操作操控的是JVM堆外内存数据不受GC的影响所以不会出现操作暂停的情况为什么呢因为用了MappedByteBuffer此种阻塞并未影响到接口整体的ops。