Java日志最佳实践(游戏项目)

Java日志最佳实践(游戏项目)

窥一斑而知全豹(阿里、唯品会)

阿里日志规约

1. 【强制】应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架
SLF 4 J 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2. 【强制】日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
3. 【强制】应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:
    appName _ logType _ logName . log 。 logType :日志类型,推荐分类有
    stats / desc / monitor / visit 等 ;logName :日志描述。这种命名的好处:通过文件名就可知
    道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
    正例: mppserver 应用中单独监控时区转换异常,如:
mppserver _ monitor _ timeZoneConvert . log
    说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
4. 【强制】对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方
式。
    说明: logger . debug( " Processing trade with id : " +  id + "  and symbol : " +  symbol);
    如果日志级别是 warn ,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,
会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
    正例: ( 条件 )
    if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    }
    正例: ( 占位符 )
    logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
5. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log 4 j . xml 中设置 additivity = false 。
正例:  
6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过
关键字 throws 往上抛出。
    正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
7. 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使
用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
8. 【参考】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别, error 级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出 error 级别。

唯品会日志日志规约

Rule 1. 【强制】应用中不可直接使用日志库(Log4j、Logback)中的API,而应使用日志框架SLF4J中的API

使用门面模式的日志框架,有利于维护各个类的日志处理方式统一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static Logger logger = LoggerFactory.getLogger(Foo.class);

Rule 2. 【推荐】对不确定会否输出的日志,采用占位符或条件判断

//WRONG
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日志级别是info,上述日志不会打印,但是会执行1)字符串拼接操作,2)如果symbol是对象,还会执行toString()方法,浪费了系统资源,最终日志却没有打印。

//RIGHT
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
但如果symbol.getMessage()本身是个消耗较大的动作,占位符在此时并没有帮助,须要改为条件判断方式来完全避免它的执行。

//WRONG
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol.getMessage());

//RIGHT
if (logger.isDebugEnabled()) {
  logger.debug("Processing trade with id: " + id + " symbol: " + symbol.getMessage());
}

Rule 3. 【推荐】对确定输出,而且频繁输出的日志,采用直接拼装字符串的方式

如果这是一条WARN,ERROR级别的日志,或者确定输出的INFO级别的业务日志,直接字符串拼接,比使用占位符替换,更加高效。

Slf4j的占位符并没有魔术,每次输出日志都要进行占位符的查找,字符串的切割与重新拼接。

//RIGHT
logger.info("I am a business log with id: " + id + " symbol: " + symbol);

//RIGHT
logger.warn("Processing trade with id: " + id + " symbol: " + symbol);

Rule 4. 【推荐】尽量使用异步日志

低延时的应用,使用异步输出的形式(以AsyncAppender串接真正的Appender),可减少IO造成的停顿。

需要正确配置异步队列长度及队列满的行为,是丢弃还是等待可用,业务上允许丢弃的尽量选丢弃。

Rule 5. 【强制】禁止使用性能很低的System.out()打印日志信息

同理也禁止e.printStackTrace();

例外: 应用启动和关闭时,担心日志框架还未初始化或已关闭。

Sonar-106: Standard outputs should not be used directly to log anything
Sonar-1148: Throwable.printStackTrace(...) should not be called

Rule 6. 【强制】禁止配置日志框架输出日志打印处的类名,方法名及行号的信息

日志框架在每次打印时,通过主动获得当前线程的StackTrace来获取上述信息的消耗非常大,尽量通过Logger名本身给出足够信息。

Rule 7. 【推荐】谨慎地记录日志,避免大量输出无效日志,信息不全的日志

大量地输出无效日志,不利于系统性能,也不利于快速定位错误点。

记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

Rule 8. 【推荐】使用warn级别而不是error级别,记录外部输入参数错误的情况

如非必要,请不在此场景打印error级别日志,避免频繁报警。

error级别只记录系统逻辑出错、异常或重要的错误信息。

