前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务日志体系最佳实践

微服务日志体系最佳实践

原创
作者头像
笏真
修改2022-12-02 09:58:23
7910
修改2022-12-02 09:58:23
举报
文章被收录于专栏:技术架构

前言:第一次在腾讯云平台写自己的一些经验总结,认真的说,我还没成为腾讯云的使用者,也是近期一些人或事儿让我对腾讯云平台有了新的认知,东西很不错,其腾讯人也很专业。

当下,微服务已经不是一个新奇的名词,微服务技术体系的运用,让我们能快速、独立的实现服务的开发、测试、及交付部署,耦合度越来越低,但同时也带来一些复杂度的问题,如服务链路越来越长,服务系统间交互越来越频繁,一旦出现问题,那么排查的难度将呈指数倍递增。而系统日志成为我们发现异常、排查异常的唯一切入点,如何设计我们的微服务日志体系,或者说什么样的日志体系更能便于我们监控、排查异常?以下是我总结的一些日志体系最佳实践,但愿能帮助到一些有困惑的同学。

微服务架构(百度拔的图)
微服务架构(百度拔的图)

一、统一输出路径

无论你的系统是docker镜像部署,或是云平台ECS,或是物理机实例,统一日志的输出路径,将有利于我们快速的找到日志的所在,即使不参与该系统开发的同学,也能方便的找到。

我们一般在服务器运行程序用户家目录下创建logs目录,用与日志输出的唯一根路径。

如,spingboot项目,我们可以在application.properties文件中指定:

# 日志目录

代码语言:javascript
复制
logging.path=./logs

同时,需要在logBack日志配置文件里声明使用。

如:

代码语言:javascript
复制
<file>${logging.path}/${spring.application.name}/service-info.log</file>

注:spring.application.name为application.properties里配置的项目名称。

二、统一日志分类及日志隔离

我们在统一目录后,让大家快速进入日志目录,但日志分类有哪些?我们该记录哪些类型的日志?这也是我们需要考虑的,丰富的日志类型,更有利于我们快速的定位问题。

一般而言,我们的服务作为客户端,但也同时会作为服务端,同时,项目中也会用到数据库、缓存、消息、异步调度等中间件,这些都是我们需要监控的项,那么也都应该有日志记录,那么他们也需要统一的分类以及入口。

我们一般,在log的目录下,还会有其他日志分类目录,如:

registry:服务注册及发现相关日志,

scheduler:异步调度任务相关日志,

runtime:系统服务启动相关日志,

msg:消息中间件相关日志,含消息的发布、订阅摘要日志

traceLog:这个日志就很重要了,记录了服务访问、调用的相关信息,如结果状态、访问服务地址、耗时等,一般由技术框架支持打印。

appName:appName即项目名称,该目录下存放系统自定义日志,如服务请求的摘要、详细日志,数据库摘要、详情日志,三方服务访问摘要、详情日志,以及相关核心业务的日志,一般都是业务系统自定义的。

当然,这里还可以包括其他的一些系统中间件的日志分类目录。

在这里还需要说明的是,我们采用此分类,可以将中间件日志和业务日志进行隔离开来,通过不同的存储的隔离,达到不影响我们线上问题排查的目的。

在这里推荐下,蚂蚁Sofa的日志隔离体系框架sofa-common-tools ,有兴趣的可以去做研究。

三、统一输出格式

这里顾名思义,就是我们的日志输出要遵循统一的规则,这样,不仅仅有利于我们做好日志监控,更有利于我们跨系统的日志查看。

在以上提到的目录中,除业务自定义日志外,其他的都需要我们通过技术框架去实现,所以这里是很好统一的,但前提是大家已经统一了技术栈。

而业务自定义日志,一般我也推荐使用统一的格式,尤其是服务被访问、数据库访问、三方服务访问的摘要和详情日志,需要统一。

在这里,推荐一套我定义的服务访问摘要日志和详情日志格式:

摘要日志:

代码语言:javascript
复制
[(tntInstId,0a02ba811656639422496100241949)][(com.ys.demo.LogDemo,serverDigest)][(10ms,01,TE0051101002,0,5)]

解释:

其中“[”、“]”、“(”、“)”这些只是分割符,为了一眼就看清日志,而“,”是一个关键点,对于某些日志监控分析平台,可以作为日志的分隔符,进行日志可视化操作。

