第三章 审计日志结构:字段与存储方式

审计日志说白了就是数据库的「黑匣子」。谁、什么时候、干了什么、结果如何,全记录在里边。我记得刚开始做审计模块时,总觉得多记几个字段无所谓,结果日志表一个月涨了 30GB……嗯,字段设计不对,后面全白费。这一章咱们就捋清楚:哪些字段是必须的,日志又该放哪儿。

3.1 核心字段清单

先列一个我常用的字段模板。你直接拿去用,基本覆盖了所有审计场景。

字段名类型说明我的备注
log_idBIGINT自增主键分区键首选
event_timeDATETIME(3)操作发生时间,精确到毫秒索引必备
db_userVARCHAR(64)数据库登录用户不是应用用户,注意区分
client_ipVARCHAR(45)客户端 IP(支持 IPv6)用于溯源
session_idVARCHAR(64)会话编号关联同一事务多个操作
object_typeVARCHAR(32)操作对象类型:TABLE, VIEW, PROC…枚举值,不要自由文本
object_nameVARCHAR(256)对象全名(库.表)注意特殊字符转义
actionVARCHAR(32)操作:SELECT, INSERT, UPDATE, DELETE, ALTER, DROP, GRANT…尽量用标准 SQL 动词
sql_textTEXT原始 SQL 语句可能很长,建议压缩存储
rows_affectedBIGINT影响行数(-1 表示无法计算)排查批量误操作
successTINYINT0=失败,1=成功一定不能空
error_msgTEXT失败时的错误信息截断前 1024 字符即可
extra_infoJSON扩展信息,比如绑定变量值我习惯存应用上下文

我自己一般在 event_time 和 db_user 上建联合索引。为什么?排查时最常用的两个过滤条件就是「谁在什么时间段干了什么」。

注意: 千万别把 sql_text 直接做成索引。我见过有人拿整条 SQL 做前缀索引,结果索引比表还大。实在要查 SQL 内容,用全文索引或者外挂 Elasticsearch。

3.2 存储方式选型

日志存哪?这个选择直接决定了后续查询和维护的代价。我经历过三种主流方案,优缺点给你摆一下。

方案优点缺点适合场景
独立审计表(同库)实现简单,事务一致影响业务库性能,备份变大日志量 < 100 万行/天
独立审计库(同实例)与业务库隔离,可单独优化实例整体 IO 压力仍在中等规模,100 万~1000 万行/天
外部存储(文件 / ES / 云日志)几乎不影响数据库,可弹性扩容插入延迟,排查时需连外部系统海量日志,>1000 万行/天

我个人倾向先用独立审计库。为什么呢?同库方案我之前吃过亏:有一次业务高峰,审计日志写入把 tempdb 撑爆了,整个库都挂掉。后来改成独立库,再没出过这个幺蛾子。

小技巧: 如果你选外部存储,建议在业务数据库里只存一个「日志指针表」,包含 log_id、event_time、external_uri 三个字段。查询时先查指针表拿地址,再取详细内容。这样既能快速过滤,又不占主库空间。

3.3 分区与归档策略

日志表永远在增长。不分区,等表超过 1 亿行,查询直接慢成幻灯片。我习惯按月分区,保留最近 12 个月。

CREATE TABLE audit_log (
  log_id      BIGINT NOT NULL,
  event_time  DATETIME(3) NOT NULL,
  -- 其余字段略
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(event_time)) (
  PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
  PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
  -- 每月一个分区
  PARTITION p_future VALUES LESS THAN MAXVALUE
);

为什么要留一个 p_future?你想想看,如果上个月忘记建分区,新的日志会直接报错。留着最大分区兜底,再定期用 REORGANIZE PARTITION 拆出来。

归档经验: 超过 6 个月的历史日志,我会导出到 CSV 并压缩,然后删除对应分区。保留最后 6 个月在线即可满足绝大多数审计排查。更老的日志归档到对象存储,万一要查,再按日期拉回来。
审计日志字段与存储方式 核心日志字段 event_time db_user client_ip session_id object_type object_name action rows_affected success error_msg sql_text (可压缩 / 外置) 存储方式 独立审计表(同库) 事务一致,影响业务库性能 独立审计库(同实例) 隔离性好,IO 压力仍在 外部存储(文件 / ES / 云日志) 弹性扩容,查询需跨系统 我的做法 独立审计库 + 按月分区 6个月在线,更早归档到对象存储

上面这张图把字段和存储串起来了。你仔细看箭头方向——所有字段流最终汇入存储层。但是注意,这里没有「最佳方案」,只有「适合你的方案」。如果你每日日志量不到 10 万行,同库审计表完全够用;如果超过 500 万行,我建议直接上外部存储。

避坑指南: 我曾经为了省事,把审计日志直接写进业务库的同一个实例。结果半夜做数据归档时,业务表被锁了 20 分钟,线上告警炸了。从此以后,审计库单独实例,连存储过程都分开。

3.4 字段设计细节

再补充几个容易踩坑的点。

  • 时间精度: 一定要用 DATETIME(3) 记到毫秒。我见过用 TIMESTAMP 的,结果同一秒内多个操作排不出顺序,排查时气到拍桌子。
  • 用户标识: 千万别只记应用层用户名。有一次安全问题,DBA 要找真正的数据库登录名,结果日志里全是 app_user,彻底抓瞎。必须记 CURRENT_USER() 返回的原始账户。
  • SQL 截断: VARCHAR(4000) 不够用,但用 TEXT 又太占用行空间。我习惯存前 2000 字符,超长部分用 MD5 存校验码,必要时通过应用日志补全。
  • JSON 字段: MySQL 5.7+ 的 JSON 类型非常好用,可以直接在应用层传额外键值对。比如在 extra_info 里记 {"app_module":"订单","request_id":"abc123"},排查效率翻倍。
警告: 不要把敏感信息(密码、身份证、卡号)直接写入审计日志!有一次我同事误把 UPDATE 用户 SET password=sha2('123456') 的原始 SQL 完整录了进去,合规检查当场挂掉。敏感字段要么脱敏,要么不记。