开源:基于.Net开发的数据库自治诊断平台——DBPilot

GitHub:https://github.com/SkyChenSky/DBPilot
NuGet:https://www.nuget.org/packages/DBPilot
如果对你有帮助,欢迎 Star,这是对开源作者最直接的支持。  

这篇文章连代码大概 8000 字,可能花您 15 分钟阅读。

我有话想说

  我有一个心愿:基于 .NET 做一个对后端开发日常工作真正有帮助的开源项目。

  我也有一个心病:优化数据库性能的方案我很丰富(都整理在多年前那篇《后端思维之数据库性能优化方案》里了),可实际工作中每次遇到性能问题要定位根源,都是一件很头疼的事情。

  心愿与心病,就差一次行动。这一次,我和 Claude Code + GLM 5.3 结对,完成了 DBPilot——一个数据库自治诊断平台——的开发与开源。心愿已了,心病也一并了了。

  下面,就是这桩“了心事”的完整交代。

前言

  关注我博客的朋友应该有印象,我那篇方法论里整理的优化“八大方案”——减少数据量、分库分表、空间换性能、缓存、一主多从、换存储……那是典型的“事前”思维:在设计与架构层面把性能问题消解在发生之前,治的是“未病”。

  但现实是,没有系统能把所有问题都防在门外。等故障真的发生了——特别是那些“来无影去无踪”的偶发飙高——你会发现手里缺的不是方案,而是证据原因:问题出在哪、从什么时候开始的、谁干的、为什么好好的突然就有问题了。这正是开头那个“心病”的由来。

  而 DBPilot,干的就是“事后”那一环——持续采集、留痕、定位问题根源,治“已病”时的那一脉。跟那篇方法论放在一起正好互补:方法论告诉你怎么解决,DBPilot 帮你找到根源

  先说清楚它是干嘛的:自托管的数据库性能诊断系统。你给它几个数据库实例的连接信息,它持续采集性能数据,然后在一个 Web 控制台里完成 DBA 的日常工作。支持监控 SQL Server(2008~2022)、MySQL 8.0+、PostgreSQL 13+ 三种引擎,NuGet 上一行命令装齐。

为什么要做这个东西?

  先说说现实的处境。不是每家公司的数据库都上了云监控,也不是每家都配得起 DBA,大多数时候数据库出了状况,都是咱们开发自己顶上去——可处理数据库性能问题这件事,别说得心应手了,连“快速”两个字都谈不上。

  最磨人的是那种偶发的数据库报警:一刹那的飙高,等你有反应登上去看,现场已经没了,报警过了就是过了,什么记录、什么证据都不剩。你安慰自己“偶尔一下而已”,于是不了了之。

  我一直认为:问题的反复出现,是因为没有找到根本原因——有效的技术手段,只需要使用一次。隐患不理,只会一直在悄悄长大,持续久了,某天突然爆发——整个系统缓慢与阻塞,连锁反应一片。这个时候想定位?无从下手,只能凭着经验、记忆去猜:“上周是不是发过版?那天好像跑了张大报表?”

  从技术上说,这事儿的根源就一句:DMV 是内存里的累计值,重启就清零——不留痕就没有历史,没有历史就没有趋势,没有趋势你就说不清“它是从什么时候、因为什么开始恶化的”

  没有证据,猜,是唯一的排查手段。

  商业云产品确实解决了这个问题,体验也不错,但要么贵,要么把你锁死在自家云上。对于很多中小公司、还有像我这样“自己的数据库自己疼”的个人开发者,一直缺一个部署轻、引擎全、开箱即用的选择。

  于是就有了 DBPilot。说句实在话,开发过程中遇到头疼的问题是常有的事,而优秀的开发人员,得学会利用工具协助自己解决问题——无论是我以前写 IDE 插件、整理工具库,还是现在做一个数据库自治诊断的开源项目,都是同一个道理。

  此外,一开始我只是想给自己写个工具,写着写着发现这个东西的完成度已经值得分享出来了——那就开源吧,反正软件工程没有银弹,但至少可以多一个选择。

五脏俱全

  老规矩,直接上图

overview

  性能洞察是我最喜欢的功能:按平均活跃会话(AAS)把负载拆解成 CPU / 锁 / IO / 等待,一眼看出是哪类资源打满了,还能下钻到单条 SQL 的贡献。资源到底是谁吃掉的,不用再猜了。

