1. 起因:静态参数与动态负载之间的拧巴
我维护的一套订单查询系统,跑在两个8C16G的实例上,平时QPS稳定在两千左右,配置也自认为调得“差不多了”。某天凌晨两点多,告警平台连续弹了三条:Threads_connected超过500,连接超时开始出现,慢查询数量是平日的六倍。爬起来看了下监控,发现是夜里定时的对账任务在拉全量订单数据,一下子把连接池打满了。当时的处理方式很直接:临时把max_connections从500调到1000,再把innodb_buffer_pool_size从6G调到10G,重启后继续睡。
可没过两周,白天的大促压测又来了,这次是CPU先扛不住。我再上去调,发现上次加buffer pool时,把操作系统预留内存和MyISAM的部分缓存空间压得太狠,swap开始上涨。来回折腾了几轮以后,我意识到一个很基本的问题:一套写死的my.cnf,根本不可能同时适配夜间批处理、白天高并发查询、凌晨备份、周末报表这几种完全不同的负载形态。也就是从那一刻起,我开始琢磨基于开源工具搭一套MySQL自适应优化系统,把“人肉看指标、人肉改参数”这件事系统化。
1.1 手工调参的本质问题
先说清楚手工调参为什么扛不住,这不是态度问题,是流程问题。
第一是反应滞后。告警产生、值班人员被叫醒、登录跳板机、看监控、分析根因、改参数、确认生效,这一套下来至少二三十分钟。数据库参数相关的故障里,很多是“量变引起质变”——连接数缓慢爬升、缓存命中率缓慢下降,等到触发告警时往往已经接近瓶颈。人工介入的这半小时,业务侧可能已经积累了大量超时。
第二是参数之间存在耦合。调大max_connections,每个连接都要消耗线程栈内存;调大innodb_buffer_pool_size,又挤压了操作系统文件缓存和同机其他进程的内存空间。只盯单一指标,很容易按下葫芦浮起瓢。我那次调buffer pool导致swap上涨,就是这么来的。
第三是经验不可复制。不同的人值班,面对同样的监控数据,做出的决策可能完全不同。有人偏好保守,有人偏好激进,还有人压根不熟悉某个参数的业务语义。几台实例还能靠人肉,等集群从几台扩到几十台、上百台,每一台的负载特征还不一样,人肉调参的复杂度是指数级上升的。
1.2 自适应优化的目标:不是AI,是闭环
很多人听到“自适应”就往AI、机器学习上想,我当时反而刻意避开了这个方向。我的目标很朴素:把DBA在常规调参场景下的决策过程固化成一套可重复的闭环——
采集指标 → 评估状态 → 生成决策 → 执行变更 → 验证效果 → 异常回滚
每一步都留审计记录,每一次变更都能回答“为什么改、依据是什么、改之前和改之后差多少”。能做到常见的、低风险的参数调整不再依赖人工值班,而高风险、需要重启的变更仍然保留人工审批环节,这套系统就算成功了一半。
2. 整体架构与开源组件选型
搞清楚目标之后,接下来就是架构设计。我没有选择从零开发采集器,也没有引入重量级商业产品,而是围绕Prometheus生态搭建,自己只写评估和执行业务逻辑。整体分四层,每一层的职责非常单一。
2.1 四层闭环长什么样
- 采集层:Prometheus +
mysqld_exporter,每15秒抓取一次实例状态,指标全部落进TSDB。这个层只负责“如实记录”,不掺任何判断逻辑。 - 评估层:一个常驻的Python调度任务,每5分钟跑一次规则引擎,从Prometheus拉最近10到30分钟的聚合指标,逐条评估我们预设的规则集。这个层只负责“判断该不该动”,不直接操作数据库。
- 执行层:独立于评估进程的执行脚本,只接收经过校验的变更指令,通过MySQL客户端下发
SET GLOBAL。这个层负责“动手”,但动手之前必须经过重重安全阀。 - 保障层:变更记录表、冷却期机制、自动回滚定时器、告警通知。这个层负责“兜底”,确保系统做错了也能自己恢复。
四层之间通过一个简单的指令队列解耦:评估层把“建议变更”写入队列,执行层消费队列并执行,执行结果写回变更记录。这样做的好处是,哪怕执行脚本挂了,评估层也不会一直重复下发指令;哪怕评估层写入了错误建议,执行层的安全阀也能拦下大部分风险。
2.2 选型对比:为什么是Prometheus + mysqld_exporter
我实际对比过几个方案,简单列一下当时的取舍:
| 方案 | 优势 | 劣势 | 我的结论 |
|---|---|---|---|
| PMM (Percona Monitoring and Management) | 指标丰富,开箱即用,自带很多可视化面板 | 组件较厚重,架构偏大,建议功能更面向人工辅助而非无人值守 | 不适合我们这种轻量自研场景 |
| Zabbix | 监控告警能力强,模板也多 | 细粒度性能指标不全,做决策数据源需要大量自定义采集项 | 拿来做监控可以,做决策底座要补的东西太多 |
| Prometheus + mysqld_exporter | 指标覆盖全,PromQL做时间窗口聚合非常方便,社区资料丰富 | 只提供数据,评估和执行业务逻辑需要自己写 | 采用,符合“只做必需的自定义”原则 |
mysqld_exporter提供的指标几乎覆盖了所有调参后续需要的输入:连接数、线程数、buffer pool命中情况、表缓存状态、临时表使用、复制延迟等。更重要的是Prometheus的rate()、increase()、分位数查询这些内置函数,让我不需要预先固化采样方式,想看的窗口期随时可以聚合,这在调规则阶段特别重要。
2.3 指标采集与时间窗口的工程细节
采集配置上有几个细节值得注意。scrape_interval我设置为15秒,每台实例大概新增几百个时间序列,对Prometheus本身没有压力。mysqld_exporter的运行账号只给了PROCESS、REPLICATION CLIENT、REPLICATION SLAVE等最小权限,绝对不给业务库的读写权限。
评估窗口的设置是这套系统少踩坑的关键。我们统一不取瞬时值,而是至少取10到30分钟的中位数或90分位数。原因是数据库指标抖动太频繁了,一条慢SQL、一次锁等待都会让瞬时值失真。比如判断buffer pool是否不足,直接看当前命中率很容易误判,要看rate()之后的趋势:
# 命中率趋势:近30分钟读请求中的磁盘读占比 1 - ( rate(mysql_global_status_innodb_buffer_pool_reads[10m]) / rate(mysql_global_status_innodb_buffer_pool_read_requests[10m]) )这类PromQL语句在调试规则时极其好用。还有一个特别坑的地方:mysqld_exporter导出的很多状态变量是counter类型,直接取value没有任何意义,必须用rate()或者increase()包裹。我刚开始写规则时就是忘了这一点,拿裸值当采样结果,导致预研阶段的分析结论全部作废。
3. 参数与指标的映射:规则集怎么设计
评估层是整个系统的“大脑”,而规则集就是大脑里的知识库。我在设计规则时没有追求数量,反而刻意保持了克制——只收录那些参数含义清晰、调整风险可控、效果可验证的规则,一共维护了二十来条,真正高频触发的也就五条左右。
3.1 每条规则的四段式结构
为了让规则可审计、可调试,我把每一条规则都强制写成四段式:触发条件、前置检查、执行动作、验证条件。
- 触发条件:什么指标在什么区间内持续了多久,例如“buffer pool命中率低于95%且持续30分钟”。
- 前置检查:执行前必须确认的额外事实,例如“当前是否有长时间事务”“复制是否处于延迟状态”“同机内存余量是否充足”“是否正在执行备份任务”。
- 执行动作:具体调整哪个参数、步长限制是多少。步长限制是我重点强调的部分,每次调整不允许超过当前值的15%。
- 验证条件:动作生效后,预期指标应该往哪个方向变化;如果一段时间内没变化甚至恶化了,则触发回滚。
以buffer pool规则为例,代码骨架大概是这样的:
def rule_buffer_pool(ctx): # 触发条件:命中率低于95%且持续30分钟 hit_rate = ctx.metric("buffer_pool_hit_rate", window="30m") if hit_rate >= 95: return RuleResult(skip=True, reason="hit rate ok") # 前置检查:空闲内存至少多出2G if ctx.free_memory_mb < 2048: return RuleResult(skip=True, reason="not enough free memory") # 前置检查:存在未提交的长事务时不调整 if ctx.long_running_transactions > 0: return RuleResult(skip=True, reason="long txn in progress") # 执行动作:增长量不超过当前值的15%,且预留1024MB给系统 delta = min(int(ctx.current_buffer_pool_mb * 0.15), ctx.free_memory_mb - 1024) if delta < 256: return RuleResult(skip=True, reason="too small delta") return RuleAction(param="innodb_buffer_pool_size", new_value=ctx.current_buffer_pool_mb + delta)这种写法有几个好处。第一,别人review代码时不需要翻文档,规则本身就是说明书。第二,某个规则出了问题,可以单独下线,不影响其他规则。第三,每一层判断都可以输出一个明确的“跳过原因”,方便复盘为什么系统没动作。
3.2 高价值参数-指标对照表
我整理了一张实战中最常使用的参数映射表,基本覆盖了这个系统80%的收益来源:
| 参数 | 关键指标 | 触发场景 | 调整动作 | 风险等级 |
|---|---|---|---|---|
innodb_buffer_pool_size | 命中率、数据总量、可用内存 | 命中率持续低于95%且数据量接近池大小 | 每次增加10%~15% | 中 |
max_connections | Threads_connected、Threads_running、aborted连接数 | 连接使用率超过80%但运行线程数不高 | 每次增加20% | 中 |
table_open_cache | Opened_tables增量、Open_tables当前值 | 打开表数量接近上限且Opened_tables快速上涨 | 每次增加25% | 低 |
thread_cache_size | Threads_created速率 | 每秒新建线程较多且连接波动大 | 每次增加16~32 | 低 |
innodb_log_file_size | 日志写速率、checkpoint年龄 | 日志切换频繁导致checkpoint与写入争抢 | 静态参数,只出建议工单 | 高 |
注意最后一行,我把innodb_log_file_size单独拎出来是因为它在大多数版本里不支持动态调整,需要重启生效。对这类参数,系统绝不允许自动执行,只能生成一条“建议变更工单”推给值班DBA,由人决定是否在维护窗口处理。
3.3 为什么用可解释规则,而不是训练一个模型
这一节我想多说几句,因为这是很多人问得最多的一个问题。当时的结论很明确:在这个场景里,规则引擎优于机器学习模型。
- 动作空间很小。数据库核心调参参数其实就几十个,每条参数的业务语义清晰,规则完全可以覆盖,不需要模型去做特征抽象。
- 样本数据严重不足。一个生产实例一年可能只有几十次参数变更,而且每次都伴随不同的业务背景,这种样本量根本训练不出稳定模型,强行上只会过拟合加不可解释。
- 审计是硬要求。每次自动变更都要能回答“为什么动”和“依据是什么”,决策树和规则集可以直接给出答案,黑箱模型给不出。
- “自适应”的价值在于响应速度,而不是“学习能力”。规则引擎每5分钟跑一次,能感知分钟级的负载变化;如果靠模型定期重训练,更新周期反而拖慢了响应。
4. 执行链路:安全阀、持久化与回滚
规则引擎判断“该动了”之后,真正的考验才开始。无人值守的自动变更,最怕的不是判断错误,而是判断错误之后没有任何挽回机制。所以执行链路的每一环,我都在问同一个问题:“如果这里出错了,系统能不能自己发现并恢复?”
4.1 动态参数与静态参数的边界
第一步是严卡只处理动态参数这条线。
动态参数指的是执行SET GLOBAL即可在运行期生效、无需重启的参数,比如max_connections、table_open_cache、thread_cache_size、innodb_buffer_pool_size(5.7之后支持动态调整)。静态参数则包括innodb_log_file_size、innodb_flush_method等,改完必须重启,重启意味着连接中断,绝对不能由无人值守系统自动执行。
系统里维护了一张参数字典表,记录了每个参数在不同版本下是否支持动态调整。因为我们的集群横跨MySQL 5.7和8.0,同一参数的行为并不完全一致,这张表必须严格跟随版本管理,否则很容易出现“规则认为可以动态调整,实际执行却需要重启”的险情。
4.2 执行器每一步都在防什么
执行器看似简单,就是一条SET GLOBAL命令,但我在它外围做了六道防护:
- 幂等保护:同一参数在冷却期内不允许重复调整。每次变更成功后会写入变更表,冷却期默认30分钟,确保规则不会因为“阈值仍然没达标”而连续触发。
- 步长限制:每次最多调整15%。宁可多跑几轮,也不允许一次跨大步。这是为了防止某个误判导致参数瞬间跳到危险区间。
- 并发互斥:全局一把锁,同一时间只允许一条变更在途。变更前后都要查询当前生效值做比对,防止执行结果和预期不一致。
- 拓扑一致性:主从环境下,从库先调整、观察5分钟确认正常后,主库再调整。避免出现主从切换后参数不一致导致的性能震荡。
- 权限最小化:执行账号只给必要的权限,绝不能有
DROP或对业务库的写权限。即使执行器被攻破,影响也被限制在参数调整范围内。 - 错误码收敛:执行脚本对MySQL返回的错误码有白名单,比如权限不足、参数值非法这类错误会被识别并终止,而不是盲目重试。
执行命令本身很简单,但外围判断一点都不简单:
mysql -h 127.0.0.1 -u autotune -p'xxxx' -e \ "SET GLOBAL max_connections = 1200;"4.3 配置持久化和重启一致性
这里有一个特别容易忽视的坑:SET GLOBAL只对运行中的实例生效,不会写入配置文件。如果哪次机器重启,所有自动调整都会静默丢失,实例瞬间回到调参前的状态,而规则引擎又会按照新负载重新调整,等于白忙活一轮。
我们的做法是:每次变更成功后,立即把新值回写到my.cnf对应配置段,写之前备份原文件。配置文件解析用现成的库而不是正则替换,避免搞坏注释和配置段结构;写完后用mysqladmin ping确认实例健康,再继续下一条变更。
MySQL 8.0自带的SET PERSIST机制可以自动持久化,但因为集群里还有5.7节点,我最终统一走了“运行时SET GLOBAL+ 手动回写配置文件”的路线,保持两套版本一套逻辑,避免维护成本翻倍。
4.4 指标恶化的自动回滚
回滚机制是整个系统的底牌。我对回滚的定义不是“把参数改回去”,而是**“确认变更无效或引发副作用之后,恢复到变更前状态”**。
回滚的触发条件有两个方向:一是目标指标没有按预期改善,比如buffer pool扩了但命中率仍然不涨;二是关联指标出现恶化,比如调大连接数后Threads_running异常升高,或者CPU、复制延迟明显劣化。系统会在变更后10分钟自动做一次验证评估,不达标就触发回滚。回滚同样走完整的安全阀、审计和配置文件回写流程,并向告警系统推一条“自动调整失败,已回滚”消息。
我的底线是:自动系统允许做错,但绝不允许做错之后不恢复。
5. 灰度、效果与翻车复盘
这套系统从搭建到稳定运行,用了将近三个月。前一个半月其实都在做灰度验证和规则修正,真正放开自动执行是最后一个多月的事。这个过程踩了不少坑,也积累了一些可以分享的实测数据。
5.1 先跑两周“只建议不执行”
我把上线过程分成了四个阶段,每一阶段都有明确的出口条件:
- 只采集,不评估:确认指标完整性和数据准确性,大概跑了一周。
- 开启评估,只出建议:规则引擎每天产出推荐,由DBA人工审批是否执行。这个阶段跑了两个星期,每周Review一次推荐列表与实际调整的差异。
- 只自动执行低风险参数:先放开
thread_cache_size、table_open_cache这类几乎不会出事的参数,观察一周。 - 放开中风险参数自动执行:确认低风险参数稳定后,才逐步放开
max_connections和innodb_buffer_pool_size。
两个星期“只建议不执行”阶段有一个重要产出:我们发现规则引擎的推荐里大约只有七成是人工也会做的决定,剩下三成里大部分属于时间窗口误判——比如把夜间批处理导致的表扫描误判为业务增长。后来给指标打上了“按小时区分负载形态”的标签,并让规则识避免在备份和批处理窗口内触发,误报率才明显降下来。
5.2 实测效果:数字会说话
拿我们那套读多写少的订单查询集群来说,系统稳定运行三个月后的变化是:
max_connections告警次数从每月15次左右降到3次以内。- 连接拒绝(aborted connects)数量明显减少,白天高峰期的连接超时基本消失。
- buffer pool命中率从92%左右提升并稳定在98%以上。
- 因为参数问题导致的凌晨紧急处理工单,从每个月四五次降到基本没有。
当然也要说清楚边界:这套系统只解决“配置型瓶颈”。如果瓶颈本身在SQL写法、表结构设计或者硬件容量,参数调整解决不了任何问题。我们的慢查询数量虽然因为连接不再被拒而下降,但真正拖底的那些大SQL,靠的是后面单独做的SQL审核和改写,跟这套系统没关系。
5.3 三个真实的坑
复盘过程中最值得记录的三个坑,分享出来希望大家别重复走。
第一个坑:内存超卖。某个实例上除了mysqld,还跑着两个Java微服务。系统当时看可用内存还有4G,自动把buffer pool上调了一轮,结果Java服务开始频繁Full GC,业务响应时间飙高。教训是:可用内存计算必须把同机其他进程的预留算进去。后来我们为每个实例单独配置了一个“可安全增长上限”,采集层和规则层都拿这个上限做校验,不再裸用系统空闲内存。
第二个坑:主从不一致。早期执行器只改了主库,没同步改从库。后来发生一次主从切换,流量打到从库时,从库的max_connections还是旧值,瞬间拒绝连接。教训是:**拓扑变更必须原子化,要么都改,要么都不改。**从那时起,所有涉及连接数、线程类参数的主从调整,都强制走“从库先行、主库后行、都成功才算成功”的流程。
第三个坑:备份任务污染数据。备份期间buffer pool命中率会显著下降,规则引擎把这种正常的系统性波动误判为容量不足,触发了一次不必要的扩容。教训是:评估窗口必须排除已知的系统性波动窗口。后来所有规则都加了一条前置检查——“当前是否在执行备份”,并且对备份时段的数据做了打标剔除。
5.4 给后来者的一条忠告
在运维侧跑了半年之后,我最大的体会是:这套系统真正承担的不是“自动优化”四个字,而是把常规判断自动化,把异常判断留给人。规则引擎的维护成本并不低,每个参数背后的业务语义需要不断沉淀,但它的确把我们从凌晨的重复劳动里解放了出来,让我们有精力去处理真正需要脑子的东西——SQL改写、表结构变更、容量规划。
最后再分享一个细节。上线初期,我把每条规则的触发阈值都调得偏保守,宁可不出手,不可乱出手。因为在无人值守的自动变更场景里,一次误调整造成的信任损失,远比十次保守观望要大。这个尺度,比任何算法参数都重要,希望准备做类似系统的朋友能尽早想明白这一点。