微服务日常巡检的检查顺序
微服务巡检不是每天把所有指标看一遍。更有效的做法,是先确认用户路径是否异常,再沿入口、依赖、线程与连接池、JVM 和基础设施逐层缩小范围。顺序清楚,值班人员才能知道下一步该看什么,也能避免一看到 CPU 抖动就重启服务。
巡检项要跟随系统风险和依赖变化维护。新接入消息队列、修改线程池、升级 JDK 或调整注册中心后,对应检查也要更新;一份多年不变的静态清单,很快会与真实架构脱节。
第一层先看用户请求和近期变更
从关键入口的成功率、目标延迟和任务完成情况开始,并按路由或业务类型分组。整体平均值正常,可能仍有一个高价值接口持续失败。流量接近零时成功率也可能失真,应同时看请求量和绝对错误数。
把当前异常与最近发布、配置变更、依赖升级和流量变化放在同一时间轴。时间接近不等于根因已经确认,但它能帮助确定优先排查范围。没有异常时,日常报告也应标明当前版本和上次变更,方便后来对比。
健康检查只是一条信号。Liveness 回答进程是否需要重启,Readiness 决定实例能否接流,业务路径则验证关键功能。端口可访问不能证明线程池、数据库和注册状态正常;反过来,下游短时波动也不应触发所有实例的 Liveness 失败并反复重启。
第二层检查依赖和流量放大
查看上游到本服务、再到数据库、缓存、消息队列和其他 RPC 的调用关系。重点是超时、重试、连接等待和拒绝。入口流量没有变化、内部调用却明显增加,可能出现重试放大或重复消费。
依赖异常时,先确认是所有实例都失败,还是某个可用区、连接池或目标版本集中出错。DNS、证书和权限错误通常不会通过原样重试恢复;连接中断或明确的临时错误才可能在预算内有限重试。巡检报告需要保留错误分类,而不是统一写成“下游不稳定”。
服务发现要同时看控制面与调用端实际视图。注册中心有实例,不代表客户端缓存已经更新;实例心跳正常,也不代表业务处理能力正常。可以抽查注册版本、实例状态和一次真实路由结果,但不要让巡检绕过正常负载均衡直接修改注册信息。
第三层看线程池、连接池和队列
Java 服务常见的“CPU 不高却很慢”,往往与等待有关。检查业务线程池的 active、pool size、queue size 和 reject,数据库连接池的 active、idle 与等待时间,以及 HTTP 客户端的连接获取与进行中请求。每个指标都要带池名称,不能把某个自定义executor.queued当成整个 Tomcat 或 WebFlux 的状态。
队列长度必须结合处理速率。短时出现几个等待任务未必有问题,持续增长且完成速率跟不上入口才说明无法收敛。无界队列不会显示“满”,却会让内存和等待时间不断增长,因此配置巡检还要确认队列类型与容量。
连接池达到上限时,不要立刻调大。先看连接为何长期占用、超时是否生效、下游是否变慢。扩大每个实例的池后,再乘以副本数,可能超过数据库或服务端能承受的总连接。
第四层再进入 JVM
JVM 巡检关注堆、非堆、GC 暂停、分配速率、线程和类加载,但没有一条固定阈值适合所有服务。先建立当前 JDK、GC 和负载下的基线,再看趋势与用户延迟是否同时变化。
Metaspace 的max可能没有配置成有限值,此时简单计算使用比例没有意义。更值得观察的是类加载数量、使用量是否在稳定流量下持续增长,以及 Full GC 后能否回落。动态代理、脚本和频繁创建类加载器只是可能原因,需要结合 Heap Dump、类加载统计和版本变更验证。
GC 暂停也不能孤立解读。记录暂停分布、发生原因、堆占用和分配速率,再与请求长尾对齐。偶发暂停不一定影响用户,持续高分配导致频繁回收才需要继续定位。Heap Dump 与线程 Dump 可能包含业务数据,应限制触发、访问和保留时间。
Actuator 数据先由监控系统统一采集
Spring Boot Actuator 与 Micrometer 可以暴露运行指标,但具体名称、标签和可用性取决于版本与已注册组件。建立 Prometheus 等采集系统后,日常巡检优先查询统一时序数据,而不是额外脚本并发轮询每个 Pod。后者会制造新负载,也难以保留趋势。
小型环境确实需要只读脚本时,要把“指标缺失”与“指标为零”分开,并保护 Actuator 入口。下面的示例只检查健康状态,不打印响应正文;服务地址来自受控配置,超时和并发都有限。它不能替代认证、TLS 和集中监控。
from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from typing import Literal import requests @dataclass(frozen=True) class Service: name: str base_url: str @dataclass(frozen=True) class Result: service: str status: Literal["UP", "DOWN", "UNKNOWN"] reason: str def inspect(service: Service) -> Result: try: response = requests.get( f"{service.base_url}/actuator/health/readiness", timeout=(1, 2), ) if response.status_code != 200: return Result(service.name, "DOWN", f"http_{response.status_code}") payload = response.json() status = payload.get("status") if status == "UP": return Result(service.name, "UP", "readiness_up") return Result(service.name, "DOWN", "readiness_not_up") except (requests.RequestException, ValueError) as exc: return Result(service.name, "UNKNOWN", type(exc).__name__) def inspect_all(services: list[Service]) -> list[Result]: with ThreadPoolExecutor(max_workers=min(4, len(services))) as executor: futures = [executor.submit(inspect, service) for service in services] return [future.result() for future in as_completed(futures)]UNKNOWN不能显示成绿色健康。网络、认证或响应格式问题都需要单独处理。脚本也不应自动重启实例;先保留证据,再由有权限和审计的处置流程决定动作。
告警与日报处理不同时间尺度
需要立即通知的,是正在影响用户并且有人可以处理的状态,例如关键路径持续失败、队列无法收敛或全部实例不可用。容量趋势、Metaspace 增长和依赖版本偏差更适合进入日报或工单。把所有异常都发成电话告警,只会消耗值班注意力。
去重不应仅按Service + Metric。同一根因可能让几十个服务同时报警,应按依赖或调用拓扑聚合;同一指标在不同集群又可能是两起事件,需要保留环境标签。静默规则设置开始、结束与负责人,避免维护窗口结束后告警仍被永久压制。
每条告警附上当前值、基线、持续时间、受影响路径和一条只读排查入口。若接收人无法根据内容决定继续观察、限流或升级,就应重新设计这条告警。
巡检本身也需要预算
高频、昂贵的管理查询会影响被观察系统。健康接口保持轻量,指标由拉取系统按容量采集,日志查询限制时间和结果量。不要在业务高峰自动触发 Heap Dump,也不要让巡检账号拥有修改配置或重启服务的权限。
定期演练指标缺失、注册异常、线程池拒绝和下游超时,确认告警能触发、说明足够、恢复条件明确。规则修改后先回放历史数据,检查是否把已知波动重新变成噪声。
一套可用的巡检顺序,应该让值班人员从用户影响走到具体资源,再回到变更和依赖证据。它不追求指标最多,而是尽快回答:现在是否影响用户,问题在哪一层,谁需要采取什么动作。