前言:第一次在腾讯云平台写自己的一些经验总结,认真的说,我还没成为腾讯云的使用者,也是近期一些人或事儿让我对腾讯云平台有了新的认知,东西很不错,其腾讯人也很专业。
当下,微服务已经不是一个新奇的名词,微服务技术体系的运用,让我们能快速、独立的实现服务的开发、测试、及交付部署,耦合度越来越低,但同时也带来一些复杂度的问题,如服务链路越来越长,服务系统间交互越来越频繁,一旦出现问题,那么排查的难度将呈指数倍递增。而系统日志成为我们发现异常、排查异常的唯一切入点,如何设计我们的微服务日志体系,或者说什么样的日志体系更能便于我们监控、排查异常?以下是我总结的一些日志体系最佳实践,但愿能帮助到一些有困惑的同学。
一、统一输出路径
无论你的系统是docker镜像部署,或是云平台ECS,或是物理机实例,统一日志的输出路径,将有利于我们快速的找到日志的所在,即使不参与该系统开发的同学,也能方便的找到。
我们一般在服务器运行程序用户家目录下创建logs目录,用与日志输出的唯一根路径。
如,spingboot项目,我们可以在application.properties文件中指定:
# 日志目录
logging.path=./logs
同时,需要在logBack日志配置文件里声明使用。
如:
<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 ,有兴趣的可以去做研究。
三、统一输出格式
这里顾名思义,就是我们的日志输出要遵循统一的规则,这样,不仅仅有利于我们做好日志监控,更有利于我们跨系统的日志查看。
在以上提到的目录中,除业务自定义日志外,其他的都需要我们通过技术框架去实现,所以这里是很好统一的,但前提是大家已经统一了技术栈。
而业务自定义日志,一般我也推荐使用统一的格式,尤其是服务被访问、数据库访问、三方服务访问的摘要和详情日志,需要统一。
在这里,推荐一套我定义的服务访问摘要日志和详情日志格式:
摘要日志:
[(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)三个属性。
/**
* 错误上下文
*/
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技术体系,大家一看都懂,不做过多阐述。
八、日志监控及告警
打印再多日志,都是为了排查问题。而监控,是你发现异常的最佳方案,你不可能24小时盯着服务器的日志,你非常人,咱就不说了。
需要注意的是,添加监控,还需要添加告警,否则就是无效监控,告警的阈值,需要按照自身业务情况而定,我们不可能保证每个请求都能百分百的请求成功,但一般需要保证999的可用率,也就是允许千分之一的失败,当你的业务请求量很大时,比如每秒达到10WQPS,那你可以允许一定的错误存在,但如果你一天就几个请求,那么即时一个业务异常,也应该被感知和排查。
监控及告警不是一劳永逸的,需要一个磨合的过程,不在磨合过程中,我们逐步调整监控阈值及监控项,当前请求错误率、几分钟类错误次数等等监控方案你值得拥有。实际上,不是所有的异常都需要我们关注,异常告警太多,又不用关注的,容易引起我们的关注度疲劳,而错过一些关键的告警,所以日志告警降噪也非常的重要。
以上是我的一些微服务日志体系的浅显实践经验,大家可按需采纳。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。