
简介基于Dubbo的服务管理与监控系统项目面向需要落地分布式服务治理的Java工程师与架构师。资源打包为一个完整可运行的前后端工程包含服务注册发现、服务治理、监控告警、配置管理等核心功能模块并配套相应的管理后台页面能帮助读者快速理解这套微服务架构下的监控方案。压缩包内全部文件共698个前端页面以HTML、CSS、JavaScript为主后端逻辑以Java源码为核心同时配有多样的配置文件如xml、properties、yml以及sql数据库初始化脚本和bat/sh安装启动脚本内置MongoDB、Lucene、MySQL等依赖环境的配置与启停命令目录结构完整便于直接部署与二次开发。整个压缩包仅4.37MB代码量较为精简省去了冗余依赖能轻松通读核心逻辑。目前已有31人学习下载尤其适合正在学习Dubbo原理、想参考真实监控系统产品级实现的开发者也适合作为高校软件工程项目的实战蓝本。1. 基于Dubbo的服务管理与监控系统先看清它和Zabbix的区别很多团队的监控体系里已经有Zabbix盯着主机CPU和网络但一旦线上Dubbo服务出现接口超时、调用链断裂Zabbix只能告诉你机器负载升高却说不清是哪个provider的哪个方法拖垮了调用链。这套基于Dubbo服务管理与监控系统抓的正是服务维度接口调用次数、平均耗时、错误率、活跃服务列表。它把MongoDB当监控数据存储、Lucene做检索索引、MySQL保存服务元数据再通过Bootstrap渲染成管理面板。适合正在做Dubbo服务治理、需要自建轻量监控平台的团队也可以作为学习Dubbo SPI和Filter机制的完整案例。2. 环境初始化install-mongodb.bat、install-lucene.bat、install-mysql.bat 脚本拆解2.1 这三套组件在系统里的角色从项目根目录的start-mongodb.bat、start-lucene.bat、start-mysql.bat以及对应的install-*.bat能看出这套系统选择了「MySQL MongoDB Lucene」的混合存储架构。我把这个选择拆开讲组件端口在系统中的职责为什么选它MySQL3306保存服务元数据、用户权限、监控规则事务一致性强适合关系型配置MongoDB27017接收 Dubbo Filter 上报的调用监控数据写入吞吐高适合时序型日志Lucene9000内置HTTP服务给监控数据建立倒排索引支持模糊搜索全文检索效率远高于MySQL LIKE这里要注意Lucene 本身是一个 Java 库不是独立服务。所以install-lucene.bat做的通常不是安装 Lucene而是部署一个基于 Lucene 的索引服务常见做法是写一个 Spring Boot 应用暴露 REST 接口或者只是把 Lucene 的依赖目录解压到本地 Maven 仓库。我一般会在项目中看到这个脚本时先确认它到底调了mvn install还是直接复制 jar。2.2 写一个可用的 install-mongodb.bat 样例由于原项目脚本内容未完整公开我这里补一个最常见的安装脚本写法你做 Windows 部署时可以照抄echo off set MONGO_HOMED:\software\mongodb-win32-x86_64-4.4.2 if not exist %MONGO_HOME%\bin\mongod.exe ( echo [INFO] MongoDB binary not found, download manually. exit /b 1 ) xcopy /E /I /Y %MONGO_HOME% D:\server\mongodb\ if errorlevel 1 ( echo [ERROR] Copy MongoDB failed. exit /b 2 ) echo [INFO] MongoDB installed to D:\server\mongodb这段脚本的逻辑是先检查 MongoDB 的二进制包是否已经存在如果存在就复制到D:\server\mongodb复制失败则退出。注意xcopy的/E表示复制子目录包含空目录/I当目标不存在时自动创建目录/Y跳过确认。实际部署时install-*.bat通常还会做两件事写入 Windows 服务注册表、初始化数据目录。如果你不想让脚本在执行中弹确认框/Y必须加否则后台运行会一直卡住。2.3 start-*.bat 的启动顺序start-mongodb.bat和start-mysql.bat是服务启动脚本但不能同时打开三个窗口乱点。正确顺序是先启动 MySQL因为后端的服务元数据表要在这时候建好。再启动 MongoDB让监控写入端准备好。最后启动 Lucene 索引服务否则监控数据写入时没有索引可查。对应start-mongodb.bat的参考内容echo off set MONGO_DB_PATHD:\server\mongodb\data\db if not exist %MONGO_DB_PATH% mkdir %MONGO_DB_PATH% mongod --dbpath %MONGO_DB_PATH% --port 27017 --bind_ip 127.0.0.1 --logpath D:\server\mongodb\logs\mongod.log --fork echo MongoDB started on port 27017这里--fork在 Windows 上并不生效Windows 需要start /b mongod ...或者使用mongod --install注册成系统服务。如果你直接跑这个脚本会看到--fork被忽略这是常见的坑。正确的 Windows 写法是start /D D:\server\mongodb\bin mongod.exe --dbpath D:\server\mongodb\data\db --port 27017 --bind_ip 127.0.0.1注意--dbpath指向的数据目录必须存在否则 mongod 会直接退出。--bind_ip建议只绑定内网地址服务监控系统不需要对公网暴露。MySQL 的启动脚本类似但还要额外设置--character-set-serverutf8mb4否则中文服务名存入后查询乱码。2.4 setting.bat 是配置入口项目里的setting.bat通常是环境变量初始化脚本。因为多个模块都需要知道 MongoDB 和 MySQL 的地址所以把所有配置集中到一个脚本里再用call setting.bat引入。内容类似set MYSQL_HOST127.0.0.1 set MYSQL_PORT3306 set MYSQL_USERmonitor set MYSQL_PASSWORD123456 set MONGO_HOST127.0.0.1 set MONGO_PORT27017 set LUCENE_INDEX_DIRD:\server\lucene\index set DUBBO_REGISTRYnacos://127.0.0.1:8848这里我把DUBBO_REGISTRY指到了 Nacos原因是原生 Dubbo 默认注册中心过于简单在服务数量超过 50 个时推送延迟明显换成 Nacos 后服务健康检查与配置推送都更稳定。如果你的环境仍然是 ZooKeeper把这一行改成zookeeper://127.0.0.1:2181就行。setting.bat还有一个好处是统一管理端口如果默认端口被占用只需要改这一个文件不用翻所有启动脚本。3. 服务监控数据采集Dubbo Filter 与 MongoDB 的写入链路3.1 从注册中心拿服务列表还是从调用链拿监控点这套系统的服务管理部分会定期从注册中心拉取 Provider 和 Consumer 列表。我最初做这个功能时试着直接解析 Dubbo 的 URL 参数发现太脆弱后来改走 Dubbo 官方提供的RegistryService接口稳定多了。如果你用的是 Nacos可以直接 curlcurl -X GET http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceNamecom.example.UserServicegroupNameDEFAULT_GROUP返回 JSON 里会带hosts数组每个元素包含ip、port、healthy、weight等字段。这个接口用于服务列表的“管理”但“监控”要的是每次调用的耗时和异常所以还得靠自定义 Dubbo Filter。这一段选取注册中心接口的逻辑其实也是 Dubbo 面试题里偏向实践的一道题面试官通常会问Dubbo 注册中心挂了服务还能调用吗答案是能因为消费端有本地缓存列表。3.2 自定义 Dubbo Filter 采集调用指标Dubbo 提供了org.apache.dubbo.rpc.FilterSPI 扩展点实现它并注册为Activate(group provider)就能在 Provider 端拦截到每次真实调用。下面的代码是这套系统最核心的采集部分Activate(group provider, order -100) public class MonitorFilter implements Filter { private static final String MONGO_COLLECTION invoke_metrics; Override public Result invoke(Invoker? invoker, Invocation invocation) throws RpcException { long start System.currentTimeMillis(); Result result null; boolean success false; try { result invoker.invoke(invocation); success !result.hasException(); return result; } finally { long cost System.currentTimeMillis() - start; String serviceKey invoker.getInterface().getName(); String methodName invocation.getMethodName(); String host RpcContext.getContext().getRemoteHost(); saveMetric(serviceKey, methodName, host, cost, success); } } private void saveMetric(String service, String method, String host, long cost, boolean success) { Document doc new Document(service, service) .append(method, method) .append(host, host) .append(cost, cost) .append(success, success) .append(time, new Date()); MongoClients.create(mongodb://127.0.0.1:27017) .getDatabase(dubbo_monitor) .getCollection(MONGO_COLLECTION) .insertOne(doc); } }这段代码的问题很明显每个请求都创建一次 MongoClient性能极差。正确做法是用连接池并做批量写入。我一般会把 MongoClient 做成静态单例然后攒到一定条数再批量插入private static final MongoClient MONGO_CLIENT MongoClients.create(mongodb://127.0.0.1:27017); private void batchSave() { ListDocument documents new ArrayList(); // 从队列里取出积攒的 metrics MONGO_CLIENT.getDatabase(dubbo_monitor).getCollection(invoke_metrics) .insertMany(documents); }Activate注解里group provider表示这个 Filter 只在 Provider 端生效order -100是为了让它排在内置 Filter 后面避免在上下文还没初始化时读取RpcContext。RpcContext.getContext().getRemoteHost()拿到的是调用方 IP最终写入 MongoDB 时要用Document对象避免手拼 JSON 出错。3.3 监控数据落库后的聚合查询原始监控数据是每调用一次写一条时间长了 MongoDB 里的文档数量会非常大。所以一般会有一个定时任务每分钟把原始数据按 service method 聚合成一分钟一条的摘要数据db.invoke_metrics.aggregate([ { $match: { time: { $gte: ISODate(2024-01-01T00:00:00Z), $lt: ISODate(2024-01-01T00:01:00Z) } } }, { $group: { _id: { service: $service, method: $method }, count: { $sum: 1 }, avgCost: { $avg: $cost }, maxCost: { $max: $cost }, errorCount: { $sum: { $cond: [$success, 0, 1] } } } } ])这段聚合脚本把一分钟内的调用次数、平均耗时、最大耗时、错误数直接算出来写入另一个 collection前端查摘要表只需要扫几千条数据。注意$cond的使用$success字段是布尔值聚合框架里不能直接做布尔判断用$cond显式返回 0 或 1 才能参与$sum。为了控制存储成本我通常会给 MongoDB 做定期清理表结构大概是这样collection内容生命周期invoke_metrics原始调用数据每条调用一条保留7天invoke_metrics_min按分钟聚合后的摘要数据保留30天service_registry注册中心拉取的服务列表每次全量更新4. Lucene 索引与 Bootstrap 面板让监控数据可以被搜到、被看懂4.1 为什么用 Lucene 而不是 MySQL 的 LIKE监控数据除了看聚合图表还经常要按服务名、方法名、异常信息做模糊查询。MySQL 的LIKE %UserService%在数据量破千万后全表扫描会拖垮线上库。Lucene 走倒排索引查询时是在索引文件里二分查找速度比全表扫描快两个数量级。这就是项目里install-lucene.bat存在的意义把 MongoDB 里的数据同步进 Lucene 索引目录提供全文检索能力。4.2 用 Lucene 构建监控索引的完整代码假设我们要索引两个字段服务名和方法名。每次从 MongoDB 读取一批新记录交给 Lucene 的IndexWriterpublic class MonitorIndexer { private Directory directory; private Analyzer analyzer new StandardAnalyzer(); private IndexWriterConfig config new IndexWriterConfig(analyzer); public void buildIndex(ListMetric metrics) throws IOException { config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); try (IndexWriter writer new IndexWriter(directory, config)) { for (Metric m : metrics) { Document doc new Document(); doc.add(new StringField(service, m.getService(), Field.Store.YES)); doc.add(new StringField(method, m.getMethod(), Field.Store.YES)); doc.add(new TextField(fullText, m.getService() m.getMethod(), Field.Store.NO)); writer.addDocument(doc); } } } }StringField适用于服务名这种整体匹配的场景它不分词TextField会先分词再建索引所以我把 service 和 method 拼成一个fullText字段这样搜索关键字时只要fullText:UserService就能命中。写入时Directory一般用FSDirectory.open(Paths.get(D:\\server\\lucene\\index))这里没有指定索引版本是因为新版 Lucene 的 API 已经去掉了版本号参数。字段类型的选择可以参考这个表字段类型说明serviceStringField精确匹配不分词methodStringField精确匹配不分词fullTextTextField分词用于关键字搜索与之对应的查询代码public ListString search(String keyword) throws Exception { Directory directory FSDirectory.open(Paths.get(D:\\server\\lucene\\index)); try (IndexReader reader DirectoryReader.open(directory)) { IndexSearcher searcher new IndexSearcher(reader); Query query new QueryParser(fullText, new StandardAnalyzer()) .parse(QueryParser.escape(keyword)); TopDocs topDocs searcher.search(query, 100); ListString result new ArrayList(); for (ScoreDoc doc : topDocs.scoreDocs) { result.add(reader.document(doc.doc).get(service)); } return result; } }QueryParser.escape(keyword)这一步很多人会漏。用户输入的关键字如果包含 - || !这些特殊符号解析时会被当成语法直接报语法异常先escape再传进去等于是告诉 Lucene 把用户输入当普通文本处理。搜索返回的ScoreDoc里的doc是内部文档 ID不是业务 ID所以要用reader.document(doc.doc)重新拿回被存储的字段。4.3 Bootstrap 渲染管理面板setting.bat 控制阈值后端将 Lucene 查询结果和 MongoDB 聚合结果拼成 JSON前端用 Bootstrap 表格和卡片展示。这里的bootstrap.css出现在项目根目录说明页面是用静态资源直接引用的没有走前端构建工具。面板一般分三块服务列表来自注册中心展示 provider 的 IP、端口、权重。调用监控来自 MongoDB 聚合表展示 QPS、平均耗时、错误率。搜索框调用 Lucene 的 REST 接口输入UserService或方法名直接搜。阈值在setting.bat里也有定义比如set ERROR_THRESHOLD0.05 set SLOW_THRESHOLD500ERROR_THRESHOLD0.05表示错误率超过 5% 就标红SLOW_THRESHOLD500表示单次调用耗时超过 500ms 算慢调用。Bootstrap 面板的展示逻辑就是拿聚合数据跟这两个阈值比较然后决定给表格行加table-warning还是table-danger样式。4.4 监控系统与农业大棚环境监控的相似点如果你做过农业大棚环境监控系统你会发现它跟 Dubbo 监控的架构是相通的大棚里的一堆温度湿度传感器对应这里的 Dubbo Provider传感数据上报到网关对应自定义 Filter 上报到 MongoDB大棚后台的实时曲线图对应这里的 Bootstrap 面板。区别在于 Dubbo 监控更强调服务间调用关系而农业大棚更强调空间位置与时间序列。理解了这套对应关系把 Dubbo Filter 的采集逻辑换成 MQTT 传感器上报监控面板换成 ECharts 折线图就是一个农业大棚环境监控系统。5. 进阶排错索引合并、数据校验和 Windows 脚本编码坑5.1 用 Dubbo 原生命令验证数据是否正确服务管理监控系统上线后先抓一次真实的 Dubbo 调用对比数据。Dubbo Admin 里有“服务详情”页面能看到每个方法的调用次数拿它和本系统 MongoDB 聚合结果比对。如果偏差不大说明 Filter 采集链路正常如果差很多多半是 Filter 在异步线程里丢了RpcContext。排查方法是观察RpcContext.getContext().getRemoteHost()是否在finally块里拿不到值解决方法是把它提前到 try 块开始处保存成局部变量。5.2 Lucene 索引段合并与清理Lucene 每个 commit 都会产生一个段文件长期运行会产生大量小段拖慢搜索。定时任务里要加上段合并writer.forceMerge(1);forceMerge(1)把所有小段合并成一个段代价是执行期间的 IO 会明显升高所以建议在凌晨监控数据写入低峰执行。注意强制合并完成后要调用writer.commit()否则索引内容可能丢失。5.3 三个最容易踩的坑第一个坑是 Windows 的 bat 脚本编码。项目里中文注释或中文路径如果没保存为 GBK运行时会乱码甚至直接退出。解决办法是把setting.bat保存成 GBK或者改成chcp 65001切换到 UTF-8。第二个坑是 MongoDB 与 Lucene 数据不一致。应用异常重启时监控数据可能写入 MongoDB 但还没来得及建索引第二天搜索就少了这条记录。稳妥做法是在索引构建任务里加一个起始时间戳每次增量索引时扫最后 5 分钟的数据用幂等删除重写的方式补一遍。第三个坑是服务端口冲突。start-mongodb.bat默认 27017如果本地之前装过 MongoDB 作为默认 Windows 服务脚本再启动一个实例会失败。先执行sc query mongodb看服务是否存在存在就先sc stop mongodb再跑项目脚本。最后提一个实用技巧把setting.bat里所有端口和路径改成相对路径的写法然后整个项目目录打压缩包换机器部署时只需要改setting.bat里面的BASE_DIR后面的脚本都用%BASE_DIR%\xxx引用这样就不用挨个改脚本里的绝对路径了。本文还有配套的精品资源点击获取