1 日志模块
在第一章我们知道,服务通常是长期运行,且没有UI界面的可执行程序,我们很难直观的观察到服务的运行状况。因此,在服务器开发和运维过程中,我们一般把服务日志作为记录服务运行状态最重要的手段之一。
通常我们会将日志输出到一个日志文件,每条日志输出时会记录时间戳以及关键的数据信息,便于我们事后复原服务的运行逻辑。
我们先实现一个简单的日志模块。
这里使用单例模式,包装了CLogger类,对外可以使用两个宏定义函数,LOGINIT初始化一个日志文件;HYLOG输出一条日志,日志支持可变参数,可以使用占位符进行字符串格式化输出,这个后面在演示。
服务端在使用单例模式、全局变量、静态变量时,要特别注意,需要保证多线程安全。
这样实现的好处在于,只要包含了这个头文件,在任意地方调用宏定义打日志的函数,都可以直接将日志打印到默认日志文件。如果不这样处理,就需要将日志模块的指针传递到使用的位置,使用繁琐,且不利于业务层和基类公用一个日志文件。
我们使用日志模块,希望能够拥有以下几个特点:
方便使用,直接调用日志输出函数,就可以将日志打印到默认日志文件(单例模式+宏定义函数实现)
支持生成多个日志实例,根据需求不同,将日志打印到不同的日志文件中
日志需要保证线程安全,单个线程输出的时序不能乱,多个线程输出的日志按照时间先后输出
文件IO读写是一个耗时操作,因此希望日志输出是异步执行,文件IO操作在单独的线程中处理,不要因为打日志影响业务线程的执行效率。
服务程序尽量不要一直持有日志文件句柄,以便其他应用程序操作文件,比如日志采集程序等
尽量减少文件IO的频率,不要每条日志,打开一次文件,输出后再关闭文件,可以制定缓存策略,一次性输出多条日志。
当然,也有日志实现机制是日志线程一直保持日志文件句柄,日志始终保持打开状态,有新的日志信息,立即输出到文件中。这样有较高的日志输出时效性,也不用频繁开关文件,但会有文件占用问题,本文不采用这种方式。
1、CLogger实现
根据以上需求,日志模块实现机制设计如下:
设计一个LoggerMgr类,用于管理日志模块,在独立的日志线程中,定期触发日志模块检测,符合条件的情况下将缓存日志数据落盘。
LogFile类将日志文件抽象化,实际的文件IO封装到这个类里面。类中维护一个log的缓存队列,业务层中的log输出调用,将其直接添加到队列中即可返回。
检测函数,每50ms调用一次,发现缓存的数据超过1000字节(可调整),或缓存数据超过30s(可调整),就可以将缓存内容落盘。
缓存机制容易出现的一个问题是,有时服务异常宕机,缓存数据来不及落盘,导致最后的部分日志数据丢失。
最后是Logger模块,对外提供日志处理接口,对日志内容做一些处理,增加时间戳和进程号等。日志线程安全在LogFile中处理,Logger接受到日志接,直接调用LogFile的处理函数就行。
2、学会打日志
上面实现了日志输出功能,在业务中调用日志输出函数,即可将日志内容输出到日志文件中。
但这仅仅是开始,在工程实践中,更重要的是学会有效的打日志。
打日志的目的一般有两点:统计信息,错误排查。
统计信息一般按照统计需求,按固定格式输出信息即可,因为后面有对接的数据分析,一般不会有什么问题。
针对错误排查,一般有以下几点需要注意:
1、日志要输出关键信息
日志输出的目的是记录信息,当我们看到日志文件中输出了某条日志,需要从这条日志中得到尽量多的信息。
首先,单条日志要携带足够关键的信息。
比如一个内存分配失败的报警日志,如果只输出“内存分配失败”,我们看到这条日志,会发现系统运行时出现了内存分配失败的情况。如果只有一个地方打印了这条日志,我们可以通过字符串匹配查到日志输出的位置,如果有多个地方采用了同样日志内容,就无法确定是执行哪个逻辑时出现的异常。出了问题之后,只知道出问题了,对于解决问题没有一点帮助。
此时,尽量增加一些定位内容,比如函数名,日志所在文件行号;如果有涉及上下文环境的信息更好,比如用户id,房间号等等。
其次,高频出现的日志要有其必要性。
比如每次收到玩家消息,都打印一条日志“收到xx玩家消息”。服务运行一段时间后打开日志,发现80%都是这类信息,对于了解服务状态和排查问题并无作用,这类日志就需要精简。
2、不同类型的日志分类标注
按照业务逻辑的不同,可以对日志进行分类标注,关键的日志信息,甚至可以单独打印到一个日志文件。比如用户的登录登出操作,支付相关操作等,可以单独列出一个日志文件进行记录。
同一系列的操作,最好打印相同的关键字标注,比如用户创建房间请求,可以统一加上“创建房间”几个字,排查问题时,搜索关键字,可以把相应的日志一起提取出来,综合分析。
3、按照不同需求进行日志分级
在不同的阶段,服务需要的日志详细程度是不同的。在开发阶段,可能需要追踪一个函数中不同分支的执行情况,日志仅用于调试业务逻辑。在小规模发布阶段,需要打印稍微详细的日志,追踪一个用户在各个业务上的操作;在正式发布后,服务已经稳定,只需要关注总体数据和错误异常即可。
有的程序员习惯在开发阶段打很多日志,在研发交付时注释或者删除不必要的日志,这无形带来了很多额外的工作,而且有时漏删除一些调试日志,会出现发布后,打印了大量无效日志的情况。分级日志可以很好的解决这个问题。
一般我们可以将日志分为以下几个级别:Trace,Debug,Info,Warn,Error,Fatal。在开发过程中,根据不同的需求,给日志分出相应的级别,在不同阶段,通过日志开关配置,选择输出不同级别的日志。下面简单介绍各个级别的含义。
Trace:追踪日志,可以对某个函数流程,或业务流程进行详细的日志输出,追踪逻辑运行细节,通常在开发阶段使用。
Debug:比Trace粒度要大些,用于调试阶段,排查问题,打印一些关键流程的数据。
Info:主要是服务运行的一些信息,关键的业务内容。通常用于生产服务的日志输出,服务发布后可以打开这个级别。
Warn:需要引起关注的警告信息。通常不会引发错误异常,但需要相关人员进行排查,并且通过修改逻辑或流程能够消除这些警告。如果通过任何手段都不能去掉警告信息,需要考虑这条日志打印Warn级别是否合适。
Error:系统出现了错误,触发了一些不应该执行的逻辑或数据异常,程序本身还可以通过异常处理解决,比如中止当前会话、踢掉这个用户,结束当前任务等等。此时服务还可以为其他用户提供服务。
Fatal:服务出现了严重错误,且服务自身无法处理这种异常,服务基本失去了继续提供服务的能力。出现这种错误,基本可以停止应用程序了。
学会高效的打印日志,可以提高开发和调试效率,线上服务出现问题后,也能快速的定位问题并找到解决方案。
3、日志获取和采集
最简单的日志获取方式,是直接登录服务器,将日志文件下载到本地。
小公司多采用这种方式,开发人员权限较高,可以直接登录生产服务。但随着公司规模增大,开发人员的权限会逐步受到限制,开发尽量和生产隔离,取而代之是由专门的运维人员进行生产环境的操作,开发人员只需要交付可用的服务应用程序即可。这时,仍然可以找运维到服务器上下载服务文件。
随着业务的扩展,线上部署的服务器越来越多,这时可能开发出一些日志采集工具。开发人员不能直接登录生产服务,但可以通过采集工具,找到对应的服务器,通过工具下载日志文件。这样可以进行更精细的权限控制。
再进一步发展的话,运维可能会部署第三方日志采集工具,比如filebeat等,通过配置将很多台服务上的日志文件,采集发送到统一的日志处理服务,对日志进行加工处理。
由于篇幅所限,相关内容不再展开,有兴趣的朋友可以自行查找相关内容学习。
4、日志统计、分析和报警
当日志文件被统一采集后,可以进行进一步的统计、分析和报警等操作。
统计分析工具可以使用ELK,这是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana。加上第三节提到的FileBeat可以组成一个高效的日志采集、统计、分析、搜索、存储的系统。
Elasticsearch提供了很多报表展示工具,可以快速搜索日志内容,如果大家在工作中用到了这个系统,可以具体查询使用方式。
对于报警,可以根据运维需要自行定义报警内容,设置触发报警的日志内容和条件。报警一般可以设置为邮件通知报警,短信报警,工作沟通工具(钉钉)报警等。
开发和运维人员如果能够第一时间获知服务的运行异常,可以更及时的进行处理,有效的降低生产环境的故障,减少对线上用户造成的影响。
小结
本文主要实现了一个简单的日志模块,相关代码可以在GitHub上获取。链接如下:
在实际开发过程中,基类库往往已经提供了完善的日志打印功能,如何高效的使用日志模块,达到提高开发效率的目的,通常是更为重要的。
后面日志获取、采集、统计、分析、报警等内容,稍微做了些简单介绍,提供了一些可参考的方向,大家可以根据实际需要进行进一步的深入学习。
Last updated