高併發系統-使用自定義日誌埋點快速排查問題

背景

在高併發的系統中,通常不會打印除參數校驗失敗或捕獲異常之外的日誌,防止對接口的性能產生影響。

那對於請求不符合預期的情況,我們如何快速找到是哪塊邏輯影響的至關重要。

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

以上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章