insight

  性能趋势:CPU / 内存 / PLE / QPS·TPS / IO / 磁盘用量,10 秒粒度,长区间自动降采样。图上还可以叠加事件竖线——死锁、慢 SQL、计划变更发生在哪个时刻,悬停就能看到,“变慢”和“出事”的时间对应关系一目了然。

metrics

  死锁分析:自动捕获死锁事件,图形化展示环路与各方语句、持锁/等待关系。

deadlocks

deadlocks-detail

  阻塞分析:实时阻塞树(头阻塞者、链路、等待时长)+ 历史统计与趋势。那个“睡着拿锁”的头会话,DBPilot 会把它的最后一条语句也补查出来。

blocking

blocking-detail

  索引使用率慢查询日志:读写计数识别无用索引(附删除/禁用脚本),超阈值 SQL 自动留档(完整文本、耗时、IO、指纹)。

index-usage

slowsql

  还有 Top SQL(相同指纹自动合并、噪音一键排除)、执行计划(版本快照 + 变更事件 + 前后资源对比 + 计划树)、缺失索引(优化器推荐 + 建索引脚本)——篇幅原因不一一贴图了,仓库 README 里有完整截图。

引擎支持

  监控能力矩阵直接看表(◐ 表示降级形态):

能力
SQL Server
MySQL 8.0+
PostgreSQL 13+
SQLite
平台存储
✅ 单 exe + 单 db 文件
会话 / 阻塞
Top SQL(指纹合并)
慢日志
✅(XE 事件)
✅(slow_log 表)
◐ 模板榜
性能趋势
死锁分析
✅ 事件明细 + 图形
◐ 趋势
执行计划快照/变更
缺失索引建议
索引使用率
碎片扫描

  注意表里其实藏着两个独立的轴:横着看是“某引擎作为被监控对象能提供什么数据”,第一行“平台存储”则是“某引擎作为历史数据仓库够不够格”。两个轴任意组合——平台库用 PostgreSQL、同时监控 SQL Server 和 MySQL,完全没问题。SQLite 这一列只服务存储轴:它不能作为被监控实例接入,但作为平台库,它就是“单 exe + 单 db 文件”的嵌入式最小部署,连数据库服务器都不用单独准备。

  每一格的降级原因和引擎前置条件(比如 PG 要装 pg_stat_statements 扩展、MySQL 要开 performance_schema),README 里都写得清清楚楚——连接测试的向导也会逐项自检,缺什么权限、要改什么参数,直接告诉你。

AI集成 

  这是我做这个项目时最兴奋的部分,也是跟同行讨论得最多的部分。

  DBPilot 内置了一个只读的 MCP Server,把平台的指标、慢 SQL、死锁、阻塞、索引证据以 11 个只读工具交给 AI agent,配好之后直接用自然语言问:

“用 dbpilot 查一下实例最近一小时的负载,有没有变慢的 SQL或者死锁?并对工作区项目里的代码进行定位与修复建议”

  怎么用起来?两步。第一步,appsettings.json 里打开开关、配个足够随机的 key(默认关闭):

"DBPilot": {
  "Modules": { "Mcp": { "Enabled": true } },
  "Mcp": { "ApiKey": "换成一个足够随机的 key" }
}

  第二步,把 MCP Server 注册进你的 AI 工具。Claude Code 一行命令:

claude mcp add --transport http dbpilot http://localhost:5200/mcp --header "X-Api-Key: <你的key>"

  Codex CLI 则是在 ~/.codex/config.toml 里加一段:

[mcp_servers.dbpilot]
url = "http://localhost:5200/mcp"

[mcp_servers.dbpilot.http_headers]
X-Api-Key = "<你的key>"

  claude mcp list 验证通过后,会话里直接开口问就行(示例里用的是 5200 端口的 Sample,按你实际部署的端口改)。这也是说 DBPilot “AI 就绪”的原因:诊断系统的证据链已经铺好,AI agent 即插即用。