记录日志的目的和原则

  1. 目的
    • 快速、准确追踪、定位线上问题
    • 统计、监控、报警
  2. 原则
    • 这些日志真的有人看吗?
      • 不要输出无效日志
    • 看到这条日志你能做什么?
      • 日志的可读性要好
    • 能不能给问题排查带来好处?
      • 日志要携带一些必要的上下文信息,准确、全面
    • 不多不少

最佳日志实践(游戏项目)

  1. 统一管理SLF4J#Logger
    • 封装统一的Logs接口,通过此接口获取Logger
    • 使用时通常可结合"import static"
    • 没有必要每一个class内都声明一个Logger
    • 注:当前日志实现是logback
  2. Logger分类
    • system
      • 系统日志、包括系统启停(info)、系统运行监控日志等(debug、每分钟)
      • 通过该日志可以知道系统是否顺利启动、开启debug日志后可以知晓当前系统整体状态
    • persistence
      • 持久化日志,主要是mongo/redis的数据存储、加载日志,较重要
      • 通过该日志可以知道如一个玩家数据的序列化时间,整体一个调度周期的序列化时间,加载一个玩家数据的时间,回档的判断等
    • net
      • 网络层日志,主要包括request/response/notify等(通常这部分是debug日志,在上线前期打开,后期随着版本稳定可以关闭),也包括自定义网络层的一些日志
      • 通常查询线上bug现在业务日志查询,如果需要上下文的request/response等详细信息可去net日志查询
    • business
      • 业务日志,主要包括各业务模块自己的一些日志打印,如方法如参的检查,业务操作的记录等。这个日志是通常排查问题经常用到的
      • 如果有比较大或者比较大的业务模块,日志比较多需要经常查看的话,可以单独出来一个模块日志
    • schedule
      • 定时调度日志,目前主要包括quartz的相关日志。在查询定时任务相关的bug时,此日志比较重要
    • devops
      • 包括游戏与外部运维、运营、三方sdk交互的日志,如gm、推送、global等
    • robot
      • 用于机器人压测的行为录制
    • third
      • 三方依赖库日志,包括revenant框架的日志和部分比较重要的三方库日志
      • 注:1. 需要使用slf4j 2. thrid logger是基于"package name"
    • error
      • 错误日志单独一个文件,主要是一些重要的错误和异常
      • 注:并没有一个单独的这样logger,而是各个模块logger的error除了追到自己的文件外,还要统一汇总到error.log
  3. 日志格式规范
    • 禁用的conversion specifier(logback)
      • C/class、F/file、L/line、M/method、caller
      • its use should be avoided unless execution speed is not an issue
    • logback.xml#推荐pattern
      • 必须字段:d、t、p、m 可选字段:c
      • %d{HH:mm:ss.SSS} [%t] %-5p %c{0} - %m%n
        • 这个输出了logger,主要用于三方库的logger
      • %d{HH:mm:ss.SSS} [%t] %-5p - %m%n
    • msg
      • 必选项2:class.method (用来定位,相当于手动实现了"class.method")
      • 必选项2:如果是玩家相关的日志,最好记录rid(用来定位会话标识)
        • 这有另外一个很重要的作用是可以写过滤器,过滤输出某个指定出错玩家的debug日志
      • 必选项2:精炼内容
        • 记录日志的时候需要考虑当线上出现问题的时候,我记录的日志内容是否能对我排查问题起到作用?是否会出现该记录的没有记录?
        • 需要review是否通顺,别人是否可以看懂,能否提供真正有意义的信息
  4. 日志级别
    • 只使用debug、info、warn、error四个级别
    • error-当系统出现较严重的问题时会经常去错误日志查看
      • 较重要的异常(注:并非所有异常都记录error,有些可能为warn)
      • 较重要的错误信息(注:逻辑参数检查不通过这种不是error)
    • warn
      • 主要用来记录如handler参数校验不通过
      • 用来记录部分不重要的异常、非正常的条件分支(业务流程预期不符)
    • info-通常来说优先根据info日志可初步定位
      • 主要包括如系统状态变化日志
      • 业务流程的核心处理、关键动作
      • 业务流程的状态变化
    • debug
      • 调试信息,如request/response
    • 通常上线前期debug日志会打开,后随着稳定会关闭debug
      • 当出现bug时或者棘手的问题时,需要动态的开启debug
      • 如果只是某个玩家出现问题,那么开启debug日志量会非常大,所以需要有基于rid判断的日志过滤器(RidFilter extends Filter)
  5. 日志性能
    • 推荐使用AsyncAppender
      • 避免阻塞业务逻辑线程
      • queueSize和discardingThreshold都要设置,即允许丢失日志,否则队列满会造成业务线程阻塞
    • 优先使用{}占位符进行参数格式化,除非参数调用消耗较大需要增加isEnabled条件判断
    • 避免使用代价较大caller data(C/F/L/M)
    • 再向进一步提升性能的话,可考虑log4j2(Asynchronous Loggers based on the LMAX Disruptor library)
  6. 日志细节
    • 个人不建议用一个工具类来再包装一层debug/info/warn/error方法
      • 这种实现通常只能声明一个logger,无法控制多个模块的level
      • 行号不准确,需要通过具体的logger传入'fqcn'实现
    • 记录日志一定不能抛出异常
      • 可能会影响业务逻辑,需要check日志格式化参数调用是否可能抛出异常
    • 记录异常要用规范的格式
      • log.error("filter error:",e) // 这个是正确的方式
      • log.error("filter error:{}",e) // 这个是错误的方式
      • 其实只要看org.slf4j.Logger接口的api就了解了#error(String msg, Throwable t)
    • 建议使用SiftingAppender,避免logback内声明过多的appender
      • 如果需要配合AsyncAppender使用,则外部只需要声明一个该Appender即可,然后其appender-ref指向sift(SiftingAppender admits one and only one nested appender)
    • 线上记录日志避免使用System.out、System.err、e.printStackTrace()
      • 不用担心日志框架没有启动,这个日志框架内部会使用标准输出打印
      • 所以程序启动时刻的标准输出和标准错误不能丢(即不能重定向到/dev/null),另外一个原因是一些三方日志没有使用slf4j,也可能会直接向控制台写日志
    • 如果使用nohup启动程序,建议将标准输出和标准错误都重定向到/dev/null,否则nohup默认的重定向文件nohup.out会越来越大
      • >/dev/null 2>&1
      • 不过当日志框架未初始化时就报错的还是,则还是需要修改启动脚本输出stdout和stderr
      • 所以优先建议使用python启动程序(使用sh库)
    • 一些重要的错误可以和报警系统关联起来
      • 如可以指定通知到钉钉
    • 一些较重要的系统信息建议接入监控系统
      • 如通过grafana这种图表化的形式直接的看到系统信息的变化曲线
    • 线上可根据需要动态的调整各个logger的level
      • 可直接修改logback.xml
      • 可通过'脚本化'的方式获取到对应的logger调用setLevel即可(需要转为对应实际的logger如ch.qos.logback.classic.Logger)
    • 要考虑日志文件的大小
      • 日志太大,则很难grep
      • 所以要通过线上测试酌情考虑根据文件大小滚动文件
      • 日志的maxHistory建议为15天

参考

  1. 最佳日志实践(v2.0)
  2. 优秀日志实践准则
  3. 唯品会日志规约
  4. 阿里巴巴Java开发手册#日志规约
  5. slf4j faq
  6. logback layouts
  7. logback filters
  8. Apache Log4j 2
  9. log4j日志封装说明
  10. Java日志性能那些事
  11. Java日志的一些细节
  12. Logback SiftAppender
  13. 提升logback的性能
  14. SiftAppender的问题

你可能感兴趣的:(Java日志最佳实践(游戏项目))