Java写的CloudWatch指标导出器,让Prometheus轻松采集AWS监控数据

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个用Java开发的轻量级工具,能把AWS CloudWatch里的各种监控指标(比如EC2 CPU使用率、RDS连接数、Lambda调用次数等)实时转换成Prometheus能直接抓取的格式。支持Java 8+,编译用mvn package,运行就一条java -jar命令,指定端口和YAML配置文件(自带example.yml参考),默认走9106端口。认证走AWS标准凭证链:环境变量(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)、IAM角色、~/.aws/credentials文件都行;最小权限只要cloudwatch:ListMetrics和cloudwatch:GetMetricStatistics,如果要用标签筛选功能(aws_tag_select),再加个tag:GetResources就行。配置全靠YAML,可以按命名空间、指标名、维度(如InstanceId、LoadBalancerName)、统计方式(Average/Sum/Maximum)、采样周期(比如5分钟)和抓取间隔灵活设置。带Dockerfile,build完就能docker run跑起来;项目结构规范,含完整Maven配置(pom.xml)、源码(src/main)、测试(src/test)、许可证(LICENSE)、说明文档(README.md)、贡献指南(CONTRIBUTING.md)和安全策略(SECURITY.md),适合嵌入现有Prometheus监控体系,也方便二次开发或定制指标采集逻辑。

1. 项目概述:为什么我们需要一个“CloudWatch导出器”?

在 AWS 上跑生产服务的团队,几乎没人能绕开 CloudWatch——它是云上最原生、最全面的监控数据源:EC2 的 CPUUtilization、RDS 的 DatabaseConnections、ALB 的 HTTPCode_ELB_5XX_Count、Lambda 的 Invocations 和 Duration、ECS 任务的 MemoryUtilizationPercent……这些指标每天都在产生海量时序数据。但问题来了:如果你的监控栈已经统一用 Prometheus + Grafana,那 CloudWatch 就成了一个“数据孤岛”。你没法直接在 Grafana 里写 aws_ec2_cpu_utilization{instance_id="i-0a1b2c3d4e5f67890"} 这样的 PromQL 查询,更没法把 CloudWatch 指标和你自定义的 JVM 指标(比如 jvm_memory_used_bytes)放在同一张看板里做关联分析。

这时候,官方方案是 CloudWatch Exporter for Prometheus(由 AWS Labs 维护),但它用的是 Go 编写,配置偏静态,对 Java 技术栈团队来说,调试、定制、打 patch 都不够“顺手”。而我们今天聊的这个项目,就是为 Java 工程师量身打造的一套解法:它不是一个黑盒二进制,而是一个可读、可调、可 debug、可嵌入、可扩展的 Java 工程。它不替代 CloudWatch,而是做它的“翻译官”——把 CloudWatch API 返回的 JSON 响应,实时转换成 Prometheus 的 /metrics 端点能吐出的文本格式(即 OpenMetrics 格式)。你不需要改 Prometheus 配置去对接 AWS,只需要把它当成一个标准的 exporter 加进 scrape_configs,剩下的交给它。

关键词里的“CloudWatch导出器”不是泛泛而谈,它特指一种协议桥接层:一边是 AWS SDK v2 的异步 HTTP 客户端,另一边是 Prometheus Java Client 的 CollectorRegistry;中间是 YAML 驱动的指标发现逻辑与采样调度引擎。而“Prometheus Java客户端”这个标签,恰恰点出了它的技术底色——它没有自己造轮子去实现指标注册、Gauge/Counter 封装、HTTP 暴露逻辑,而是深度集成 io.prometheus:simpleclient_httpserverio.prometheus:simpleclient_hotspot,这意味着它天生支持 JVM 自身指标(GC 次数、线程数、堆内存使用率)的自动暴露,你甚至可以在同一个端口下同时看到 cloudwatch_cpu_utilizationjvm_gc_collection_seconds_count,这对排查“到底是业务慢还是 JVM 卡顿”这类混合故障特别关键。

