高并发场景下的一些日志实践

JDK : 1.8 Log4J : 2.x



前言

如何打印出精简、便于线上排查问题的日志,是一门艺术,不在本文的篇幅中。本文主要讲一些跟性能相关的日志实践。



在高并发场景下,业务日志往往是应用性能的一大杀手。这么说大家可能没有概念,那么看一个实际的案例。可以看Promise面向零售结算页的某个核心应用A压测过程中的火焰图:





从火焰图可以看出:该应用的CPU消耗大头是Log4j日志,CPU%消耗占比超过了50%。再参考磁盘写速度,MDC分钟级平均后的数据:峰值磁盘写速度接近100Mb/秒;如果只看秒级数据,周期性的秒级峰值可以达到近500Mb/秒(注:周期性是由Log4j刷盘策略及Buffer配置引起的)

另外,JVM GC也会消耗较大比例的CPU%。GC的CPU%消耗也是因为日志输出太大,导致堆内存很快打满频繁地GC导致的。

所以之前在该应用的性能调优实践中,仅通过业务日志优化就能得到近100%的吞吐量提升。



约定俗成

Promise系统中面向用户下单的应用,是流量最大、最为核心的应用。这些应用的特点通常是CPU密集型的,那么这些应用的日志我们有一些约定俗称的要求:

1.控制日志输出量:根据经验观察,当磁盘写> 30Mb/s 时,响应时间和吞吐量有很大影响;当磁盘写>= 100Mb/s时,响应时间和吞吐量急剧下降。单机吞吐量通常为关日志时的10%~20% 。(容器规格参考:通常是8c8g50g)

2.日志文件规划:接口出入参单独输出到一个文件;业务日志分层设计,避免所有的业务日志都输出到同一个文件,加大文件锁的竞争。

3.生成日志字符串过程中禁用:String.format、JSON或XML序列化、反射。

4.使用StringBuilder生成日志字符串,适当情况下可以考虑给一个初始的capacity,避免日志生成过程中StringBuilder底层大量的扩容、数组复制。(注:非高并发应用可以忽略这条要求)



关于第4点,可以结合JDK源码分析:

StringBuilder初始化时默认的capacity是16,从AbstractStringBuilder的源码可以看到:此时StringBuilder内部存放字符的容器char[]的长度是16。我们做工程的都知道,对于业务日志来说,这点大小的数组用来塞牙都不够。所以在append过程中必然会触发多次的数组复制(在大业务日志内容输出的应用中,我们可以很容易地在火焰图中抓到这部分的CPU%消耗)。

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{


    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16); 
    }


    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }


private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }



abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;


    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }





最终会通过native的System.arraycopy完成数组的复制,实现StringBuilder内置容器char[]的自动扩容:

public class Arrays {


public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }



Log4J调优

PatternLayout配置

官方文档PatternLayout,明确描述了以下几种参数(%C、%F、%l、%L、%M)对性能有影响。在高并发场景下,可以考虑禁用一些参数,如%L。

%C、%M这些参数可能对线上问题排查有帮助,可以保留使用。



















有几个需要注意的地方:

•PatternLayout中的%L只对同步日志有效;

•异步日志想要在日志中输出行号,必须在Log4j xml中添加属性includeLocation="true":

<Loggers>
 <Root level="info" includeLocation="true">
 <AppenderRef ref="RandomAccessFile"/>
 </Root>
 </Loggers>



•PatternLayout中的%L 影响的是常规日志中是否打印(classname:lineNumer),而异常堆栈信息中无论如何都会带上lineNumber。



异步日志实践

首先参见Log4j官网文档“Asynchronous Loggers for Low-Latency Logging”,全异步日志的性能表现是一枝独秀的(Loggers all async),远超过同步/异步混合日志(Async Appender mixed Sync Logger)、同步日志(Sync)。







解释下全异步日志(Loggers all async)、同步/异步混合日志(Async Appender mixed Sync Logger)、同步日志(Sync)这几个概念。

同步日志(Sync)

这个很容易理解,Log4j1.x就只支持同步日志,业务线程负责执行业务逻辑和日志输出(从生成到落盘),所以性能很差(Latency和Throughput都很差)。



同步/异步混合日志(Async Appender mixed Sync Logger)

在Log4j内部处理流程中,Logger和Appender是先后顺序的两个阶段,Logger负责日志的过滤、日志事件(LogEvent)的生成,Appender负责日志事件的处理(可以落盘到文件、数据库、Kafka、Redis等等,除了官方提供的Appender还可以根据自己的需求自定义Appender)。





Log4j 2.x版本开始提供AsyncLogger和AsyncAppender,让用户自由地选择配置。如果只配置了AsyncAppender,相当于Log4j处理流程中只有后半段是异步的(通过BlockingQueue实现),前半段还是同步。这种情况可以视为同步/异步混合日志场景。



全异步日志(Loggers all async)

了解了上述两个场景后,全异步日志也就容易理解了。Log4j内部处理流程中Logger和Appender两个阶段都是异步的,并且一个JVM进程内,所有的Logger文件都配置为异步,就可以认为是符合全异步日志场景。