image

  有位同行在上一篇文章的评论区问过我一个特别好的问题:Agent 读到的数据库状态是快照还是实时的?偶发飙高那种“来无影去无踪”的场景,Agent 看到的会不会已经是事发现场之后的数据? 这个问题引发了我不少思考,借这篇文章把答案展开说清楚。

  是快照为主,而且我认为快照恰恰是必需的——正因为“来无影去无踪”,才需要把当时的情况记录下来,事后分析与归因本身就是解决途径之一。DBPilot 的证据体系由三块拼成:瞬时帧靠 DMV 采样、历史趋势靠自采样差值、事件类证据(阻塞/死锁/慢 SQL)靠独立机制留档——死锁走扩展事件文件、慢 SQL 走阈值触发,不受采样节奏限制。三块拼起来的证据链,Agent 拿到的是跟 Web 页面一样的完整现场。唯一例外是实时阻塞,那个是查询时由 DBPilot 实时连实例取的数据——连库的依然是平台,不是 Agent。

  这个系统背后的思想是:MCP 是证据提供者,推断完全交给 agent。Web 页面能看到什么,MCP 就提供什么,人和机器的信息口径是一致的。裁剪策略上,MCP Server 只做了两类裁剪——SQL 文本长度和返回行数,出发点是 token 消耗和安全性。如果 AI 拿着跟人一样的证据还是定位不到问题,那说明的不是 AI 不行,而是系统还需要补充维度数据、减少漏采信息——这是把“诊断准确率”从玄学变成了可迭代工程的正反馈。

  安全性上也值得说一句:Agent 全程不直连数据库。MCP Server 给出去的每一条证据,都来自 DBPilot 已经采集落库的结果——Agent 手里没有数据库的连接串和账号,不存在“它把所有库、表、数据翻个遍”的风险;它能拿到的,只是平台按诊断口径整理好的证据,再叠加只读工具、ApiKey 鉴权、默认关闭这三道闸。对“让 AI 碰生产库”心存顾虑的团队来说,这层边界让 AI 接入变得可控:证据交给 agent,敏感信息始终在你自己手里

快速开始

  最快路径,NuGet 包走起:

mkdir dbpilot && cd dbpilot
dotnet new web
dotnet add package DBPilot

  Program.cs 完整代码,就这么多:

using DBPilot.AspNetCore.Extension;
using DBPilot.Core.Providers;

var builder = WebApplication.CreateBuilder(args);
builder.AddDBPilot(o => o.PlatformEngine = DbpilotEngine.SqlServer);  // 平台库引擎

var app = builder.Build();
app.UseDBPilot();
app.Run();

  appsettings.json 一条连接串,指向一个空库(表结构启动时自动创建):

{
  "DBPilot": {
    "ConnectionString": "Server=...;Database=dbpilot;User Id=...;Password=...;TrustServerCertificate=true"
  }
}

  dotnet run,浏览器打开 http://localhost:5000,默认账号 admin / dbpilot@2026(部署后记得改密码),然后实例管理 → 新增接入第一个被监控实例就行了。实时页面立即可用,历史数据随运行积累。

  喜欢从源码构建的话,仓库里带了四个 Sample(SQL Server :5200 / MySQL :5201 / SQLite :5203 / PostgreSQL :5204),clone 下来填个连接串就能跑。

思想与实现

  功能截图谁都会贴,这部分才是文章的主体。做架构这么多年,我越来越觉得方案的好坏不在用了什么新东西,而在于对问题的理解够不够深。下面按业务架构、项目架构、具体实现三层来讲——业务架构回答“凭什么能定位问题”,项目架构回答“凭什么能走得远”,具体实现回答“数字凭什么值得信”。

业务架构

arch-business

  业务架构层面,我全部的思考都围绕一件事:怎么让开发人员方便、快速地定位到问题根源。一张图总结如上,拆开是四件事。

诊断链完整。

  诊断系统的本体不是页面和图表,是证据。DBPilot 的证据体系由三块拼成:瞬时帧靠 DMV 采样、历史趋势靠自采样差值、事件类证据(阻塞/死锁/慢 SQL)靠独立机制留档。为什么要三块?因为任何单一机制都有盲区——采样有频率上限,偶发突刺可能漏采;而事件留档(死锁走扩展事件文件、慢 SQL 走阈值触发)不受采样节奏限制,正好把“来无影去无踪”的场景兜住。三块拼起来,才能尽最大可能覆盖偶发问题的事后归因。

定位要快。

  证据齐了,还得让人顺着它快速走到根因,不能让用户在十个页面之间来回翻。DBPilot 的几条主路径都是按“下钻”设计的:性能洞察按平均活跃会话(AAS)把负载拆解成 CPU / 锁 / IO / 等待,一眼看出是哪类资源打满了,再下钻到单条 SQL 的贡献;性能趋势图上可以直接叠加事件竖线——死锁、慢 SQL、计划变更发生在哪个时刻,悬停就能看到,“变慢”和“出事”的时间对应关系一目了然;Top SQL 用指纹把同形语句合并成模板,配合噪音排除,榜首永远是真凶而不是背景音。

  还有一条贯穿的约束:实时与历史,一套口径。实时页面直查被监控实例,历史页面读平台库,两路数据共用同一套噪音排除规则、指纹算法、时间窗语义——避免“实时页看得到的 SQL,历史页里消失了”这种最伤信任的场景。这条约束往后延伸到 AI,就是前面那句话:Web 页面能看到什么,MCP 就提供什么。

AI 支持。

  诊断链不光要给人用,也要给 AI 用。DBPilot 内置只读的 MCP Server,把上面这些证据以工具的形式交给 AI agent——这就是前面“AI 就绪”那节展开说的:MCP 是证据提供者,推断完全交给 agent,人和机器的信息口径是一致的。

安全两条红线。

  性能安全:诊断工具自己变成性能问题,是最大的讽刺。DBPilot 常规运行对被监控实例的开销 <1% CPU:采集只读内存里的元数据视图(SQL Server 的 DMV、PG 的 pg_stat_*、MySQL 的 performance_schema),不扫业务表、不产生物理 IO、不对用户对象加锁;历史数据全部写平台库,不碰被监控实例。你甚至可以自证——跑一天后查一下监控账号的累计 CPU 秒数,那就是真实开销:

SELECT login_name, SUM(cpu_time)/1000 AS cpu_seconds_total
FROM sys.dm_exec_sessions
WHERE host_process_id IS NOT NULL
GROUP BY login_name;

  这里有个隐含取舍:把采样频率拉到毫秒级可以减少漏采,但开销就不可接受了。在证据完整性和实例健康之间,我选择偏向实例健康,漏采的缺口交给事件留档机制去补。

  数据安全:被监控实例的连接信息全程只存在平台库里;MCP 侧前面说过——Agent 不直连数据库、只拿只读证据、ApiKey 鉴权、默认关闭。给 AI 的边界和给人的边界,是同一套

项目架构

arch-project

Core 零引擎知识。

  包结构自底向上是 Common → Storage → Core → AspNetCore 四层:Common 基础工具、Storage 实体与方言抽象、Core 采集与诊断的全部业务、AspNetCore 组合出口(Web/API/调度);引擎包(SqlServer / MySql / PostgreSql / Sqlite)平行放在旁边,每个引擎实现一份“数据源适配”。

  关键约束只有一条:Core 里不允许出现任何引擎名判断。每个引擎包用一个特性类自声明“我是谁、我不支持哪些功能”,注册中心按实例的引擎把请求路由到对应的 Provider。

  这条约束是接第三个引擎时收敛出来的。前期我也在 Core 里 if else 判引擎名,每加一个引擎多一串分支,改一处漏三处,后来全部推翻重构成能力自声明——现在加一个新引擎,Core 一行代码都不用改。让依赖单向、让引擎知识留在引擎包里,前期费点劲,后期全是复利。

  自声明的另一面是诚实降级。三种引擎的数据面差异很大:PostgreSQL 没有慢日志对等物、MySQL 没有死锁事件流,DBPilot 不硬造假数据——无对等数据源的功能,UI 自动隐藏或降级成对等形态(PG 的死锁页展示趋势、慢 SQL 降级为模板榜),接入向导逐项自检,缺什么权限、要改什么参数直接告诉你。宁可少展示,也不误导。顺带一个红利:平台库用什么引擎、监控什么引擎,成了完全独立的两个轴,任意组合——这就是前面引擎支持那一节说的“两个独立的轴”的实现根基。

微信截图_20260914211818

按需分离部署。

  DBPilot 的进程形态不是写死的,而是按部署需要选择:同一个程序,通过角色开关跑成“Web + 采集”一体、纯 Web、纯采集器三种形态,1 个采集器 + N 个 Web 的多进程部署完全支持——个人开发者一个进程跑完,公司级要把 Web 层横向扩容,用的都是同一套代码。要分离时再分离,不需要时一体跑,这是按需的分离,不是强制的拆分。

  代码里就是两个预设,一行切换:

builder.AddDBPilot(o => o.WebOnly());       // 纯 Web:多实例横向扩容,只读平台库
builder.AddDBPilot(o => o.CollectorOnly()); // 纯采集器:只采集落库,不起 Web
// 都不调(默认):Web + 采集一体,个人部署一个进程跑完

  采集器内部,所有定时采集任务共用一个三段式骨架(CollectRunner),伪代码大致是:

// 1. 加载启用的实例
var instances = await LoadEnabledAsync();

// 2. 并行检测:多实例互不拖慢,单实例失败进退避,不炸整批
var results = await ForEachEnabledAsync(instances, async instance =>
    await DetectAsync(instance));          // 只连被监控实例、只产内存结果

// 3. 串行落库:统一写平台库,失败回退游标
foreach (var r in results)
    await PersistAsync(r);

  为什么落库要串行?表层原因是 ORM 的上下文对象非线程安全,但更本质的考虑是:平台库是全局单写点,把写收敛到串行队列,天然规避了并发写冲突,也让“采集成功但落库失败回退游标”这个一致性保障变得简单——落库失败时旧游标快照回退,下一拍重采,不丢数据。检测并行保证的是“监控 50 个实例时,一个实例网络抖动不会拖住其他 49 个”。

具体实现

arch-impl

  架构说得再漂亮,魔鬼还是在细节里。挑四个最有故事性的实现,抠到代码层。

数据源取舍。

  SQL Server 圈里两个“名门正派”的数据源——dm_os_wait_stats 等待统计和 Query Store——我都没有用,说说为什么。

  dm_os_wait_stats 在我认知里是实例级聚合数据——从服务器启动累积到现在,谁也说不清“这一分钟是谁在等”,噪音多、归因粒度不够。DBPilot 用的是 dm_exec_requests 按会话采样:每隔几秒采一帧正在执行的请求,通过 session id 把“在发生这个事情”记录下来,等待类型的桶化归并(CPU / 锁 / IO / 其他)是在采样帧上做的,粒度是会话级而非实例级。

  Query Store 很好,真的,但要 SQL Server 2016 以上——而我给自己定的目标是兼容 2008。很多传统企业的存量实例版本老得超出想象,恰恰是它们最缺监控。所以 Top SQL 走的是 dm_exec_query_stats 差值采集 + 指纹归一化(下面细说),老版本实例一样有完整的 Top SQL 历史趋势。

指纹与差值。

  Top SQL 要解决两个问题。

  一是同形语句合并WHERE id=1WHERE id=2 是两条语句,性能视角却是同一个模板。DBPilot 直接用引擎原生指纹归一——SQL Server 的 query_hash、MySQL 的 DIGEST、PostgreSQL 的 queryid,拿不到时用归一化后的语句文本做哈希兜底。

SELECT * FROM orders WHERE id = 1;  -- query_hash = 0x9A2F07…
SELECT * FROM orders WHERE id = 2;  -- query_hash = 0x9A2F07… ← 同一指纹,合并为一个模板

  二是累计值要差分dm_exec_query_stats 这类视图给的是从启动累计到现在的值,直接展示毫无意义。每一拍记一份快照,与上一拍相减,得到这个区间的真实增量。难点在三个边界:

  • 实例重启:累计值清零,差分是负的天文数字——这一拍不落库,只重建基线;
  • 新指纹首见:没有上一拍可差——落全量,它确实在这个区间执行了这么多次;
  • 负差值:没重启却差出负数,说明计划被驱逐出缓存、计数器归零过——跳过这一拍,基线同步成当前值。

  三个边界,落到代码就三行:

var diff = current.Total - last.Total;                   // 与上一拍快照差分
if (restarted || diff < 0) { last = current; return; }   // 重启 / 负差:不落库,重建基线
await SaveAsync(firstSeen ? current.Total : diff);       // 首见落全量,否则落增量

  边界不处理,趋势图上就会出现“负 QPS”或莫名断崖,用户对数据的信任就没了。