至于“AWS指标采集”,它解决的从来不是“能不能采”,而是“怎么采得稳、采得准、采得省”。比如:你不会想让 exporter 每 15 秒就全量 ListMetrics 一次(那会触发 API 限流);也不会希望所有 EC2 实例的 NetworkIn 指标都用 1 分钟粒度去 GetMetricStatistics(成本翻倍且无意义);更不想因为某个命名空间里突然多了几百个新指标,就把整个 exporter 的内存撑爆。这个 Java 导出器的设计哲学,就是把“可控性”刻进每一行代码:采样间隔、统计周期、维度过滤、并发请求数、失败重试策略、缓存 TTL……全部可配、可观察、可降级。它不是一个“开箱即用就完事”的玩具,而是一个你愿意放进 CI/CD 流水线、写单元测试、加 Jaeger 链路追踪、甚至在生产环境里开着 JFR 录制 GC 行为的正经服务。

我去年在给一家做跨境支付的客户做监控体系升级时,就踩过没这种导出器的坑。他们当时用的是 Python 写的简易脚本轮询 CloudWatch,结果某天 RDS 命名空间新增了 200+ 个 Performance Insights 指标,脚本没做任何限流和缓存,直接把 CloudWatch API 调用频率干到每秒 80+ 次,触发了账户级限流,连带影响了 CloudTrail 日志投递。后来换成这个 Java 版本,光靠配置里的 cache_ttl: 300(5 分钟缓存 ListMetrics 结果)和 max_concurrent_requests: 5 两个参数,就把峰值 QPS 压到了 3 以下,而且还能通过 /actuator/prometheus 端点实时看到 cloudwatch_list_metrics_totalcloudwatch_get_metric_statistics_errors_total 这类内部指标,真正做到了“可观测即运维”。

2. 整体架构与设计思路拆解

这个导出器不是简单地把 AWS SDK 调用封装成一个 HTTP handler,它的核心价值在于三层抽象:发现层(Discovery)→ 采集层(Collection)→ 暴露层(Exposition)。这三层之间完全解耦,靠事件驱动和配置驱动串联,这也是它能兼顾灵活性与稳定性的根本原因。

2.1 发现层:动态指标发现,而非静态硬编码

很多初版 CloudWatch 导出器会要求你在 YAML 里手动列出所有要采集的指标,比如:

metrics:
  - namespace: "AWS/EC2"
    metric_name: "CPUUtilization"
    dimensions:
      - name: "InstanceId"
        value: "i-0a1b2c3d4e5f67890"

这在测试环境没问题,但一上生产就崩:EC2 实例是自动伸缩组(ASG)动态创建的,RDS 实例可能按周滚动重建,ALB 名字更是随 CI/CD 流水线生成。硬编码等于放弃自动化。这个 Java 版本的破局点,是引入了 CloudWatch ListMetrics API 的智能分页与缓存机制

它的工作流程是这样的:
1. 启动时,根据配置中的 namespaces 列表(如 ["AWS/EC2", "AWS/RDS"]),并发发起 ListMetricsRequest
2. 对每个命名空间,它不是一次性拉取全部指标(CloudWatch 默认只返回 500 条),而是自动处理分页令牌(nextToken),直到遍历完该命名空间下所有指标;
3. 关键来了:它会对返回的指标列表做两层过滤——先按 metric_name 白名单(如 ["CPUUtilization", "NetworkIn", "DatabaseConnections"])筛,再按 dimensions 模式匹配(支持通配符 * 和正则表达式,比如 dimension_pattern: "^(InstanceId|DBInstanceIdentifier)$");
4. 最终生成一个内存中的 MetricDefinition 清单,每条记录包含:完整命名空间、指标名、维度组合(如 [{"name":"InstanceId","value":"i-0a1b2c3d4e5f67890"}])、统计方式(Average)、周期(300 秒)等元信息;
5. 这个清单会被写入一个带 TTL 的 Guava Cache(默认 5 分钟),后续采集请求都从缓存读,避免高频 ListMetrics。

提示:example.ymldiscovery.cache_ttl: 300 这个参数,不是随便设的。CloudWatch 的指标元数据变更频率很低(通常以小时计),设成 300 秒既能保证新鲜度,又能把 ListMetrics 调用频次压到最低。如果你的业务有“分钟级动态创建资源”的场景(比如 Fargate 任务),可以调小到 60,但务必配合 discovery.max_retries: 2 防止缓存击穿。

2.2 采集层:异步、批处理、带背压的指标拉取

发现只是第一步,真正的性能瓶颈在采集。CloudWatch 的 GetMetricStatistics 是一个昂贵的 API:每次调用最多只能查 1 个指标 + 1 组维度 + 1 个统计周期,且返回的数据点数量受时间范围限制(最长 14 天,最多 1440 个点)。如果对 100 个 EC2 实例的 CPU 指标逐个串行调用,耗时会轻松突破 30 秒,远超 Prometheus 默认 10 秒抓取超时。

