首页 > 其他分享 >性能调优之日志打印的坑

性能调优之日志打印的坑

时间:2024-02-10 21:44:05浏览次数:24  
标签:性能 打印 调优 堆栈 格式 日志 CPU

目录

摘要:推荐异步+随机采样打印日志,日志能少则少,而且只打印核心信息,切勿打印无用日志内容,切勿在循环中打日志。

综述

  在任何系统中,日志对服务的重要性不言而喻,它是反映系统运行情况的重要依据;它轻巧、简单,与我们形影不离,使得我们在排查问题时无需绞尽脑汁。被线上服务问题毒打过的人都认可日志的重要性,但看似不起眼的日志,却是一把双刃剑,隐藏着各式各样的“坑”,如果使用不当,不仅不能助我们一臂之力,反而会成为服务“杀手”,所以,你知道有哪些场景可能导致性能问题吗?今天楼兰胡杨和各位老铁聊聊高并发系统下 Java 日志性能那些事,同时提供一套异步+随机采样方案能让程序与日志“和谐共处”。

日志打印流程

  Java 日志打印对服务的影响包括多种因素,例如日志级别、日志输出目标和日志格式等。下面的流程图展示了 Java 日志打印的一般流程和对 CPU 的影响。


日志打印流程图

  根据以上流程图可以得出以下结论:

  • Java 日志打印会增加代码的执行时间,因为需要执行额外的日志打印语句。

  • 日志级别的选择会影响 CPU 的占用率。较低的日志级别(如 INFO 或 DEBUG)会导致更多的日志打印语句被执行,增加 CPU 的负担。

  • 日志输出目标的选择也会影响 CPU 的占用率。将日志输出到控制台会导致额外的 I/O 操作,增加 CPU 的负担。

  日志格式的复杂程度也会影响 CPU 的占用率。使用更复杂的日志格式可能会导致更多的字符串操作,增加 CPU 的负担。

不合理的书写方法

  本章节我将介绍过往线上遇到的日志问题,并逐个剖析问题根因。不同场景的日志书写格式:

// 格式1
log.debug("User id = " + userId);

// 格式2
if (log.isdebugEnable()) {
    log.debug("User id = " + userId);
}

// 格式3
log.debug("User msg {}!", JSON.toJSONString(user));

// 格式4,既然加了开关,说明是核心日志,打印info级别
if (日志开关已开启) {
  log.info("User msg {}!", JSON.toJSONString(user));
}

  如上四种写法,我相信大家或多或少都在项目代码中看到过,那么它们之间有什么区别?对性能会造成什么影响?如果此时关闭 DEBUG 级别日志,差异就显露出来了。

格式1 即使它不输出日志,依然需要执行字符串拼接,属于资源浪费。

格式2 缺点是需要加入额外的判断逻辑,增加了废代码,一点不优雅。

格式3 缺点是仍然需要根据系统配置的日志级别判断是否打印日志,并且需要提前序列化对象为JSON字符串,但是,不需要拼接字符串。

格式4 推荐在高并发系统中使用,优点是根据Boolean类型日志开关判断是否走日志打印逻辑,开关关闭时,不必校验是否需要打印日志。

线上环境打印大量日志消耗性能

  尽量多的日志,能够把用户的请求串起来,更容易断定出问题的代码位置。对于业务庞杂的分布式系统,任何日志的缺失对于程序员定位问题都是极大的障碍。所以,被线上问题折磨过的的程序员在开发代码过程中,为了以后线上出现问题能尽快定位问题并修复,肯定会设置合理的日志级别,而且只打印刚需日志。

  提问:为何 INFO 日志打多了,性能会受损(此时 CPU 使用率很高)?根本原因:同步打印日志时,磁盘 I/O 成为瓶颈,导致大量线程 Block。

  可以想象,如果日志都输出到同一个日志文件时,此时有多个线程都往文件里面写,是不是就乱了套了。那解决的办法就是加锁,保证日志文件输出不会错乱,如果是在高峰期,锁的争抢无疑是最耗性能的。当有一个线程抢到锁后,其它的线程只能 Block 等待,严重拖垮用户线程,表现就是上游调用超时,用户感觉卡顿。

  如下是线程卡在写文件时的堆栈:

