基于Prometheus的MySQL自适应优化系统:从手工调参到自动闭环 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, window30m) if hit_rate 95: return RuleResult(skipTrue, reasonhit rate ok) # 前置检查空闲内存至少多出2G if ctx.free_memory_mb 2048: return RuleResult(skipTrue, reasonnot enough free memory) # 前置检查存在未提交的长事务时不调整 if ctx.long_running_transactions 0: return RuleResult(skipTrue, reasonlong 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(skipTrue, reasontoo small delta) return RuleAction(paraminnodb_buffer_pool_size, new_valuectx.current_buffer_pool_mb delta)这种写法有几个好处。第一别人review代码时不需要翻文档规则本身就是说明书。第二某个规则出了问题可以单独下线不影响其他规则。第三每一层判断都可以输出一个明确的“跳过原因”方便复盘为什么系统没动作。3.2 高价值参数-指标对照表我整理了一张实战中最常使用的参数映射表基本覆盖了这个系统80%的收益来源参数关键指标触发场景调整动作风险等级innodb_buffer_pool_size命中率、数据总量、可用内存命中率持续低于95%且数据量接近池大小每次增加10%~15%中max_connectionsThreads_connected、Threads_running、aborted连接数连接使用率超过80%但运行线程数不高每次增加20%中table_open_cacheOpened_tables增量、Open_tables当前值打开表数量接近上限且Opened_tables快速上涨每次增加25%低thread_cache_sizeThreads_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_size5.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 -pxxxx -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改写、表结构变更、容量规划。最后再分享一个细节。上线初期我把每条规则的触发阈值都调得偏保守宁可不出手不可乱出手。因为在无人值守的自动变更场景里一次误调整造成的信任损失远比十次保守观望要大。这个尺度比任何算法参数都重要希望准备做类似系统的朋友能尽早想明白这一点。