标签: 运维

  • 异常日志

    异常日志

    日志是应用运行中被动或者主动输出,记录了应用相关信息的工具。

    日志一般有多个日志级别,包括TRACE、DEBUG、WARN、INFO、ERROR、FATAL。

    其中TRACE,DEBUG一般生产环境不输出,INFO属于一般信息,用于记录应用相关参数,运行状态等,WARN级别属于轻微的警告,用于一些不应该出现的或者轻微的错误,不影响应用。而ERROR和FATAL就是我们所说的异常了,这种级别的日志用于记录会影响业务进行和程序运行的情况。

    合理的输出异常本身对于应用的维护性有着很大的帮助。

    异常的类别

    要输出合理的异常信息,首先要明确有哪些异常,在我看来,异常无非有三种

    程序异常

    程序异常就是我们所说的真的挂了,也就是编程语言或者其运行环境自身提示异常,比如空指针异常。这种异常发生应用就已经出错的。这种错误不应该是开发人员手动输出的,而一般由公共的Exception Handler处理。这种异常理论上不应该直接输出,而是应该通过参数检查来避免,当检查不通过时应该转为输出可读的异常信息

    调用下游依赖异常

    现在巨型应用慢慢开始拆分,就出现了上下游调用的情况。比如一个财务系统,可能涉及到一些人员的信息展示,而这些信息可能由另外一个专门的人员服务来负责。那么当调用下游时就有可能出错,可能是返回不正确(null)也可能是超时,特别是互联网公司,很多超时都是200毫秒,网络抖动就会报错。

    业务报错

    对于任何输入都应该有验证,如果传入了错误的参数或者缺乏必要的参数,这种是逻辑上需要预处理的,而且技术上这种异常不是来自正常请求,因为调用方应该提供正确参数,若有问题应该在测试阶段暴露。

    还有一种报错来自应用框架。现代应用开发很少从底层弄上来,一般会使用一些框架,比如Spring Franework。这种框架自身会输出两种日志,一种是启动日志,一种是异常日志。

    比如Spring MVC在http请求缺乏必要参数时会抛出异常,这种异常大部分应该被开发人员手动转为有意义的异常。当然这个要看情况。

    异常日志的意义

    异常本身就代表了应用没有正常运行了,而异常日志的意义我个人觉得有几类:

    需要代码修复

    这种主要针对第一种异常,代码真的报错了,不管是什么原因,都代表这种情况可能出现,那么这里就需要人员去处理,处理之后重新上线。

    如果测试是充分的,那么这种异常一般是异常的调用或者异常的数据产生(异常不代表数据是错的,这里主要强调非正常数据,数量不大),那么这种问题尽快修复就行了。

    需要沟通

    这种主要是针对第二种异常,下游依赖出现问题的时候,那么首先要通知下游的服务提供方,它们可能有自己的监控,也可能没有,也可能这种异常本身没法被提供方的监控所感知,不管怎样,首先要让相关人员知晓情况,然后再说评估影响,作出数据修复或者继续观察。

    需要知晓

    有些异常可能只是让人心里有数,比如不正确的外部参数,这种如果自己的应用没有问题,那么一般都是不正常的请求来源,不需要提供正确的响应,不如说这种情况下的异常才是正确行为。

    但是这种情况都需要相关人员知晓,如果异常进一步扩大或者极速增加,那么还是需要采取一些防御手段的。

    合理的异常日志内容

    异常日志的内容要有合理有意义,主要是能够良好支撑我们对于日志的期望。日志框架的选择和收集方式我们这里不谈,主要说说日志的内容。

    互联网公司提供产品和服务给用户,通常这种服务是全天候的,根据业务的情况还可能出现非工作日业务量更大的情况,那么就需要有团队成员值班。而一个人应该了解自己团队所负责的服务和业务,但是对于每一个模块,每一行代码不可能都非常熟悉,那么异常日志就是提供一个快速判断,进而作出正确决策的输入来源。

    首先完整的堆栈信息是必要的,特别是对于程序异常,光跑出一个NullException但是没有具体的代码行数是没有意义的,完整的堆栈可以快速定位,这种情况的保证一般是框架自带的,偶尔也有手动输出的,只要不忘记传入exception就行了。

    logger.error("计算配送单{}时长失败", id, e);

    如果错误是框架统一异常处理的,一般会输出堆栈信息,比如Spring的DefaultErrorWebExceptionHandler

    对于下游调用异常的情况,首先为了方便沟通,需要输出的是调用方的信息,比如

    logger.error("调用人员服务超时,当前订单{}", id, e)

    上例的异常输出就可以快速判断下游所属,还有影响自己业务系统的范围。

    当然并不是所有异常都会直接影响系统功能,比如后台显示订单信息,在某些业务场景下,可以接受部分信息丢失,比如订单信息重要,但是涉及到的人员电话信息没有获取成功。那么这种情况日志中需要明确说明,对于核心业务没有影响。

    logger.error("调用人员服务失败,显示订单信息{}缺失人员电话",id , e)

    这样的日志很容易让人知道数据是不需要修复的,对于数据需要修复的,需要明示

    logger.error("调用骑手服务失败,无法写入骑手{}考勤数据,将影响奖惩数据", id, e);

    对于不同的服务还要注意输出的时机,比如对外提供HTTP API的服务,对于一些错误,我们可能返回的依然是200状态码,但是其中包含了错误信息,这种错误返回并不会被监控系统自动捕获,这种情况可以选择手动输出异常,然后再返回响应给前端。

    以上是比较合理的输出,还有一些比较常见不合理的输出方式,比如二次抛出同样的错误

    catch (NoRiderException e) {
           logger.error("No rider id: {} available",id , e);
           throw new UserServiceException("Nouseravailable", e);
    }

    我还注意到网易技术团队的博客提到了一种模版方法,就是任何一个异常,都尝试填充这个模版,从团队统一规范的层面保证异常日志的输出。详细博客参考这里。模版如下:

    log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [Probably need to do]   [params] .”);
    log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [please contact xxx@xxx]   [params] .”);

    我个人觉得接口信息应该在堆栈中就有,而联系人信息一般到组就行了,如果实在需要邮箱组这种联系方式,还是用一个经常维护的团队邮件组比较好,毕竟存在人员流动的情况,也有组织变动的情况。

    写在最后

    总的来说异常的输出其实是应用可维护性还有人员对于系统理解程度的体现,什么异常对于系统有影响,什么异常是致命,写下相关代码的人最清楚。合理的异常日志能极大帮助相关人员快速定位问题,减少在线错误报警,另一方面也是减轻开发人员的工作量。

  • 数据库迁移工具Flyway对比Liquibase

    很多应用的运行是需要数据库支持的,而随着快速迭代,产品更替的节奏加快,除了产品本身需要不断更新以外,数据库也需要做出合适的管理了。

    为什么需要数据库迁移管理

    比如第一个版本的产品只包含了最基本的功能,而第二版本就需要增加评论功能,这就涉及到数据结构的修改(包括创建新表,修改旧表的列,增加已有表的列等等)。直接进入产品数据库修改数据库并不适合快速的开发节奏,不仅仅不安全,更多的情况下数据库可能并不对外或者并不适合对外直接暴露连接,比如PAAS平台的数据库以服务的形式直接提供。

    对比代码管理的一些实践,很明显在数据库方面做的还欠缺很多。比如代码管理中我们有

    • 版本管理(svn,git等等)
    • 持续集成技术
    • 良好的发布工具和流程

    而在数据库方面会遇到很多问题

    • 某台数据库现在是什么状态
    • 修改变更的脚本是否已经应用
    • 对于生产环境的紧急修复有没有被应用在测试环境
    • 如何创建一个新的数据库实例

    数据库迁移工具可以很好的管理这些问题,并提供了以下特性

    • 从迁移脚本中创建新的数据库
    • 检查数据库状态
    • 从一个版本快速到达另外一个版本

     Flyway和Liquibase

    数据库迁移工具很多,这里我们选择Flyway和Liquibase来说主要是两个原因,一是它们都是Java生态圈的,其次就是Spring Boot提供了这两者的内建支持,可以很快应用到产品中。

    Flyway相对简单,直接将你需要执行的SQL语句保存为文件,放入应用中执行即可。比如

    V1__init-database.sql
    V2__add-comment.sql

    Flyway的好处在于简单,而且直接书写SQL并不需要额外的学习。

    Liquibase相对就复杂了很多,它支持四种格式

    • xml
    • json
    • yaml
    • sql

    如果使用过Flyway就会有一定的体会,Flyway的简单是有代价的,举个简单的例子,如果我们开发环境是h2数据库,而测试环境和产品环境是MySQL,这里就有一个问题,SQL语句并不是一个广泛兼容的语言,有些关键字是独有的,而我们并不希望放弃这部分功能。这种情况下你就需要书写两套SQL迁移文件。Spring Boot是内建这种支持的,可以从目录上做区分。

    而Liquibase可以根据数据库的情况为你生成最后的迁移语句,同时因为数据库变动首先是被Liquibase解析,所以也可以简单支持回滚。

    来看一个Liquibase的例子,以XML为例,我个人觉得yaml更简洁,但是经常有对齐的问题。

    <?xml version="1.0" encoding="UTF-8"?>
    
    <databaseChangeLog
            xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
            xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
            http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
        <changeSet id="1" author="nvoxland">
            <createTable tableName="person">
                <column name="id" type="int" autoIncrement="true">
                    <constraints primaryKey="true" nullable="false"/>
                </column>
                <column name="firstname" type="varchar(50)"/>
                <column name="lastname" type="varchar(50)">
                    <constraints nullable="false"/>
                </column>
                <column name="state" type="char(2)"/>
            </createTable>
        </changeSet>
        <changeSet id="2" author="nvoxland">
            <addColumn tableName="person">
                <column name="username" type="varchar(8)"/>
            </addColumn>
            <addLookupTable
                existingTableName="person" existingColumnName="state"
                newTableName="state" newColumnName="id" newColumnDataType="char(2)"/>
        </changeSet>
    </databaseChangeLog>

    Liquibase支持大部分常见的数据库变动操作,比如建表,删表,变动字段等等。

    Liquibase可以在不使用SQL的情况下造成数据库变动,其可读性更高一些,特别是团队并不直接使用SQL而整体相关知识储备不完善的情况下优势更明显。

    结论

    两款数据库迁移工具其实定位上是差别的,一般我的倾向是小项目,整体变动不大的用Flyway,而大应用和企业应用用Liquibase更合适。

  • Spring Boot 日志管理和收集

    Spring Boot 日志管理和收集

    日志对于应用程序的重要性不言而喻,社区也有各种各样的日志框架。

    Spring Boot从实现上不一依赖于任何日志框架的实现,只有对于common-logging API的依赖,对应的抽象实现是LoggingSystem

    依靠这一层的抽象,对于日志等级的配置可以完全在Spring Boot中实现,比如application.properties

    logging.level.org.springframework.web=INFO
    logging.level.org.hibernate=DEBUG

     

    对于具体的日志框架实现可以选择 Java Util Logging, Log4J, Log4J2 或者 Logback。从mvn库的依赖和下载数量来看Logback的使用数量确实有较大的优势。

    对于具体实现部分的配置还是由实现框架本身管理,比如Logback对应的logback.xml文件。

    大部分日志的输出一般会对应两部分,一部分是stdout,一部分是日志文件。

    对于大规模部署的云原生应用,直接输出到stdout,然后从docker中收集日志也是一个不错的选择的。

    市面上日志聚合和搜索平台实在太多,有部分需要从代码层面做出修改,但是最简单的办法还是从日志实现本身入手。比如
    loggly提供了对于logback的支持。

    只需要在配置加上对应的appender即可。

    <configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    </appender>
    <appender name="loggly" class="ch.qos.logback.ext.loggly.LogglyAppender">
    <endpointUrl>http://logs-01.loggly.com/inputs/[your secret key]/tag/logback</endpointUrl>
    <pattern>%d{"ISO8601", UTC} %p %t %c{0}.%M - %m%n</pattern>
    </appender>
    <appender name="loggly-async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="loggly" />
    </appender>
    <root level="info">
    <appender-ref ref="loggly-async" />
    </root>
    <root level="ERROR">
    <appender-ref ref="STDOUT"/>
    </root>
    </configuration>

    效果如下图
    DashBoard
    Search