第三章 审计日志结构:字段与存储方式
审计日志说白了就是数据库的「黑匣子」。谁、什么时候、干了什么、结果如何,全记录在里边。我记得刚开始做审计模块时,总觉得多记几个字段无所谓,结果日志表一个月涨了 30GB……嗯,字段设计不对,后面全白费。这一章咱们就捋清楚:哪些字段是必须的,日志又该放哪儿。
3.1 核心字段清单
先列一个我常用的字段模板。你直接拿去用,基本覆盖了所有审计场景。
| 字段名 | 类型 | 说明 | 我的备注 |
|---|---|---|---|
log_id | BIGINT | 自增主键 | 分区键首选 |
event_time | DATETIME(3) | 操作发生时间,精确到毫秒 | 索引必备 |
db_user | VARCHAR(64) | 数据库登录用户 | 不是应用用户,注意区分 |
client_ip | VARCHAR(45) | 客户端 IP(支持 IPv6) | 用于溯源 |
session_id | VARCHAR(64) | 会话编号 | 关联同一事务多个操作 |
object_type | VARCHAR(32) | 操作对象类型:TABLE, VIEW, PROC… | 枚举值,不要自由文本 |
object_name | VARCHAR(256) | 对象全名(库.表) | 注意特殊字符转义 |
action | VARCHAR(32) | 操作:SELECT, INSERT, UPDATE, DELETE, ALTER, DROP, GRANT… | 尽量用标准 SQL 动词 |
sql_text | TEXT | 原始 SQL 语句 | 可能很长,建议压缩存储 |
rows_affected | BIGINT | 影响行数(-1 表示无法计算) | 排查批量误操作 |
success | TINYINT | 0=失败,1=成功 | 一定不能空 |
error_msg | TEXT | 失败时的错误信息 | 截断前 1024 字符即可 |
extra_info | JSON | 扩展信息,比如绑定变量值 | 我习惯存应用上下文 |
我自己一般在 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 个月在线即可满足绝大多数审计排查。更老的日志归档到对象存储,万一要查,再按日期拉回来。
上面这张图把字段和存储串起来了。你仔细看箭头方向——所有字段流最终汇入存储层。但是注意,这里没有「最佳方案」,只有「适合你的方案」。如果你每日日志量不到 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 完整录了进去,合规检查当场挂掉。敏感字段要么脱敏,要么不记。