异常和日志使用思考

在应用运行时,日志和异常是发现线上问题的两种有效途径。那么如何打印日志、抛出异常,才能使发现问题变得快捷而有效呢?这就是今天所有讨论的问题。

异常

异常可以是程序提供的一种自检手段,也是问题发生的主要感知手段。通常情况下异常和日志相辅相成,当遇到无法处理的异常时,捕获后进行日志打印,进行问题记录。

异常定义

通常情况下,语言定义的异常无法满足业务所需。所以,对于不同业务场景,需制定与之相关且完善的异常文档,包括错误码,错误描述和发生场景等。这样,当团队成员就可以根据打印的异常信息定位相关问题。

异常抛出和捕获

异常描述了当前程序出现的不符合预期的情况。在抛出异常时,首先要确定抛出的异常匹配当前的异常场景,其次要在合适的地方捕获异常。关于抛出和捕获的一个原则是:

  1. 提前抛出
  2. 延迟捕获

也就是说,在发现程序出现不符合预期情况的时候就应该及早抛出异常了。根据抛出异常的类型,也有对应的捕获策略:

  • 程序能够处理的异常:在合适地点捕获异常,并进行处理
  • 程序不能够处理的异常:在源头捕获异常,并转化为用户可见的信息,如“系统繁忙,请稍后再试”等提示信息。

比如用户执行下单操作,首先进入下单业务的代码逻辑,可能包含两个子方法:

  1. 检查库存方法
  2. 下订单方法

在库存检查方法中,通过微服务访问库存。此时微服务访问超时,所以抛出超时异常。对于该异常,可尝试再重试几次(防止偶尔的网络波动),若重试均失败,则表明当前网络出现故障,不再尝试,而是往上抛出。由下单操作方法捕获异常,并转为“系统繁忙,请稍后再试”提示信息。如果在访问库存时,发现库存不足,该情况无法由程序自行处理,所以抛到上层,由其转为“库存不足,无法下单”信息发送给用户。

日志

当应用发生线上问题时,日志是最主要且最先的找到问题的方式。好的日志,能够快速定位问题,使得排查问题变得简单高效。所以如何设计好的日志,应该是程序员必备的基础素养。
首先需要明确的是,当今软件开发越来越趋向团体化开发,单兵作战的时代逐渐远去。一个复杂应用通常是由多名程序员共同开发(每人负责一个模块),所以可能会出现这样一种情况:程序员A发现线上出现了一个ERROR日志,但是这个ERROR实际上是由程序员B写的功能模块产生的。这时,这个日志应该起到什么作用呢?

  1. 这个日志能够让程序员A意识到ERROR是由程序员B的代码引起的,并交给程序员B进行解决。
  2. 这个日志能够让程序员A(具有相关业务的大概知识)进行初步排查并解决;解决不了,再转交给B解决。

显然1是良好日志需要达到的基本条件,而2则是优秀日志的前提条件。事实上,我一直在强调“团队工作”,“任何具有大概业务知识的人都能读懂日志”。这是因为在我看来,日志编写也是一种规范。就像Java有《Java开发手册》,日志也需要有《日志编写手册》,至少在一个团队内部需要达成对一种日志编写模式的共识。

日志等级

日志等级用来标明问题的紧急程度。常用的日志框架Logback划分出了以下几类(从轻微到严重):

  1. TRACE/DEBUG:这两类日志通常情况下只会在开发环境打印,用于梳理整个业务的链路过程等。个人认为,这两类日志是业务流程的日志化体现,即这两类日志足够映射出一个业务的完整执行流程。
  2. INFO:这是生产环境下最常用的日志级别,且表明应用正常运行。我认为其面向两类场景:
    • 系统:在系统初始化、状态改变、接收请求等情况下打印,表明系统正常运行时的行为轨迹。
    • 业务:这部分需要视业务而言。对于ToB类业务,用户的某些增删改操作是敏感的,也需要进行记录。
  3. WARN:表明系统出现了问题,但是这些问题主要是由用户操作不当、系统波动、服务超时等引起的。或者是对当前状态的预警,比如磁盘容量超过阈值等。
  4. ERROR:表明系统出现了急需解决的问题,并且这些问题是由系统自身设计不恰当导致的。

明确日志等级的含义后,翻阅日志信息时就能对问题类型有个大概了解。

日志分类

按功能划分,日志可分为三类:

  • 诊断日志:请求入口和出口、外部服务调用和返回等。
  • 统计日志:用户访问统计、计费日志(用户使用流量等)。
  • 审计日志:管理用户行为等。

对于不同功能的日志,可以存放至不同日志文件加以区分。

日志格式

日志格式也需要明确,除了常用的时间、等级、线程等,对于日志主体内容,我比较喜欢的方式是:[调用栈(当前类.当前方法)]: [业务记录(成功/失败)], [参数] [异常信息]。比如:

OrderManager.placeOrder: success order, uid = 23233

就表明用户23233成功下单,且当前位于OrderManager中的placeOrder方法。

日志内容

这里对日志中主体内容进行简单定义:

  1. 日志内容有助于定位问题,并尽量简洁且清晰的打印问题出现原因。
  2. 日志内容需要含有足够的上下文信息,帮助问题复现和排查。

总而言之,日志是用来帮助了解系统状态和排查线上问题的,其内容的撰写也要以该原则作为出发点。当然,由于业务的不同特性,需要对日志进行相关适应性改造。

日志跟踪

当今应用架构趋向于微服务架构,每一次业务流程的执行都以为着多个微服务相互协作调用。那么如何以全局视角,得到业务的跨服务的完整链路日志就显得尤为重要。比较优雅的解决方法就是通过链路跟踪中间件,如阿里的EagleEye等。该类中间件赋予每条链路唯一标识,并附加于日志记录中,由此可得到整个链路执行过程的所有日志(当然链路跟踪中间件的功能不止如此)。笨点的方法则是,在业务上定义唯一标识,如RequestId,并默认打印。

日志报警

对日志设置匹配规则,周期性对日志进行扫描。当日志打印出某个问题时,自动报警,使得报警监控早于用户反馈。

日志分割和清理

当日志增长到一定阈值时,就需要进行分割。分割可以按天或按小时,决定因素是方便快速查找日志。另外,过多的日志堆积会耗尽磁盘空间,因此需要定期清理日志,可以清理半个月、两个月前的日志,尽量按照业务需求进行清理。

参考资料