☰
MySQL general_log日志详解:开启、分析、避坑与SQL审计实践
2026/10/17 5:06:54 网站建设 项目流程

排查线上问题的时候,最怕的就是“谁发了什么SQL”这件事说不清楚。业务日志只有一两句报错,应用日志里看不到完整语句,数据库端能看到的只有连接数和慢查询,真要还原现场,最后还是得靠MySQL自带的general_log。这个日志会把客户端发过来的每一条SQL原样记下来,开启方式也很简单,一条SET GLOBAL命令就够了。不少人一听“会记录所有语句”,第一反应就是太笨重、平时不爱碰,可真到了需要审计、需要追根溯源的时候,它是绕不开的兜底工具。这里就把general_log日志详解一遍,从它记录什么、为什么默认关闭,到怎么正确开启、怎么分析、怎么避免日志把磁盘塞爆,一次性说清楚。内容也适合刚接触MySQL的DBA、运维,以及被各种疑难SQL问题反复折腾过的后端开发。

1. general_log到底是什么,它和那一堆日志怎么区分

1.1 MySQL日志家族里,它处在什么位置

MySQL的日志种类很多,error log、slow query log、binlog、relay log、general_log,名字长得像,职责完全不同。很多人一开始会把general_log和binlog搞混,觉得都是记录SQL的东西,其实差距非常大。binlog记录的是数据变更,用于主从复制和数据恢复;relay log是从库在复制链路里用的中继日志;而general_log记录的是一切到达服务端的语句和连接事件,不管你有没有改数据,哪怕只是一条SELECT、一次连接池心跳,都会被记下来。

日常提到的几个日志可以简单对比如下:

日志类型默认状态主要记录内容
error log开启MySQL启动、运行、出错时的诊断信息
general_log关闭客户端连接、断开,以及所有到达服务端的SQL
slow query log关闭执行时间超过阈值的慢查询
binlog看配置数据变更明细,服务于主从复制和恢复
relay log从库开启从库接收并执行的中继日志事件

听名字就能感觉到,general_log是最“重”的一种。它没有过滤条件,不挑语句类型,所有客户端发来的消息都会记录。一次连接建立了记一笔,连接断开了记一笔,SELECT扫全表记一笔,连连接池的Ping探活都会记。这也是它默认关闭的根本原因,信息量太大会把日志系统压垮。

1.2 一条SQL从连接到落盘的记录路径

想理解general_log,先要理解它记录的时间点。一个客户端连接进来,MySQL先做握手和认证,这时general_log会先写入一条Connect记录,里面包含了这条连接的来源账号和IP。认证通过后,客户端发来SQL,服务端在收到语句之后、正式执行之前,就会把这条SQL写入日志。

这个特性非常重要。它意味着只要SQL到达了服务端,哪怕后面语法解析失败、执行报错,general_log里也照样能看到。慢查询日志只记录执行慢的语句,binlog只记录数据变更,而general_log能看到“这条SQL曾经来过”。排查那种应用层报SQL错误、但数据库层却没留下痕迹的情况,靠的就是这一点。

还要注意一个很容易误解的地方:general_log记录的是“服务端收到的SQL”,不是“数据库内部执行的每一个细节”。也就是说,你调用一个存储过程,general_log里看到的是那条CALL语句,而存储过程内部一段一段执行业务逻辑的SQL不会展开记录。触发器里执行的东西也不会因为触发器被触发而在general_log里单独出现。这个点后面在特殊场景里会再展开。

1.3 为什么默认关闭,这笔性能账怎么算

很多刚接触MySQL的人会不理解,这么好用的日志为什么默认关着?把开关打开不就行了?问题就出在“每一条SQL”这几个字上。线上业务高峰期一个实例每秒几千次查询是常态,每一次查询都要追加写一行日志,这份I/O开销不是所有机器都扛得住。

算一笔最简单的账。假设业务高峰期QPS是3000,每条SQL加连接信息平均约300字节,开一个小时日志量就是:3000乘300字节乘3600秒,大约3.2GB。这还只是保守估算,实际生产环境里一条SQL几百字节很常见,如果应用里有大字段、长IN条件或者没做分页的查询,一条语句上KB也不奇怪,日志膨胀的速度会更快。

除了磁盘空间,更重要的是对正常写入的影响。日志写入和业务写入混在同一块磁盘上时,日志会把磁盘I/O抢走一部分,本来能抗住的QPS可能就顶不上去了。这是general_log不适合长期开启的核心原因。它不是不好用,而是不能当做常驻日志来用,更适合按需开启、用完即关。

2. 开启与配置:避免从第一步就踩坑

2.1 动态开关与参数持久化

general_log最方便的一点是支持动态开启,不需要重启MySQL。先确认当前状态,再打开总开关,关键参数是general_log和general_log_file、log_output。

-- 查看当前状态 SHOW VARIABLES LIKE 'general_log%'; -- 开启通用查询日志 SET GLOBAL general_log = 'ON'; -- 如果想指定日志文件位置(可选项,建议明确指定) SET GLOBAL general_log_file = '/data/mysql_log/general.log'; -- 再次查看确认 SHOW VARIABLES LIKE 'general_log%';

general_log和log_output都是全局变量,没有会话级别这一说。在任意一个会话里执行SET GLOBAL,其他所有会话立即生效。用的时候要注意,排查完了一定要记得关回来,再执行一次SET GLOBAL general_log = 'OFF'即可。

如果希望持久化到配置文件,在my.cnf或my.ini的[mysqld]段下加几行:

[mysqld] general_log = 1 general_log_file = /data/mysql_log/general.log log_output = FILE

强调一下文件权限问题。如果general_log_file指向的目录不是MySQL数据目录,必须确保MySQL进程的启动用户对那个目录有写权限,否则日志不会生成。我早年就踩过这种坑,排查了半天以为参数没生效,结果只是目录权限不对,MySQL连日志文件都没创建出来。

MySQL 8.0可以用SET PERSIST把参数固化到配置表里,但我个人建议还是老老实实改配置文件。原因很现实:配置文件是大家都看得见的运维台账,出了问题时追溯配置变更,比翻一张系统表要直观得多。

2.2 输出方式选FILE还是TABLE

log_output这个参数决定日志写到文件还是写到系统表,主要就两个选项:FILE和TABLE,也可以配成BOTH同时写两遍。默认是FILE。

输出方式存储位置优势劣势适用场景
FILE磁盘文件性能可控、便于用shell工具处理要自己按行解析长时间、高流量排查
TABLEmysql.general_log表直接SQL查询表底层是CSV引擎,没有索引短时间、小流量定位
BOTH文件和表都写可交叉验证双倍写入开销一般不推荐

mysql.general_log这张表看着是系统表,底层其实是CSV存储引擎,不支持索引。数据量小的时候用SELECT过滤没感觉,几十万行、上百万行之后会明显变慢。所以在表模式开日志时,我强烈建议只开一小段时间,比如十分钟半小时,拿到关键SQL就马上关掉。别指望长期开着日志再用SQL慢慢查,那个体验会很痛苦。

还有一个细节容易忽略:log_output不只是管general_log,也会影响慢查询日志的输出。如果你在排查慢查询问题时也开了这个参数,注意别把现有慢日志的行为改了。

2.3 两套可以直接抄的推荐配置

根据场景不同,我这里给出两套实际中比较稳妥的组合。

第一套是常规排查场景,低峰期开,短时间用,推荐FILE模式:

SET GLOBAL log_output = 'FILE'; SET GLOBAL general_log_file = '/data/mysql_log/general.log'; SET GLOBAL general_log = 'ON'; -- 排查结束 SET GLOBAL general_log = 'OFF';

第二套是小流量、需要快速SQL过滤定位的场景,用TABLE模式,用完清表:

SET GLOBAL log_output = 'TABLE'; SET GLOBAL general_log = 'ON'; SELECT * FROM mysql.general_log WHERE argument LIKE '%目标表%'; -- 或者按时间过滤查询 -- 排查结束关闭 SET GLOBAL general_log = 'OFF'; -- 清空临时遗迹 TRUNCATE TABLE mysql.general_log;

文件模式下日志文件名前面已经说了,默认是在MySQL数据目录下以主机名命名的log文件,具体值用SHOW VARIABLES LIKE 'general_log_file'就能看到。表模式下直接查mysql.general_log表即可,不过清表需要足够权限,否则TRUNCATE会报错。

3. 日志内容拆解与实战分析方法

3.1 文件模式怎么读、怎么筛

FILE模式下,每行日志的大致结构是这样的:

2024-05-18T10:24:15.123456Z 8 Query SELECT * FROM orders WHERE order_id = 1024 2024-05-18T10:24:15.987654Z 8 Query SELECT * FROM users WHERE phone = '13800000000' 2024-05-18T10:25:01.000001Z 9 Connect root[root] @ localhost [] as anonymous on

这里第一列是时间戳,带微秒;第二列是thread_id,也就是连接在线程层的编号;第三列是command_type,命令类型;第四列是这次命令的具体内容。不用纠结排版里的制表符,按空白拆分就行,列的含义很清楚。

常见的command_type有这么几种:

  • Connect:客户端建立连接
  • Query:普通SQL语句
  • Prepare/Execute:预处理语句的两个阶段
  • Ping:连接池探活
  • Quit:连接退出

看到大量Ping不用慌,那是连接池在配置connection_test,属于正常心跳。反而是那些卡在Connect和Quit之间的长连接,才是需要进一步观察的对象。

文件模式最大的好处是可以直接用shell工具处理。实时跟踪日志、按时间段过滤、按关键字找特征,效率都很高:

# 实时跟踪新增日志 tail -f /data/mysql_log/general.log # 找出所有SELECT和UPDATE语句 grep -E "SELECT|UPDATE" /data/mysql_log/general.log # 按时间段过滤 awk '$1 >= "2024-05-18T10:00:00" && $1 <= "2024-05-18T10:30:00"' /data/mysql_log/general.log # 按来源账号过滤 grep "app_user@app_host" /data/mysql_log/general.log

用grep抓关键字、用awk框时间段,配一下sort | uniq -c还能快速统计哪种SQL出现的次数。这种方式适合日志量大的场景,不开数据库连接就能做初步分析。

3.2 表模式怎么查、怎么追

TABLE模式下,日志落在mysql.general_log表里,先看下表结构:

SHOW CREATE TABLE mysql.general_log\G

核心列就这几列,理解清楚之后过滤起来很顺手:

列名含义
event_time记录时间,精度到微秒
user_host连接来源,形如“账号@IP”
thread_id线程编号,可用来跟踪同一连接的连续操作
server_id本机服务器ID
command_type命令类型
argument语句内容或命令详情

表模式最大的优势是可以直接写SQL查。比如我要找某个时间窗口内,所有涉及订单表的UPDATE语句:

SELECT event_time, user_host, thread_id, LEFT(argument, 300) AS sql_text FROM mysql.general_log WHERE event_time >= '2024-05-18 10:00:00' AND event_time <= '2024-05-18 10:30:00' AND argument LIKE '%UPDATE%orders%' ORDER BY event_time;

注意,这个表没有索引,WHERE过滤条件写得再多也是全表扫描。数据量小的时候无所谓,数据量大起来就要命。所以我的经验是,表模式只适合小流量或者短时间排查,日志量一大还是老老实实切回FILE模式,或者先关掉日志再慢慢翻。

thread_id是个很关键的分析入口。同一个thread_id下连着的多条记录,就是同一个连接从建立到退出之间做过的所有事。你可以先抓到一条可疑SQL的thread_id,再把它对应的连接前后所有记录都拉出来,这样能看到这个连接在这个时间点前后还干了什么其他操作。这是一个典型的“顺藤摸瓜”思路。

3.3 一次真实的批量修改定位过程

说一个很典型的场景。某天凌晨3点,系统里一批订单状态被人批量改成了“已取消”,业务日志查不到来源,应用日志里没有完整SQL。当时我们提前开启general_log,等第二天异常重演。

排查流程是完全可以直接复制的:

第一步,确认这是低峰期且流量不大,选TABLE模式,开启日志。

SET GLOBAL log_output = 'TABLE'; SET GLOBAL general_log = 'ON';

第二步,异常发生后,用SQL过滤出涉及目标表的语句。

SELECT event_time, user_host, thread_id, LEFT(argument, 500) AS sql_text FROM mysql.general_log WHERE argument LIKE '%orders%' AND event_time BETWEEN '2024-05-19 03:00:00' AND '2024-05-19 03:30:00';

第三步,从结果里锁定一条典型的批量UPDATE,记下thread_id。

第四步,用这个thread_id把该连接前后的动作全部拉出来。

SELECT event_time, command_type, LEFT(argument, 200) AS sql_text FROM mysql.general_log WHERE thread_id = 4521 ORDER BY event_time;

这一步能还原出这个连接干了什么:先SELECT查询了某批订单,然后执行UPDATE,最后断开。行为模式非常像定时任务脚本。顺着user_host里的来源IP去查业务网关,最后定位到某台机器上的调度任务,整个分析过程没超过十分钟。

第五步,收尾。关闭general_log,清理临时数据:

SET GLOBAL general_log = 'OFF'; TRUNCATE TABLE mysql.general_log;

这个案例的关键不在SQL多高端,而在thread_id这个字段串起了整条证据链。只看单条SQL,只能知道“有这条语句”;把thread_id串起来,才能知道“这个连接当时到底想干什么”。

4. 常见坑与排查技巧实录

4.1 磁盘被日志写爆后的应急处置

general_log最常见的生产事故,就是把磁盘写满。因为日志是持续追加的,而且没有自带的清理机制,开得越久,文件越大。

还是用前面那个计算公式:日志增长速度约等于QPS乘平均SQL长度。假设QPS是2000,平均每条SQL带连接信息约400字节,1小时就是2000乘400乘3600,差不多2.88GB。一个活动大促库开启半天,磁盘飘红是非常正常的。

一旦发现磁盘告警,正确步骤是先关总开关,再清文件。顺序不能反:

-- 先停掉写入 SET GLOBAL general_log = 'OFF';

然后到服务器上把日志文件移走或清理。这里特别提醒,不要直接对正在写的日志文件执行清空或删除操作就完事。MySQL进程可能还持有文件句柄,删了文件之后空间不见得立刻释放,而且新日志还可能继续写进已删文件的空洞里。最稳妥的就是先停日志、再处理文件、最后确认是否还需要继续排查。

如果有保留价值的日志文件,可以先rename成带日期的归档文件,压缩存好。确认排查已经闭环后,再把归档一并清掉。

4.2 开启后性能明显下降的根本原因

开了general_log之后数据库性能明显下降,这不是错觉,也不是MySQL出了bug,而是必然的代价。每条SQL都要走到日志写入的链路,这就额外消耗了CPU和I/O。如果日志和业务数据在同一块盘上,日志写入还会和普通SQL争抢磁盘带宽,高峰期QPS很容易掉一截。

那怎么缓解?我能给的建议比较实际:

  • 不要把general_log长期开启,排查期按需开,定位问题之后立刻关。
  • 如果确实需要开一段时间,把general_log_file指向独立磁盘或独立挂载点,让日志写入和业务数据I/O分开。
  • 选择业务低峰期开启,降低对核心请求的影响。
  • 不要为了“可能用得上”而长期开着,真正需要的时候再开,时间窗口够用就好。

log_output = TABLE不一定比FILE快。表模式虽然写的是CSV文件,但也有额外的一层表接口调用,而且后续查询慢的问题更严重。所以在高流量场景下,FILE模式才是相对稳的选择。

4.3 存储过程、预处理语句与复制场景的认知误区

general_log有几个常见的认知误区,值得单独拉出来讲。

第一个误区是“开了general_log就能看到存储过程里每一步SQL”。前面说过,general_log记录的是服务端从客户端收到的内容。你调用一个存储过程,日志里只有那条CALL语句,存储过程体里的INSERT、UPDATE、SELECT不会一条条展开。如果业务的核心逻辑都在存储过程里,光靠general_log审计是看不到内部细节的,这时候得用binlog或者专门的审计手段配合。

第二个误区是“预处理语句的参数会完整显示”。使用Prepared Statement时,general_log里能看到Prepare模板和Execute阶段的信息,但在一些协议组合下,实际参数不一定完整地出现在日志文本里。你不能指望靠general_log把预处理语句的传参完全还原。它更适合分析语句模板和调用频率。

第三个误区是从库审计。从库上开general_log,能不能看到从主库同步过来的变更,跟版本、复制线程的实现都有关系。我在实际工作中不会把从库的general_log当审计工具来用,审计和溯源要在主库做,从库日志顶多作辅助验证。这个定位想清楚了,很多坑自然避开了。

4.4 日志清理与安全收尾要做什么

最后聊聊安全。general_log里记录的是所有到达数据库的SQL,这意味着它可能包含业务敏感数据。登录接口如果直接在SQL里拼接密码,general_log就会把明文密码存进去;订单、手机号、身份证什么的同理。日志文件如果权限没控制好,等于把核心数据送到了仓库门口。

所以在安全收尾这块,我的做法一般是这样:

  • 日志文件权限设置为600或640,只允许DBA账号读取,不给普通开发开默认读权限。
  • mysql.general_log表的访问权限也需要控制,不是所有人都有权SELECT。
  • 排查结束后及时清理或归档日志,归档文件建议压缩加密再保存。
  • 如果日志包含敏感数据,存留时间要按内部的安全规范来,不要无限期放磁盘上。

另外,所有动态开启的参数,排查结束后要确认最终状态。别开了日志关掉,但配置文件里还留着相关参数,下次重启又把日志带回来,到时候又是一场磁盘告警。

关于general_log,我自己操作了这么多次之后,经验可以浓缩成两句话:平时别开,排查期按需开,开完一定关。它就像临时拉起的监控探头,而不是可以长期挂在墙上的常备设施。把输出方式选对(FILE还是TABLE)、时间窗口框好、目标表找对,再搭配thread_id和user_host顺藤摸瓜,绝大多数SQL层面的疑难杂症都能在很短时间内定位。真要追存储过程内部的逻辑,或者需要一套完整的SQL审计方案,那就要换binlog审计或专业审计插件的玩法了,那是另一套方法论。最后再提醒一句,排查完记得把参数和日志都收拾干净,别给自己埋雷。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询