Stack Trace is:
java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.logging.log4j.core.appender.OutputStreamManager.writeBytes(OutputStreamManager.java:352)

- waiting to lock <0x000000063d668298> (a org.apache.logging.log4j.core.appender.rolling.RollingFileManager)
  at org.apache.logging.log4j.core.layout.TextEncoderHelper.writeEncodedText(TextEncoderHelper.java:96)
  at org.apache.logging.log4j.core.layout.TextEncoderHelper.encodeText(TextEncoderHelper.java:65)
  at org.apache.logging.log4j.core.layout.StringBuilderEncoder.encode(StringBuilderEncoder.java:68)
  at org.apache.logging.log4j.core.layout.StringBuilderEncoder.encode(StringBuilderEncoder.java:32)
  at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:228)
  .....

  提问:假设不关心 INFO 排查问题,是不是生产只打印 ERROR 日志就没性能问题了?

  同样的,ERROR 日志量也不容小觑,假设线上出现大量异常数据,或者下游大量超时,瞬时会产生大量 ERROR 日志,此时还是会把磁盘 I/O 压满,导致用户线程 Block 住,最终导致 CPU 使用率快速增长。

最佳实践

去掉不必要的异常堆栈打印

  明显知道的异常,就不要打印堆栈,省点性能,任何事一旦邂逅高并发,意义就不一样了。

try {
    System.out.println(Integer.parseInt(number) + 100);
} catch (Exception e) {
    // 改进前
    log.error("parse int error : " + number, e);
    // 改进后
    log.error("parse int error : " + number);
}

  如果Integer.parseInt发生异常,导致异常原因肯定是出入的number不合法,在这种情况下,打印异常堆栈完全没有必要,可以去掉堆栈的打印。另外,捕获自定义异常后直接打印异常信息即可,不必再打印堆栈。

异步随机采样打印日志|done

  生产环境,尤其是 QPS 高的服务,小编推荐大家采样【异步+随机采样】打印策略,异步打印就不解释了,随机采样可以根据每次请求的唯一标识,如订单ID、用户ID+当前时间戳或者每次访问的请求ID等,随机打印32%或者50%等。当然开启异步打印有丢失日志的风险,比如服务器被强行终止时,没有来得及打印的日志就会丢失,但是,这些丢失的日志相对于QPS>100的整体日志而言,可以忽略不计。

日志的输出格式|done

  我们看下不同日志输出模式的区别:

