首页 > 其他分享 >高并发系统-使用自定义日志埋点快速排查问题

高并发系统-使用自定义日志埋点快速排查问题

时间:2024-05-21 10:41:58浏览次数:20  
标签:String 自定义 message void context 埋点 public 日志

背景

在高并发的系统中,通常不会打印除参数校验失败或捕获异常之外的日志,防止对接口的性能产生影响。

那对于请求不符合预期的情况,我们如何快速找到是哪块逻辑影响的至关重要。

Pfinder提供的链路监控,更多的是性能层面的监控,无法满足我们上述的诉求。

下面我将通过自定义通用上下文,添加日志埋点,解决上述存在的问题。

通用上下文 CommonContext

作用

创建通用上下文的作用,是为了跟踪一个请求的生命周期,然后根据请求的特殊标识,决定是否记录关键日志,然后返回给调用方,以识别具体执行了什么逻辑,以便快速排查问题。

包含

一个通用上下文,除了要包含记录日志的字段,也可以存储一些通用参数,计算中间结果等等。

示例

@Slf4j
@Data
public class CommonContext {
    // 日志
    private StringBuffer logSb = new StringBuffer();
    // 日志开关
    private boolean isDebug;

    // 通用参数
    private boolean compare = false;
    // 中间结果
    private Set<Integer> targetSet = new HashSet<>();

    public void clearContext() {
        targetSet = Collections.emptySet();
        compare = false;
    }

    public void debug(String message) {
        if (!isDebug || StringUtils.isEmpty(message)) {
            return;
        }
        logSb.append(message).append("\t\n");
    }

    public void debug(String format, Object... argArray) {
        if (!isDebug) {
            return;
        }
        String[] msgArray = new String[argArray.length];
        for (int i = 0; i < argArray.length; i++) {
            msgArray[i] = JSON.toJSONString(argArray[i]);
        }
        FormattingTuple ft = MessageFormatter.arrayFormat(format, msgArray);
        logSb.append(ft.getMessage()).append("\t\n");
    }

    public void debugEnd() {
        if (!isDebug) {
            return;
        }
        String msg = logSb.toString();
        log.info(msg);
    }

}

使用

简单是使用如下:

@Override
public Response method(Request request) {
    if (checkParam(request)) {
        log.error("request param error:{}", JSON.toJSONString(request));
        return Response.failed(ResponseCode.PARAM_INVALID);
    }
    CallerInfo info = Profiler.registerInfo(Ump.getUmpKey(xxxx), false, true);
    ParamVO paramVO = request.getParam();

    try {
        CommonContext context = new CommonContext();
        context.setDebug(Constants.SPECIAL_UUID.equals(request.getUuid()));

        Long userId = paramVO.getUserId();
        context.setCompare(paramVO.getCompare());

        context.debug("输入参数:{}", paramVO);
        List result = userAppService.match(context, paramVO);
        context.debug("输出结果:{}", result);
        context.clearContext();

        Response response = Response.success(result);
        context.debugEnd(response);
        return response;
    } catch (Exception e) {
        log.error("method error", e);
        Profiler.functionError(info);
        return Response.failed(ResponseCode.ERROR);
    } finally {
        Profiler.registerInfoEnd(info);
    }
}

说明:

  1. 当识别到指定的 uuid ,我们开启了上下文日志开关

  2. 对于单个入参的情况,context.clearContext();不执行也可以,但是对于批量接口,在遍历处理的时候,需要在每个循环体内执行context.clearContext();,防止一些中间结果对后需循环的影响。

  3. 在关键的地方,我们可以通过 context.debug()记录日志,然后设置到 Response#message随响应返回,进而快速识别问题。

存在的问题

在记录日志的时候,我打印了如下日志:

context.debug("activityList:{}", activityList.stream()
        .map(ActivityInfo::toString)
        .collect(Collectors.joining("######")));

单从代码来看,好像没什么问题。

来看接口性能,如下:
image.png
tp99达到恐怖的35s!
image.png
CPU使用率居高不下!

