美文网首页
Java日志最佳实践(游戏项目)

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

作者: landon30 | 来源:发表于2018-08-28 22:59 被阅读0次

    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 。
    正例: <logger name="com.taobao.dubbo.config" 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<ILoggingEvent>)
    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日志最佳实践(游戏项目)

          本文链接:https://www.haomeiwen.com/subject/itucwftx.html