这个 Java 导出器的应对策略是“三管齐下”:
- 异步非阻塞:底层用的是 AWS SDK v2 的 CloudWatchAsyncClient,所有 GetMetricStatistics 调用都是 CompletableFuture 异步发起,线程不阻塞;
- 批量合并:它会把同一命名空间、同一指标名、同一统计方式、同一周期的请求,按维度值进行哈希分组(比如所有 AWS/EC2/CPUUtilization 的请求,按 InstanceId 分成 N 组),然后对每组维度并发发起请求(最大并发数由 collection.max_concurrent_requests 控制,默认 10);
- 智能背压:当 CompletableFuture.allOf() 检测到某批次请求中超过 30% 失败,或平均响应时间超过 5 秒,它会自动触发降级——跳过本次采集,复用上一轮缓存的指标值,并记录 cloudwatch_collection_degraded_total 计数器。

这里有个容易被忽略的细节:collection.sample_interval(采样间隔)和 cloudwatch.period(CloudWatch 数据周期)不是一回事。前者是 exporter 主动发起采集的频率(比如每 60 秒拉一次最新数据),后者是你要从 CloudWatch 获取哪个粒度的历史数据(比如 period: 300 表示查最近 5 分钟的平均值)。它们的关系是:sample_interval 应该 ≥ period,否则会出现数据覆盖或空洞。example.yml 里设的 sample_interval: 60period: 300,意味着它每分钟去 CloudWatch 查一次“过去 5 分钟的平均 CPU 使用率”,这样既能保证数据新鲜(延迟 ≤ 60 秒),又不会因频繁查询小周期数据而推高成本。

2.3 暴露层:原生 Prometheus 兼容,不止于指标

暴露层看似最简单(不就是启动一个 HTTP Server 吗?),但恰恰是它决定了你能否真正融入现有监控生态。这个导出器用的是 io.prometheus:simpleclient_httpserver,但它做了两处关键增强:

第一,多端点支持:除了标准的 /metrics(暴露 CloudWatch 指标 + JVM 指标),它还内置了 /health(返回 { "status": "UP", "cloudwatch": "UP" })和 /actuator/prometheus(Spring Boot Actuator 风格,兼容 Prometheus Operator 的 ServiceMonitor 探针)。更重要的是,它把自身运行时的关键指标也注册进了 CollectorRegistry:
- cloudwatch_discovery_duration_seconds:ListMetrics 耗时直方图
- cloudwatch_collection_duration_seconds:GetMetricStatistics 耗时直方图
- cloudwatch_collection_errors_total:按错误类型(ThrottlingException, InvalidParameterValue, AccessDenied)分组的计数器
- exporter_uptime_seconds:进程启动时长(Gauge)

第二,指标命名与标签规范化:CloudWatch 原生指标名带斜杠(如 AWS/EC2/CPUUtilization),Prometheus 不允许,所以它自动转换为下划线(aws_ec2_cpu_utilization)。维度值(如 InstanceId=i-0a1b2c3d4e5f67890)会被转为 Prometheus 标签,但会做安全清洗:去掉非法字符(/, :, ),长度截断(默认 64 字节),并添加 cloudwatch_dimension_ 前缀避免和业务标签冲突。example.yml 里的 label_prefix: "cw_" 就是控制这个前缀的。

注意:如果你启用了 aws_tag_select 功能(通过 tag:GetResources 权限获取资源标签),它会把 Name=prod-db, Environment=prod 这类标签也作为 Prometheus 标签注入,但会强制加上 tag_ 前缀(如 tag_Name="prod-db"),这是为了和原始维度标签严格区分开,防止 label cardinality 爆炸。

3. 核心配置解析与实操要点

YAML 配置是这个导出器的“大脑”,它不像某些工具那样只有几个开关,而是提供了一套完整的指标治理 DSL。我们来逐段拆解 example.yml 的真实含义,并告诉你哪些参数必须改、哪些可以不动、哪些改了会踩大坑。

3.1 全局配置:端口、认证、日志

server:
  port: 9106
  context_path: "/"

aws:
  region: "us-east-1"
  credentials:
    type: "default" # 可选: default, static, profile, container
    # static 下才需要 access_key_id / secret_access_key
    # profile 下才需要 profile_name

