尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

线上故障排查指南:从监控告警到限流熔断降级的完整实践

线上故障排查指南:从监控告警到限流熔断降级的完整实践 鉴定业务系统一旦进入大促或热点事件最常见的现象不是业务量平稳上涨而是告警列表突然被刷屏。订单服务超时、鉴定报告查询变慢、文件上传失败、支付回调丢失各种异常像失火现场一样同时冒烟。这篇文章从一次线上告警出发讲清楚如何用监控指标先定位问题如何通过压测复现故障以及限流、熔断、降级这三类防护手段到底该怎么配置最后给出一套可以直接复用的排查链路和上线前检查清单。适用读者包括负责后台服务的开发、运维和测试人员尤其是正在维护电商、二手交易或商品鉴定类系统的团队。这类系统的核心业务是用户提交商品图片和资料鉴定师审核后出具报告用户查询报告并付费。链路本身并不复杂复杂的是当大量请求在短时间内涌入时任何一个环节都可能成为瓶颈。只有先弄清楚线上故障的真实形态才能决定接下来是加机器、加缓存还是加限流规则。1. 先理解线上告警背后到底发生了什么1.1 一个典型鉴定系统会经过哪些链路以“提交鉴定订单”为例一次完整请求通常会经过客户端通过网关进入鉴定服务。服务先做身份认证和权限校验。读取商品类目和鉴定师排班信息通常命中 Redis。创建订单记录写入 MySQL。上传商品图片到对象存储返回图片地址。发送消息到 MQ通知鉴定师工作台刷新任务列表。返回订单号和下一步操作提示。这条链路里服务端、数据库、缓存、对象存储、消息队列任何一个出现慢请求用户感知到的都是“转圈”“提交失败”或“页面超时”。1.2 不同“起火”现象对应的问题层面线上告警不会直接告诉你“数据库连接池满了”或“Redis 挂了”它只会告诉你“接口超时率超过阈值”。需要从现象反推层面。告警现象可能问题层面优先检查对象接口 P99 延迟升高应用线程阻塞、锁竞争、GC 异常线程栈、GC 日志、慢调用链数据库连接池用满慢 SQL、锁等待、连接回收不及时MySQL 慢查询、锁监控Redis 响应变慢大 Key、热点 Key、内存淘汰Redis 慢日志、bigkey 扫描MQ 消息积压消费速度小于生产速度消费者线程数、消费逻辑耗时文件上传失败对象存储带宽、签名过期、网络抖动对象存储状态、上传日志5xx 突然增多实例崩溃、启动失败、配置错误应用日志、注册中心实例列表不要一上来就怀疑某个中间件而是先给现象分类。错误率、延迟、吞吐量、饱和度这四类黄金指标分别对应不同的排查方向。1.3 为什么突发流量比常态流量更容易搞垮系统常态流量下系统各层资源都处于舒适区。数据库连接池有富余线程池有空闲缓存命中率稳定。突发流量的危险在于它会在短时间内同时触发多个瓶颈缓存大量过期后请求穿透到数据库。数据库连接池被占满后新请求开始排队。线程池任务堆积导致响应时间指数级上升。前端超时重试再次放大流量。更麻烦的是这些现象会互相放大。数据库变慢导致连接不释放连接不释放导致服务线程阻塞服务线程阻塞导致健康检查失败最终实例被摘除。原本一台实例能承担的流量在故障过程中反而变成了累赘。所以处理突发流量不能只靠扩容必须让系统本身具备“吸收冲击”的能力这就是限流、熔断、降级要解决的问题。2. 监控先行没有指标排查就靠猜2.1 监控体系需要哪些组件一个可用的监控体系至少包含指标采集、存储查询、告警通知和日志检索。常见组合是组件作用Prometheus采集和存储时序指标Grafana展示监控大盘和资源趋势Alertmanager将告警路由到钉钉、企业微信、邮件SkyWalking / Zipkin追踪分布式调用链路ELK / Loki收集和检索应用日志不一定要上非常重的组件但至少要保证应用层、数据库层、缓存层三类指标是连续可查的。如果故障发生时只能登录服务器敲top说明监控还停留在救火而不是防火阶段。2.2 Spring Boot 应用接入 Prometheus 的最小配置以 Java 服务为例先引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency然后在application.yml中暴露 Prometheus 端点management: endpoints: web: exposure: include: health,info,prometheus,metrics metrics: tags: application: appraisal-service这里故意将application: appraisal-service作为固定标签目的是在多个服务实例上报指标时可以按应用名聚合查询。Prometheus 采集配置scrape_configs: - job_name: appraisal-service metrics_path: /actuator/prometheus static_configs: - targets: [10.0.0.11:8080]配置完后访问/actuator/prometheus能看到类似http_server_requests_seconds_count的指标。这个指标是判断接口吞吐量的基础也是设置告警规则的依据。2.3 告警规则要设置成“提前发现”而不是“事后确认”很多团队把告警阈值设得太高等到实例宕机才收到消息。更好的做法是分阶段设置错误率超过 1% 就告警而不是到 30% 才告警。P99 延迟连续 3 分钟超过 500ms 就告警。实例离线 1 分钟就告警而不是 15 分钟。下面是 Prometheus 告警规则示例groups: - name: appraisal-alerts rules: - alert: InstanceDown expr: up{jobappraisal-service} 0 for: 1m labels: severity: critical annotations: summary: 鉴定服务实例离线 description: 实例 {{ $labels.instance }} 已经离线超过 1 分钟 - alert: HighErrorRate expr: | rate(http_server_requests_seconds_count{status~5..}[5m]) / rate(http_server_requests_seconds_count[5m]) 0.01 for: 3m labels: severity: warning annotations: summary: 5xx 错误率超过 1% description: 最近 5 分钟错误率超过 1%需要检查应用日志和上游依赖这里for: 3m是为了防止瞬时抖动造成告警轰炸。持续才告警临时波动只记录不打扰。2.4 学习环境与生产环境的差异化要求学习环境跑通监控只需要一台 Prometheus 和一台被监控服务但生产环境必须考虑更多维度学习环境生产环境指标保留周期几天即可至少 30 天以上便于趋势分析告警通知可选必须接入值班组并设置分级日志采集本地文件即可集中式日志平台按 TraceID 串联监控覆盖范围单服务网关、服务、数据库、缓存、MQ、对象存储全链路权限控制不严格生产监控大盘和数据需要按角色授权生产环境还应该在监控大盘中内置“故障时间线”记录每次发布、回滚、弹性扩容和告警触发时间方便后续复盘。3. 压测复现先制造一次可控的“着火现场”3.1 压测不是看系统能扛多少而是看故障如何发生没有压测过就直接上生产遇到突发流量时几乎无法判断系统能支撑多久。推荐的做法是定期对核心接口做短时压测重点观察从正常到异常的过程中哪个指标先恶化。以鉴定系统为例需要压测两个核心场景提交鉴定订单写多读少压力在数据库和文件上传。查询鉴定报告读多写少压力在缓存和数据库。压测数据要尽量接近真实数据量不能只有几千条测试数据否则数据库和缓存表现会失真。3.2 用 JMeter 发起一次突发流量压测可以在 JMeter 中构建一个“查询鉴定报告”的 HTTP 请求脚本名称为report-query.jmx线程数、持续时长、QPS 通过命令行参数传入jmeter -n -t report-query.jmx -l result.jtl -e -o report/ -Jthreads200 -Jduration120其中-n表示非 GUI 模式。-l result.jtl保存原始结果。-e -o report/生成 HTML 报告。-Jthreads200设置并发线程数。-Jduration120设置持续时长 120 秒。压测过程中需要同步观察服务端指标而不是只等结束后看报告。通过curl快速查看 Prometheus 指标curl -s http://localhost:9090/api/v1/query?queryrate(http_server_requests_seconds_count{uri/api/identification/report}[1m]) | jq这样能在压测中途判断接口吞吐量是否还能跟上负载。3.3 压测中重点看哪些指标压测时关注四个层次接口层QPS、RT、错误率。应用层线程池活跃数、GC 频率、堆内存使用率。中间件层Redis 命中率、MySQL 活跃连接数、MQ 积压量。系统层CPU、内存、磁盘 I/O。如果接口 RT 已经飙升但应用 CPU 和内存都正常就要怀疑下游依赖或连接池问题。如果 GC 频繁且 Full GC 明显则要检查堆内存和对象创建速率。3.4 从现象倒推根因的方法压测结束后不要只记录“系统扛不住”要回答为什么扛不住。推荐按这个顺序分析看错误率最高的接口是哪些。看该接口的调用链有几段耗时。看耗时最长的是应用内部逻辑还是下游调用。看是否出现连接池等待、线程阻塞、GC 长暂停。用线程栈确认阻塞点。只有当根因清楚了后续的限流阈值和扩容策略才有依据。否则很可能出现“加了更多机器但性能依然差”的情况。4. 防护策略限流、熔断、降级不是橡皮图章4.1 限流防止系统被瞬间打满限流的作用是在入口处拒绝多余流量保护系统不被压垮。常见算法有计数器、滑动窗口、漏斗和令牌桶。实战中更推荐滑动窗口或令牌桶因为它们能平滑突发流量避免出现“前 1 秒全被放过后 1 秒全被拒绝”的问题。以阿里 Sentinel 为例给“提交鉴定订单”接口配置 QPS 限流rules: - resource: POST /api/identification/order count: 500 grade: 1 limitApp: default strategy: 0 controlBehavior: 2参数含义count单机每秒允许通过的请求数。grade1 表示按 QPS 限流0 表示按并发线程数限流。controlBehavior2 表示匀速排队适合有缓冲队列的场景0 表示快速失败。这里要注意限流阈值不是随意拍的。它应该来自压测结果比如单机最大可支撑 800 QPS那么线上可以设置 500 QPS留出冗余。4.2 熔断防止故障在服务间传染熔断用于保护下游依赖。当调用“鉴定报告查询”接口失败率达到阈值时熔断器打开后续请求直接返回降级结果不再打到下游。Resilience4j 配置示例resilience4j.circuitbreaker: instances: identificationReportService: slidingWindowSize: 20 failureRateThreshold: 50 waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 5参数含义slidingWindowSize滑动窗口大小统计最近 20 次调用。failureRateThreshold失败率达到 50% 时打开熔断器。waitDurationInOpenState熔断打开后等待 30 秒进入半开状态。permittedNumberOfCallsInHalfOpenState半开状态下允许试探的请求数。这里的关键是half-open试探逻辑。如果没有半开状态熔断恢复只能靠时间可能会把未恢复的下游又压垮。4.3 降级保核心功能放掉非核心功能降级是主动放弃部分功能换取核心功能的可用性。鉴定系统里常见的降级策略查询鉴定报告时如果图片分析服务超时返回缓存中的基础信息不展示动态解析结果。提交订单时如果优惠券计算服务不可用先跳过优惠校验只创建订单等后续补偿。发送通知消息失败时不阻塞主流程先落库再由定时任务重试。降级一定要在接口设计阶段就想好不能在故障发生时才临时加判断逻辑。4.4 参数选择速查表参数含义设置太小的影响设置太大的影响推荐思路限流 QPS单机每秒最大请求数正常流量被误伤系统可能被打垮基于压测结果留 30% 余量熔断失败率阈值触发熔断的失败比例抖动就熔断影响体验故障扩散拖垮上游结合超时重试策略综合判断熔断窗口时长熔断打开后等待恢复时间恢复过频下游恢复后仍无法访问先短后长配合半开试探降级超时时间调用下游的最大等待时间正常慢请求被降级线程长时间阻塞参考 P99 延迟留 50% 余量限流和熔断不是一劳永逸的。每次大促或者功能迭代后都要根据监控数据重新校准阈值。5. 代码实现中容易埋雷的地方5.1 线程池没有隔离一个接口拖垮整个服务很多服务所有接口共用一个线程池。当某个接口出现慢调用时线程池被占满其他接口也无法处理请求。这就是典型的“线程池饥饿”。推荐做法是按业务拆分线程池。比如鉴定任务的异步处理单独分配一个线程池private final ThreadPoolExecutor identificationExecutor new ThreadPoolExecutor( 10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new NamedThreadFactory(identification-order), new ThreadPoolExecutor.CallerRunsPolicy() );这里的CallerRunsPolicy是关键。队列满了之后任务不会抛弃而是由提交任务的线程来执行起到天然背压的效果。不要默认使用AbortPolicy否则线上会直接抛出RejectedExecutionException。5.2 缓存雪崩、击穿、穿透鉴定报告查询是典型的读多写少场景缓存使用不当会直接引发数据库故障。缓存穿透恶意请求大量查询不存在的报告 ID导致请求每次都打到数据库。解决方法是缓存空值或者使用布隆过滤器。缓存击穿某个热点报告过期瞬间大量请求同时查询数据库。解决方法是加互斥锁重建缓存。缓存雪崩大量缓存同时过期数据库压力瞬间骤增。解决方法是增加随机过期时间。示例给缓存过期时间增加随机值。Duration baseTtl Duration.ofMinutes(30); long randomTtl ThreadLocalRandom.current().nextLong(5 * 60); redisTemplate.expire(cacheKey, baseTtl.plusSeconds(randomTtl));5.3 数据库连接池配置不合理连接池太小正常流量排队连接池太大数据库被拖垮。常见的 HikariCP 配置spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000connection-timeout设置为 3000 毫秒很关键。数据库连接不可用时请求会快速失败而不是长期阻塞。生产环境要根据数据库规格和 QPS 动态调整maximum-pool-size不要照抄网上的 100、200。5.4 日志刷屏和慢 SQL故障发生时最容易出现“日志刷屏”和“慢 SQL 堆积”两个问题。日志刷屏会占满磁盘 I/O让本可恢复的服务变得更慢。线上日志级别要控制在 INFO 以上异常堆栈只在 WARN 和 ERROR 级别输出一次不要在循环中重复打印。慢 SQL 需要先定位再优化。通过 MySQL 慢查询日志确认 Top SQLSELECT report_id, order_id, status, created_at FROM identification_report WHERE order_id ? ORDER BY created_at DESC LIMIT 1;如果order_id没有索引这条 SQL 在高并发下会瞬间拖垮数据库。排查时可以执行EXPLAIN SELECT report_id, order_id, status, created_at FROM identification_report WHERE order_id ? ORDER BY created_at DESC;看到typeALL或rows很大就要考虑加复合索引。6. 一次完整排查案例鉴定订单服务大面积超时6.1 故障现象某个业务大促当天监控显示“提交鉴定订单”接口 P99 延迟从 300ms 飙升到 5000ms5xx 错误率超过 8%。约 20 分钟后数据库连接池告警。此时用户反馈无法提交订单部分鉴定师工作台也出现任务加载失败。6.2 排查步骤按顺序执行以下排查不要跳步。第一步确认影响范围。curl -s http://localhost:9090/api/v1/query?querysum(rate(http_server_requests_seconds_count{status~5..}[5m])) by (uri) | jq从结果看只有/api/identification/order和/api/identification/report两个接口错误率很高。第二步查看实例资源。top -Hp $(pgrep -f appraisal-service)CPU 和内存均未到瓶颈排除应用本身被打满的可能。第三步查看 GC 和线程状态。jstat -gcutil pid 1000 10 jstack pid thread_dump.txtGC 正常但线程栈中发现大量线程阻塞在数据库连接池等待获取连接的位置。第四步检查数据库。SHOW GLOBAL STATUS LIKE Slow_queries; SHOW PROCESSLIST;发现大量 SQL 在执行类似SELECT ... FROM identification_report WHERE order_id ?时没有走索引同一个report_id的查询在一秒内出现几百次。第五步检查缓存。通过 Redis 监控发现热点报告的缓存刚好在同一时间过期大量查询直接穿透到 MySQL。6.3 根因与修复根因是热点报告缓存过期后发生缓存击穿相同请求并发打到数据库数据库查询因缺少索引变得很慢连接池被占满后新提交订单的请求也开始阻塞最终形成雪崩。修复动作给identification_report.order_id添加复合索引ALTER TABLE identification_report ADD INDEX idx_order_id_created_at (order_id, created_at);对热点报告缓存增加互斥锁只允许一个线程去数据库加载String lockKey lock:report: reportId; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (Boolean.TRUE.equals(locked)) { try { Report report reportMapper.selectByReportId(reportId); redisTemplate.opsForValue().set(cache:report: reportId, report, Duration.ofMinutes(30)); return report; } finally { redisTemplate.delete(lockKey); } }对查询报告接口增加限流规则单机 QPS 控制在压测值的 70%。6.4 修复后的验证修复后重新运行压测查询报告接口 P99 回到 300ms 以下数据库活跃连接数稳定在 10 到 15 之间。再查看监控5xx 错误率归零订单提交接口恢复正常。这个案例说明很多线上故障不是靠重启能解决的。缓存策略、索引设计和限流规则必须提前准备好否则故障会在最忙的时候找上门。7. 上线前与故障后的可复用检查清单7.1 上线前检查清单检查项是否完成备注核心接口是否压测过是/否确认单机吞吐量和瓶颈点是否有监控大盘和告警规则是/否覆盖应用、数据库、缓存、MQ是否配置限流规则是/否阈值来自压测结果是否配置熔断和降级策略是/否下游依赖异常时可快速恢复缓存是否处理穿透、击穿、雪崩是/否空值缓存、互斥锁、随机过期数据库慢 SQL 是否优化是/否通过 EXPLAIN 确认索引线程池是否隔离是/否核心业务和非核心业务分开日志是否包含 TraceID是/否故障时能串联整条调用链回滚方案是否明确是/否发布异常时能快速回滚值班和告警联系人是否确认是/否避免故障时找不到人7.2 故障后 Review 清单故障处理完不代表结束还需要复盘根因是否明确而不是只看到“数据库慢”。相同问题是否存在于其他接口或模块。告警阈值是否在故障真正发生时提前触发了。限流和熔断参数是否需要调整。有无补充回归用例覆盖本次故障场景。故障时长、影响范围是否记录到文档中。这些清单不需要一次性做得非常重但至少要确保每次上线前能过一遍尤其是大版本和大促场景。回到最开始的问题线上故障最怕的不是故障本身而是不知道从哪查起。只要监控到位、压测常在、限流熔断配置合理突发流量带来的并不是灾难而是一次检验系统鲁棒性的机会。这套排查链路和检查清单可以应用到大多数业务系统不只是鉴定类平台。希望下次你面对告警刷屏时第一反应不是重启实例而是打开监控、确认范围、定位根因、再决定如何修复。
返回列表