ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Cloud Gateway 全链路监控与故障自愈实战指南

Spring Cloud Gateway 全链路监控与故障自愈实战指南 在微服务架构里Spring Cloud Gateway 是所有流量进出系统的第一道门它一旦抖动影响面不是某个服务而是整个入口。我做过一个真实落地的项目目标就是给网关加上完整可用的全链路监控能力并且让它在依赖的下游服务出现故障时能快速自愈而不是靠人工半夜起来重启。这篇文章把我从方案选型、埋点采集、链路追踪接入、熔断降级配置到实际故障演练的完整过程都梳理出来希望能帮那些正打算做网关监控和自愈的团队少踩几个坑。如果你负责的微服务体系里还处在“网关只有路由功能”的阶段那这篇文章可以直接当参考。哪怕你还没上 Spring Cloud Gateway里面关于指标设计、日志归集、链路追踪和故障自愈的组合思路也可以迁移到其他网关或代理层上。我会直接讲做法、讲配置、讲踩过的坑不绕弯子也不讲纯理论。1. 网关监控的第一性问题先明确要观察什么做全链路监控最容易犯的错就是一上来就接一堆组件Prometheus、Grafana、Zipkin、SkyWalking 全堆上结果面板一大堆真正出问题时还是两眼一抹黑。我自己的经验是先回答一个问题网关作为入口它最需要被观察的到底是什么1.1 网关监控的三个维度可用性、性能、流量网关和普通业务服务不一样它的核心职责是转发所以监控维度要围绕“有没有活着”、“转得快不快”、“流量往哪走”来设计。可用性维度网关进程本身是否存活路由表是否完整链路是否全部可用。对应指标就是进程状态、健康检查通过率、路由数量变化。性能维度每个路由的平均响应时间、P99延迟、连接池占用、线程池活跃数、CPU和内存。这些决定用户体验。流量维度每秒请求数、按路由拆分的流量分布、失败率、重试率。这些能帮你判断流量异常是攻击、热点还是下游问题。我基于 Spring Cloud Gateway 做的监控本质上是把 Spring Boot Actuator 的已有指标和自定义的网关过滤器指标结合起来而不是另起炉灶。Actuator 提供了基础的 health、metrics 端点但默认的指标对网关来说太粗了比如它不会告诉你是哪个 route 失败率高所以必须再做一层细化。1.2 为什么默认的 Actuator 指标不够用Actuator 暴露的 http.server.requests 确实包含 uri 和 status 等信息但拿到网关里你会发现两个尴尬的地方uri 是原始请求路径而不是匹配到的 RouteId。看起来同一个路径 /api/user/100 和 /api/user/200 会被拆成两个指标如果路径里有随机参数指标基数直接爆炸。指标里没有路由匹配结果也没有 filter 执行耗时你根本不知道网关自己花在路由匹配和过滤链上的时间是多少。所以我在网关里做了一层自定义指标按 RouteId 和下游状态码组合统计请求次数与耗时同时记录“未匹配到路由”的请求数。这样一来面板上能直接看到哪个路由慢、哪个路由失败多、哪些流量在网关层就被拒绝了定位问题非常直观。1.3 我最终选型的监控组件组合关于组件选型我的原则是“不追新选团队能维护的”。最终落地的组合是组件用途Spring Boot Actuator暴露健康检查和基础指标Micrometer指标注册与导出标准Prometheus指标拉取和告警规则Grafana可视化面板Loki网关日志聚合只存关键日志ZipkinSleuth/Tracer分布式链路追踪这套组合的优点是全链路用的都是兼容 Spring Boot 标准方式的组件后续升级框架成本低而且 Prometheus Grafana 几乎成了监控标配招人也好招。如果你本身就在用 SkyWalking那链路追踪部分可以不用 Zipkin但指标和日志部分仍然推荐 Micrometer。2. 指标采集与自定义过滤器让每个路由的“健康状态”可见监控的第一步是把指标做出来。这一节我会直接给出可用的配置和核心代码逻辑重点说清楚为什么要在过滤器里做指标以及怎么避坑。2.1 暴露 Prometheus 端点的基础配置先加依赖在 pom.xml 里补充这几个关键项dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency然后配置 application.ymlmanagement: endpoints: web: exposure: include: health,metrics,prometheus endpoint: health: show-details: always metrics: tags: application: gateway-service注意Spring Boot 2.x 和 3.x 在指标命名和默认行为上略有差异但整体思路一致。如果你用的是 Spring Cloud Gateway 3.x对应 Spring Boot 2.x这套配置可以直接跑如果是 Gateway Server MVC 这种新版本本质上也是同一套 Actuator 机制。2.2 自定义 GlobalFilter 实现路由级指标默认指标粒度不够所以我写了一个全局过滤器在每个请求进来和出去时记录耗时并打上 routeId、status、exception 等标签。核心逻辑类似这样Component public class RouteMetricFilter implements GlobalFilter, Ordered { private final MeterRegistry meterRegistry; private final String M_GATEWAY_REQ gateway_http_requests_total; public RouteMetricFilter(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { long start System.nanoTime(); return chain.filter(exchange).doFinally(signalType - { long costMs TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); String routeId exchange.getAttribute(gateway_route_id); if (routeId null) { routeId unknown; } Integer status exchange.getResponse().getStatusCode() ! null ? exchange.getResponse().getStatusCode().value() : 0; Counter.builder(M_GATEWAY_REQ) .tag(routeId, routeId) .tag(httpStatus, String.valueOf(status)) .tag(signalType, signalType.name()) .register(meterRegistry) .increment(); Gauge.builder(gateway_route_latency_millis, () - costMs) .tag(routeId, routeId) .register(meterRegistry); }); } Override public int getOrder() { return -1; // 尽量早执行 } }这里有个很重要的细节routeId 怎么来的如果你的路由是配置文件写死的那直接使用 exchange.getRoute().getRouteId() 就行。如果是用动态路由比如从配置中心读取则需要在 RouteDefinitionRouteLocator 加载路由后把 routeId 放到 ServerWebExchange 属性中。我在实际项目中是在 RouteRefreshListener 之外用了一个前置过滤器统一塞 routeId避免 GlobalFilter 读取不到。2.3 Prometheus 拉取与 Grafana 面板设计配置好指标后Prometheus 里加一个 job 拉网关端口scrape_configs: - job_name: gateway metrics_path: /actuator/prometheus static_configs: - targets: [gateway-host:8080]Grafana 面板上我最常用的四个图表是每秒请求数按 routeId 分组堆叠各路由P99延迟用 histogram_quantile 计算各路由 5xx 错误数JVM 线程状态与 GC 耗时如果你不想从零画面板可以直接导入 Micrometer 相关的社区面板改一下 job 名和指标名就好。但我个人建议至少自己画三个核心图请求量、延迟、错误率因为这三个是排查问题的入口其他指标都是辅助。3. 日志与链路追踪把“一次调用”完整串起来指标能告诉你“哪里不对劲”但要回答“这个请求经历了什么”必须依赖日志和链路追踪。网关是所有流量的汇聚点如果不在这里做全链路上下文透传后面排查问题会特别痛苦。3.1 网关层如何设计结构化日志网关日志和普通业务日志不一样必须带上 traceId、routeId、请求路径、耗时、状态码。我在项目里用 Logback 的 MDC 自定义 filter 实现日志模板类似pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread] [%X{traceId}] [%X{routeId}] %logger{40} - %msg%n/pattern然后通过一个 GlobalFilter 在请求链路上设置 MDCpublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String traceId exchange.getRequest().getHeaders().getFirst(X-Trace-Id); if (traceId null) { traceId UUID.randomUUID().toString().substring(0, 16); } MDC.put(traceId, traceId); MDC.put(routeId, getRouteId(exchange)); return chain.filter(exchange).doFinally(s - MDC.clear()); }这里要留意 WebFlux 的异步模型MDC 的线程切换问题很典型。在 Spring WebFlux 中同一个请求的处理可能跨多个线程直接用替代 Sender 的 MDC 可能在 filter 返回后被清除导致后续日志没有 traceId。我当时的做法有两种方案一是通过 Reactor Context 传递 traceId在日志框架层再放回 MDC二是用 Brave 的 Tracing 机制自动处理 W3C trace context。如果你是跟着 Sleuth 走的建议直接使用 Sleuth 自带的日志机制它会自动处理链路上下文。3.2 接入 Sleuth Zipkin 的关键配置Spring Cloud Sleuth 在 Spring Cloud 2021.0 之后移入了 Micrometer Tracing所以现在的推荐方式是dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-tracing-bridge-brave/artifactId /dependency dependency groupIdio.zipkin.reporter2/groupId artifactIdzipkin-reporter-brave/artifactId /dependency配置方面最重要的是设置采样率。生产环境千万别到处 1.0默认就是 0.1但有些团队会改成 1.0结果链路数据量巨大同时影响网关吞吐。一般建议按接口重要性配置两个采样率普通请求0.1核心下单流程1.0Sleuth 采样器可以按 span 名称或 tag 做扩展我个人比较推荐先全局 0.1然后在关键接口的 Route 上增加自定义 tag 并提高采样率。这样既覆盖了主要链路也不会因为数据量过大拖垮应用。3.3 我在接入追踪时遇到的三个真实坑第一个坑是用 Spring Cloud Gateway 的 WebFlux 模型时Sleuth 和 Reactor 的 context 传播版本不兼容。具体表现是 zipkin 里能看到网关的 span但是下游服务的 parentId 对不上。解决方式就是升级到 Spring Cloud 2021.0.3 以上并确保自己的自定义 Reactor 装饰器没有手动清空 context。第二个坑是网关会把下游响应头里的 traceId 覆盖掉。如果你在网关里设置了 ServerWebExchange 的 response headers可能会覆盖下游传来的 traceId。要默认把下游 traceId 明确传递到前端或其他调用方需要显式设置响应头 X-Trace-Id让它对用户体验友好。第三个坑是日志和 zipkin 的 traceId 对不上。因为 Sleuth 默认生成的 traceId 是 16 位长度而 Brave 可以支持 32 位或 64 位。如果日志用 16 位、zipkin 用 64 位人工查看时很痛苦。建议把日志模板里的 %X{traceId} 统一成 micrometer-tracing 提供的 traceId不要混用。4. 故障自愈的实战机制熔断、重试、限流的正确组合监控的最终目的是“自动处理”否则监控只是事后的取证明。这一节讲自愈也就是当网关发现下游异常时如何在不人工干预的情况下继续为用户提供服务或者至少不把网关拖垮。自愈不是灵丹妙药它是一系列策略的组合。4.1 为什么我选了 Resilience4j 而不是 Hystrix 或 Sentinel老的 Spring Cloud 项目里用 Hystrix 很常见但 Hystrix 已经停止维护了。Sentinel 功能强但需要部署控制台团队如果用不熟反而容易误操作。Resilience4j 是轻量级限流熔断库纯 Java 实现和 Spring Cloud Gateway 的 WebFlux 模型兼容性好并且支持 CircuitBreaker、RateLimiter、Retry、Bulkhead、TimeLimiter 多种模块。对于网关场景我主要用了 CircuitBreaker Retry RateLimiter 三种组合。4.2 在路由级别配置熔断器Spring Cloud Gateway 有内置的 RequestRateLimiter filter但熔断和重试需要通过 Spring Cloud CircuitBreaker GatewayFilter 或手动配置。推荐在 application.yml 里配置spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - name: CircuitBreaker args: name: userServiceCB fallbackUri: forward:/fallback/user - name: Retry args: retries: 2 statuses: BAD_GATEWAY, GATEWAY_TIMEOUT, SERVICE_UNAVAILABLE methods: GET backoff: firstBackoff: 200ms maxBackoff: 2s factor: 2.0这里有个特别重要的点只对 GET 请求重试。如果对 POST 或 PUT 自动重试可能导致重复下单或重复写入这是生产事故。如果你必须对写操作做重试请确保下游接口幂等或者只针对网络异常如连接超时重试而不是对业务错误码重试。断路器路径不要设置成空务必指定 fallbackUri。哪怕是返回一个静态错误的本地转发也比让用户直接看到 500 好。4.3 手动实现断路器事件的监控与自愈通知单单配置了 CircuitBreaker用户得到 fallback 后就结束了吗不行还要通知相关责任人否则熔断发生时我们可能还在睡觉。我的做法是注册一个全局事件监听器Bean public CircuitBreakerEventObserver circuitBreakerObserver() { return new CircuitBreakerEventObserver() { Override public void onStateTransition(CircuitBreakerEvent event) { if (event.getStateTransition().getToState() CircuitBreaker.State.OPEN || event.getStateTransition().getToState() CircuitBreaker.State.HALF_OPEN) { sendAlert([网关] user-service 熔断状态变化: event); } } }; }生产环境更简单的方式是监控 Prometheus 指标 circuit_breaker_state 或 resilience4j_circuitbreaker_state。告警规则设定网关某路由熔断状态为 open 超过 1 分钟时触发告警。这样我们不用人工盯着也能知道故障正在发生或恢复。4.4 限流设置在自愈中的作用熔断保护的是“下游挂了别把网关拖死”限流保护的是“流量太大时主动降级”。在自愈体系里限流是最后一道防线。我用的是 Redis RateLimiter配置如下- name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 redis-rate-limiter.burstCapacity: 100 key-resolver: #{userKeyResolver}注意RequestRateLimiter 需要 Redis 支持。在网关关键路径上如果 Redis 不可用这个过滤器本身可能成为故障源。因此建议把 Redis 配置好主从或哨兵同时把限流器 key-resolver 的失效策略设置为不阻塞请求。具体做法是key-resolver 里使用 getCacheKey 时如果获取不到 key返回一个常量“fallback_user”让限流照常工作但不要做太复杂的逻辑。5. 故障自愈演练实录一次下游服务“挂了”的完整处理过程本节的灵感来自我自己在测试环境的一次演练。场景是 user-service 被故意 kill网关如何依赖监控与自愈能力保持可用。整个过程可以拆成几个阶段也顺便给大家看看排查思路。5.1 阶段一发现故障与告警我在压测脚本里同时对 /api/user/100 发起 200 次并发请求。当 user-service 被 kill 后几秒钟内 Prometheus 里 gateway_http_requests_total 立刻出现大量 500/503 状态码而且 Grafana 中 user-service 路由的P99延迟从过去的 12ms 飙到 500ms 以上。告警规则很快触发收到钉钉通知。这里有个很容易被忽略的点网关默认对下游连接失败未必返回 503而可能返回 500 或 502。所以告警规则不能只盯 5xx 总数还要把 502 和 503 单独列出来。我后来的告警规则拆成“网关侧 5xx 总量”和“路由级 503 趋势”两种。5.2 阶段二熔断生效与降级响应因为配置了 CircuitBreaker当连续失败率超过阈值后断路器状态从 CLOSED 变为 OPEN。这时后续请求直接进入 fallback 处理而不再继续调用已经不存在的 user-service。从 Grafana 上看错误率快速下降用户侧收到统一的降级响应而不再是无尽的 timeout。这里值得体验一下的是 HALF_OPEN 状态的探测。Resilience4j 默认会在一定时间窗口内放行一次请求如果成功就关闭断路如果失败就继续保持 OPEN。下线的服务重新启动后自动恢复效果立竿见影这就是“自愈”的关键。5.3 阶段三恢复后自动闭环等 user-service 重新起来网关的下游实例列表通过注册中心自动刷新。断路器从 HALF_OPEN 过渡到 CLOSED服务恢复。整个过程我没有重启网关也没有人工干预。这都得益于熔断器的探测机制和注册中心的动态刷新。但自愈并不是万无一失。我有一个被现实教育过的场景如果下游服务只是启动“成功”但依赖的数据库还没连上仍然会在前几十个请求返回 500而这时断路器可能直接被这些假启动请求打回 OPEN 状态。为了避免这种抖动我后来在 user-service 的启动检查里增加了数据库连接池可用性检查同时把断路器滑动窗口设成 10 秒内至少 5 次失败才打开而不是一有失败就熔断。5.4 阶段四复盘时要注意的监控数据故障结束后我会直接拉取这次时间段的 zipkin 链路找出所有走到 fallback 的调用确认 fallback 的类型和触发原因。如果全链路里看不出网关到下游的 span可能是链路上下文丢失问题我会优先排查 traceId 日志。如果能看到下游 span 且超时时间很长则要优化下游连接池和超时时间。复盘阶段我强烈建议把原始请求日志和 traceId 关联起来比如在 Loki 里直接搜 traceId看网关日志是否记录了导致失败的异常堆栈。因为我当初就遇到过一次断路器状态变为 OPEN 的原因不是下游超时而是网关连接池被占满。这个在指标上表现为“下游耗时不高但网关线程阻塞”如果只看慢 SQL 日志根本找不到原因。6. 生产环境加固的五个细节超时、并发、配置、语义、演练做完核心监控和自愈配置距离真正上生产还差一点。我总结了五个特别容易被忽略但影响巨大的细节建议你一条条核对。6.1 显式设置超时时间不要依赖下游网关默认的 HttpClient 超时时间可能比你想象的长。在 Spring Cloud Gateway 里设置全局超时spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 3s这个时间要结合下游 P99 设置。如果下游最慢 5 秒你设 3 秒就会大量触发超时熔断如果下游承诺 2 秒内必须返回你设 5 秒就是对故障的纵容。我的实践是先按下游 SLA 的 P99 乘以 1.5 作为网关超时阈值然后逐步收紧。6.2 防止并发不足导致自愈失效如果网关配置了线程池隔离或信号量隔离要估算并发上限。我用 Resilience4j 的 Bulkhead 为每个路由设置了最大并发数如 50否则当下游故障时大量堆积的请求会占满线程池导致正常请求也被阻塞。这里推荐看线程数、等待队列和拒绝请求数三个指标。6.3 配置项纳入配置中心管理所有路由、熔断阈值和限流参数都不要写死在代码里建议通过 Nacos 或 Spring Cloud Config 动态刷新。更新阈值时无需重启网关可以快速调整。但要注意动态刷新会丢失当前计数状态如果你正处在熔断 OPEN 状态刷新配置可能导致熔断器重置。所以生产环境修改熔断阈值时最好先确认故障已经恢复。6.4 对fallback URI做语义化处理fallback 应该是用户可理解的降级返回或者在网关层直接返回一个标准错误码。不要让 fallback 转发到某个会再次失败的 controller。我推荐在 fallback 接口里做全局统一结果封装比如返回 code10012description系统繁忙请稍后再试这样前端可以对应处理。6.5 把故障演练变成例行任务自愈机制如果从不演练等真出事时大概率发现某个环节配置错了。我建议每个月做一次“下游随机停服演练”用脚本随机停掉一个核心测试环境服务观察告警、熔断、降级和恢复的完整过程。演练结果记录下来哪里没自动化就继续补齐。这不是走形式而是真正让系统具备自愈能力的唯一方法。这五个细节看起来都不难但每个点都直接影响自愈效果。比如超时时间设太长重试发不可控并发上限设太小正常流量下也可能触发降级fallback 设计不当用户看到的错误比死掉的服务还难懂。做网关监控和自愈不只是搭几个组件而是要当成一个长期打磨的基础设施来维护。我的体会是任何一个环节的设置都要既有监控可观察又能自动处理同时通过演练确认它真的能跑通。这样系统才能真正称得上“全链路监控与故障自愈”。
返回列表