简介:基于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」的混合存储架构。我把这个选择拆开讲:
| 组件 | 端口 | 在系统中的职责 | 为什么选它 |
|---|---|---|---|
| MySQL | 3306 | 保存服务元数据、用户权限、监控规则 | 事务一致性强,适合关系型配置 |
| MongoDB | 27017 | 接收 Dubbo Filter 上报的调用监控数据 | 写入吞吐高,适合时序型日志 |
| Lucene | 9000(内置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_HOME=D:\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_PATH=D:\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-server=utf8mb4,否则中文服务名存入后查询乱码。
2.4 setting.bat 是配置入口
项目里的setting.bat通常是环境变量初始化脚本。因为多个模块都需要知道 MongoDB 和 MySQL 的地址,所以把所有配置集中到一个脚本里,再用call setting.bat引入。内容类似:
set MYSQL_HOST=127.0.0.1 set MYSQL_PORT=3306 set MYSQL_USER=monitor set MYSQL_PASSWORD=123456 set MONGO_HOST=127.0.0.1 set MONGO_PORT=27017 set LUCENE_INDEX_DIR=D:\server\lucene\index set DUBBO_REGISTRY=nacos://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,可以直接 curl:
curl -X GET "http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=com.example.UserService&groupName=DEFAULT_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() { List<Document> 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 的IndexWriter:
public class MonitorIndexer { private Directory directory; private Analyzer analyzer = new StandardAnalyzer(); private IndexWriterConfig config = new IndexWriterConfig(analyzer); public void buildIndex(List<Metric> 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 已经去掉了版本号参数。字段类型的选择可以参考这个表:
| 字段 | 类型 | 说明 |
|---|---|---|
| service | StringField | 精确匹配,不分词 |
| method | StringField | 精确匹配,不分词 |
| fullText | TextField | 分词,用于关键字搜索 |
与之对应的查询代码:
public List<String> 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); List<String> 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_THRESHOLD=0.05 set SLOW_THRESHOLD=500ERROR_THRESHOLD=0.05表示错误率超过 5% 就标红,SLOW_THRESHOLD=500表示单次调用耗时超过 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引用,这样就不用挨个改脚本里的绝对路径了。
本文还有配套的精品资源,点击获取