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

京东云开发者
• 阅读 97

背景

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

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

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("######")));

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

来看接口性能,如下:
高并发系统-使用自定义日志埋点快速排查问题
tp99达到恐怖的35s! 高并发系统-使用自定义日志埋点快速排查问题
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表达式,下面来看升级前后接口性能变化:
高并发系统-使用自定义日志埋点快速排查问题

以上。

作者:京东零售 张云鹏

来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
浩浩 浩浩
3年前
Android平台的崩溃捕获
1、简述在日常开发中,崩溃日志的捕获,至关重要。有一个好的日志,有利于开发者快速定位问题并解决。对于Android平台,我们可以使用现成的产品来捕获崩溃日志,这些产品包括Bugly、Firebase、友盟等,这些产品经过多年的迭代,对于日志捕获得比较全,也有很好的兼容性。但是作为开发者,我们不能仅仅满足于使用,最好还是知道其中背后的原理。要知道原
Stella981 Stella981
2年前
Spring Boot + Spring Cloud 构建微服务系统(八):分布式链路追踪(Sleuth、Zipkin)
技术背景在微服务架构中,随着业务发展,系统拆分导致系统调用链路愈发复杂,一个看似简单的前端请求可能最终需要调用很多次后端服务才能完成,那么当整个请求出现问题时,我们很难得知到底是哪个服务出了问题导致的,这时就需要解决一个问题,如何快速定位服务故障点,于是,分布式系统调用链追踪技术就此诞生了。ZipKinZipkin是一个
郑天寿 郑天寿
9个月前
什么是专有云(专属)?
专有云是基于公有云网络进行服务集群独立部署,既能享受公有云的稳定链路服务,又具备资源隔离的优势。适用对象:高消息量、高并发、有实时数据监控服务诉求以及对品质有要求高的客户部署方式:在公有云上有独立的计算集群服务特点:自既能享受公有云的稳定链路服务,又具备资
分布式系统中的分布式链路追踪与分布式调用链路
在分布式系统中,由于服务间的调用关系复杂,需要实现分布式链路追踪来跟踪请求在各个服务中的调用路径和时间消耗。这对问题排查和性能监控都很重要。常用的分布式链路追踪实现有基于日志的和基于分布式追踪系统的两种方式:
京东云开发者 京东云开发者
7个月前
JAVA应用CPU跳点自动DUMP工具 | 京东物流技术团队
背景在做系统监控时,CPU的使用率是一个关键的指标,它反映了系统的性能稳定性以及是否存在异常情况,能帮助我们了解系统的负载情况。通过监控CPU使用率,可以判断系统是否正常运行或者是否存在性能问题。如果CPU使用率过高,可能表示系统存在资源瓶颈,需要进行优化
京东云开发者 京东云开发者
7个月前
关于「日志采样」的一些思考及实践
一、背景:系统日志可用于追踪用户操作轨迹,异常情况下,合理的日志有助于快速排查、定位问题,毫无疑问,打印日志对于系统是很重要的。当业务规模较小时,大家都倾向于享受日志带来的便利,从而忽略日志带来的潜在的负面影响,缺乏对日志的管控。在JD当前用户量、业务规模
京东云开发者 京东云开发者
5个月前
技术分享-日志链路追踪
1.背景简述在技术运维过程中,很难从某服务庞杂的日志中,单独找寻出某次API调用的全部日志。为提高排查问题的效率,在多个系统及应用内根据统一的TraceId查找同一次请求链路上的日志,根据日志快速定位问题,同时需对业务代码无侵入,特别是在高频请求下,也可以
京东云开发者 京东云开发者
1个月前
JAVA应用CPU跳点自动DUMP工具
背景在做系统监控时,CPU的使用率是一个关键的指标,它反映了系统的性能稳定性以及是否存在异常情况,能帮助我们了解系统的负载情况。通过监控CPU使用率,可以判断系统是否正常运行或者是否存在性能问题。如果CPU使用率过高,可能表示系统存在资源瓶颈,需要进行优化
API 小达人 API 小达人
9个月前
实用干货丨Eolink Apikit 配置和告警规则的各种用法
API在运行过程中可能会遇到各种异常情况,如响应时间过长、调用频率过高、请求参数错误等,这些异常会对系统的稳定性和性能产生严重影响。因此,对API进行异常监控和告警是非常必要的。本文将介绍EolinkApikit中使用的告警规则,帮助开发者和运维人员更好地监控和管理API。