通过分析,发现查询到的 activityList 个数较多,且单个对象较大,在执行上述日志打印逻辑的时候,消耗了较多的CPU资源,进而影响了接口性能。

注释该段代码,tp99降低至15ms左右。

但实际上,我还是存在打印该列表的诉求。

升级

上述问题的根本原因是:不论我是否开启日志打印,日志中的计算逻辑总会执行。

那有什么办法,只在开关开启的情况下,打印该日志呢?

借鉴log4j,使用lamba表达式延迟打印

Log4j存在如下API:

org.apache.logging.log4j.Logger#info(java.lang.String, org.apache.logging.log4j.util.Supplier<?>...)

手动控制是否打印详情信息

将打印列表的诉求拆分如下:

  1. 对于特大的列表,不打印

  2. 对于较小的列表,打印

升级后的CommonContext

package org.example;

import com.alibaba.fastjson.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MessageFormatter;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;

@Slf4j
@Data
public class CommonContext {
    // 日志
    private StringBuffer logSb = new StringBuffer();
    // 日志开关
    private boolean isDebug;
    // 日志开关是否记录详细日志
    private boolean isDebugDetail;

    // 通用参数
    private boolean compare = false;
    // 中间结果
    private Set<Integer> targetSet = new HashSet<>();

    public void clearContext() {
        targetSet = Collections.emptySet();
        compare = false;
    }

    public void setDebugDetail(boolean debugDetail) {
        if (debugDetail) {
            isDebug = true;
        }
        isDebugDetail = debugDetail;
    }

    public void debug(String message) {
        if (!isDebug || StringUtils.isEmpty(message)) {
            return;
        }
        logSb.append(message).append("\t\n");
    }

    public void debug(String format, Object... argArray) {
        if (!isDebug) {
            return;
        }
        String[] msgArray = new String[argArray.length];
        for (int i = 0; i < argArray.length; i++) {
            msgArray[i] = JSON.toJSONString(argArray[i]);
        }
        FormattingTuple ft = MessageFormatter.arrayFormat(format, msgArray);
        logSb.append(ft.getMessage()).append("\t\n");
    }

    public void debug(String message, Supplier<?>... paramSuppliers) {
        if (!isDebug) {
            return;
        }
        commonDebug(message, paramSuppliers);
    }

    public void debugDetail(String message, Supplier<?>... paramSuppliers) {
        if (!isDebugDetail) {
            return;
        }
        commonDebug(message, paramSuppliers);
    }

    private void commonDebug(String message, Supplier<?>... paramSuppliers) {
        String[] msgArray = new String[paramSuppliers.length];
        for (int i = 0; i < paramSuppliers.length; i++) {
            msgArray[i] = JSON.toJSONString(paramSuppliers[i].get());
        }
        FormattingTuple ft = MessageFormatter.arrayFormat(message, msgArray);
        logSb.append(ft.getMessage()).append("\t\n");
    }

    public void debugEnd() {
        if (!isDebug) {
            return;
        }
        String msg = logSb.toString();
        log.info(msg);
    }

}

说明:

  1. 这里引入的Supplierjava.util包的,更通用。

  2. 保留了对于简单的参数,不使用lambda的方式。

  3. lambda的延迟计算已验证,可放心使用。

升级后使用

CommonContext context = new CommonContext();
context.setDebug(Constants.SPECIAL_UUID.equals(request.getUuid()));
context.setDebugDetail(Constants.SPECIAL_UUID2.equals(request.getUuid()));

需要注意: setDebugDetail() 需要在 setDebug后执行,否则isDebug标识会被覆盖。

context.debugDetail("activityList:{}", () -> activityList.stream()
        .map(ActivityInfo::toString)
        .collect(Collectors.joining("######")));

将所有有计算逻辑的日志升级为 lamba表达式,下面来看升级前后接口性能变化:
image.png

以上。

 

作者:京东零售 张云鹏

来源:京东云开发者社区

标签:String,自定义,message,void,context,埋点,public,日志
From: https://www.cnblogs.com/Jcloud/p/18203466

