最先要考虑的问题其实是:多条件阈值判断这块逻辑,到底应该写在 InfluxDB 内部,还是写在你自己的 Java 服务里。这个选择做不好,后面会遇到两类问题:一类是告警判断被写死在业务代码里,时间窗口一改就要发版;另一类是过度依赖 InfluxDB 原生告警,组合条件稍微复杂一点就写不出查询。
我最近在一个模拟项目 X 里重构告警链路,核心需求是:Java 服务持续把性能指标写入 InfluxDB,然后对 CPU、内存、磁盘、接口错误率这些指标做多条件阈值判断,超限后触发告警,并把告警状态反馈到业务侧处理。整个方案跑完,我最大的体会是——Check 告警不是一个“开关”,而是“数据库负责统计、Java 负责组合判定、Webhook 负责推送”这样一条流水线。这篇文章就把完整链路拆开讲清楚。
1. 告警判断放在哪:业务层轮询和 InfluxDB 原生 Check 不是二选一
先说背景。大多数 Java 团队接 InfluxDB,起步写法都是这样:定时任务每隔固定时间去查一次指标,把平均值、最大值取出来,然后在 Java 里做if (cpu > 90) alert()。这种写法在指标少、团队小的时候完全没问题,但一旦到了几十台机器、十几个监控指标,问题就开始集中爆发。
1.1 应用层轮询为什么会越写越重
应用层轮询最大的问题不是性能,而是职责错位。你写了一个定时任务,每 30 秒跑一次查询,查完之后自己算阈值、自己发告警、自己记录状态。听起来很灵活,但实际维护时会遇到三个麻烦:
- 时间窗口的语义很难统一。接口错误率要看 5 分钟窗口,CPU 要看 1 分钟窗口,磁盘要看 10 分钟窗口,Java 里每个任务都要单独维护窗口逻辑。
- 告警状态只能自己缓存。Java 进程一重启,上次是 warn 还是 crit 就丢了,很容易出现重复告警或者漏告警。
- 聚合逻辑写在代码里,无法复用。InfluxDB 本身擅长做降采样、窗口聚合,你在 Java 里用 List 自己算均值、百分位,纯属浪费数据库能力。
我见过最夸张的一个项目,告警定时任务里同时维护了 20 多个 CronTask,每个任务里有自己的 Redis key 存状态,光是状态清理的 bug 就够排查半天。所以从某个阶段开始,团队会自然地往 InfluxDB 原生 Check 去靠。
1.2 InfluxDB 原生 Check 的能力边界
InfluxDB 2.x 里的 Check 可以理解为数据库内部的一个周期任务,它会按照你设定的every间隔反复执行一段查询,然后根据查询结果输出一条带_level字段的记录。_level的取值通常是ok、warn、crit、unknown,这个字段是后面一切告警推送的判定依据。
原生 Check 的优点很明显:状态保存在数据库内部,查询窗口精准,告警历史在_monitoringbucket 里留痕。但它也有边界——如果只是给单个字段配一个简单阈值,在 UI 上点几下就完成;一旦要说“CPU 超过 85 且内存超过 80 才算 crit”,或者“三个指标里任意两个超限才告警”,UI 上的 threshold 配置就不够用了。
这时候,要么用自定义 Check(自定义 Flux 查询),要么把组合判断拉回 Java。我最终选择的是两条路都走,下面会展开说明。
1.3 我的最终链路设计
综合下来,我采用的场景划分是:
- 单指标、单阈值、重复性强的告警,交给 InfluxDB 原生 Check。
- 多指标、组合条件、需要业务动作的告警,由 Java 查询数据后自行判断。
- Check 输出的
_level状态统一落到_monitoringbucket,Java 定时读取作为兜底。 - 推送统一走 Webhook,Java 接到 Check 通知后转成业务侧的消息格式。
Java 业务服务 —写入指标—> InfluxDB InfluxDB —原生Check—> Webhook —> 告警接收服务 Java 业务服务 —Flux查询—> InfluxDB —组合判定—> 业务动作这个结构的核心是:InfluxDB 永远只负责时序数据的存储与聚合,真正决定“要不要发告警”的组合判断,逻辑上由 Java 做一层收敛。这样既能利用数据库的高效窗口计算,又能把复杂判断保持在高灵活性的一方。
2. 多条件 Check 的 Flux 表达:聚合窗口内组合字段级阈值
如果确定某个告警场景要在 InfluxDB 内部完成,那就必须写对自定义 Check 的查询。多条件 Check 的难点不在查询语句本身,而在如何把多个字段对齐到同一个时间片内做比较。
2.1 多条件组合的三种常见模式
按我遇到过的需求,多条件阈值判断基本能归成三类:
| 组合模式 | 含义 | 实际例子 |
|---|---|---|
| AND 组合 | 两个条件同时满足才告警 | CPU > 85% 并且内存 > 80% |
| OR 组合 | 任一条件满足就告警 | 错误率 > 5% 或 P99 延迟 > 300ms |
| N-of-M 组合 | M 个指标中至少 N 个超限 | CPU、内存、磁盘中任意两个超限 |
这三种模式在 SQL 里写很容易,但 InfluxDB 的数据模型是“字段分散在不同行”的。比如一个 measurement 里同时有cpu_usage、mem_usage、disk_usage三个 field,在查询结果里它们是不同的行记录,没法直接在filter里同时比较。所以需要先做一次 Pivot,把字段从列变成对应的行内列。
2.2 用 Flux 的 pivot 和 map 把多字段映射成 _level
以一条典型的多条件 Check 为例:每分钟计算一次 CPU 和内存的平均值,如果两个都超限则输出crit;如果只有一个超限输出warn。Flux 查询是这样写的:
from(bucket: "prod-data") |> range(start: -2m) |> filter(fn: (r) => r._measurement == "perf") |> filter(fn: (r) => r._field == "cpu_usage" or r._field == "mem_usage" or r._field == "disk_usage") |> aggregateWindow(every: 1m, fn: mean, createEmpty: false) |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value") |> map(fn: (r) => ({ r with _level: if r.cpu_usage > 85.0 and r.mem_usage > 80.0 then "crit" else if r.cpu_usage > 85.0 or r.mem_usage > 80.0 then "warn" else "ok", _value: r.cpu_usage }))这里有两个关键细节。
第一个是pivot。它把cpu_usage、mem_usage、disk_usage从三行数据变成同一行里的三个列,这样在map函数里才能用r.cpu_usage这种方式直接访问字段值。不 pivot 的话,每个 field 单独成行,没法做跨字段的 AND 判断。
第二个是输出_level字段。InfluxDB 的自定义 Check 正是通过每行结果里的_level来判断状态的。查询里必须显式构造这个字段,并且建议同时输出_value,否则 UI 里展示告警详情时会缺少数值信息。
不同 InfluxDB 版本对自定义 Check 的字段要求会有细节差异,比如部分版本要求在
map里保留_time和_measurement。建议在测试环境先用同一条查询手工执行,确认输出行里包含_level后,再把它存成 Check。
2.3 Check 的执行节奏与内部状态输出
创建 Check 时除了查询脚本,还要配置every和offset。every是执行间隔,offset是偏移,作用是把执行时间往后挪,避免刚写入的数据还来不及聚合就被查到。
Check 每次执行后,会产生一条状态记录,落在_monitoringbucket 里。这条记录自带_check_name、_check_id、_level这些标签,也就成了 Java 侧做兜底查询的数据源。后面会在第 3 节演示如何读取。
3. Java 与 Check 的双通道联通:写入数据、回读告警状态、触发业务动作
这一节就是标题里“Java 结合”真正落地的地方。我用三个环节来讲:怎么把指标写进去、怎么查 Check 状态、怎么接收 Check 推送。
3.1 Java 客户端依赖与连接管理
官方 Java 客户端的坐标是com.influxdb:influxdb-client-java。我在项目里用的是 6.x 版本,Maven 依赖如下:
<dependency> <groupId>com.influxdb</groupId> <artifactId>influxdb-client-java</artifactId> <version>6.11.0</version> </dependency>连接方式也很直接:
InfluxDBClient client = InfluxDBClientFactory.create( "http://127.0.0.1:8086", "my-token-xxx".toCharArray(), "my-org", "prod-data");有一点容易忽略:InfluxDBClientFactory.create里最后两个参数分别是 org 和默认 bucket。之后用WriteApi和QueryApi时如果不显式指定,就会用默认值。多个服务共用同一个 client 实例没问题,它是线程安全的,但同时要记得在应用关闭时调用client.close()。
3.2 写入监控指标:Point 建模
写入端用Point构造就行。我习惯把同一时刻采集到的多个指标放在同一个 Point 里,这样在 Flux 中做 pivot 跨字段判断时,天然就在同一时间片内:
WriteApi writeApi = client.getWriteApi(); Point point = Point.measurement("perf") .addTag("host", "app-node-01") .addField("cpu_usage", 87.4) .addField("mem_usage", 82.1) .addField("disk_usage", 72.3) .time(Instant.now(), WritePrecision.MS); writeApi.writePoint("prod-data", "my-org", point);写入频率按窗口大小决定即可。如果告警窗口是 1 分钟,写入频率建议不低于 15 秒采集一次,否则窗口内的 mean 值平滑效果不好,容易出现毛刺。
3.3 回读 Check 状态:从 _monitoring 里捞告警
即使原生 Check 已经把告警推送出来了,我也会在 Java 侧做一个兜底读取,逻辑是定期查询_monitoringbucket 中最近一次非 ok 的状态:
from(bucket: "_monitoring") |> range(start: -5m) |> filter(fn: (r) => r._measurement == "statuses") |> filter(fn: (r) => r._check_name == "cpu-memory-and-threshold") |> filter(fn: (r) => r._level != "ok") |> last()Java 端读取时:
QueryApi queryApi = client.getQueryApi(); String flux = "from(bucket: \"_monitoring\") " + "|> range(start: -5m) " + "|> filter(fn: (r) => r._measurement == \"statuses\") " + "|> filter(fn: (r) => r._check_name == \"cpu-memory-and-threshold\") " + "|> filter(fn: (r) => r._level != \"ok\") " + "|> last()"; List<FluxTable> tables = queryApi.query(flux); for (FluxTable table : tables) { for (FluxRecord record : table.getRecords()) { String level = (String) record.getValueByName("_level"); String checkName = (String) record.getValueByName("_check_name"); // 执行你的业务动作 } }这个做法的价值在于:原生 Check 的执行是在 InfluxDB 内部,不受 Java 服务状态影响。哪怕 Java 侧定时任务挂了一段时间,重启后依然能从_monitoring里把最近的告警状态补回来,不会丢状态。
3.4 Webhook 通道:让 Check 自己找上门
比轮询更及时的方式是让 Check 推送告警。InfluxDB 的通知规则(Notification Rule)会把 Check 的状态变化发到通知端点(Notification Endpoint)。端点类型支持 HTTP 请求,也就是 Webhook。
Java 侧只需要提供一个接收端点,这里以 Spring Boot 接口为例:
@PostMapping("/influx/alert") public ResponseEntity<Void> receiveAlert(@RequestBody Map<String, Object> payload) { Map<String, String> tags = (Map<String, String>) payload.get("tags"); String checkName = tags.get("checkName"); String level = tags.get("level"); alertProcessor.process(checkName, level, payload); return ResponseEntity.ok().build(); }要提醒的是,Webhook 推送是幂等性最弱的一环,网络抖动时可能出现重复推送。所以接收端我会在处理器里做去重,按checkName + 时间戳 + level作为 Redis 去重键。
4. 两个高价值实战场景:交易接口质量告警与主机资源多条件告警
理论讲完还是要落到具体场景。我在这套架构上做了两个最有代表性的案例,一个偏业务指标,一个偏基础设施指标。两者的差异正好说明了多条件判断的两种写法。
4.1 场景一:交易接口错误率与延迟组合判断
这类告警想要表达的是“接口开始恶化”而不是“偶发抖动”。单个指标超阈值很容易误报,比如某一次慢请求拖高了 P99,但错误率很低,业务其实没有受损。
我用的判断规则是:最近 5 分钟平均错误率 > 3%,并且最近 5 分钟 P99 延迟 > 250ms,两个条件同时满足才触发 crit。Flux 还是走 pivot 思路:
from(bucket: "api-metrics") |> range(start: -6m) |> filter(fn: (r) => r._measurement == "apiQuality") |> filter(fn: (r) => r._field == "errorRate" or r._field == "p99Latency") |> aggregateWindow(every: 1m, fn: mean, createEmpty: false) |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value") |> map(fn: (r) => ({ r with _level: if r.errorRate > 3.0 and r.p99Latency > 250.0 then "crit" else if r.errorRate > 2.0 or r.p99Latency > 200.0 then "warn" else "ok", _value: r.errorRate }))窗口取 5 分钟,aggregateWindow里每隔 1 分钟打一个点,这样查询结果最近 5 个点左右。InfluxDB 的 Check 执行时会对这些点逐行判断,只要任意一个点满足 crit,状态就会变成 crit。
这个场景里我踩过一个坑:如果p99Latency这样的字段在某些分钟内没有数据,pivot 之后会出现r.p99Latency == null,Flux 里 null 跟数值比较永远返回 false。结果就是明明错误率超标了,但只产出 warn。解决办法是在 map 之前加一层fill(column: "p99Latency", value: 0.0),或者把 map 里的判断写成exists r.p99Latency这样的空值保护。
4.2 场景二:基础设施资源 N-of-M 告警
另一个常见需求是主机资源告警,规则不是“单指标超限就告警”,而是“以下三个指标中任意两个达到阈值,再告警”。这是因为单指标偶尔峰值很常见,两个资源同时打满,通常是更明确的状态异常。
我在 Java 端用组合判断更直观。先写一条 Flux 查询,把最近一分钟的主机资源数据按小时聚合拿到:
from(bucket: "prod-data") |> range(start: -2m) |> filter(fn: (r) => r._measurement == "perf") |> filter(fn: (r) => r._field == "cpu_usage" or r._field == "mem_usage" or r._field == "disk_usage") |> aggregateWindow(every: 1m, fn: mean, createEmpty: false) |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")Java 侧逐行判断:
int thresholdCount = 0; boolean cpuHigh = record.getValueByName("cpu_usage") != null && ((Number) record.getValueByName("cpu_usage")).doubleValue() > 90.0; boolean memHigh = record.getValueByName("mem_usage") != null && ((Number) record.getValueByName("mem_usage")).doubleValue() > 90.0; boolean diskHigh = record.getValueByName("disk_usage") != null && ((Number) record.getValueByName("disk_usage")).doubleValue() > 95.0; if (cpuHigh) thresholdCount++; if (memHigh) thresholdCount++; if (diskHigh) thresholdCount++; if (thresholdCount >= 2) { alertService.sendResourceAlert(host, cpuHigh, memHigh, diskHigh); }这段代码看起来简单,但有一个容易被忽略的前提:cpu_usage、mem_usage这些字段必须来自同一个 Point 写入时间,或者经 aggregateWindow 对齐到同一窗口。如果业务侧写入时把三个字段分成了三个 Point 交给 InfluxDB,pivot 之后可能因为时间戳不同而分成多行,组合判断就会失效。
实际操作建议:像主机资源这类采集项,采集端尽量一次构造一个 Point 写入。同一个时刻的指标必须落在一个时间戳上,后续做跨字段阈值判断才顺。
4.3 为什么复杂判断建议留在 Java
原生 Check 在多条件组合里确实能做,但每加一个条件,Flux 脚本的可读性就下一个台阶。尤其是 N-of-M 这类逻辑,在 Flux 里写出来基本是几个布尔变量加 if 的组合,坑非常多,比如字段缺失、类型转换、时间对齐。维护一个只能靠文档解释的 Flux 脚本,不如把它写成 Java 里的纯逻辑函数,能单测、能覆盖、能加日志。
所以我的原则是:阈值简单、状态展示也简单的用 Check;阈值复杂、触及业务动作的用 Java。两者共用同一份 InfluxDB 数据源,避免同一套指标被查两遍。
5. 生产环境最容易翻车的三个告警细节:空窗口、重复推送与检查节奏
最后写几个我在真实环境里踩过的细节问题。这些内容在官方文档里都只提了一句,但实际影响非常大。
5.1 空窗口会吞掉告警条件
前面提到了createEmpty,这里展开讲。aggregateWindow在不满足窗口时间范围时,默认不会产生空窗口。如果某个窗口里由于采集端停机没有数据,那么这条告警条件的检查就不会发生。对单指标 Check 来说,可能表现为“该告警没告警”;对多条件 Check 来说,表现为其中一个条件被跳过。
解决办法分两种:
- 如果希望“指标缺失也算异常”,用
createEmpty: true再配合fill把 null 填成值。 - 如果希望“缺失不算异常”,就用
createEmpty: false,但必须在 Java 侧对返回空数组的情况做单独处理,比如记录一条“无数据”日志。
5.2 Check 推送不是天然幂等
InfluxDB 的通知规则在触发时可能因为网络超时、端点不可用而重复调用。更常见的是,一个 Check 每次执行都会输出一条 status,如果 Java 侧只按时间来记录,同一轮告警会被重复处理。
我的降噪思路是:把_check_name + _level作为状态键,只有当 level 从warn升级到crit,或者从crit恢复为ok时才触发真正的外部动作;同一个 level 内反复推送,只更新数据库状态,不再发短信或企业微信群消息。这个状态机用 Redis 或者内存 ConcurrentHashMap 都可以实现。
5.3 检查纪律:every、offset 与写入频率的配合
Check 的every不能设置得比聚合窗口还短,否则每次执行时窗口内数据都不完整,聚合出来的值会偏低。也不要设置得过长,否则告警延迟会变成用户可感知的故障。
我这里给一个参数配合经验表,适用大多数监控场景:
| 指标类型 | 写入频率 | Check every | 查询窗口 | 备注 |
|---|---|---|---|---|
| 主机 CPU/内存 | 15s | 1m | 2m | offset 5s,确保窗口数据完整 |
| 接口错误率 | 10s | 1m | 5m | 需要更长的平滑窗口 |
| 磁盘空间 | 1m | 2m | 10m | 趋势类,不需要高频检查 |
我见过有人在 Check every 设成 10s,窗口却还是 5m,结果每个 Check 执行都会产生大量重复状态记录,最后推送服务直接被打爆。记住一个原则:every是执行频率,窗口是判断范围,这两者不要混在一起。
最后再分享一个实际体会:把多条件阈值放进 InfluxDB 还是 Java,不是纯粹的技术选型,而要看谁离业务逻辑更近。业务侧告警,比如“接口响应变慢且错误率上升”,这种状态判断逻辑应该交给 Java,因为后面往往要跟着重启容器、切流量这类动作。基础设施侧告警,比如“三资源中两项超标”,则可以放到 InfluxDB Check 里,让数据库自己周期检查,Java 只消费结果。两条路同时存在并不冲突,反而让告警链路更稳。