http://studygolang.com/articles/10321
最佳日志实践的第一版便是在那个时候完成的,里面包含了我们在开发和运维过程中的一些好的实践。最初起名“最佳日志实践”实在有些标题党,不过由于起名字是一件比写代码更困难的事儿,我就继续沿用这个名字吧。有几个原因让我一直想要对那篇文章进行整理和扩充:
开始正文吧...
日志用来记录用户操作、系统运行状态等,是一个系统的重要组成部分。然而,由于日志通常不属于系统的核心功能,所以常常不被团队成员所重视。对于一些简单的小程序,可能并不需要在如何记录日志的问题上花费太多精力。但是对于作为基础平台为很多产品提供服务的后端程序,就必须要考虑如何依靠良好的日志来保证系统可靠的运行了。
好的日志可以帮助系统的开发和运维人员:
不好的日志导致:
日志从功能来说,可分为诊断日志、统计日志、审计日志。
诊断日志, 典型的有:
统计日志:
审计日志:
对于简单的系统,可以将所有的日志输出到同一个日志文件中,并通过不同的关键字进行区分。而对于复杂的系统,将不同需求的日志输出到不同的日志文件中是必要的,通过对不同类型的文件采用不同的日志格式(例如对于计费日志可以直接输出为Json格式),可以方便接入其他的子系统。
理想的日志中应该记录不多不少的信息。
所谓不多,是指不要在日志中记录无用的信息。实践中常见到的无用的日志有:1)能够放在一条日志里的东西,放在多条日志中输出;2)预期会发生且能够被正常处理的异常,打印出一堆无用的堆栈;3)开发人员在开发过程中为了调试方便而加入的“临时”日志
所谓不少,是指对于日志的使用者,能够从日志中得到所有需要的信息。在实践中经常发生日志不够的情况,例如:1)请求出错时不能通过日志直接来定位问题,而需要开发人员再临时增加日志并要求请求的发送者重新发送同样的请求才能定位问题;2)无法确定服务中的后台任务是否按照期望执行;3)无法确定服务的内存数据结构的状态;4)无法确定服务的异常处理逻辑(如重试)是否正确执行;5)无法确定服务启动时配置是否正确加载;6)等等等等
输出日志时要考虑日志的使用者,例如如果日志主要由系统的运维人员来看,那就不能输出:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426
至少应该是:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426, Message: callback request (to http://example.com/callback) failed due to socket timeout
整理一下通常情况下会遗漏的日志:
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 1 try
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 2 try
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) success
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) success
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth failed
[INFO] RequestID:b1946ac92492d2347c6235b4d2611185, content digest does not match
[INFO] RequestID:b1946ac92492d2347c6235b4d2611186, request ip not in whitelist
[INFO] RequestID:b1946ac92492d2347c6235b4d2611184, auth failed due to token expiration
[INFO] RequestID:b1946ac92492d2347c6235b4d2611185, content digest does not match, expect 7b3f050bfa060b86ba781151c563c953, actual f60645e7107917250a6408f2f302d048
[INFO] RequestID:b1946ac92492d2347c6235b4d2611186, request ip(=202.17.34.1) not in whitelist
我们通常使用的日志库,将日志基本分为以下几类(从低到高):
TRACE – The TRACE Level designates finer-grained informational events than the DEBUG
DEBUG – The DEBUG Level designates fine-grained informational events that are most useful to debug an application.
INFO – The INFO level designates informational messages that highlight the progress of the application at coarse-grained level.
WARN – The WARN level designates potentially harmful situations.
ERROR – The ERROR level designates error events that might still allow the application to continue running.
FATAL – The FATAL level designates very severe error events that will presumably lead the application to abort.
开发人员对于何种日志输出为何种级别通常有自己的理解,那在实践中,是否所有的日志级别都有必要存在,哪些操作需要记入日志,哪种错误应该记为WARN级别,而哪种错误又为ERROR级别呢?关于该问题,可以参考StackOverflow上的一个讨论。
此处对贴子中的一些观点,加上我们在平时运维过程中遇到的相关问题进行归纳:
有一点可以肯定,好的日志就像好的文章一样,绝不是一遍就可以写好的,而需要在实际的运维过程中,结合线上问题的定位,不断地进行优化。最关键的一点是,团队要重视日志优化这件事情,不要让日志的质量持续降低(当项目变大时,项目的代码也存在一样的问题,越写越乱)。
此处有以下几个比较好的实践:
RequestID的作用
一个系统通常通过RequestID来对请求进行唯一的标记,目的是可以通过RequestID将一个请求在系统中的执行过程串联起来。该RequestID通常会随着响应返回给调用者,如果调用出现问题,调用者也可以通过提供RequestID帮助服务提供者定位问题。
RequestID的生成:
需要根据实际的使用场景来选择:
RequestID = md5(time.Now() + random.Int())
./decode.sh 4b2c009a0a7800000142789f42b8ca96
Thu Nov 21 11:06:12 CST 2013
10.120.202.150
4b2c009a
RequestID串联起来的日志系统:
通常一个服务由若干个子系统组成,拿网易对象存储举例,它包含了前端负载均衡节点、存储逻辑服务器、元数据集群、分布式存储集群、图片处理集群、音视频处理集群、缓存集群等。通常一个请求需要由若干个子系统,甚至所有的子系统的协同处理。这时,如果某个请求出错,再要定位到具体的出错原因就比较复杂了,因为常常需要到数十台机器上去定位日志。
当时的思路在负载均衡节点接收到请求后,就为请求生成一个全局唯一的RequestID,该请求所经过所有子系统系统,均基于该RequestID记录日志,这样通过将所有的日志收集起来,就可以通过这一个RequestID来得到完整的系统处理日志了。
然而这并不是一件容易做的事情:所有的系统间调用都需要进行改造,所有的日志输出的地方都要统一格式,而我们采用的有些开源组件实际上很难支持这种做法。
不过,有了这样的认识,我们组在开发新的底层分布式文件系统时,接口传入的第一个参数就是RequestID了。
上文已经讨论过,DEBUG日志和INFO日志的一个重要的区别是,INFO日志用于记录常规的系统运行状态,请求的基本的输入和输出等,对于定位一般的问题已经足够了。而DEBUG日志则详细的记录了一个请求的处理过程,甚至是每一个函数的输入和输出结果,遇到一些隐藏比较深的问题时,必须要依赖DEBUG日志。
然而,由于DEBUG级别的日志数量比INFO级别的数量多很多(通常差一个数量级),如果长期在线上服务器开启DEBUG级别的日志输出,日志量太大。再比如,有时候仅仅是由于某一个用户的访问模式比较特殊导致了问题,如果将整个服务(特别是一个服务部署了很多台节点时)都临时调整为DEBUG级别日志输出,也非常不方便。
下面介绍一种我采用的方式:
我们的系统采用如下的业务架构(简化版):
在业务处理层的Proxy中,实现如下逻辑:当接收到的HTTP请求的QueryString中包含"DEBUG=ON"参数时,就将所有的DEBUG级别的日志也输出:
在负载均衡层的Openresty中,实现如下接口:管理员可以配置将哪个用户的哪个桶的哪个对象的哪种操作(注:这是对象存储中的几个概念)输出为DEBUG日志,Openresty会对每个请求进行过滤,当发现请求和配置的DEBUG日志输出条件相匹配时,则在请求的QueryString中新增"DEBUG=ON"参数。
通过这种方式,管理员可以随时配置哪些请求需要输出为DEBUG级别的日志,可以大大提高线上定位问题的效率。
服务在接收到一个请求的时候,记录请求的接收时间(T1),在请求处理完成待发送的时候,会记录请求发送时间(T2),通常一个请求的日志都记为INFO级别,然而当出现请求处理时间(T2-T1)超过一定时间(如10s)时,可以将该日志提升为WARN级别。通过该方法,可以预先发现系统可能存在的一些问题。
同样的慢操作日志还可以用来记录系统一些外部依赖的处理时间,如一个服务可能依赖外部认证服务器来进行认证授权。通过记录每次认证请求的时间并将超出预期时间的请求日志采用WARN级别输出,可以尽早发现认证服务器是不是需要扩容等问题。
慢日志的时间阈值应该是可以动态调整的,这样在进行系统优化时,可以将该报警时间阈值逐渐调小,不断地对系统进行优化。
通过对日志中的关键字进行监控,可以及时发现系统故障并报警,这对于保证服务的SLA至关重要。
服务的监控和报警是一个很大的话题,此处只说日志监控报警需要注意的一些问题:
上线后日志观察
每一次上线完成后,除了对系统进行完整的回归测试外,还需要对日志进行观察,特别是当上线新功能以后,可以通过日志确认新功能是否工作正常。
日志输出到不同的文件
在性能测试时遇到的另一个问题是,当并发量很大时,可能会有一些请求处理失败(如0.5%),为了对这些错误进行分析,需要去查这些错误请求的日志。而由于这种情况下日志量巨大,使得对错误日志的分析变得困难。
这种情况下可以将所有的错误日志同时输出到一个单独的文件之中。
日志文件的大小
日志文件不宜过大,过大的日志文件对于日志监控,问题定位等都会带来不便。因此需要进行日志文件的切分,日志文件应该按天来分割,还是按照小时来分割,应该根据日志量来决定,原则就是方便开发或运维人员能快速查找日志。
为了防止日志文件将整个磁盘空间占满,需要定期对日志文件进行删除。例如,在收到磁盘报警时,可以将两个月以前的日志文件删除。此处比较好的实践是:
对文中提出的所有建议总结如下: