很多人在MySQL遇到问题的时候,第一反应就是打开慢查询日志、binlog,但常常忽略一个更基础、更直接的家伙——general_log。它不像慢查询日志那样只记录执行慢的语句,而是事无巨细地把到达MySQL服务器的每一条请求都记下来,包括连接建立、断开,以及每一条SQL文本。正因为如此,它在绝大多数生产环境里是默认关闭的,也成了很多人眼里“听过但没用过”的功能。这篇就从一个实际运维和开发双修的角度,把这个日志的方方面面讲透。
1. 先搞清楚 general_log 到底是干什么的
1.1 它能记录什么
general_log 是 MySQL 的通用查询日志,核心功能就一句话:记录所有客户端发往 MySQL 服务器的连接事件和 SQL 语句。不管SQL写得好不好、执行快不快、有没有命中索引、最终是成功还是报错,只要MySQL收到了这个语句,就会写进general_log。这一点和慢查询日志有本质区别,慢查询只记录执行时间超过阈值的语句,而general_log不挑不拣,全都收。
举个例子,你在应用里执行了一条 SELECT * FROM users WHERE id = 1,MySQL收到之后,general_log里会依次出现这样的内容:
text复制2025-01-15T10:23:45.123456Z 12345 Query SELECT * FROM users WHERE id = 1
注意这里面的几项信息:
- 时间戳:精确到微秒,记录的是语句到达服务器的时间。
- 线程ID:
12345是MySQL为该连接分配的线程编号,可以用来关联同一个连接的多条SQL。 - 命令类型:
Query表示这是一条查询语句,还有一些其他类型,比如Connect表示连接建立、Quit表示连接断开、Prepare表示预处理语句的prepare阶段。 - SQL原文:完整记录客户端发送过来的SQL文本,一字不差。
很多朋友说general_log“日志量太大、没什么用”,其实是因为没有真正理解它的价值。它最大的用处不是日常监控,而是事后审计和问题回溯。比如有一天线上突然有人改动了一张核心表的数据,业务方坚称“不是我们改的”,这时候binlog只能告诉你“这条语句执行了”,但如果想知道“到底是哪个连接、从哪个客户端发起的”,general_log往往能提供线索。
1.2 它和 binlog、慢查询日志的边界
这里一定要把 general_log 和另外两个常见日志的区别理清楚,很多人一开始学的时候就混淆了。用一张表来说明:
| 对比项 | general_log | binlog | 慢查询日志 |
|---|---|---|---|
| 记录范围 | 所有到达MySQL的SQL/连接事件 | 所有变更数据的操作(DDL/DML) | 执行时间超过阈值的SQL |
| 是否记录SELECT | 记录所有SELECT | 通常不记录(除非开启row格式下的某些情况) | 只记录超时的SELECT |
| 记录时机 | 收到语句就写 | 事务提交时写 | 语句执行完成后判断 |
| 主要用途 | 审计、问题回溯 | 主从复制、数据恢复 | 性能优化 |
| 是否默认开启 | 关闭 | 取决于配置,通常开启 | 关闭 |
从这张表能看出来,三个日志各管一摊。general_log管的是“谁来了、发了什么”,binlog管的是“数据变成了什么样”,慢查询管的是“谁比较慢”。实际排查问题的时候,经常需要把这几类日志搭配起来看,单靠任何一个都不够完整。
我自己就遇到过这样一个场景:线上有个报表统计接口每天凌晨3点会跑一条特别重的SQL,导致数据库CPU飙高。慢查询日志里抓到了这条SQL,但不知道是哪个业务方发的。后来我打开general_log,通过SQL文本和线程ID反查,发现竟然是一个已经下线的老定时任务还在跑,而那个任务所在的机器配置已经很低,跑一次要很久。如果当时没有general_log,这个“幽灵任务”估计还得折腾好几天才能定位。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 打开 general_log 之前先想清楚:日志写到哪里去
2.1 log_output 参数的两种选择
MySQL 里控制 general_log 输出位置的参数叫 log_output,可以设置三个值:FILE、TABLE、NONE。
FILE:写到文件系统,这是最常用的方式。日志文件路径由general_log_file参数指定,可以自定义文件名和路径。写入文件的好处是性能开销相对较小,日志查看方便,用tail -f就能实时看。TABLE:写到mysql.general_log表里。这个表是CSV存储引擎,可以直接用SQL查询,方便做条件过滤。但坑也在这里——CSV引擎不支持索引,数据量一旦大了查询会非常慢,而且写入性能比文件差不少。NONE:不记录日志。这个值一般单独用,比如你想彻底关闭general_log,可以同时设置log_output = NONE。
实际经验:在绝大多数场景下优先选FILE。不只是因为性能更好,更重要的是日志查询方式灵活。比如你可以用 grep、awk 这些标准命令行工具做分析,也可以把日志文件交给ELK之类的日志系统统一收集处理。如果你选了TABLE,还得考虑这张表不断膨胀的问题,清理起来反而更麻烦。
2.2 和 general_log_file 配合时的细节
当 log_output=FILE 时,比如你设置了:
ini复制general_log = ON
general_log_file = /var/log/mysql/mysql-general.log
log_output = FILE
这里有几个容易踩的细节:
一是文件权限。MySQL是以mysql用户运行的,所以日志文件所在的目录必须让mysql用户有写权限。如果目录权限不对,MySQL启动时可能直接报错,或者运行中写日志时报错然后静默失败。我见过不少新手在 /root 下建了日志目录,结果MySQL根本写不进去。
二是文件轮转。文件日志不会自动拆分,会一直往同一个文件里追加。如果你是长期开启general_log,一定要做日志切割。常见做法是借助 logrotate,每天切割一次,保留最近7天或30天的日志。一个简单示例配置:
bash复制/var/log/mysql/mysql-general.log {
daily
rotate 30
missingok
notifempty
compress
sharedscripts
postrotate
/etc/init.d/mysql rotate > /dev/null 2>&1 || true
endscript
}
注意 postrotate 里执行的命令因MySQL的安装方式而异。源码安装、apt安装、docker容器里的MySQL,重载日志的方式都不太一样。如果忘记这一步,切割后MySQL还在往旧文件里写,日志就会丢失。
三是文件大小。就算做了按天切割,单天日志也可能非常巨大。比如一个业务高峰期每秒有几百条SQL,一天下来轻松上百MB甚至几个GB。所以建议同时做大小限制,logrotate里在按天切割之外,再加上 size 500M,两者取先触发的条件。
3. 完整操作指南:临时开关与永久配置
3.1 临时开启:一条SQL搞定排查
最常用也最安全的做法是临时开启general_log,排查完立刻关掉。执行方式非常简单:
sql复制SET GLOBAL general_log = 'ON';
这样全局生效,但不需要重启MySQL。排查结束之后,执行:
sql复制SET GLOBAL general_log = 'OFF';
就关闭了。这种方式特别适合在问题复现期间短时间开启,比如怀疑某个时间段内有异常SQL访问,或者某个接口在特定操作下发了奇怪的SQL,等操作复现完就关掉。
为什么我强烈建议用临时的而不是直接改配置文件呢?因为general_log实在太能写日志了,如果长时间开启,日志文件膨胀速度远超预期,磁盘瞬间被打满也不是不可能。我在一次压测的时候开了一小时general_log,日志文件直接到了2GB多,还好及时发现。所以临时排查,用完即关,是个好习惯。
3.2 永久开启:要写进配置文件
如果你确实需要长期开启general_log,比如做数据库审计、满足合规要求,那就得写进配置文件,防止MySQL重启后失效。
在 my.cnf 或 my.ini 的 [mysqld] 段下添加:
ini复制[mysqld]
general_log = ON
general_log_file = /var/log/mysql/mysql-general.log
log_output = FILE
然后重启MySQL生效:
bash复制systemctl restart mysqld
重启之后验证一下状态:
sql复制SHOW VARIABLES LIKE 'general_log%';
SHOW VARIABLES LIKE 'log_output';
确认 general_log 是 ON,general_log_file 指向正确的路径,log_output 是 FILE。
3.3 日志内容实时查看技巧
日常排查时,我不建议直接去打开整个日志文件。因为文件太大了,打开几GB的文件既慢又占内存,还容易把终端刷爆。推荐两种方式:
一是 tail -f 实时跟踪,适合看当前正在发生的SQL:
bash复制tail -f /var/log/mysql/mysql-general.log
二是按时间过滤,比如查最近5分钟内的日志:
bash复制awk '$0 >= "2025-01-15T10:20:00" && $0 <= "2025-01-15T10:25:00"' /var/log/mysql/mysql-general.log
这种方式对大文件非常友好,不会把整个文件读进内存,而是按行流式处理。
4. 从 general_log 里读出有价值的信息:字段拆解与真实案例
4.1 日志行的完整格式
general_log 文件每一行记录一个事件,常见信息的格式可以拆解成这样:
| 字段 | 含义 | 示例 |
|---|---|---|
| 时间戳 | 事件发生时间,精确到微秒 | 2025-01-15T10:23:45.123456Z |
| 线程ID | 连接线程编号 | 12345 |
| 命令类型 | Connect/Query/Quit/Prepare等 | Query |
| SQL内容 | 客户端发来的完整语句 | SELECT * FROM t WHERE id=1 |
命令类型里几个常见的含义要做个解释:
Connect:客户端建立连接。Quit:客户端正常断开连接。Query:执行一条SQL语句,包括SELECT、INSERT、UPDATE、DELETE、DDL等。Prepare:预处理语句的prepare阶段。Execute:预处理语句的execute阶段。Connect Out:主从复制时,从库发起连接主库的事件。Change user:客户端切换用户。
看到一个有意思的现象:预处理语句在general_log里会分成两条记录,一条是Prepare带SQL模板,一条是Execute带具体参数。比如:
text复制2025-01-15T10:23:46.123456Z 12346 Prepare SELECT * FROM users WHERE id = ?
2025-01-15T10:23:46.234567Z 12346 Execute SELECT * FROM users WHERE id = 1001
对于排查ORM框架发出的SQL,这个特性非常有用。你能清楚看到框架到底是怎么做预处理的,参数替换是否符合预期。
4.2 案例一:定位“幽灵SQL”的来源
之前遇到过一个非常典型的问题:MySQL每隔几分钟就会出现一次锁等待超时,但业务方排查了很久都找不到是谁发的SQL。当时我临时打开了general_log,同时结合 SHOW PROCESSLIST 观察,最终在general_log里发现了一条来自应用服务器的 DELETE FROM cache_table WHERE expire_time < NOW()。
问题在于:这条SQL通过ORM框架发出,但业务代码里根本找不到。后来翻代码历史才找到原因——某个同事在维护缓存清理功能时,把定时任务注册到了另一个服务的配置中心里,那个服务随后下线了,但定时任务的注册信息没清理,导致一个“僵尸服务”还在定期执行清理。没有general_log,我们只能看到锁等待的表名,根本不知道SQL是谁发的。
这个案例给我们的启示是:当线上出现来源不明的SQL时,general_log可能是唯一能帮你反查来源的地方。配合数据库账号、客户端IP等信息,就能一步步缩小范围。
4.3 案例二:从 general_log 看 ORM 到底发了什么 SQL
很多ORM框架都会做复杂的SQL自动拼接,开发者在代码里写的是链式调用,但真实发到数据库的SQL可能是另一回事。比如有些同学用MyBatis时,动态SQL标签写得很花哨,一个简单的条件查询在特定参数组合下可能变成了全表扫描。
排查这类问题,有两个路径:一是看ORM打印的SQL日志,二是看数据库端的general_log。区别在于ORM日志可能经过框架自身处理,而general_log里记录的是数据库真实收到的内容,更接近真相。
我遇到过的一个实际案例:联表查询明明加了索引,但线上就是慢。翻了代码发现开发用了某个ORM的findAll方法,看起来只查了一张表,但该ORM自动追加了N+1查询逻辑,每次循环都去数据库查一次子表。这条SQL在general_log里表现得非常明显——同一个线程ID在同一时间段内发出了几百条结构相同但参数不同的SELECT。如果不是在数据库端看到这个现象,单看应用日志可能根本发现不了。
5. 性能影响到底有多大:别被“日志量太大”吓住,但也要心里有数
5.1 实际测量和主要开销点
关于general_log的性能影响,网上很多说法比较极端:有人说开了会拖垮MySQL,有人说影响不大。真实情况介于两者之间,关键是搞清楚开销从哪里来。
general_log的主要开销有以下几个方面:
- 磁盘I/O:每一条SQL都要追加写入日志文件,高并发场景下这是最大的开销。磁盘越慢,影响越大。
- 用户态到内核态的切换:每次写日志都涉及系统调用,如果日志文件和数据库数据文件在同一块磁盘上,会互相争抢I/O。
- 日志表方式额外开销:如果
log_output=TABLE,写入mysql.general_log表还有额外的行格式化和存储引擎开销,性能比文件方式差很多。
我自己做过一个不太严谨但很有参考价值的测试:在一台普通SSD的机器上,用sysbench跑只读压测,不开general_log时QPS大约8000,开了之后降到6500左右,损耗接近20%。这个数字听上去不小,但注意这是在极端压测场景下。正常业务很少有持续满负荷的SQL峰值,而且压测时的SQL都是短小精悍的,每一条都写日志,自然影响放大。如果业务本身SQL更长,日志量更大,影响会更明显。
所以,一个基本判断是:生产环境不建议常开,排查问题时短开是完全可以接受的。如果你需要在一个时间段内持续观察,可以挑业务低峰期开一两个小时,问题定位完立刻关掉。
5.2 降低影响的三板斧
如果真的必须长时间开,也有几个降低影响的措施:
第一,日志文件放到独立磁盘。 把 general_log_file 指向一块和数据文件物理隔离的磁盘,比如单独的云盘或机械盘,避免日志写入和数据读写的I/O竞争。
第二,不要用TABLE方式。 虽然TABLE方式查询起来方便,但写入性能比FILE差很多。需要查询时,可以把FILE日志导入到分析库或者用日志搜索工具处理。
第三,配合慢查询日志使用漏斗策略。 平时关掉general_log,开着慢查询日志做性能监控。一旦慢查询日志里出现可疑SQL,再短时间打开general_log去反查SQL来源。这样两个日志各司其职,既保住了性能,又能保证排查效率。
5.3 日志清理与空间管理
长期开启general_log最容易被忽视的问题就是磁盘空间。建议通过以下几个手段控制:
- 设置logrotate自动切割,保留天数控制在7~30天内。
- 每天检查日志目录的磁盘占用,设置监控告警。
- 如果对历史日志有分析需求,可以定期把切割后的日志压缩归档,比如用
gzip或xz压缩,压缩比通常能达到10:1以上。 - 如果日志量实在太大,考虑使用pipeline把日志直接接入ELK或云日志服务,这样原始文件就不用保留了。
一个常见误区:有人觉得可以把 general_log_file 指向 /dev/null 来“假开启”。这个做法虽然技术上可行,但完全没意义——日志根本不会保存下来,也就失去了排查问题的意义。如果只是想让某个功能认为日志在写而实际不写,那等于没开。
6. 避坑总结:这些细节不注意,开了也是白开
6.1 general_log 和只读实例的关系
如果你用的是只读实例,在只读实例上开启general_log是没问题的。只读实例收到的所有查询都会记录在general_log里,这在排查读写分离架构下“读逻辑异常”的问题时很有用。比如应用层明明应该走从库,但某些原因导致SQL发到了主库,在主从两边的general_log里对比一下就能发现流量分发是否正确。
但注意:只读实例上修改 SET GLOBAL general_log = 'ON' 通常可以执行,因为这不是写数据操作。不过一旦实例发生主从切换,新主库可能不会继承这个设置,需要重新配置或者写好配置文件保证重启后仍生效。
6.2 general_log 表损坏了怎么办
如果你曾经过 log_output=TABLE,那么 mysql.general_log 这张表是CSV引擎。CSV引擎的容错性很差,如果MySQL异常退出,可能留下 general_log.CSV 和对应的 general_log.CSM 文件不一致的情况,导致后续读这张表时报错。
处理办法:停机后删除这两个文件,然后重新创建表。
sql复制DROP TABLE mysql.general_log;
CREATE TABLE mysql.general_log (
event_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
user_host MEDIUMTEXT NOT NULL,
thread_id BIGINT(21) NOT NULL,
server_id INT(10) UNSIGNED NOT NULL,
command_type VARCHAR(64) NOT NULL,
argument MEDIUMTEXT NOT NULL
) ENGINE=CSV DEFAULT CHARSET=utf8mb4;
注意,直接DROP这张系统表在有些权限模型下可能没权限,需要高权限账号执行,或者把 log_output 改成 FILE 后,让MySQL自动重建。总之,能用FILE就不要用TABLE,这个原则能帮你避开很多莫名其妙的坑。
6.3 事件调度器冲突问题
MySQL的事件调度器(Event Scheduler)本身执行的任务也会被general_log记录。如果你开了事件调度器,又开general_log做排查,你会发现日志里会出现很多调度器内部执行的语句,这些不是人为发送的,分析时要先过滤掉。
过滤方法很简单,查看线程ID对应的 user_host 字段。调度器执行的语句一般 user_host 是 root@localhost,和应用的连接用户有明显区别。不过也有例外,所以最好还是先明确自己的业务账号是什么,再有针对性地过滤。
6.4 权限过高会记录密码吗
这是个很实际的安全问题。MySQL连接时可以带密码,但general_log里记录的是连接成功之后的信息,不会记录连接时发送的密码。但需要注意:如果你在SQL里直接写了密码相关的明文字符串,会被原样记录。比如执行 SET PASSWORD = 'abc123',这条语句连同明文密码都会出现在general_log里。
所以生产环境要特别注意:不要在SQL语句里明文携带敏感信息。比如有些运维脚本会把数据库密码写在SQL里执行,一旦开了general_log,密码就明文躺日志文件里了,这是严重的安全隐患。正确的做法是通过配置中心或环境变量传入密码,避免出现在SQL文本中。
6.5 注意时区问题
general_log里的时间戳默认是UTC时区,不是服务器的本地时间。如果你的应用在业务上用的是东八区,分析日志时如果不做时区换算,会误判SQL的实际执行时间。
查看当前时区:
sql复制SHOW VARIABLES LIKE 'time_zone';
如果是 SYSTEM,日志时间戳跟随系统时区;如果是 +00:00 或 UTC,就是UTC时间。分析日志时按实际时区换算即可。曾经有一次我排查一个“每天凌晨3点数据库CPU飙高”的问题,因为忘了算时区,把时间往前推了8小时,找错方向的SQL,白白浪费了整整一个下午。
最后再分享一个实用小技巧
很多同学问,general_log里的SQL太多,怎么快速找到自己关心的那条?我常用的一个做法是:在开启general_log之前,先在业务代码里加一个特征注释,比如 /* TRACE_20250115 */ SELECT ...,然后日志里直接grep这个注释,就能快速定位到目标SQL在日志中的位置,再顺着这个位置看前后上下文,了解它前后还执行了什么。这个方法在排查复杂接口问题的时候特别好使,等于给日志加了个临时路标。
另一个技巧是:用general_log辅助分析预处理语句的执行频率。有些ORM的预处理语句走的是同一条SQL模板,只有参数不同。在general_log里按Prepare语句的SQL文本去重统计,能帮你看出哪些SQL模板被高频调用,即使它们本身执行很快,也可能因为调用量过大对数据库造成压力。这种问题靠慢查询日志几乎发现不了,但用general_log一眼就能看出来。
general_log这个工具,平时用不上,一旦遇到疑难问题,它能顶大用。希望这篇能帮你把它的原理、用法、坑点都理清楚。下次再遇到“不知道数据库里发生了什么”的问题,先别急着翻其它日志,想想你是不是该把这个老朋友打开了。
