饿了好几天的店铺流量终于被拉回来之后,我才真正意识到,饿了么CPS系统的Java服务,最怕的不是业务逻辑写错,而是服务还活着,但是已经“病入膏肓”。那段时间接口时不时超时,订单同步偶尔卡顿,半夜被电话叫醒已经是家常便饭。后来我们把资源监控和告警机制彻底搭起来,情况才好转。今天这篇文章,就把我在饿了么CPS系统里给Java服务搭建资源监控与告警机制的过程、选型思路、阈值设计、踩坑记录完整分享出来,给正在做同类业务的同学一个参考。
这套东西适合谁看?如果你负责的Java服务是典型的CPS返佣、订单同步、分账结算这类高并发、强依赖中间件的系统,或者你刚接手一个线上服务,发现全靠人工盯日志、被用户投诉了才知道出问题,那这篇文章就是给你准备的。不需要你有多深的基础,只要会用Spring Boot,跟着思路走就能落地。
1. 把CPS系统的“脾气”摸清楚,再谈监控
1.1 饿了么CPS系统的业务链路与资源瓶颈
CPS的全称是Cost Per Sale,按成交付费。饿了么CPS系统说白了就是一套返佣结算系统,用户通过推广链接下单,我们根据订单归属给推广方计算佣金。所以这套Java服务有一个很明显的特征:请求量极具脉冲性。午餐、晚餐前后的外卖高峰期,订单相关的接口QPS能瞬间冲到日常的十几倍,而且对下单回传、订单状态变更通知这类接口的实时性要求极高,晚几秒就可能影响到返佣归因。
这类服务的资源瓶颈往往不在单机CPU算力上,而在线程、内存、数据库连接池、以及下游Redis缓存的稳定性上。订单归因要查Redis,订单数据要写MySQL,佣金计算要跑异步任务,任何一个环节出现资源争抢,整条链路都会被拖垮。线上环境里,服务不是一个人写的,谁也不敢保证自己负责的那段代码没有内存泄漏、没有线程滞留,所以不做资源监控,基本等于蒙着眼睛开车。
1.2 监控方案选型:为什么我推荐Prometheus组合拳
做资源监控的工具有很多,老的Zabbix、新的SkyWalking、云厂商自带的监控平台,我都接触过。但在CPS这种以Java服务为主、微服务拆分较细、中间件依赖重的场景里,我最推荐的是 Prometheus + Grafana + Alertmanager 这一套,配合Spring Boot Actuator暴露指标。
选它有几个很实际的原因:
- Java生态支持好,Spring Boot应用通过Micrometer可以直接把JVM、线程池、HTTP请求等指标暴露成Prometheus格式,基本不用写额外的采集代码
- 告警规则灵活,PromQL查询语言功能很强,能按服务、按实例、按接口维度做同比、环比、连续多周期判定,而不是僵硬的阈值比较
- 部署轻量,相比SkyWalking对字节码增强、链路数据存储的要求,Prometheus本身就是一个二进制,抓取指标库也不算重
- Grafana做可视化太方便,社区里有现成的JVM监控面板,导入就能用,节省大量时间
Zabbix也不是不能用,但它的Agent对Java服务的细粒度指标支持差一些,画出来的图也不够灵活。云厂商自带监控虽然省事,但一旦你在本地机房或者自建K8s上跑,就绑死了。所以除非公司已经有统一的监控平台且运维不愿意放开,否则自己搭一套Prometheus组合拳,性价比最高。这里我踩过最深的坑是:监控系统和业务系统必须是两套独立部署、独立资源占用的设施。 我一开始图省事,把Prometheus跑在业务服务所在的同一台机器上,结果业务服务一OOM,机器负载飙升,Prometheus先挂了,告警也发不出来,属于典型的骑着马找马。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java服务资源监控:哪些指标值得盯,盯到什么程度
2.1 JVM内存与GC监控:内存溢出和停顿的照妖镜
在饿了么CPS系统里,JVM监控是重中之重。因为订单归因、返佣结算这类逻辑里,会有不少明细数据的聚合查询,数据量大时,一个订单批量回传可能就要一次性加载几万条的明细做计算。如果代码里再有一处不留神把大对象放进了静态Map,老年代内存就像漏水的水缸,迟迟不见下降,最后就是Full GC越来越频繁,接口响应时间从几十毫秒飙升到几秒。
JVM层面我主要盯着几个指标:
- 堆内存使用量:包括Eden、Survivor、Old三个区域的使用率。Old区如果持续高于70%且GC之后不回落,基本就是大对象一直在堆积
- GC次数和GC耗时:Young GC每分钟多少次,Full GC多久一次、单次耗时多长,这两个数值直接反映JVM的“呼吸”是否顺畅
- 非堆内存:Metaspace这个区域最容易忽略,但动态生成类(CGLIB、反射、热部署)多的服务,Metaspace会慢慢涨,涨到太大一样会触发频繁GC
- 线程数:JVM里的活动线程数、阻塞线程数、死锁线程数,这个后面单独讲
参考阈值方面,以一台4核8G,堆内存分配4G的服务为例:Young GC频率如果超过每秒一次,说明对象创建速度过快;Full GC如果一天超过5次,就要开始查了;单次Full GC耗时如果超过500ms,线上接口延时基本能明显感知。JVM监控不能只看当前值,要把GC趋势图拉出来看48小时或一周的变化,很多内存问题都是量变引起质变,当前值看着正常,趋势已经走高。 我也被这个问题坑过,排查一个订单明细查询的接口时,单看当前内存完全正常,翻趋势图才发现每天凌晨批量任务一跑,内存就涨一轮,连续几天之后Old区被推高到一个危险水位。
2.2 线程池与Tomcat线程:高并发下的隐性杀手
CPS系统里有很多异步操作,尤其是返佣计算、任务发放、数据同步这类适合用线程池异步化的逻辑。线程池的参数设置,在低峰期根本看不出来问题,一到高峰期就原形毕露。最常见的情况是核心线程数设置过小、队列容量设得过大,任务疯狂堆积在BlockingQueue里,看起来请求都进来了,实际一个都没在处理,接口全部假死。
线程池监控要拿到的指标包括:
- 当前活跃线程数
- 线程池中执行任务的数量,包括正在执行的、排队中的
- 被拒绝的任务数量
- 核心线程数、最大线程数的配置值
线程池的拒绝任务数这个指标特别重要,一旦出现拒绝任务,哪怕只有一次,都说明线程池已经崩溃边缘了,对应的系统一定会出现大批超时或失败重试。我在代码里手动实现了ThreadPoolExecutor的beforeExecute和afterExecute方法埋点,将活跃线程数和队列大小定期写到Metrics里,这样Grafana上就能直接看到每个业务线程池的负荷。
除了自定义线程池,Tomcat容器线程也是监控重点。Spring Boot内置的Tomcat线程池,默认最大线程数是200,一旦CPS高峰期并发请求超过这个数,后面的请求就会排队等待,表现为接口RT飙升但CPU使用率不高。监控指标要看tomcat.threads.busy和tomcat.threads.config.max这两个值,如果busy经常打到max的80%以上,就该扩容线程池或优化接口性能了。
2.3 CPU、内存与磁盘等基础资源:先看资源再看代码
Java服务跑在云主机或容器里,光看JVM内部是不够的。系统级的CPU使用率、内存使用率、磁盘IO、网络吞吐和负载值,是整个服务运行的基础。有时候JVM状态看着正常,但机器CPU已经被别的进程吃满,Java程序再优化也没有意义。
系统指标里重点注意:
- CPU使用率:长期超过80%,就要看是JVM线程还是系统进程耗的CPU,用top -Hp命令到线程级别定位
- Load Average:这个值如果长期超过机器的逻辑核数,说明系统严重过载,排队执行的进程或线程太多了
- 磁盘空间与IO:日志文件、GC日志、监控指标本身都会占磁盘,磁盘满了服务会直接报错。CPS系统的订单回调日志量特别大,每天几个GB很正常,不处理的话磁盘空间告警比业务告警来得还勤快
- 内存使用率:Linux的free命令看到used内存高不用慌,可能是page cache,但如果swap使用率经常大于0,说明物理内存已经不够了,性能会急剧下降
2.4 中间件资源监控:Redis与MySQL是高并发链路的地基
CPS系统几乎所有的订单归因、推广位配置、账户佣金余额都缓存在Redis里,订单落库则全部依赖MySQL。这两个中间件一旦出问题,Java服务本身再健康也白搭。
Redis监控的重点:
- 连接数:每个CPS实例都会创建Jedis或Lettuce连接池,连接数瞬间暴涨,大多是因为代码里获取连接后没有归还,或者并发量超过了连接池上限
- 内存使用率:Redis的内存是稀缺资源,如果设置了maxmemory且淘汰策略是allkeys-lru,高峰期的缓存命中率会急剧下降,大量请求穿透到数据库
- 慢查询和命中率:CPS系统里订单归因的Key通常业务复杂,存在大量Hash和ZSet操作,慢查询一多,接口RT就会被拉高
MySQL监控的重点:
- 慢SQL数量和耗时:这是最直观的指标。CPS的订单查询SQL经常带多表关联,一个索引没建好,高峰期能把数据库拖垮
- 连接池使用率:Java服务里常用HikariCP,连接池的活跃连接数达到最大连接数的80%以上,就要考虑扩容或排查连接泄漏
- 主从延迟:订单数据的读操作一般走从库,主从延迟一旦超过几秒,用户查到的订单状态就会和实际不一致
3. 监控落地的关键步骤与配置细节
3.1 接入Actuator和Micrometer,把JVM指标暴露出来
要在Prometheus里看到Java服务的指标,第一步是让服务把自己的运行状态变成可抓取的HTTP接口。Spring Boot 2.0以上的项目,接Prometheus非常简单,核心是引入两个依赖。
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后配置一下暴露的端点,把prometheus这个endpoint打开:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,prometheus
metrics:
tags:
application: ${spring.application.name}
配置完成后,启动服务,访问 /actuator/prometheus 接口,如果能看到一大片以 jvm_、tomcat_、http_server_requests_seconds 开头的指标,就说明Micrometer已经自动把JVM相关的指标暴露出来了。这里有个细节:Redisson、Lettuce、HikariCP这些常用的客户端库,Micrometer都有自动埋点,不需要手动上报,但前提是版本不能太旧。 我当年就是被一个老项目的Jedis版本拖住,Redis连接池的指标始终是空的,后来把Jedis换成Lettuce,指标立刻就出来了。
3.2 Prometheus抓取配置与Grafana展示大盘
Prometheus的抓取配置比较简单,只需要告诉它哪些服务需要监控、多久抓一次。CPS系统里服务实例不少,白天高峰期会扩到十几台,所以我用基于文件的服务发现,把实例IP和端口写到targets里,扩缩容时自动更新。
yaml复制scrape_configs:
- job_name: 'cps-java-service'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['10.0.1.11:8080', '10.0.1.12:8080', '10.0.1.13:8080']
labels:
app: 'cps-order-service'
抓取间隔我建议15秒,太短会给业务服务带来额外压力,太长会错过告警的黄金节点。抓取间隔和告警判定周期要匹配,比如告警规则里要求“持续3分钟异常”才触发,那么15秒一个点就是12个持续异常数据点,这样能滤掉很多毛刺。
Grafana这边不用从零开始画,直接在Grafana官网的Dashboard市场里搜索“JVM Micrometer”,会出来一堆别人分享的源码面板,找一个评分高的导入即可。导入后改一下数据源,把Prometheus地址填进去,JVM堆内存、GC次数、线程数、CPU使用率这些图表就全有了。我自己的习惯是再加一个“订单核心链路”面板,把接口QPS、P99耗时、Redis连接数、MySQL慢查询数量这几个指标放在同一个页面上。这样一次告警过来,扫一眼面板就能定位问题大致在哪个环节。
3.3 别忘了把GC日志打开,关键时刻能救命
监控体系里最容易被忽略但线上排查价值极高的,是GC日志。很多时候内存问题在监控图上只能看到Old区上涨、Full GC频繁,但具体是什么对象占满了堆,还得靠GC日志配合堆dump才能查清楚。所以服务启动参数里务必加上GC日志输出。
推荐的基础JVM参数模板如下:
bash复制-Xms4g -Xmx4g
-Xmn1g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/jvm/dump/
-Xlog:gc*:/data/logs/jvm/gc.log:time,uptime,level,tags
我见过太多服务OOM之后直接崩溃重启,连dump都没留下,事后根本没法复盘。加了-XX:+HeapDumpOnOutOfMemoryError之后,至少在OOM的那一刻能把堆内存快照留下来。GC日志也要做日志轮转,不然一个运行半年的服务,GC日志文件能涨到几十GB,又把磁盘空间给吃没了。
4. 告警机制:规则怎么定,风暴怎么防
4.1 告警规则的分层设计:从P0到P2分级响应
资源监控如果只看图表不配告警,那等于白搭。没有告警机制,白天可能还记得看几眼监控面板,晚上就只能靠运气了。告警规则的设计,我强烈建议按影响范围和紧急程度分层,而不是所有指标一视同仁。
我自己在CPS系统里是分三层:
- P0:直接影响用户下单和返佣的,比如核心下单回传接口的错误率超过1%、服务实例超过一半掉线、Redis不可用。这类告警必须7x24小时短信加电话,人不在工位也要停下手里的事处理
- P1:服务质量下降但业务还可用的,比如接口P99耗时超过2秒、Full GC触发频率过高、磁盘空间不足80%、MySQL主从延迟超过30秒。这类走钉钉机器人或者企业微信告警群,要求几分钟内有人响应
- P2:趋势类风险,比如堆内存水位48小时内连续上涨、线程池队列增长到容量的60%。这类可以只在白天工作时间推送,作为预防性提醒
4.2 用PromQL把CPS业务特有的指标写成告警规则
Prometheus的告警规则是写在YAML里的,核心逻辑就是PromQL表达式加一个持续时间。下面是我用的两条典型规则。
第一个是接口错误率告警。CPS系统里订单状态回传接口(/api/order/notify)一旦出错,用户下单后推广方就收不到归因,直接影响佣金结算:
yaml复制groups:
- name: cps-alerts
rules:
- alert: 订单回传接口错误率过高
expr: |
sum(rate(http_server_requests_seconds_count{uri="/api/order/notify",status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count{uri="/api/order/notify"}[5m])) > 0.01
for: 3m
labels:
severity: P0
annotations:
summary: "订单回传接口5xx比例超过1%"
第二个是线程池拒绝任务告警。这属于那种“平时很安静、一炸就是大事”的指标。我先在代码里用Micrometer暴露了自定义的cps_async_pool_rejected_total计数器,然后写规则:
yaml复制 - alert: 返佣计算线程池任务被拒绝
expr: |
increase(cps_async_pool_rejected_total[5m]) > 0
for: 1m
labels:
severity: P1
annotations:
summary: "返佣计算线程池出现任务拒绝,请立即查看线程池队列"
写PromQL的时候要特别小心计算口径。比如接口错误率,必须把“5xx的数量/总请求数”而不是“5xx数量/成功数”作为表达式,用成功率做分母会把问题放大好几倍。还要考虑接口本身存在正常的4xx错误,比如参数校验失败,所以分母里到底包不包含4xx、5xx,各家业务不一样,但一定要想清楚再写。还有一点,告警规则里的for: 3m不是摆设,它是防止瞬时毛刺误报的关键。CPS高峰期某一次GC停顿可能造成一两秒的接口超时,拉高错误率,但这样持续三分钟的概率很低。如果没有for条件,这种毛刺就会变成半夜三点吵醒你的“狼来了”。
4.3 告警通知路由与静默策略,告别“告警风暴”
告警不仅要有,还要能准确送达正确的人。我建议告警消息统一由Alertmanager接收,再由它转到钉钉、企业微信、短信或电话平台。Alertmanager的路由规则可以按告警级别决定发给谁:
yaml复制route:
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 2h
routes:
- match:
severity: P0
receiver: p0-phone-and-dingding
repeat_interval: 30m
- match:
severity: P1
receiver: p1-dingding
这套配置的核心逻辑是把同类告警聚合起来。比如同一个接口的错误率告警,8台机器同时触发,本来会收到8条消息,但group_by: ['alertname']会让Alertmanager把它们合并成一条通知,而不是轰炸式地发8条。repeat_interval也很关键,P0告警半小时重复一次,P1告警两小时重复一次,避免一条没处理完的告警每5分钟就推一次,最后群里全是刷屏。
静默策略主要用于已知问题的维护窗口。比如每周二凌晨的定时任务全量批量算佣金,这段时间数据库主从延迟肯定偏高,但那属于计划内现象,不需要告警。我提前在Alertmanager里配置silence,到点自动静默,等任务结束再恢复告警。这要是没配好,就会出现告警风暴,一群人半夜被拉起来,一查是正常任务,确实挺折腾人。
5. 常见问题与排查技巧实录
5.1 Full GC频繁导致接口大面积超时的实战案例
有一次CPS线上系统在晚上8点高峰期集中报P1告警,订单归因接口P99从200ms飙升到3秒,而且告警面板上Old区内存使用率一直在95%以上,Full GC每2分钟就触发一次。
排查思路是这样:先看监控图,确认Old区涨上去之后GC回收不下来,说明堆里有大量存活对象在堆积。接着做jmap堆dump,配合MAT工具分析,发现有个static的Map对象,里面存了几百万个推广商品明细,总大小近2GB。原来是某位同事为了减少SQL查询,把推广商品全量拉到内存缓存,还没做淘汰策略。高峰期用户下单实时回传时,会不断从这个Map里查数据并发起金额计算。因为Map只增不减,老年代被塞满,GC时间越来越长。
这个问题最终通过将商品明细缓存从进程内存迁到Redis ZSet并设置过期时间解决的。以后遇到这种内存持续增长、Full GC加密的告警,第一反应一定是先jmap dump再分析,千万不要在复现不了的环境里面一顿瞎改。 线上OOM或者内存异常已经是可复现的现场了,直接把现场留好,比什么猜测都强。
5.2 线程池队列堆积引发的“假死”排查
另一次是返佣对账的异步任务线程池队列几乎被打满,但业务接口却正常。告警触发之后,一开始很容易让人误判为接口问题,后来打开自定义线程池监控面板,发现“活跃线程数”只有20,但是“队列积压任务数”在持续增长。
这个现象很典型:ThreadPoolExecutor默认使用无界队列时,线程数永远只会增长到核心线程数,不会增长到最大线程数。你去问任何一个Java基础扎实的同学,他都能倒背如流;但真实线上出问题的时候,很多人第一反应却是加机器、加并发,而不是去查配置。后来我们把异步任务的线程池改成有界队列,拒绝策略设为调用者执行(CallerRunsPolicy),再配合前面那条“线程池拒绝任务”的告警规则,问题才算彻底闭环。
线程池参数不是拍脑袋定的,我一般是按高峰期任务量、单任务平均执行时长和可容忍的排队时间来算。 比如返佣计算任务高峰期每秒新增50个任务,单个任务平均耗时200ms,我们允许任务最多排队5秒,那队列长度就设置250左右,核心线程数按“每秒任务数 x 平均耗时”估算,大致就是50x0.2=10,再留2~3倍的余量。
5.3 慢SQL与缓存穿透引发的连锁资源告警
还有一次挺有意思的告警是Redis连接数、MySQL活跃会话数同时飙升,看起来像Redis或数据库宕机,实际上根因是慢SQL。本来订单详情查询都走Redis缓存,但某次大促商品数据调整之后,商品的缓存Key规则变了,新Key在缓存里查不到,所有请求直接打到MySQL。MySQL里那几条关联了多个大表的复杂SQL,在无缓存的情况下响应时间要2秒起步。
连接池在等待SQL返回期间是不会释放连接的,于是连接数迅速涨满。请求处理线程也全部卡在数据库调用上,Tomcat线程池、数据库连接池和接口RT的告警几乎同时爆发。这类问题的排查思路是“追源头”:先看中间件资源指标,再看链路追踪里的耗时分布。 线上要有SkyWalking或类似的链路系统,一眼就能发现耗时全在JDBC调用上。如果没有链路系统,就需要看MySQL慢日志,把执行时间超过1秒的SQL捞出来,对着业务场景修索引或改缓存策略。
5.4 告警误报与监控数据缺口
告警机制刚上线那段时间,最头疼的不是没告警,而是告警太灵敏,几乎天天误报。CPU使用率超过80%就告警,结果CPS大促高峰期CPU上90%其实很正常,持续个十分钟任务跑完就回落了。后来我把CPU告警规则改成了“CPU使用率大于85%持续10分钟,且Load Average大于逻辑核数的1.5倍”同时满足才触发。加了双条件之后,误报率直线下降。
另一个容易踩的坑是监控数据缺口。Prometheus抓取Java服务如果有一两个点失败,Grafana图上可能出现断线,但不会告警,因为PromQL查询时null值默认不触发。等业务方反馈“刚刚是不是挂了”的时候,你回看监控,才发现那段数据断了一截,只能证明服务“可能”有问题,具体发生了什么完全不知道。所以Prometheus自身的“targets up”状态必须单独挂一条告警,比如某个服务实例持续3分钟抓取失败,就视为服务不可用,及时通知人工介入,这个比任何业务指标的告警都要提前一步。
6. 一些沉淀下来的监控经验,直接抄就行
最后分享几条非常零碎但很实用的经验。第一,监控面板上的指标不是越多越好,每个团队应该有自己的一套核心指标清单,CPS系统我建议至少要覆盖接口QPS、P99耗时、线程池活跃数、JVM堆内存、GC频率、Redis连接数、MySQL慢SQL数这七项。先把这个大盘做到位,再往细了加。第二,告警规则要定期根据线上实际运行情况进行优化,比如大促前根据压测结果调整阈值,大促后进行复盘,看看哪些告警是“喊狼来了”,哪些真正的隐患没有暴露出来。第三,任何监控和告警配置的变更都要走代码审查流程,Prometheus的rules文件、Alertmanager的配置文件,都和业务代码一样应该有版本管理。我见过有人在线改告警规则,语法写错之后整个Alertmanager直接罢工,那段时间所有告警都发不出来,比不建监控还可怕。
我自己这几年的体会是,资源监控和告警机制不是一个一劳永逸的项目,它是跟着业务增长、代码演进、团队变化不断调整的“活系统”。CPS系统的特点是流量脉冲大、链路长、对实时性要求高,所以一定要给资源监控提前做好量级预估,做长趋势判断,不要等线上出了严重故障才开始搭。把监控和告警当成业务系统的一部分去认真对待,而不是搞完就丢给运维,Java服务才能真正跑得让人省心。