server.port 是最直观的,但 context_path 很关键。如果你打算用 Nginx 做反向代理(比如把 https://monitor.example.com/cloudwatch/ 代理到后端 http://exporter:9106/),就必须把 context_path 设为 /cloudwatch/,否则 /metrics 请求会 404。我见过太多人卡在这一步,最后发现是路径没对齐。

aws.credentials.type 是认证模式开关。default 表示走 AWS 标准凭证链(推荐),它会按顺序检查:
1. 环境变量 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
2. ECS Task Role 或 EC2 Instance Profile(IAM 角色)
3. ~/.aws/credentials 文件里的 default profile
4. ~/.aws/config 文件里的 default profile(带 region

如果你在 Kubernetes 里部署,强烈建议用 IAM Roles for Service Accounts(IRSA),即让 Pod 的 ServiceAccount 绑定一个 IAM Role,这样连环境变量都不用设,最安全。type: "static" 只应在开发测试时用,生产环境绝对禁止硬编码 AKSK。

3.2 发现配置:精准定位你要的指标

discovery:
  cache_ttl: 300
  max_retries: 2
  namespaces:
    - "AWS/EC2"
    - "AWS/RDS"
    - "AWS/ELB"
  metric_names:
    - "CPUUtilization"
    - "NetworkIn"
    - "DatabaseConnections"
    - "HTTPCode_ELB_5XX_Count"
  dimension_patterns:
    - "^(InstanceId|DBInstanceIdentifier|LoadBalancerName)$"

这段配置决定了“导出器知道哪些指标存在”。namespacesmetric_names 是白名单,必须精确匹配 CloudWatch 控制台里看到的名字(大小写敏感!)。dimension_patterns 是正则表达式,用来筛选维度名。注意:它匹配的是维度 InstanceId),不是维度 i-0a1b2c3d4e5f67890)。如果你想只采集特定实例,应该在 collection.metrics 里用 dimensions 字段硬编码,而不是在这里过滤。

max_retries: 2 是防抖关键。ListMetrics 在高并发下偶尔会返回 ThrottlingException,设成 2 次重试能极大提升发现成功率。但别设成 5,否则一次失败发现会拖慢整个启动过程。

3.3 采集配置:定义如何拉取、聚合、暴露指标

collection:
  sample_interval: 60
  metrics:
    - namespace: "AWS/EC2"
      metric_name: "CPUUtilization"
      statistics: ["Average"]
      period: 300
      dimensions:
        - name: "InstanceId"
          value: "*"
      aws_tag_select:
        - "Name"
        - "Environment"

这才是真正的“业务逻辑”。我们逐字段看:

  • sample_interval: 60:Exporter 每 60 秒触发一次采集循环。它和 Prometheus 的 scrape_interval 是独立的,但建议保持一致(比如都设 60s),否则会出现数据点对不齐。
  • statistics: ["Average"]:CloudWatch 支持 Average, Sum, Maximum, Minimum, SampleCount 五种统计方式。Average 最常用,但对计数类指标(如 Invocations),你应该用 Sum,否则会得到“平均每秒调用次数”,而不是“总调用次数”。
  • period: 300:告诉 CloudWatch “我要查过去 5 分钟的数据”。这个值必须是 CloudWatch 支持的周期(60, 300, 3600, 86400 秒),不能写 120 或 600。
  • dimensions 是灵魂所在。value: "*" 表示通配所有该维度的值(比如所有 EC2 实例),这是动态发现的基础。但要注意:* 通配符只在 discovery 阶段生效,在 collection 阶段,它会被替换成实际发现的维度值列表。如果你写 value: "i-0a1b2c3d4e5f67890",那就是静态采集,失去了自动化意义。
  • aws_tag_select:启用后,它会调用 tag:GetResources API,根据当前指标关联的资源 ARN(如 arn:aws:ec2:us-east-1:123456789012:instance/i-0a1b2c3d4e5f67890)去查标签。["Name", "Environment"] 表示只注入这两个标签。实测下来,这个 API 调用比 GetMetricStatistics 还贵,所以千万别写 ["*"],那会把所有标签都拉下来,cardinality 直接爆炸。

3.4 高级配置:为生产环境兜底