相关文章

  • spring boot如何自定义注解
    总共分三步:1、创建一个注解importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target(ElementType.METHOD)//注解的目标为方法@Retention(Retention......
  • .NET6中使用Log4net记录日志(二)记录日志到SqlServer数据库
    1、引用NuGet包(System.Data.SqlClient) 2、创建SqlServer数据库表(ProgramLog)CREATETABLEProgramLog(IdINTIDENTITY(1,1)PRIMARYKEY,[Date]DATETIME,--记录时间[Level]NVARCHAR(128),--日志级别[RunTime]VARCHAR(128),--执行时长[Thread]NVARCHAR(256),--线程号[Lin......
  • 即构 UIKits 重磅发布!高效开发与自定义UI兼备,打造互动场景新标杆
    即构UIKits上线,新一代场景化实时互动SDK!即构科技发布了首款面向中小团队的整合型实时互动产品UIKits,基于场景化最佳实践,整合RTC、IM、直播、美颜等多款产品,打造了音视频通话UIKit(CallKit)、互动直播UIKit(LiveStreamingKit)、语聊房UIKit(LiveAudioRoomKit)等多个场景互动SDK......
  • Zabbix添加自定义监控项
    1.查看zabbix_agent配置文件cat/etc/zabbix/zabbix_agentd.conf|grep-v"^$"|grep-v"^#"上图中,自定义监控项的配置文件可以放到Include项目中,Include可以有多个2.添加或修改自定义监控项配置文件cd/etc/zabbix/zabbix_agentd.d/vicustomized_cpu.confUserParameter......
  • 如何正确实现一个自定义可序列化的 Exception
    最近在公司的项目中,编写了几个自定义的Exception类。提交PR的时候,sonarqube提示这几个自定义异常不符合ISerializablepatten.花了点时间稍微研究了一下,把这个问题解了。今天在此记录一下,可能大家都会帮助到大家。自定义异常#编写一个自定义的异常,继承自Exception,其中......
  • 日志过滤器
    1.安全相关的关键字认证失败regex复制代码Failed\s+passwordInvalid\s+userauthentication\s+failureFailed\s+publickeyFailed\s+keyboard-interactive/pam用户相关regex复制代码useradd\[userdel\[usermod\[sudo:\s+.+:.*\s+COMMAND=SSH相关regex复制代码sshd:......
  • 『手撕Vue-CLI』添加自定义指令
    前言经上篇『手撕Vue-CLI』添加帮助和版本号的介绍之后,已经可以在控制台中输入nue--help来查看帮助信息了,但是在帮助信息中只有--version,--help这两个指令,而vue-cli中还有很多指令,例如create,serve,build等等,所以本章将继续添加自定义指令,例如create指令。添加create......
  • Windows 日志也可能会被篡改和删除。这可能是因为某些恶意软件试图隐藏其存在或活动,或
    Windows日志也可能会被篡改和删除。这可能是因为某些恶意软件试图隐藏其存在或活动,或者是因为攻击者试图擦除他们的痕迹。为了防止日志的篡改和删除,用户应该采取以下措施:限制对日志的访问权限,以确保只有受信任的用户才能访问和修改日志。定期备份日志,并在必要时将其保......
  • 百度 Apollo 自定义安装第三方库(以 libtorch 为例)_apollo 使用自定义库
    CSDN搬家失败,手动导出markdown后再导入博客园百度Apollo是一个非常优秀的自动驾驶框架,但我们平时在开发中也会遇到各种原repo没有处理的问题。笔者近期想用pytorch的C++前端推理模型,但是遇到了libtorch版本与pytorch版本不匹配的问题,因此想自己安装一个新版本的li......
  • 百度 Apollo 自定义模块发布——使用 Python 语言(bazel 编译 Python 模块)_bazel-bin b
    CSDN搬家失败,手动导出markdown后再导入博客园BinaryvsComponent首先说明下,Apollo的核心概念是组件,通过组件可以实现资源的自动管理和调度。CyberRT中只能使用C++语言实现Component,Python版的API只能用来写传统的二进制可执行文件,参考官方文档中这两种方式的区别:B......