上文已经说过,AsyncAppender是通过BlockingQueue实现异步,而AsyncLogger则是通过Disruptor实现异步。参见下图(图片来自“log4j异步日志原理及配置”)。







那么,如何使用全异步日志呢?一个可行的实践:

① JVM启动参数中增加:

-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector



② 当配置AsyncLoggerContextSelector作为异步日志时,确保在Log4j xml配置中使用普通的 <root><logger> 元素。





同样以Promise面向零售结算页的某个核心应用A为例,参考之前的性能压测数据:全异步日志 比 同步/异步混合日志 的吞吐量能提升10%左右(都是禁用%L的前提下)。







从一个真实案例看EventRoute的DISCARD模式

背景

Promise面向零售结算页的某个核心应用B在性能压测过程中,发现在业务日志全开启的场景下,CPU%压到50%多再也压不上去了,磁盘IO和网络IO也都没到瓶颈点。很自然怀疑性能的阻塞点是JVM Thread Block导致了CPU性能没有被充分压榨。



于是复现该场景,通过jstack抓Thread dump:

srs_service_jstack_572410_2020-09-22-21-14-23.txt



Thread Dump分析

使用PerfMa的Java线程Dump分析打开dump文件。其它非关键信息这里不做赘述,直奔关键点:



1. 可以看出,同步锁集中在log4j日志写文件,等锁线程数达到38个。基本上JSF-BZ全部被BLOCKED。







2.进一步查看持有锁的线程:JSF-BZ-20900-26-T-700,锁对象:org.apache.logging.log4j.core.appender.rolling.RollingFileManager







3.查看线程JSF-BZ-20900-26-T-700的堆栈,关键步骤有四个:

1)触发锁持有的业务代码:SiteTypeAihuishouFilter.generateMsgCode()

2)异常日志尝试向AsyncLogger对应的RingBuffer里发布(publish)一个日志事件(logEvent)

3)Log4j先进行RingBufferFull检验,当发现环形队列已满(所有slot都被日志对象占用),此时异步日志会转同步日志,由当前的业务主线程(即JSF-BZ线程)同步写磁盘。

4)因为配置了Rolling File,日志在落盘前会检查是否要Rollover(同步锁)







4.随机验证等待锁的线程堆栈,可以得出结论:这些BLOCKED的线程中的业务代码都在尝试往同一个日志文件同步写(相同的package路径,决定了同一个Logger -> Appender -> log文件)。







Log4J源码分析

分析完线程堆栈信息后,我们结合Log4j源码来验证刚才的想法:

1)异常日志的发布(publish)

logEvent尝试向RingBuffer插入(tryPublish),如果因RingBuffer已满而发布失败,则会进入handleRingBufferFull代码分支。

public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBufferLogEvent> {
    private void publish(final RingBufferLogEventTranslator translator) {
        if (!loggerDisruptor.tryPublish(translator)) {
            handleRingBufferFull(translator);
        }
    }
    ...
}



2)RingBuffer Full处理

先拿到当前logEvent的EventRoute,EventRoute决定了日志事件以什么方式落到哪个目的地。它本质上是一个枚举,支持3种模式:ENQUEUE、SYNCHRONOUS、DISCARD。

先只关注SYNCHRONOUS模式,Log4J的实现是使用当前业务主线程来做日志同步写。(性能瓶颈点!一旦RingBuffer满了导致异步日志转同步日志时,就是性能停滞不前的开始)

public class AsyncLogger extends Logger implements EventTranslatorVararg<RingBufferLogEvent> {
    private void handleRingBufferFull(final RingBufferLogEventTranslator translator) {
        final EventRoute eventRoute = loggerDisruptor.getEventRoute(translator.level);
        switch (eventRoute) {
            case ENQUEUE:
                loggerDisruptor.enqueueLogMessageInfo(translator);
                break;
            case SYNCHRONOUS:
                logMessageInCurrentThread(translator.fqcn, translator.level, translator.marker, translator.message,
                        translator.thrown);
                break;
            case DISCARD:
                break;
            default:
                throw new IllegalStateException("Unknown EventRoute " + eventRoute);
        }
    }


    /**
     * Enqueues the specified log event data for logging in a background thread.
     * <p>
     * This re-uses a {@code RingBufferLogEventTranslator} instance cached in a {@code ThreadLocal} to avoid creating
     * unnecessary objects with each event.
     *
     * @param fqcn fully qualified name of the caller
     * @param level level at which the caller wants to log the message
     * @param marker message marker
     * @param message the log message
     * @param thrown a {@code Throwable} or {@code null}
     */
    private void logWithThreadLocalTranslator(final String fqcn, final Level level, final Marker marker,
            final Message message, final Throwable thrown) {
        // Implementation note: this method is tuned for performance. MODIFY WITH CARE!


        final RingBufferLogEventTranslator translator = getCachedTranslator();
        initTranslator(translator, fqcn, level, marker, message, thrown);
        initTranslatorThreadValues(translator);
        publish(translator);
    }
    ...

3) Rolling File检查