advanced:
  collection:
    max_concurrent_requests: 10
    timeout_seconds: 10
  discovery:
    max_concurrent_namespaces: 3
  jvm:
    expose_hotspot_metrics: true
  • max_concurrent_requests: 10:这是并发控制的闸门。CloudWatch 默认每账户每区域 50 QPS,但这是所有 API 的总和。如果你的导出器占了 10 并发,其他服务(比如 Terraform Apply)就只剩 40。设成 10 是平衡点,既能保证采集速度,又留足余量。如果发现 cloudwatch_collection_errors_total{error_type="ThrottlingException"} 持续上升,就该调低它。
  • timeout_seconds: 10GetMetricStatistics 的单次超时。CloudWatch 在数据量大时可能响应慢,设成 10 秒比默认的 30 秒更激进,能更快失败、更快重试,避免线程池被占满。
  • max_concurrent_namespaces: 3:ListMetrics 是跨命名空间并发的,设成 3 意味着最多同时查 3 个命名空间(如 AWS/EC2, AWS/RDS, AWS/ELB),避免瞬间打满 CloudWatch 的 ListMetrics 限额(默认 5 QPS)。
  • expose_hotspot_metrics: true:开启 JVM 自身指标。它会自动注册 jvm_memory_pool_used_bytes, jvm_threads_current, jvm_gc_pause_seconds 等 30+ 个指标。这些指标和 CloudWatch 指标在同一端点暴露,让你一眼看出“是不是 GC 导致了指标采集延迟”。

4. 实操过程与核心环节实现

现在我们来走一遍从零开始,把这个导出器跑起来的完整流程。我会以 Linux/macOS 为例,Windows 用户请自行替换 mvnjava 命令路径。

4.1 环境准备与构建

首先确认 Java 版本:

java -version
# 输出应为:openjdk version "11.0.20" 2023-01-17
# 或 openjdk version "17.0.8" 2023-07-18
# Java 8 也能跑,但强烈建议用 11+,因为 AWS SDK v2 要求 Java 11+

然后克隆仓库(假设你已下载 ZIP 并解压):

cd LFkjCRQ8yVqubXToKTFY-master-9a89cf9d111efdf32df00d30cbe82541a833ab8b
ls -la
# 你会看到 pom.xml, src/, example.yml, Dockerfile 等

构建 JAR 包(Maven 3.8+):

mvn clean package -DskipTests
# -DskipTests 是为了加速,生产环境部署前务必去掉,跑一遍 test
# 构建成功后,target/ 目录下会出现 cloudwatch-exporter-1.0.0.jar

实操心得:第一次构建可能会慢,因为要下载 AWS SDK v2、Prometheus Client、SLF4J 等依赖。建议提前执行 mvn dependency:go-offline 把依赖下全。另外,pom.xml<maven.compiler.source><maven.compiler.target> 都设为 11,如果你强行用 Java 8 编译,会报 Unsupported class file major version 61 错误。

4.2 本地运行与验证

在本地运行前,先配置 AWS 凭证。最安全的方式是用 ~/.aws/credentials

mkdir -p ~/.aws
cat > ~/.aws/credentials << 'EOF'
[default]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
EOF
chmod 600 ~/.aws/credentials

然后启动导出器:

java -jar target/cloudwatch-exporter-1.0.0.jar \
  --config-file example.yml \
  --port 9106
# --config-file 指定 YAML 配置,--port 覆盖 server.port

启动后,你会看到类似日志:

INFO  c.c.CloudWatchExporterApplication - Started CloudWatchExporterApplication in 3.212 seconds (JVM running for 3.724)
INFO  c.c.d.CloudWatchDiscoveryService - Discovered 12 metrics in namespace AWS/EC2
INFO  c.c.d.CloudWatchDiscoveryService - Discovered 8 metrics in namespace AWS/RDS

立刻验证:

curl -s http://localhost:9106/metrics | head -20
# 你应该看到:
# # HELP cloudwatch_cpu_utilization Average CPU utilization percentage
# # TYPE cloudwatch_cpu_utilization gauge
# cloudwatch_cpu_utilization{cw_namespace="AWS/EC2",cw_metric_name="CPUUtilization",cw_statistic="Average",cw_period="300",InstanceId="i-0a1b2c3d4e5f67890"} 12.34
# # HELP jvm_memory_used_bytes Used bytes of a given JVM memory area.
# # TYPE jvm_memory_used_bytes gauge
# jvm_memory_used_bytes{area="heap"} 1.23456789E8

