
简介这是一份基于Java机器学习技术的分布式系统故障诊断系统源码面向熟悉Java基础、希望将机器学习应用于运维监控场景的开发者或相关专业学生。压缩包共33个文件以27个Java源文件为核心配合5个XML配置文件与1个YAML配置文件其中Java代码承载故障检测与诊断算法逻辑XML与YAML用于管理依赖、框架参数及运行环境整体包体仅23KB结构轻量便于快速阅读与二次开发。源码包含pom.xml与src/main等标准Maven工程目录读者可从中梳理分布式环境下的数据采集、特征处理、模型训练与异常判定流程掌握Java与机器学习库结合的实现思路。已有358人学习下载适合用于课程设计、毕业设计或小型故障诊断项目的参考起点。1. 基于 Java 机器学习的分布式系统故障诊断从「报警」到「定位」的距离分布式系统的故障诊断难点从来不是「有没有问题」而是「哪里有问题、为什么有问题」。传统监控把 CPU、内存、GC、网络延迟堆在面板上指标之间靠人串成结论而基于 Java 机器学习的故障诊断系统是把历史故障样本变成训练数据让分类器在指标变化形态里直接给出故障类型和可疑节点。拆开这套源码要做的事情并不神秘采集时序指标、构造特征向量、训练分类器、把预测结果接进告警与根因定位链路。它适合手里已经有监控体系、想用机器学习把告警收敛成故障判断的团队。真正卡人的地方在数据对齐和样本标注不在算法本身。2. 分布式故障诊断的数据链路采集、特征工程与训练样本划分数据链路是整个故障诊断系统的地基。模型能学到的上限在特征向量构造完成的那一刻就定死了后面调参只是逼近这个上限。整条链路拆成采集、窗口化、样本标注三步走每一步都有对应的落地代码。2.1 先定诊断粒度节点级还是服务级故障诊断系统开工之前第一件事是确定诊断对象。节点级诊断看的是宿主机与 JVM 的运行时状态适合定位「这台机器怎么了」服务级诊断看的是 RPC 延迟、错误率、消息堆积量回答的是「这条调用链上哪个环节坏了」。一个完整的分布式系统故障诊断系统通常两层都要但第一版先做透一层。我的建议是从节点级入手指标好采集、故障注入好做、样本标注清晰训练出来的模型也容易解释。下面是一张第一版就够用的特征清单按采集来源分组| 特征类别 | 采集来源 | 典型指标 | 关联故障 | | JVM 运行时 | JMX / Micrometer | 堆使用率、Full GC 次数、GC 停顿、线程阻塞数 | OOM、GC 抖动 | | 主机资源 | node_exporter / SIGAR | CPU 均值、内存、磁盘 IO、重传率 | CPU 打满、磁盘故障 | | 中间件 | 客户端埋点 / JMX | 消费滞后量、连接数、队列深度 | 消息堆积、连接耗尽 | | 调用链 | OpenTelemetry 埋点 | P99 延迟、错误率、下游耗时占比 | 慢调用、依赖故障 |表格里的列不是写死就完了后面训练时要用特征重要性反过来裁剪它。比如某个版本的模型跑下来重传率的重要性一直是 0.003 以下说明采样频率或窗口设计有问题或者这个指标在样本里几乎没有变化可以直接砍掉。2.2 滑动窗口把时序指标变成特征向量机器学习模型不认原始时序点只认特征向量。常见做法是滑动窗口把每个指标最近 60 秒的采样点压成均值、P95、变化率三个统计量。窗口 60 秒、步长 30 秒意味着相邻两条特征向量有一半数据重叠。重叠不是为了增加样本而是故意让分布在窗口边缘的故障形态至少完整落进一个窗口避免错过那些只持续 40 秒的抖动。这一步在源码里通常由一个 WindowFeatureBuilder 负责下面的代码是它的核心逻辑// WindowFeatureBuilder.java把最近 60 秒的采样点压成一条特征向量 public class WindowFeatureBuilder { private final DequeMetricsSample samples new ArrayDeque(); public double[] build() { long now System.currentTimeMillis(); // 1. 清理窗口外的旧采样点 while (!samples.isEmpty() now - samples.peekFirst().ts 60_000) { samples.pollFirst(); } // 2. 分别提取 CPU、堆使用率、GC 次数的统计量 double[] cpu samples.stream().mapToDouble(s - s.cpu).toArray(); double[] heap samples.stream().mapToDouble(s - s.heap).toArray(); return new double[]{ mean(cpu), percentile(cpu, 0.95), rateOfChange(cpu), mean(heap), percentile(heap, 0.95), lastGcDelta(), rateOfChange(gcCounts) }; } }这段代码有两个值得注意的地方。第一窗口清理用的是 peekFirst 而不是遍历因为采样点按时间有序入队队头过期则淘汰均摊复杂度是 O(1)。第二GC 次数这个指标不适合取均值应该取「窗口内增量」也就是窗口末尾值减去窗口起始值否则它会被历史总量淹没模型完全学不到 Full GC 突增这个信号。实际跑训练数据时这条特征通常是 7 个故障类里区分度最高的三个之一。2.3 故障样本从哪来事故回放与主动注入特征向量有了接下来是样本标注。这步占掉整个故障诊断系统大约一半的工作量而且没法用代码跳过。样本标注有三条路。第一条是事故回放把线上历史故障时间点拉出来故障发生前 3 分钟的滑动窗口全部标成正样本。第二条是主动注入在测试环境用混沌工程工具制造确定性故障例如用 chaosblade 把 CPU 打到 80% 持续 120 秒期间采集窗口自动贴上对应标签# 注入 CPU 满载故障 120 秒制造CPU 打满的正样本 blade create cpu fullload --cpu-percent 80 --timeout 120第三条是规则辅助标注先用固定阈值把明显异常窗口粗筛出来再人工复核。无论哪条路都要控制正负样本比例。故障本来就是低频事件不采样的话正负比轻松到 1:1000随机森林会为了整体准确率把少数类全部判负漏报率直接失控。常见做法是把负样本降采样到正样本的 510 倍训练效果和训练速度都能接受。提示主动注入时一种故障类型至少要生成 30 个不同形态的样本窗口不要只注入默认参数。同样是 CPU 打满80% 和 40% 的特征形态完全不同样本多样性直接决定模型在真实故障上的泛化能力。3. 用 Java 训练故障诊断模型特征向量构造与分类器选型3.1 训练为什么留在 Java 侧很多团队第一反应是把特征导出成 CSV丢给 Python 训练再把模型搬回来。这方案不是不行但当分布式系统故障诊断服务本身是 Java 技术栈时训练留在 Java 侧有三个实际收益第一特征工程代码与训练代码共用同一个 WindowFeatureBuilder避免两套语言各自维护特征口径第二模型文件序列化后在同一个 JVM 里加载和预测省掉跨语言部署环节第三后续要做周期性重训练可以直接复用现有的调度和告警基础设施。Java 生态里 SMILE、Weka、DJL 都够用故障诊断这种中小规模表格数据任务SMILE 的随机森林实现最顺手。3.2 SMILE 随机森林的训练代码与参数故障诊断任务选随机森林做默认模型理由很实际特征之间相关性高、量纲差异大、偶发噪声多随机森林对这三件事都不太敏感。树模型不需要特征归一化CPU 百分比和 GC 次数直接进模型也不会互相压制它还能输出特征重要性给运维同学一个可解释的抓手。下面是 SMILE 训练随机森林的核心代码// 读取故障样本 CSV训练 7 类故障的随机森林 DataFrame df Read.csv(fault_samples.csv, CSVFormat.DEFAULT.withHeader()); double[][] X df.select(cpu_mean, cpu_p95, cpu_rate, heap_mean, heap_p95, gc_delta, disk_io, latency_p99, error_rate) .toArray(); int[] y df.stringColumn(fault_label).factorize().toIntArray(); RandomForest forest RandomForest.fit(X, y, SplitRule.GINI, // 分裂准则GINI 比 ENTROPY 快效果接近 300, // 树的棵数300 棵后准确率基本不再变化 4, // mtry每棵树的候选特征数 12, // 单棵树最大深度防止过拟合 1000); // 叶子节点最小样本数 Smile.write(forest, fault_model.ser);参数这里要解释两句。mtry 取 4是因为特征总数 9 个经验值是 sqrt(9) 附近如果特征扩展到 20 个mtry 应该跟着调到 56。叶子节点最小样本数 1000 是这类任务容易被忽略的参数故障样本本身带标签噪声叶子太少会把噪声学进树里上线后出现大量不可复现的告警。至于树的棵数300 和 500 的差别通常不到 0.5%收益不划算留着训练时间干别的更值。训练完成后第一件事不是看准确率而是读特征重要性。输出重要性排前三的特征拿去和运维同学确认是否符合直觉。如果 GC 增量排在最后而错误率排第一要么是样本里故障类型分布偏了要么是采集链路丢了 GC 相关数据。这一步相当于给训练数据做体检。3.3 七个故障类的混淆矩阵怎么读故障诊断的准确率是分层看的。7 类故障里CPU 打满和磁盘写满特征差异大类间混淆低而 OOM 和 GC 抖动经常互相误判因为它们共用一个信号源堆内存压力。读混淆矩阵时优先看这两类的交叉项如果比例超过 15%一般不是模型问题是特征里缺了「进程重启计数」这类能区分「已经崩了」和「还在挣扎」的指标。一个可参考的样本构成和预期混淆关系如下| 故障类型 | 样本占比 | 容易混淆的类型 | 需要补的特征 | | 节点 OOM | 18% | GC 抖动 | 进程重启计数、堆外内存 | | CPU 打满 | 21% | 线程池耗尽 | 活跃线程数、CPU 负载曲线 | | 磁盘写满 | 12% | 无 | 预留空间、目录 IO 等待 | | 网络分区 | 9% | 依赖超时 | 重传率、建连失败数 | | 依赖超时 | 15% | 网络分区 | 下游 P99、超时配置 | | 消息堆积 | 14% | 无 | 消费滞后量、分区数 | | 线程池耗尽 | 11% | CPU 打满 | 队列长度、拒绝策略计数 |这张表不是最终定稿而是训练集设计时的检查清单。每个「容易混淆」的横栏都对应一个必须在训练数据里出现的对比场景OOM 样本和纯 GC 抖动样本要同比例存在模型才有机会学会区分它们。4. 把模型接进分布式故障诊断主流程实时预测与根因定位4.1 30 秒一次的滑动窗口预测模型训练好只是第一步真正决定系统价值的是实时预测链路怎么组织。我一般把诊断服务做成一个独立 Java 进程用 ScheduledExecutorService 每 30 秒对每个节点跑一次「构建窗口 预测 判定」三步// DiagnosisJob.java每 30 秒对 node-01 执行一次故障预测 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { double[] features windowBuilder.build(node-01); int label forest.predict(features); double[] prob forest.classProbabilities(features); // 只有置信度超过阈值才发诊断事件避免抖动刷屏 if (prob[label] 0.75) { diagnoseBus.publish(new FaultEvent(node-01, labelNames[label], prob[label], System.currentTimeMillis())); } }, 0, 30, TimeUnit.SECONDS);阈值 0.75 是默认值千万别当成固定值。它的物理含义是「模型有多确定当前形态属于某个故障类」。这个值的选取依赖业务对漏报和误报的容忍度——线上没人盯的时候阈值要低宁可误报告警渠道已经刷屏的时候阈值要调高。后面第 5 章会给一套按混淆矩阵调阈值的具体办法。这里还有一个容易踩的坑窗口构建器和训练时的窗口参数必须完全一致。训练时用 60 秒窗口线上用了 90 秒特征分布直接偏移上线第一周就会出现「模型在测试集上 94% 准确率线上天天报错」的诡异现象。排查顺序永远是先核对窗口参数再怀疑模型过期。4.2 按调用图聚合结果收敛成根因单节点预测输出的是「这个节点怎么了」分布式系统还需要回答「整条链上谁先坏的」。做法是把所有故障节点的预测结果放进一张调用依赖图里从最下游开始向上游找根因只有当下游节点确认故障、且上游节点的故障特征可以用「被下游拖垮」解释时才把上游节点标记为受影响而不是根因。下面是按调用图做根因判定的简化逻辑// RootCauseFinder.java按依赖深度从深到浅扫描故障节点 public ListString findRootCauses(DependencyGraph graph, MapString, FaultEvent alarms) { ListString roots new ArrayList(); for (String node : graph.nodesSortedByDepthDesc()) { if (!alarms.containsKey(node)) continue; boolean isRoot true; for (String upstream : graph.upstreamOf(node)) { // 上游故障特征与下游一致视为受影响节点 if (alarms.containsKey(upstream) alarms.get(upstream).label alarms.get(node).label) { isRoot false; break; } } if (isRoot) roots.add(node); } return roots; }这段代码里有一个隐含假设同标签的上游节点更可能是被下游拖垮的。实际线上不总是这样比如消息堆积可能同时影响消费方和生产方。所以按调用图聚合的结果只能作为「候选根因」真正呈现给值班人员时要把每个候选根因的置信度、受影响节点数、首次告警时间三列信息一起输出。首次告警时间最早的那个节点大概率是真正的根因。4.3 告警收敛冷却时间与重复抑制预测链路跑起来后最常见的抱怨是「告警比故障恢复时间还长」。原因是同一故障会在连续多个 30 秒周期里被反复预测命中。解决办法是加冷却时间节点进入故障状态后一段时间内不再重复推送同类故障事件只更新原事件的持续时间和置信度。冷却时间的设置有几种粒度常见配置如下| 冷却维度 | 行为 | 推荐配置 | | 节点 故障类 | 同节点同类故障 N 分钟内只推一次 | 1015 分钟 | | 仅节点 | 节点内任意故障 N 分钟内不重复推 | 不推荐会掩盖故障类型演变 | | 事件订阅者 | 不同团队各自设冷却 | 按值班压力分别调整 |冷却时间按「节点 故障类」维度隔离而不是按节点一刀切节点从 OOM 演变到 CPU 打满是两个不同事件不能被同一个冷却闸拦住。注意降噪只能在事件输出层做不要改模型预测频率。模型每 30 秒跑一次是为了拿最新置信度变化这是判断「故障是否恢复」的数据基础输出层合并事件不会给值班同学造成负担。5. 用混淆矩阵和人工复核校准故障诊断策略5.1 先看漏报再看误报故障诊断系统上线前评估顺序和常规 ML 项目相反先看漏报再看误报。漏掉一次真实故障的代价是几十分钟的响应时间多一次误报只是多看一条告警。用混淆矩阵按故障类分别算召回率任何一类低于 85% 都先不要谈上线回到数据链路补特征或补样本。阈值用验证集做一次全阈值扫描for (double t 0.50; t 0.95; t 0.05) { double p eval.precisionAt(t), r eval.recallAt(t); System.out.printf(t%.2f p%.2f r%.2f f1%.2f%n, t, p, r, 2 * p * r / (p r)); }选阈值看 precision 和 recall 曲线交点附近偏低误报猛增偏高漏报回归。对告警容忍度低就选交点偏右故障影响大就选偏左。5.2 按时间切分验证集避免样本泄漏这是故障诊断项目最常见的隐性错误随机切分时同一场故障的相邻窗口会被同时分进训练集和验证集模型等于提前见过答案。正确做法是按时间切分前 80% 时间段训练、后 20% 验证并确认 7 个故障类在验证集都有覆盖。切换后准确率通常从 96% 掉到 88% 左右后者才是线上水平的真实估计。5.3 灰度影子模式模型先看不说最后一个上线技巧是影子模式模型完整跑预测但诊断事件只写日志和存储不进告警渠道。一两周后每天把预测结果和值班记录比对统计「报了但没故障」和「有故障但没报」两类清单前者找特征误判原因后者找漏报原因。改完再进告警渠道用复核数据生成第二版训练集重跑阈值扫描阈值通常能从 0.75 下探到 0.68 左右。全程记录告警收敛率去重后事件数除以原始预测命中数两周后曲线从 40% 爬到 70% 以上这个数字比准确率报告直观得多。本文还有配套的精品资源点击获取