美文网首页
审计/操作/修改日志实现思路

审计/操作/修改日志实现思路

作者: williamlee | 来源:发表于2020-12-08 16:16 被阅读0次

    审计日志类的需求一直都存在痛点,大部分采用与业务耦合的方式来实现。本文主要描述作者在工作中积累的几种方案,以及最近思考的新的实现方案,整体梳理成文。希望能给你提供思路,并且如果有更优雅的方式欢迎交流。

    业务上的审计日志主要解决三个问题:

    1. 数据回溯,例如需要查询之前值是什么,由什么修改到了什么。
    2. 行为回溯,例如是触发了什么操作才导致的数据变化。
    3. 操作人员回溯,例如谁修改的。

    以上三个方面最大的难点在于1,如果只是单纯记录本次修改后的值,实现方案会简单很多。但是如果要记录修改前的值复杂度就会高很多。

    首先淘汰的方案

    有些同学可能会想我可以每次都记录新的值,那么最终两次值之间的对比不就是新老值的不同了么?这种方案也是一个思路,但是存在三个问题:

    1. 如果不是新系统,那么日志的功能上线后第一批数据就是错的,因为老数据并没记录,存在冷启动的问题。
    2. 不和老值对比的情况下,就不知道发生了变化,就需要都记录下来。这样数据增长会非常快。
    3. 值的对比在获取数据过程中做,响应时间没办法保证,而且分页查询会比较复杂。

    所以每次只记录新数据的方案先排除掉,不在考虑范围内,下面都是新老值都记录的实现方案。

    第二淘汰的方案

    最容易想到的方案,在业务代码中编写日志逻辑,和业务代码耦合严重。流程如下:


    耦合业务流程.png

    在该方案中有些环节可以简单处理,如果原上下文中存在更新后的值那么就不需要再查询,但是需要在方法之间不断的传递;新老值对比可以抽象工具类来实现,减少重复代码。
    总体看还是侵入太大,本身不是业务流程,又是非核心功能,这种实现方式耦合太强。

    第三种淘汰的方案

    为了解决业务侵入的问题,想通过简单的配置来实现,产生了第三种方案。第三种方案采用业务集成SDK的方式,用来收集数据,上报到server来进行数据对比落库的方式实现。如下图:

    方案三,低侵入
    该方案通过mybatis的拦截器实现对update/insert/delete SQL拦截,将SQL解析。
    1. 如果是insert的那么不需要查询原值,只有新值。
    2. 如果是delete/update,那么需要解析sql,取where部分的语法节点来构造select查询来查询原值。

    将查询到的原值和SQL发送到server端。server对数据和SQL进行解析就能提取新老值了,再进行对比便知道那些变更了,那些未变更。

    这个方案侵入比较低,自动查询原值,并且为了减少计算负担将数据发送到server进行计算和落地。但是因为涉及到自动查询,这样就存在一个比较大的风险,如果业务做批量或者一次更新(update/delete)数据非常多,比如:

    update user set status = 1 where city = 110000;
    -- Query OK, 1000000 row affected
    

    那么进行原值查询的时候就会爆炸,应用肯定会变的不可用,因为查询出了100W条数据在内存中。虽然该方案也做了一些防御的手段,比如查询结果大于100条,发生次数大于2次后日志功能就会关闭;并且在执行SQL中拼接了MAX_EXECUTION_TIME(例如:SELECT /*+ MAX_EXECUTION_TIME = 200 */ * FROM TABLE),如果查询原值时间超过200ms就会停止,减少服务本身的影响。但是种种的防御手段还是不能完全保证问题的出现。

    之前采用这种方式时也是在不断的强调,不能批量,不能一次修改太多数据,由业务方自己控制,自己负责。😓

    第四种方案

    该方案并没实现,但是已验证可行,就是还没有把点串成线。在设计方案三时其实有想过这个方案但是当时问DBA告诉没有类似的办法,所以就放弃了,最近看到一些材料有了新的收获,就想起这个事是可行的。

    如果想解决不在业务中进行大量的查询那么就把获取原值的操作外置吧。那怎么外置呢?binlog是一个可行的方案。那么如何将操作和binlog进行串起来呢?首先想到的是transaction id,但是尝试了下binlog中找到的id,有个xid但是应用作为client拿不到,client可以通过下面sql获取当前的trx_id,和xid不是一个概念。

    SELECT tx.trx_id
    FROM information_schema.innodb_trx tx
    WHERE tx.trx_mysql_thread_id = connection_id();
    

    那有没有其他ID能够串起来呢?GTID能满足需求。
    GTID是5.6的新特性,开启GTID模式在主从切换时会更加准确高效。这里不介绍GTID的更多细节。

    下面如果要串起来就需要在binlog中有GITD,并且在应用端也可以获取到GTID。

    binlog中有GTID:
    这里我放一个帖子如何开启GTID:
    https://www.fordba.com/mysql57replication-mode-change-online-enable-and-disable-gtids-html.html
    开启后我们在binlog event中查看如下:

    GTID

    应用端获取GTID比较困难
    下面几篇mysql官方文档证明是可以获取到的:

    1. https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_session_track_gtids
    2. https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_session_track_state_change
    3. https://dev.mysql.com/doc/refman/8.0/en/session-state-tracking.html
    4. https://dev.mysql.com/doc/c-api/8.0/en/mysql-session-track-get-first.html
      最后一篇文章的代码片段中起到了很大的作用:
    /* extract any available session state-change information */
    enum enum_session_state_type type;
    for (type = SESSION_TRACK_BEGIN; type <= SESSION_TRACK_END; type++)
    {
    ################从这####################
      const char *data;
      size_t length;
    
      if (mysql_session_track_get_first(mysql, type, &data, &length) == 0)
      {
        /* print info type and initial data */
        printf("Type=%d:\n", type);
        printf("mysql_session_track_get_first(): length=%d; data=%*.*s\n",
               (int) length, (int) length, (int) length, data);
    ################到这####################
        /* check for more data */
        while (mysql_session_track_get_next(mysql, type, &data, &length) == 0)
        {
          printf("mysql_session_track_get_next(): length=%d; data=%*.*s\n",
                 (int) length, (int) length, (int) length, data);
        }
      }
    }
    

    将mysql的代码拉下来后修改client/mysql.cc,修改后的代码片段如下:

        else if( !batchmode )
          sprintf(buff,"Query OK, %lld %s affected",
              mysql_affected_rows(&mysql),
              mysql_affected_rows(&mysql) == 1LL ? "row" : "rows");
    ################新增开始####################
          const char *data;
          size_t length;
          if(mysql_session_track_get_first(&mysql, SESSION_TRACK_GTIDS, &data, &length) == 0)
          {
              printf("mysql_session_track_get_first: length=%d;data=%*.*s\n", (int)length, (int)length, (int)length, data);
          }
    ################新增结束####################
    

    然后再编译。以上步骤参考:
    在mysql客户端显示gtid: https://blog.csdn.net/qq_28074313/article/details/88072410
    Mac编译安装Mysql5.7.17: https://www.jianshu.com/p/1cc29d893cfc
    最终实现结果,commit后GTID就直接输出了:

    GTID-client

    以上步骤证明client端可以获取到GTID。剩下的步骤就是如何解析到GTID了,暂时没找到好办法。根据mysql官方文档说明:

    Controls whether the server returns GTIDs to the client, enabling the client to use them to track the server state. Depending on the variable value, at the end of executing each transaction, the server’s GTIDs are captured and returned to the client as part of the acknowledgement.

    GTID作为ack的一部分返回,使用Wireshark也确实能抓到了,下图:


    Wireshark

    但是最后如何解析或者是不是mysql-connector-j已经帮我们解析了只是不知如何获取是下面的难点了,后面的部分待完善。

    这种方案更加优雅,更加可以保证服务的可用性,并且在当前环境下公司内提供binlog的成熟解析方案已经成了标配,这样给该方案的时间又减少了很大的成本。综上来看是当前可以想到的审计日志类型的需求最优雅解法了,后面等完全实现再继续补充。

    相关文章

      网友评论

          本文标题:审计/操作/修改日志实现思路

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