注意:head -20 只是快速确认格式,真正要看全量指标,用 curl -s http://localhost:9106/metrics | grep "cloudwatch_" | wc -l 统计行数。如果返回 0,大概率是 example.yml 里的 namespacesmetric_names 写错了,或者你的 AWS 凭证没权限(检查 cloudwatch:ListMetrics 是否授权)。

4.3 Docker 部署:生产就绪的容器化方案

项目自带 Dockerfile,构建镜像只需一行:

docker build -t cloudwatch-exporter:1.0.0 .

Dockerfile 的关键点:
- 基础镜像是 eclipse-jetty:11-jre11-slim(轻量、安全、Java 11 原生支持)
- 把 target/cloudwatch-exporter-1.0.0.jar COPY 进镜像
- 暴露 9106 端口
- 启动命令是 java -jar /app.jar --config-file /config.yml

运行容器(以 ECS 为例):

docker run -d \
  --name cw-exporter \
  -p 9106:9106 \
  -v $(pwd)/example.yml:/config.yml:ro \
  -e AWS_REGION=us-east-1 \
  -e AWS_ACCESS_KEY_ID=YOUR_KEY \
  -e AWS_SECRET_ACCESS_KEY=YOUR_SECRET \
  cloudwatch-exporter:1.0.0

但在生产环境,绝不要用 -e AWS_ACCESS_KEY_ID!正确姿势是:

  • EC2 实例:给实例配置 IAM Role,容器内自动继承凭证;
  • ECS Fargate:在 Task Definition 的 taskRoleArn 字段指定 Role;
  • EKS Pod:用 IRSA,ServiceAccount 注解 eks.amazonaws.com/role-arn

验证容器内指标:

docker exec cw-exporter curl -s http://localhost:9106/health
# 返回 {"status":"UP","cloudwatch":"UP"}

4.4 Prometheus 集成:让它真正工作起来

在 Prometheus 的 prometheus.yml 中添加 job:

scrape_configs:
  - job_name: 'cloudwatch-exporter'
    static_configs:
      - targets: ['cloudwatch-exporter:9106'] # 如果和 Prometheus 同 Docker 网络
        # 或 targets: ['host.docker.internal:9106'] # macOS/Windows Docker Desktop
        # 或 targets: ['192.168.1.100:9106'] # Linux 直接 IP
    metrics_path: '/metrics'
    scheme: http
    scrape_interval: 60s
    scrape_timeout: 30s

重启 Prometheus 后,访问 http://<prometheus>/targets,你应该看到这个 job 是 UP 状态,并且 Labels 列显示 instance="cloudwatch-exporter:9106"

最后,写一条 PromQL 验证:

avg by (InstanceId) (cloudwatch_cpu_utilization{cw_namespace="AWS/EC2", cw_metric_name="CPUUtilization"})

如果返回了多个 InstanceId 的平均值,恭喜,你已经打通了 AWS 监控数据到 Prometheus 的最后一公里。

5. 常见问题与排查技巧实录

在真实生产环境中,这个导出器最常见的问题不是“跑不起来”,而是“跑得不稳”或“数据不准”。我把过去两年帮客户排障的经验,浓缩成一张速查表,并附上独家调试技巧。

5.1 常见问题速查表