从左往右,的日志含义:

tntInstId:租户ID,除云平台外,一般不需要,可删除,但要执行压测相关,还是建议添加上,用于区分压测流量,全链路上下文要统一。

traceId:服务链路请求唯一ID,贯穿全链路。

className – 接口名称

method – 方法名称,

time – 耗时,单位为ms

success – 成功失败标识,00成功,01失败

errorCode – 错误码,业务自定义,最好是整体的错误码格式

错误类型:系统异常、业务异常、三方异常等

错误级别:及当前异常的级别。如error、warn等,可作为监控提醒的必要条件,如warn级别的,我们是否需要添加监控。

以上是我定义的,大家可以按需选择增加或者删除,但应该统一格式。

详情日志和上边的摘要日志类似,但是会打印接口请求的入参和出参,需要注意的是,出参和入参中含敏感词,如姓名、身份证号等需要脱敏打印。

其他类型日志可参考统一,不做阐述。

四、统一日志含义

如上文所提到的,“00”表示成功,“01”表示失败,耗时单位统一为ms一样,这些都需要进行统一含义。

如此,那么我们的消息发送成功、消息消费成功、服务请求成功都可以用“00”表示。

我们一般用“00”表示成功,“01”表示失败,“03”表示服务请求超时,“04”表示服务路由失败。

五、唯一TraceId贯穿全链路

这个很好理解,我们在服务发起时,都应该生成唯一的traceId,作为全链路的唯一请求标识,traceId我们一般放在山下文中。

在全链路请求分析时,也是需要依赖此traceId进行关联,通过全链路请求视图,及统一的错误标识,可呈现是哪个系统出现错误。

在这里,出服务请求外,建议我们在消息发送是也需要将放了traceId的上下文发送出去,用于下游消费者读取。异步调度任务,也需要将traceId进行入库,在调度任务执行时,再取出来再放入上下文。

六、统一异常上下文

这个真的非常必要,统一异常堆栈,我们可以在当前服务请求处理失败时,将我们异常信息放入堆栈中,便于服务调用方可见。

在错误堆栈中,不仅仅是错误标识,还可以放入错误原因描述,正所谓堆栈,他是可放入多个,不仅仅可包含自身,还可以把下游的异常堆栈再放入其中。

我一般会定义一个ErrorContext类,其中包含一个ArrayList用于存放异常对象,异常对象含错误码信息、错误描述信息、错误发生位置(appName)三个属性。

代码语言:java
复制
/**
* 错误上下文
*/
public class ErrorContext extends ToString {
    /**
     * 错误栈,用于存储错误信息
     */
    private List<CommonError> errorStack = new ArrayList<>();
    /**
     * 第三方错误信息
     */
    private String thirdPartyError;

    private static final String SPLIT = "|";

    /**
     * 匹配当前的错误信息,返回CommonError<br>
     * 当无错误信息时,返回null值
     *
     * @return CommonError对象
     */
    public CommonError fetchCurrentError() {
        if (this.errorStack != null && this.errorStack.size() > 0) {
            return this.errorStack.get(this.errorStack.size() - 1);
        }
        return null;
    }

    /**
     * 从上下文中获取错误码信息,返回ErrorCode对象 <br>
     * 当无错误信息时,返回null值
     *
     * @return ErrorCode对象
     */
    public String fetchCurrentErrorCode() {
        if (this.errorStack != null && this.errorStack.size() > 0) {
            return (this.errorStack.get(this.errorStack.size() - 1)).getErrorCode().toString();
        }

        return null;
    }

    /**
     * 获取最早发生的异常  <br>
     * 当无错误信息时,返回null值
     *
     * @return CommonError
     */
    public CommonError fetchRootError() {
        if (this.errorStack != null && this.errorStack.size() > 0) {
            return this.errorStack.get(0);
        }

        return null;
    }

    /**
     * 添加错误栈信息<br>
     * 添加信息时,从尾部添加,故位置越靠前,错误发生时间越早,或理解为最根本错误
     *
     * @param error 公共错误对象
     */
    public void addError(CommonError error) {
        if (this.errorStack == null) {
            this.errorStack = new ArrayList<>();
        }

        this.errorStack.add(error);
    }

