实战:原理、开启与排查案例)
接手过不少MySQL生产环境的问题排查之后我越来越觉得general_log是MySQL日志体系里最被低估的一个。慢查询日志告诉你哪些SQL跑得慢binlog记录数据怎么变更error log告诉你服务出了什么异常而general_log解决的是另一类问题——客户端究竟往MySQL发了什么。它把所有到达MySQL的语句、连接建立、断开连接、甚至切换数据库的行为都原封不动地写下来。不会因为SQL执行慢就不记录也不会因为SQL没有改动数据就不记录。只要MySQL收到了指令它就记账。很多从MySQL安装教程入门的同学一开始分不清general_log和慢查询日志觉得既然要排查SQL问题慢查询日志不就够了吗。真去生产环境排查过一次就明白慢查询只能告诉你哪些SQL执行超过了阈值但它没法告诉你某一个连接连续发了二十条查询然后断开也没法帮你定位是谁在凌晨三点用某个账号做了大量查询。这些场景只有general_log能兜底。这篇文章写给两类人一是刚接触MySQL、想在本地快速把日志体系搞明白的新手二是已经在用binlog、慢查询日志但遇到偶发疑难问题想靠general_log破局的开发或运维。我会把原理、命令、格式、坑、实战案例一次讲清楚你照着操作就能上手不需要再去翻一堆零散文档。1. 先把general_log讲明白它到底帮我们记录了哪些东西1.1 一句话理解它就是MySQL的“录像机”MySQL本身有不止一套日志体系每套都有自己的侧重点。general_log的中文名叫通用查询日志最直白的理解就是录像机——MySQL每收到一个来自客户端的指令就在日志里追加一行。这里说的“指令”远比SQL语句宽泛客户端连上来时发的握手请求连接成功后自动执行的USE db每条SELECT、INSERT、UPDATE、DELETE甚至客户端断开时的Quit指令都会被记录。它记录的形式是明文的一行一条。你可以直接在命令行里用tail命令实时滚动查看也可以用文本编辑器直接打开搜索。这一点对排障特别重要因为binlog虽然也能还原执行过的语句但它是二进制格式而且默认只记录修改数据的语句查询类操作根本进不去。general_log则没有任何筛选条件连注释都能看到。我举个例子你就明白了。某天业务反馈“页面上有个按钮点了没反应”后端日志又什么都没打。这时候如果开着general_log去日志里查那个时间段内来自应用服务器IP的记录就能看到按钮触发的SQL到底有没有到达MySQL。很多时候问题根本不在数据库而是请求根本没发过来或者被中间件拦截了。有没有general_log是两条完全不同的排查路线。1.2 和慢查询日志、binlog、error log的区别为了让你不再混淆我把MySQL最常见的四类日志放到一张表里对比。这也是我面试时经常被问到的一个考点但实际工作中很多人用錯场景。日志类型记录内容格式主要用途默认是否开启general_log所有客户端指令与连接事件文本全量审计、问题复现关闭slow query log执行超过阈值的SQL文本慢SQL优化关闭可开binlog数据变更事件二进制主从复制、数据恢复开启取决于配置error log服务启动、关闭、异常错误文本故障诊断开启这里最容易被误解的就是general_log和binlog的区别。很多人会问“binlog日志可以删除吗”“general_log是不是binlog的一种”其实两者从设计目标开始就不同binlog是逻辑复制的基石MySQL主从同步靠它传输变更事件point-in-time恢复也要靠它而general_log根本不关心数据怎么变的它只关心客户端“说了什么”。一个Query事件哪怕是一条查不到任何数据的SELECT在general_log里也会完整记录而binlog里根本不会有它的身影。慢查询日志和general_log的差别也一样明显。慢查询日志有个执行时间阈值默认10秒只有超过阈值的SQL才会被写入适合优化慢SQL时用。但它有个盲区如果一条SQL执行很快却在短时间内被重复执行了一万次慢查询日志里一条都不会有可数据库的压力就是这么被拖垮的。general_log不管快慢全都记你一看频率就能发现问题。1.3 什么场景才值得打开它全量记录的代价你想清楚了吗general_log不是银弹它有明显的副作用全量记录意味着每条指令都有磁盘写入开销晚高峰每秒几千条SQL的时候日志文件会以肉眼可见的速度膨胀。正因为它有这个代价我更愿意说“该开的时候再开开完马上关”而不是建议常年常开。真正适合打开general_log的场景大概有三类。第一类是问题复现偶发报错、特定条件下才出现的死锁或慢查询常规日志捕捉不到这时候把general_log开一段时间等现场抓到就关。第二类是安全审计合规检查要求证明某个时间段内某个账号执行过哪些操作或者发现疑似被拖库、被注入的行为需要看清全貌。第三类是开发联调定位应用发过来的SQL和预期不一致通过general_log对比真实语句和期望语句快速判断问题出在应用层还是数据库层。一句话总结general_log是临时取证工具不是常驻监控手段。心态摆正了后面操作才不会变形。2. 开启和关闭的正确姿势一次把配置改到位别留半截2.1 先掌握两个关键变量general_log和general_log_fileMySQL控制general_log的核心变量有两个。一个是general_log只有ON和OFF两个值决定日志开关。另一个是general_log_file决定日志写到哪个文件。这两个变量需要一起关注因为很多人只改了开关没改路径结果是日志写到了默认位置磁盘满了都找不到文件在哪。查看当前状态的SQL很简单下面这段我几乎每次排查都会先用一遍SHOW VARIABLES LIKE general_log%; SHOW VARIABLES LIKE log_output;执行结果一般会返回三行general_log的值为OFFgeneral_log_file指向某个文件路径log_output则可能是FILE或TABLE。log_output决定了日志写到哪里FILE是文件TABLE是写入mysql.general_log这张表。默认值是FILE我建议你默认就保持FILE原因后面详细讲。关于general_log_file有一点要提前注意它只是文件名。如果你只指定了文件名没写绝对路径MySQL会把日志放在数据目录下datadir。我处理过一个案例同事以为日志写在/var/log/mysql下找了半天没找到最后发现是写进了/var/lib/mysql/data目录。所以设置完一定要立刻确认路径不要想当然。2.2 动态开关命令与“重启即失效”的永久化问题在MySQL里开启和关闭general_log是动态操作不需要重启服务。这也意味着你可以在问题发生的间隙快速打开抓完现场再关掉。常用命令是SET GLOBAL general_log ON; SET GLOBAL general_log OFF;打开之后可以立刻确认状态SHOW VARIABLES LIKE general_log%;需要注意SET GLOBAL只对本次运行生效MySQL重启后会回到配置文件里的值。如果你的生产环境MySQL经常重启而你想让general_log保持某种状态那就得改my.cnf或my.ini。在[mysqld]段落中加入general_log ON general_log_file /var/log/mysql/general.log改完配置文件后重启MySQL生效。但这里有个容易出问题的地方如果你把配置里写死了general_logON而平时又不想一直开那每次重启后日志都会自动开启很容易造成磁盘写满。我的建议是配置文件里不要写ON保持默认OFF需要的时候用SQL动态开启用完动态关闭。提示MySQL 8.0及以上版本中8.0.3之前的版本有general_log变量8.0.3以后继续沿用但MySQL 8.0.14之后增加了audit log功能注意不要混淆。常规排障仍以general_log为主。2.3 log_outputTABLE的诱惑与陷阱写入mysql.general_log表值不值得MySQL允许把general_log写入mysql.general_log表只需要设置log_outputTABLE。表面上看这是个很优雅的方案可以用SQL查询日志不用登录服务器去翻文件配合MySQL Workbench使用教程里的可视化查询能直接检索。实际用起来坑比好处多。mysql.general_log表默认使用的是CSV存储引擎。CSV引擎的特点是纯文本存储不支持索引查询全靠全表扫描。当表里积累了百万行日志后任何一条查询都会把磁盘IO拉满。更重要的是这张表的存在本身也在消耗数据库资源每写一条日志除了写文件系统还要走一遍存储引擎的插入逻辑比直接写文件更重。如果你实在想用表模式我建议至少做两件事。第一定期清理比如每天定时把表truncate掉或者用event schedule做自动清理。第二查询频率别太高毕竟没有索引谁都救不了全表扫描。我自己的经验是除非是临时为了在一个不能登录服务器、只能用SQL客户端的环境里做快速取证否则不要用TABLE模式。文件模式简单、直接、可靠配合操作系统层面的工具处理起来也方便。2.4 设置落盘位置时别忘了验证目录权限和磁盘空间设置general_log_file有个经常被忽略的环节MySQL进程对目标目录是否有写权限。用系统用户mysql运行mysqld时如果日志目录属主不是mysql或者目录权限不够MySQL会启动失败或拒绝写日志。我遇到过把日志路径改到/var/log/mysql结果该目录权限是700且属主是root日志压根写不进去服务启动直接报错。比较稳妥的做法是先用系统命令确认目录存在且属主正确mkdir -p /var/log/mysql chown mysql:mysql /var/log/mysql chmod 750 /var/log/mysql在MySQL里设置路径后用以下命令验证日志是否已经写到了新位置SET GLOBAL general_log_file /var/log/mysql/general.log; SET GLOBAL general_log ON;随后查看文件系统tail -f /var/log/mysql/general.log如果能看到连接记录说明一切正常。磁盘空间也要提前评估general_log一天能长到多大以单机每秒200条查询计算一条日志平均100字节一天就是约1.7GB。这个数字在高峰期可能翻倍所以在开启前先df -h看一眼可用空间心理要有数。3. 日志内容逐行拆解看得懂每一行才谈得上分析3.1 一条完整记录长什么样五个字段缺一不可用文本方式查看general_log时每行记录都包含几个固定字段。MySQL 8.0默认的输出格式类似下面这样2025-01-05T10:23:45.123456Z 12 Query SELECT * FROM user WHERE id 100 2025-01-05T10:23:45.234567Z 12 Quit 2025-01-05T10:23:46.987654Z 13 Connect rootlocalhost on mydb using TCP/IP 2025-01-05T10:23:47.000001Z 13 Query SELECT VERSION()我来逐个字段拆开讲。第一个是时间戳MySQL 8.0里带微秒精度格式类似ISO8601注意最后有个Z表示UTC时间。如果你习惯看本地时间可以调整系统变量log_timestamps默认SYSTEM时区生产环境我建议保持UTC方便多台服务器统一比对。第二个是线程ID也就是连接IDMySQL内部每个连接都有唯一编号通过这个编号你能把同一条链路上的多个事件串起来。第三个是命令类型常见的有Connect、Query、Quit、Init DB、Prepare、Execute等。第四个是用户和来源格式是“用户名主机名”配合数据库名能看清是谁、从哪里来、操作的是哪个库。第五个是命令具体内容Query类型的记录会带上完整SQL文本Connect会带上连接方式。我在第一次用general_log的时候犯过一个低级错误不关注线程ID只看SQL语句结果把两条不同连接的SQL混在一起分析得出了完全错误的结论。后来才养成了习惯凡是看到可疑SQL先看前面的线程ID是否一致再把这条线程从Connect开始的所有事件摘出来看。这个习惯帮我在不少看起来随机发生的故障里找到了关联性。3.2 从记录里能读出什么样的连接与执行链路实战手把手给你看一个典型的多事件序列这是一个应用连接MySQL、做了一次查询、然后正常退出的完整过程2025-01-05T10:30:00.101010Z 22 Connect app_user10.0.0.8 on orders using TCP/IP 2025-01-05T10:30:00.101202Z 22 Query SET NAMES utf8mb4 2025-01-05T10:30:00.101505Z 22 Query SELECT * FROM order WHERE order_no SO20250105 2025-01-05T10:30:00.105289Z 22 Query COMMIT 2025-01-05T10:30:00.105500Z 22 Quit线程ID都是22串起来就是一次完整请求。Connect行显示应用用户app_user从10.0.0.8连到orders库。随后三条Query分别执行了设置编码、查询订单、提交事务。最后Quit表示连接正常关闭。如果应用连接池复用了连接你会看到同一个线程ID后面跟着很多条Query这是正常的。有一种很典型的“问题现场”是这样的线程ID固定但是中间夹着一大堆重复的SELECT比如每隔几秒循环查询同一个配置表频率远高于正常业务节奏。这类循环查询靠慢查询日志根本看不出来因为每条查询只要几毫秒但在general_log里那种密集重复感会被一眼看穿。我曾经就靠这个抓过一个同事调接口时不小心在for循环里嵌套查询数据库的bugSQL每秒重复几十次把数据库连接数撑满了。3.3 和binlog的二进制格式比general_log的明文优势很明显接触过binlog的同学都知道binlog默认是二进制格式要用mysqlbinlog工具解析才能看到内容。而general_log是纯文本意味着你可以用grep、awk、sed这些Linux原生命令直接处理。这种便利在日常排障中价值极高。举个例子我要看某个时间段内所有包含“user”表的SQL一条命令就搞定grep user /var/log/mysql/general.log如果日志里有些SQL是跨行的可以用mysqlbinlog配合或者调整格式。另外binlog里的SQL并不完整binlog为了回放效率和一致性把SQL转换成了事件格式虽然能大概看出执行了哪些操作但有些细节比如原始注释、客户端连接的上下文信息是丢失的。而general_log是客户端发出的原始语句甚至包括客户端执行的SET NAMES等会话级命令信息完整度完全不同。注意general_log记录的是客户端发起时的原始文本不代表MySQL最终实际执行的结果语句。如果SQL里用了存储过程或者触发器general_log里不会记录函数内部的每条SQLbinlog则可能记录。所以不要指望靠general_log抓存储过程里的问题。4. 现场实录三个我处理过的疑难场景全靠general_log破局4.1 半夜数据库被拖垮慢查询日志里什么都没有有一年我值班监控突然报警说核心数据库的CPU从凌晨两点开始持续飙升到90%以上持续了半小时。打开慢查询日志一看干净得很没有任何超过阈值的SQL。这其实是很危险的信号说明问题不是单条SQL慢而是大量快SQL并发造成的系统压力。我登录服务器临时打开general_log观察了两分钟马上发现了端倪。日志里有一个固定线程ID在循环执行一条非常简单的查询每秒几十次查询的表就那几张。顺着来源IP和账号一查是一个定时任务脚本里面写了个循环每次循环还sleep一下但sleep时间是毫秒级实际几乎等于疯狂死循环。那条SQL本身执行只要一毫秒根本进不了慢查询日志可它把CPU打满了。找到问题后我执行SET GLOBAL general_log OFF关掉日志通知业务方修复脚本。整个排查过程不到十分钟。没有general_log我可能要抓包、看应用日志、甚至重启数据库费时费力还不一定能找到根因。4.2 同一条SQL一会儿走索引一会儿不走原始SQL文本还原真相另一个案例更加隐蔽。业务反馈某个报表查询时快时慢快的时候毫秒级返回慢的时候要几秒。慢查询日志里捕获到了慢SQL但奇怪的是相同的SQL文本有时候出现在慢日志里有时候没有。应用开发同事一开始怀疑是MySQL执行计划不稳定准备用FORCE INDEX强制索引。我用general_log把那个时间段内应用发往MySQL的所有SQL抓了出来仔细比对后发现了一个细节应用框架在拼接SQL时会根据用户筛选条件动态增加WHERE条件。看起来是同一条SQL实际有的带了order_status字段的过滤条件有的带了payment_time范围条件甚至排序字段还分ASC和DESC。单条SQL文本几乎一样但组合条件不同导致MySQL选择了不同的执行路径。这个发现直接改变了排查方向不是索引不穩定而是业务参数导致SQL形态差异太大。最终通过调整索引组合和改写SQL逻辑解决。如果没有general_log光靠慢查询日志和堆栈信息很难还原真实语句差异。4.3 合规审计谁在半夜动过核心业务表有一次客户做安全审计需要确认某一天凌晨是否有非授权人员通过数据库客户端访问过核心业务表。登录权限在运维手里但应用账号的密码多位同事都知道不能仅凭账号归属下结论。我们把那天的general_log按时间范围切出来分析先筛选出所有连接事件和针对核心表的Query。通过日志发现凌晨三点有个IP地址来自办公网段用通用开发账号连接数据库先后执行了一条TRUNCATE操作和一条大批量UPDATE。虽然账号是共用的但那个IP和操作时间戳配合办公区的门禁刷卡记录最终锁定了具体的人。整个过程不需要抓包不需要改应用代码只需要把日志留着这就是general_log作为审计证据链的价值。提示做这类审计分析时日志一定要开启UTC时间的记录并保留原始文件证据从取证到给出结论的完整链路都要可追溯。我自己在协助这类工作时会把原文拷贝到独立目录保存避免被后续操作刷屏覆盖。5. 生产环境别乱开这些坑我踩过你就不用再踩一遍5.1 磁盘被写满是我见过最多的事故很多第一次开general_log的人都会低估它的增长速度。我之前负责过一个日活百万级别的电商系统晚高峰每秒查询在3000条左右每条日志平均长度150字节一天就是近40GB的日志量。你想想如果这个库的数据盘只有100GB空闲两天就写满。写满磁盘的后果是非常严重的MySQL为了保障数据一致性会在磁盘空间不足时拒绝新的写入表现为大量INSERT、UPDATE直接卡住应用接口超时最终线上业务全面瘫痪。而且日志本身写的文件如果和数据库数据文件在同一个磁盘上还会拖累正常读写性能。我的建议是只要打算开general_log就得提前规划两件事一是把日志文件放到独立磁盘或分区尽量不要和数据文件放在同一块盘上二是设定磁盘空间告警阈值比如当磁盘使用率超过80%就报警。另外日志文件的轮转清理也不能指望手工详见后面的落地方案。5.2 性能损耗不是每条SQL慢而是每条SQL都多了一步写盘general_log对性能的影响不是体现在单条SQL的执行时间上而是体现在整体吞吐量上。每一条SQL执行完成后MySQL都要多一次写日志文件的操作这个操作是有锁的高并发场景下会形成串行化瓶颈。磁盘就更明显机械盘上频繁小文件追加IOPS很容易被打满即便是SSD大量顺序追加也会占用带宽。有一种比较准确的说法是在纯内存型的高并发负载下general_log开启后吞吐量可能下降20%到30%。我不敢说这个比例精确适用所有场景但你要有这个预期。我自己做过一个小测试在同样压测场景下开启general_log后TPS确实肉眼可见地掉了一截。所以general_log绝不能作为常备监控手段它只适合短时间、有目的性地开启。这里也有一个折中的办法如果确实需要长期对特定SQL做记录优先考虑performance_schema或慢查询日志而不是general_log。只有当你需要全量审计、无条件记录时才把general_log作为最后手段。5.3 和binlog清理策略分开别把两类日志当成一回事还有一个常见的混乱点是清理策略上的张冠李戴。binlog和general_log有各自的保留和清理机制。binlog由expire_logs_days或binlog_expire_logs_seconds控制可以用PURGE MASTER LOGS等命令手动清理而general_log是普通文本文件MySQL没有专门的清理命令只能靠操作系统层面的logrotate或者手动删除。有人问我“binlog日志可以删除吗”我的回答是binlog是主从复制和数据恢复的基础删除前必须确认从库已经消费完并且你不再需要基于binlog做时间点恢复否则会带来数据丢失风险。而general_log则可以大胆清理它只是一份排障和审计记录不会影响数据一致性。清理手段分开做。binlog按照复制延时和备份策略来管理general_log按照磁盘占用和时间窗口来管理。两者千万别共用一套清理脚本否则可能误删。6. 我推荐的落地方案既要拿到现场又要保住业务6.1 临时开启的完整操作序列从开到收一个不漏我把平时临时开启general_log的标准操作序列写出来你可直接存下来当模板用。首先确认完整状态SHOW VARIABLES LIKE general_log%; SHOW VARIABLES LIKE log_output;接着设置落盘路径并开启SET GLOBAL log_output FILE; SET GLOBAL general_log_file /var/log/mysql/general_20250105.log; SET GLOBAL general_log ON;在需要的时间窗口里保留现场结束后立刻关闭SET GLOBAL general_log OFF;抓完现场后把日志文件归档再考虑要不要分析。如果你想在日志文件名里带上日期方便后续归档对比这是个很实用的习惯。我建议每次开启都换一个新的日志文件避免和以前的日志混在一起。6.2 长期审计需求的轮转思路logrotate加定时分析如果是合规或安全团队要求保留一段时间的general_log那就不能一台服务器摆一个大文件到最后归档了。我通常会在production环境用logrotate做按天轮转比如这样配置/var/log/mysql/general.log { daily rotate 30 missingok notifempty compress delaycompress sharedscripts postrotate mysql -e SET GLOBAL general_log OFF; SET GLOBAL general_log_file /var/log/mysql/general.log; SET GLOBAL general_log ON; endscript }这个配置看起来有点绕核心是按天轮转保留30天压缩旧日志。由于MySQL自己不会主动切换日志文件所以轮转后要用postrotate脚本重新设置一下general_log_file让MySQL把新日志写到新文件上。如果不做步骤中重新设置general_log这一步MySQL会继续往被轮转掉的文件里写磁盘空间照样爆。分析方面如果只是临时找几条SQL用grep就够了。但如果你想做高频SQL统计我自己习惯用一段很短的Python脚本处理日志文件。下面这段可以作为起点import re from collections import Counter pattern re.compile(r^\S\s\d\sQuery\s(.*)$) counter Counter() with open(/var/log/mysql/general.log, r, errorsignore) as f: for line in f: m pattern.match(line.strip()) if m: first_word m.group(1).split( , 1)[0].lower() counter[first_word] 1 for keyword, cnt in counter.most_common(10): print(f{keyword}: {cnt})这段脚本只统计命令类型的分布帮你快速看出一段时间里SELECT、INSERT、UPDATE、DELETE的比例再决定要不要进一步展开某类SQL。如果你需要更长尾的分析可以升级成按SQL摘要去重统计逻辑也不复杂。重点是把“日志里有东西”变成“日志里有线索”后续才能定位问题。我个人的体会是general_log这把工具平时可以一直放在工具箱里不用但关键时刻必须拿得出、用得上、看得懂。它不复杂核心就是几个变量、一种格式、一堆经验。你把开启命令和日志格式吃透了再配上一套管理策略以后遇到问题就能按图索骥而不是两眼一抹黑地去乱翻文档。