可以看到,checkRollover方法本身就是synchronized,只有一个线程准入。就是为了防止在File Rolling的临界点,当前线程判断不需要Rolling,但是同一时钟周期内被其它线程写入日志导致了Rolling这个并发问题。

public class RollingFileManager extends FileManager {    
    /**
     * Determines if a rollover should occur.
     * @param event The LogEvent.
     */
    public synchronized void checkRollover(final LogEvent event) {
        if (triggeringPolicy.isTriggeringEvent(event)) {
            rollover();
        }
    }
    ...
}

优化方案

在上述源码分析过程中,可看出EventRoute有3中模式:ENQUEUE、SYNCHRONOUS、DISCARD。

在高并发场景下,是可以考虑DISCARD模式。当某一个AsyncLogger对应的RingBuffer满了时,日志事件直接丢弃,不再转同步写磁盘。理由也很简单,RingBuffer的slot默认数量是256*1024,已经足够大了,如果RingBuffer还是能够被打满,说明一个问题:此时磁盘写速度已经很接近上限了,异步Flush到磁盘的速度已经远跟不上日志产生的速度。这种情况下,日志的丢弃是为了更好地保护磁盘和应用。

况且,即便所有的日志都能落到磁盘,在LogBook Agent采集过程中也还是有可能会丢失。在2020年我们与LogBook团队沟通中得知:由于LogBook Agent和业务应用部署在同一个容器内,所以它为了保护业务应用也做了一些策略:

1.采集速率>1000条/s时,多余日志丢弃

2.Agent进程内存超过300MB时,reset Agent进程

3.Agent进程CPU%超过20%时,reset Agent进程

4.单条日志大小超过10KB时,截断

5....



所以,下一步需要验证高并发场景下DISCARD模式的性能。JVM启动参数中增加(参考log4j异步注意事项):

-Dlog4j2.AsyncQueueFullPolicy=Discard

压测数据对比:



场景CPU%Mem%TPM磁盘写速度
日志精简优化前50%50%68000152M/s
日志精简优化后54%69%7992669M/s
日志精简优化后+DISCARD44%46%114453153M/s





  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 阿里亿级高并发设计手册是阿里巴巴技术团队在处理超高并发场景时总结的一份指南,该手册已经开源在GitHub上。该手册涵盖了在处理高并发场景下的架构设计、性能优化、系统扩展等方面的经验和实践。 手册的目的是帮助开发人员更好地理解如何设计高并发系统,并提供一些实用的指导原则和最佳实践。它提供了用于处理大量并发请求的常用解决方案,包括负载均衡、缓存、分布式存储、分布式事务等。 手册主要分为三个部分:架构篇、性能篇和工程篇。架构篇介绍了高并发系统的设计原则和架构模式,例如分布式架构、微服务架构和数据分片等。性能篇讲解了性能优化的方法和技巧,例如使用缓存、异步化处理和并发控制等。工程篇则提供了一些工程实践,例如日志管理、监控和故障处理等。 阿里亿级高并发设计手册在GitHub上的开源是为了让更多的人可以学习和分享高并发系统设计的经验。开源社区的力量可以促进不同团队之间的合作和创新。通过在GitHub上开源,阿里巴巴也可以吸引更多的开发人员加入并为该手册贡献自己的经验和想法。 总之,阿里亿级高并发设计手册是一个非常有价值的资源,它提供了丰富的关于处理高并发场景的经验和实践,为开发人员在设计和开发高性能系统时提供了有益的指导和参考。 ### 回答2: 阿里亿级高并发设计手册是一个由阿里巴巴技术团队开发并维护的开源项目,位于GitHub平台上。该手册为开发者提供了关于处理高并发场景的设计原则和实用技巧。 该手册提供了丰富的内容,包括分布式系统设计、缓存策略、负载均衡、数据库设计等方面的指导。对于阿里巴巴这样拥有庞大用户量和极高并发的互联网公司,高并发设计显得尤为重要,因此该手册积累了许多实践经验,对于开发者来说是非常宝贵的资料。 手册中的设计原则包括:分布式和高可用、缓存设计、数据库设计、消息队列、分库分表、服务器和网络的优化等。通过阅读该手册,开发者可以了解到如何设计能够应对海量请求的系统架构、如何使用缓存来减轻数据库压力、如何选择合适的技术方案来解决高并发问题等。 此外,该手册还提供了一些常用的设计模式和技术框架,例如数据库读写分离、分布式锁、消息中间件等,这些都是在实际项目中被广泛应用的解决方案。 阿里亿级高并发设计手册的出现对于开发者来说是一个非常好的资源,可以帮助他们更好地理解和应对高并发场景的挑战。无论是对于新手还是有经验的开发者,通过阅读和学习该手册,都能够提高自己的技术水平,设计出更加高效和稳定的系统。因此,阿里亿级高并发设计手册在GitHub上备受关注和推崇。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值