    /**
     * 打印错误摘要信息
     *
     * @return 摘要信息
     */
    public String toDigest() {
        StringBuffer sb = new StringBuffer();
        for (int i = this.errorStack.size(); i > 0; i--) {
            if (i == this.errorStack.size()) {
                sb.append(digest(this.errorStack.get(i - 1)));
            } else {
                sb.append(SPLIT).append(digest(this.errorStack.get(i - 1)));
            }
        }
        return sb.toString();
    }

    /**
     * 通过重写ToString,组件完整的错误信息,会将错误栈里的错误都循环打印
     *
     * @return
     */
    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        for (int i = this.errorStack.size(); i > 0; i--) {
            if (i == this.errorStack.size()) {
                sb.append(this.errorStack.get(i - 1));
            } else {
                sb.append(SPLIT).append(this.errorStack.get(i - 1));
            }
        }
        return sb.toString();
    }

    /**
     * 转换为摘要信息<br>
     * 当错误信息为null时,返回占位符
     *
     * @param commonError 错误信息
     * @return 摘要信息
     */
    private String digest(CommonError commonError) {
        if (null == commonError) {
            return LogFormatConstants.HYPHEN;
        }

        return commonError.toDigest();
    }

    public List<CommonError> getErrorStack() {
        return this.errorStack;
    }

    public void setErrorStack(List<CommonError> errorStack) {
        this.errorStack = errorStack;
    }

    public String getThirdPartyError() {
        return this.thirdPartyError;
    }

    public void setThirdPartyError(String thirdPartyError) {
        this.thirdPartyError = thirdPartyError;
    }
}

/**
*错误信息
*
*/
public class CommonError extends ToString {
    /**
     * 错误码信息
     */
    private ErrorCode errorCode;

    /**
     * 错误描述信息
     */
    private String errorMsg;

    /**
     * 错误发生位置,一般写appName
     */
    private String location;

    public CommonError() {
    }

    public CommonError(ErrorCode code, String msg, String location) {
        this.errorCode = code;
        this.errorMsg = msg;
        this.location = location;
    }

    /**
     * 构建为摘要日志输出时使用
     *
     * @return 摘要日志
     */
    public String toDigest() {
        return this.errorCode + "@" + this.location;
    }

    @Override
    public String toString() {
        return this.errorCode + "@" + this.location + "::" + this.errorMsg;
    }

    public ErrorCode getErrorCode() {
        return this.errorCode;
    }

    public void setErrorCode(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return this.errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    public String getLocation() {
        return this.location;
    }

    public void setLocation(String location) {
        this.location = location;
    }
}

/*
* toString的通用实现
*/
public class ToString implements Serializable {
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

七、统一日志收集

这里没什么好描述的,我们不可能在众多服务器中,逐个去登录查找日志,我们打印的日志,需要统一采集、存储、分析、监控,如果不是云平台项目,采用传统的ELK技术体系,大家一看都懂,不做过多阐述。

ELK技术栈(百度拔的图)
ELK技术栈(百度拔的图)

八、日志监控及告警

打印再多日志,都是为了排查问题。而监控,是你发现异常的最佳方案,你不可能24小时盯着服务器的日志,你非常人,咱就不说了。

需要注意的是,添加监控,还需要添加告警,否则就是无效监控,告警的阈值,需要按照自身业务情况而定,我们不可能保证每个请求都能百分百的请求成功,但一般需要保证999的可用率,也就是允许千分之一的失败,当你的业务请求量很大时,比如每秒达到10WQPS,那你可以允许一定的错误存在,但如果你一天就几个请求,那么即时一个业务异常,也应该被感知和排查。

监控及告警不是一劳永逸的,需要一个磨合的过程,不在磨合过程中,我们逐步调整监控阈值及监控项,当前请求错误率、几分钟类错误次数等等监控方案你值得拥有。实际上,不是所有的异常都需要我们关注,异常告警太多,又不用关注的,容易引起我们的关注度疲劳,而错过一些关键的告警,所以日志告警降噪也非常的重要。

监控示例(百度拔的图)
监控示例(百度拔的图)
添加告警示例(百度拔的图)
添加告警示例(百度拔的图)

以上是我的一些微服务日志体系的浅显实践经验,大家可按需采纳。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档