游标与判重。

  死锁、慢 SQL 这类事件证据走扩展事件(XE)文件增量读取:记住每次读到的文件偏移做游标,下一拍从游标续读。踩过两个坑。

  一是无新事件时必须保留旧游标。早期版本图省事,无行就把游标重置,结果每拍全量重扫整个文件,CPU 和 IO 周期性尖刺。游标只有“前进”和“原地”两种状态,永远不回退(唯一例外是进程重启清零、全量重扫一次,这反而是特性:清库重灌后历史自动重建)。

var rows = ReadXe(cursor);       // 从上次读到的偏移续读
if (rows.Count == 0) return;     // 无新事件:游标原地,绝不清零
cursor = rows[^1].Offset;        // 游标只前进,永不回退

  二是事件判重。XE 时间戳是亚毫秒精度,落库列是毫秒精度,参数化查询发送 DateTime 时还会按数据库的时间网格再舍入一次——三重舍入叠加,“读回来的时间”和“刚写入的时间”能差出 1~2 毫秒,等值匹配判重失效,同一事件被当成新事件重复落库。解法:比对改用 ±3ms 邻域窗口,用大于小于夹出一个范围,而不是死等相等:

var seen = saved.Any(t => Math.Abs((t - row.Time).TotalMilliseconds) <= 3);

  这类“精度问题伪装成逻辑问题”的坑,是整个项目里最耗 debug 时间的类别,没有之一。

噪音排除。

  监控系统上线第一天,Top SQL 榜单第一名的 CPU 占比 68%。点开一看,是监控自己的巡检语句。自己的采集语句成了被监控实例的头号负载,这个笑话一点也不好笑。

  排除平台自身的 SQL,直观做法是给采集语句加注释标记 /* dbpilot */。但这里有个非常隐蔽的坑:多语句批处理里,语句级偏移量会剥离批首注释——你把标记写在批的第一行,落到 DMV 的语句文本里标记就没了,排除规则形同虚设。解法是标记必须内联到每条语句内部:SELECT /* dbpilot */ col FROM ...,让标记跟着语句走。

  在此之上还有一层层叠加的噪音治理:云厂商内部维护会话的特征排除(不然 AAS 基线凭空高出十几个会话)、已知系统巡检语句的模式库(默认几十条 LIKE 特征,用户可配置追加)、以及页面上手动“排除”生成的跨实例指纹黑名单——排除动作只对未来生效,历史差值不追补,因为采集端的语义就是“当时的口径”。

  这四个细节说下来,对应回来就是:数据源选得对(取舍)、证据可信(差值)、证据不丢(游标)、证据干净(噪音)。业务架构决定系统能不能定位问题,项目架构决定它能不能走得远,具体实现决定它的数字值不值得信

说在最后

  对项目的介绍到这里就差不多完了,后续规划说三个方向:

  一是告警促发通知与采集——偶发突刺漏采始终是采样机制的边界,计划在性能趋势上加告警策略(CPU、IOPS 等指标在某时间段突增百分比阈值),邮件短信通知的同时自动促发系统立即采集一帧,把漏采窗口进一步压小;

  二是AI 诊断的深化——接上面告警:通知发出的同时,让 AI 结合平台证据与项目源码做一站式分析定位,从“给 AI 工具”进化到“AI 直接给结论”;

  三是更多引擎能力的补齐——复制监控、表膨胀巡检这些 PG/MySQL 的候选项都在计划上。

  如果你在使用中遇到问题,或者对某个设计有不同看法,欢迎提 Issue 或者直接博客评论——讨论本身就是开源最好的部分

  回到开头那篇优化方法论:那篇治的是“未病”——设计、架构层面把问题消解在发生之前;而真到了“已病”的时候,得先用 DBPilot 把脉定位根源,再回到八大方案里对症开药。未病先防、既病快诊、对症下药——这套闭环凑齐了,才算是对数据库性能问题有个完整的交代。

  路漫其修远兮,吾将上下而求索。好了,今天就分享到这里,我们后续见。感兴趣的朋友可以留言区交流,我们一起讨论。

posted @ 2026-09-15 08:57  陈珙  阅读(355)  评论(2)    收藏  举报