问题现象可能原因快速验证命令解决方案
/metrics 返回空,或只有 JVM 指标,没有 cloudwatch_ 开头的指标discovery 阶段失败,没发现任何指标curl -s http://localhost:9106/actuator/prometheus | grep cloudwatch_discovery_total检查 example.ymlnamespacesmetric_names 是否拼写正确;检查 AWS 凭证是否有 cloudwatch:ListMetrics 权限;查看日志中 Discovered X metrics 行数
/metrics 有指标,但数值一直是 0NaNcollection 阶段 GetMetricStatistics 返回空数据点curl -s "http://localhost:9106/actuator/prometheus" | grep cloudwatch_collection_duration_seconds_count检查 period 是否超出 CloudWatch 数据保留期(EC2 默认 15 个月,但免费层只存 2 周);检查 dimensionsvalue 是否匹配真实资源 ID;用 AWS CLI 手动验证:aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value=i-xxx --start-time $(date -d '5 minutes ago' +%s) --end-time $(date +%s) --period 300 --statistics Average --output json
Prometheus 抓取失败,状态为 DOWN导出器 HTTP Server 崩溃,或网络不通curl -I http://localhost:9106/health(应返回 200);netstat -tuln \| grep 9106检查导出器日志是否有 OutOfMemoryError;增加 JVM 参数:java -Xms512m -Xmx1024m -jar ...;检查防火墙是否放行 9106 端口
指标标签过多,Prometheus 存储暴涨aws_tag_select 启用了 ["*"],或维度值本身含高基数字段(如 RequestIdcurl -s http://localhost:9106/metrics \| grep "cloudwatch_cpu_utilization{" \| head -1立即修改 example.yml,将 aws_tag_select 改为明确的 ["Name", "Environment"];在 dimensions 中避免使用 RequestIdTraceId 等唯一 ID 类维度
导出器 CPU 使用率持续 100%discoverycollection 并发过高,或 GC 频繁jstat -gc <pid>curl -s http://localhost:9106/actuator/prometheus \| grep jvm_gc_降低 advanced.collection.max_concurrent_requests 至 5;增加 jvm.expose_hotspot_metrics: true 后,用 Grafana 看 jvm_gc_pause_seconds_max 是否异常;考虑升级到 Java 17,ZGC 更友好

5.2 独家调试技巧:三步定位根因

第一步:打开 DEBUG 日志

默认日志级别是 INFO,看不到详细请求。启动时加参数:

java -Dlogging.level.com.cloudwatch=DEBUG -jar target/cloudwatch-exporter-1.0.0.jar --config-file example.yml

你会看到类似:

DEBUG c.c.c.CloudWatchCollectionService - Sending GetMetricStatisticsRequest for metric: AWS/EC2/CPUUtilization, dimensions: [{name=InstanceId, value=i-0a1b2c3d4e5f67890}]
DEBUG c.c.c.CloudWatchCollectionService - GetMetricStatisticsResponse received, data points count: 12

这能直接确认:请求发出去了没?CloudWatch 返回数据了没?数据点数量对不对?

第二步:用 /debug/metrics 端点看内部状态

这个端点不在公开文档里,但代码里埋了(DebugMetricsEndpoint 类)。访问:

curl -s http://localhost:9106/debug/metrics

返回 JSON,包含:

  • discovery_cache_size: 当前缓存的指标数
  • collection_queue_size: 待采集的指标队列长度
  • http_client_active_requests: 当前活跃的 HTTP 请求连接数
  • jvm_memory_committed_bytes: JVM 已提交内存

如果 collection_queue_size 持续 > 100,说明采集跟不上发现速度,要调大 max_concurrent_requests 或调小 sample_interval

第三步:用 JMX 深度诊断(Java 11+)

导出器集成了 io.prometheus:simpleclient_hotspot,它会自动暴露 JMX Bean。用 jconsole 连接:

jconsole localhost:9106

MBeans 标签页,展开 java.langMemoryAttributes,看 HeapMemoryUsage.used;展开 com.cloudwatchCollectorAttributes,看 LastCollectionTime(毫秒时间戳),计算距今多久,就能判断采集是否卡住。

实操心得:我在给一家游戏公司做支持时,遇到过一个诡异问题:导出器内存稳定在 800MB,但 jvm_memory_used_bytes{area="heap"} 却显示 2GB。最后发现是 simpleclient_hotspotBufferPoolMXBean 采集逻辑有 bug,它把 Direct Buffer 内存也算进了 heap。解决方案是关掉 expose_hotspot_metrics: false,改用 jvm_direct_* 指标单独监控。这个坑,只有真刀真枪跑过一周以上才能踩到。

6. 二次开发与定制化扩展

这个项目之所以被称作“适合二次定制开发”,是因为它的源码结构清晰,模块职责单一,且预留了大量 Hook 点。我来分享三个最实用的定制场景,以及对应的代码修改路径。

6.1 场景一:添加自定义指标处理器(比如把 NetworkIn 字节转为 Mbps)

CloudWatch 的 NetworkIn 单位是字节,Prometheus 里直接暴露 cloudwatch_network_in_bytes 不够直观。你想暴露成 cloudwatch_network_in_mbps,公式是 bytes * 8 / 1000 / 1000 / period

修改步骤:
1. 在 src/main/java/com/cloudwatch/collector/ 下新建 NetworkInMbpsProcessor.java
2. 实现 MetricProcessor 接口,重写 process 方法:
java public class NetworkInMbpsProcessor implements MetricProcessor { @Override public void process(MetricFamilySamples.Sample sample, double value) { // value 是原始字节数,sample.labelValues 包含维度 double mbps = value * 8 / 1_000_000 / 300; // 除以 period=300 秒 sample.value = mbps; sample.name = "cloudwatch_network_in_mbps"; // 覆盖指标名 } }
3. 在 CloudWatchCollectionServicecollectMetrics 方法里,对 metricName.equals("NetworkIn") 的指标,插入这个 processor。

这样,你不用改任何配置,只要在 example.yml 里把 metric_name: "NetworkIn" 的项加上 processor: "network_in_mbps",就能自动生效。

6.2 场景二:对接企业内部 CMDB,用主机名替换 InstanceId

很多企业 CMDB 里,InstanceId=i-0a1b2c3d4e5f67890 对应的主机名是 web-prod-01。你想把 Prometheus 标签里的 InstanceId="i-0a1b2c3d4e5f67890" 替换为 hostname="web-prod-01"

修改步骤:
1. 在 src/main/java/com/cloudwatch/dimension/ 下新建 CmdbDimensionResolver.java
2. 实现 DimensionResolver 接口,提供 resolve(String instanceId) 方法,内部调用 CMDB REST API;
3. 在 CloudWatchDiscoveryServicediscoverMetrics 方法里,对 dimension.name == "InstanceId" 的维度值,调用 cmdbResolver.resolve(value) 获取主机名,并存入 MetricDefinition.dimensions

这样,example.yml 里只需加一行 dimension_resolver: "cmdb",所有 InstanceId 标签就自动变成可读的主机名。

6.3 场景三:添加告警指标(比如 CloudWatch API 调用失败率 > 5%)

你想在 Grafana 里画一张图,显示“过去 5 分钟 CloudWatch API 失败率”,并设置告警。

修改步骤:
1. 在 src/main/java/com/cloudwatch/metrics/ 下新建 CloudWatchApiErrorRateCollector.java
2. 继承 Collector 类,重写 collect 方法,从 cloudwatch_collection_errors_totalcloudwatch_collection_total 计数器中计算比率;
3. 在 CloudWatchExporterApplicationmain 方法里,调用 collectorRegistry.register(new CloudWatchApiErrorRateCollector())

最终,/metrics 端点就会多出:

# HELP cloudwatch_api_error_rate CloudWatch API error rate in last 5 minutes
# TYPE cloudwatch_api_error_rate gauge
cloudwatch_api_error_rate 0.023

然后你就可以在 Prometheus 里写告警规则:

- alert: CloudWatchAPIFailureRateHigh
  expr: cloudwatch_api_error_rate > 0.05
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "CloudWatch API failure rate is high"

我个人在实际操作中的体会是:这个导出器最大的价值,不是它“现在能做什么”,而是它“将来能很容易地做什么”。它的 Maven 模块划分(core, collector, discovery, http)让每个功能都像乐高积木一样可插拔。我见过最猛的定制,是把整个 discovery 模块替换成从 Datadog API 拉取指标定义,从而实现了 CloudWatch + Datadog 双源指标统一暴露。只要你理解了它的三层抽象(发现→采集→暴露),任何监控数据源的接入,本质上都是在 discovery 模块里写一个新的 XXXDiscoveryService,在 collector 模块里写一个新的 XXXCollector,然后注册进去——就这么简单。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个用Java开发的轻量级工具,能把AWS CloudWatch里的各种监控指标(比如EC2 CPU使用率、RDS连接数、Lambda调用次数等)实时转换成Prometheus能直接抓取的格式。支持Java 8+,编译用mvn package,运行就一条java -jar命令,指定端口和YAML配置文件(自带example.yml参考),默认走9106端口。认证走AWS标准凭证链:环境变量(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)、IAM角色、~/.aws/credentials文件都行;最小权限只要cloudwatch:ListMetrics和cloudwatch:GetMetricStatistics,如果要用标签筛选功能(aws_tag_select),再加个tag:GetResources就行。配置全靠YAML,可以按命名空间、指标名、维度(如InstanceId、LoadBalancerName)、统计方式(Average/Sum/Maximum)、采样周期(比如5分钟)和抓取间隔灵活设置。带Dockerfile,build完就能docker run跑起来;项目结构规范,含完整Maven配置(pom.xml)、源码(src/main)、测试(src/test)、许可证(LICENSE)、说明文档(README.md)、贡献指南(CONTRIBUTING.md)和安全策略(SECURITY.md),适合嵌入现有Prometheus监控体系,也方便二次开发或定制指标采集逻辑。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值