// 格式1
[%d{yyyy/MM/dd HH:mm:ss.SSS}[%X{traceId}] %t [%p] %C{1} (%F:%M:%L) %msg%n

// 格式2
[%d{yy-MM-dd.HH:mm:ss.SSS}] [%thread]  [%-5p %-22c{0} -] %m%n

  官网对这几类模式的说明中反复强调了会影响性能。如果使用了如下属性输出,将极大的损耗性能:

 %C or $class, %F or %file, %l or %location, %L or %line, %M or %method

  各属性解释如下:

%C - 调用者的类名(速度慢,不推荐使用)
%F - 调用者的文件名(速度极慢,不推荐使用)
%l - 调用者的函数名、文件名、行号(极度不推荐,非常耗性能)
%L - 调用者的行号(速度极慢,不推荐使用)
%M - 调用者的函数名(速度极慢,不推荐使用)

  例如,log4j 为了拿到函数名称和行号信息,利用了异常机制,首先抛出一个异常,之后捕获异常并打印出异常信息的堆栈内容,再从堆栈内容中解析出行号。而实现源码中增加了锁的获取及解析过程,高并发下,性能损耗可想而知。

配置日志打印开关|done

  动态调整日志级别是常规操作,不做介绍,它既满足可随时查看 INFO日志的诉求,又能满足不需要时可动态关闭的诉求,而且不影响服务性能。借鉴此设计思想,小编推荐老铁们自定义日志打印开关。如果需要排查问题,开启相关日志的开关,定位到问题后再关闭开关:

if (日志开关已开启) {
  log.info("User msg {}!", JSON.toJSONString(user));
}

  优点是避免JSON 序列化或者字符串拼接,而且可以减少不必要的磁盘I/O操作造成的性能损耗。

总结与展望

  小编在此带你了解了日志在日常软件服务中常见的问题,以及对应的解决方法。切记,简单的东西 + 高并发 = 不简单!要对服务稳定性保持敬畏之心!

  能读到结尾说明你真是铁粉了,有任何问题请私信或者评论,看到了一定会第一时间回复。“点赞背后有感动,转发背后有收获,评论背后有认同,关注背后有价值。”如果你觉得本人提供的内容解决了你的问题,麻烦点赞、转发和关注,这是对我默默前行的最大鼓励,感谢支持!

Reference

标签:性能,打印,调优,堆栈,格式,日志,CPU
From: https://www.cnblogs.com/east7/p/18013052

相关文章

  • 编辑显示打印中文乱码
    在VSCode中显示的中文正常但打印乱码。打印别的中文正常。原因:该文件只是用正确的格式编码打开却还没有用该编码保存解法:  如果此时显示乱码,只需ctrl+z即可 效果: ......
  • STM32打印掺杂乱码
    问题:STM32打印掺杂乱码 原因:时钟太快了(不要以为是电磁干扰,接触不良,硬件出问题,多想想在发现这种情况之前做了什么操作)问题:为什么之前好好的,现在不行了原因:安装CubeMX导致的,具体原因不清楚解法:根据外部晶振调对时钟频率 效果: ......
  • openGauss学习笔记-216 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-CPU
    openGauss学习笔记-216openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-CPU获取openGauss节点的CPU、内存、I/O和网络资源使用情况,确认这些资源是否已被充分利用,是否存在瓶颈点。216.1CPU通过top命令查看openGauss内节点CPU使用情况,分析是否存在由于CPU负载过高导致的性能......
  • 打印九九乘法表
    需求打印九九乘法表代码实现packagecom.jichu.struct;publicclassForDemo03{publicstaticvoidmain(String[]args){//打印九九乘法表for(inti=1;i<10;i++){//i列for(intj=1;j<=i;j++){//j行int......
  • 打印三角形 (5行)
    需求打印三角形(5行)代码实现packagecom.jichu.struct;publicclassTestDemo{publicstaticvoidmain(String[]args){//打印三角形5行for(inti=1;i<=5;i++){//i行for(intj=5;j>=i;j--){//j一行几个左边第一个......
  • mybatis 通过拦截器打印完整的sql语句以及执行结果操作
    下面的文件放在source-fw的【jp.co.token.sikyuu.iterceptor】包下面MybatisInterceptor.javaInterceptorForQry.java下面的文件放在source-fw的【jp.co.token.sikyuu.common】包下面FastJsonUtils.java下面的文件放在WebRoot/WEB-INF/lib/路径下面fastjson-......
  • 用户行为日志概述
    用户行为日志的内容,主要包括用户的各项行为信息以及行为所处的环境信息。收集这些信息的主要目的是优化产品和为各项分析统计指标提供数据支撑。收集这些信息的手段通常为埋点。目前主流的埋点方式,有代码埋点(前端/后端)、可视化埋点、全埋点等。代码埋点是通过调用埋点SDK函数,在......
  • Logback - 日志框架
    引言在当今的企业级应用开发中,日志管理是一个不可或缺的部分。它不仅帮助我们进行错误跟踪,还能有效监控应用程序的运行状态,为性能优化提供数据支撑。SpringBoot作为一个简化Spring应用开发的框架,自带了强大的日志管理功能。在SpringBoot的众多日志管理技术中,Logback由于其高效性......
  • 如何基于 spdlog 在编译期提供类 logrus 的日志接口
    如何基于spdlog在编译期提供类logrus的日志接口实现见Github,代码简单,只有一个头文件。前提几年前看到戈君在知乎上的一篇文章,关于打印日志的一些经验总结;实践下来很受用,在golang里结构化日志和logrus非常契合,最常见的使用方式如下。logrus.WithField("addr","127.0......
  • 嵌入式中,日志调试法的一些规则
    https://mp.weixin.qq.com/s/yTInDBFbI0oM5bowx990lw在我们嵌入式开发中,打印日志是最常用的一种调试手段。合理地打印日志,可以帮助我们快速地分析问题。本篇文章我们来汇总一些嵌入式打log的一些规则。1.什么操作下加日志?(1)错误处理对于不能恢复的严重错误,日志内容应详细到......