
1. 这个需求到底难在哪里做监控告警这几年我见过太多团队在 InfluxDB 这个环节上栽跟头。表面上看InfluxDB 只是存时序数据的Java 只是负责业务逻辑的把两者一拼接就能完成数据采集—阈值判断—告警通知这条链路。可真落地的时候你会发现中间全是细节多条件到底是且还是或条件判断放在数据库侧做还是拿到 Java 里做告警发出去一条还是发出去 200 条很多项目就是倒在这些细节上的。这个需求的核心是把 InfluxDB 里存的指标数据变成可执行的告警动作。比如 CPU 使用率连续三次超过 85%、内存使用率超过 90%、同时连接数超过 5000三个条件同时满足才算故障这就是典型的多条件阈值判断。而创建 Check 告警在 InfluxDB 2.x 里指的是用 Check检查加 NotificationRule通知规则这套原生能力让数据库自己按周期跑检查、按规则发通知。所以这个问题的本质不是会不会写代码而是监控规则应该放在哪一层去执行。放 InfluxDB 侧配置省事但规则表达受限放到 Java 侧灵活可控但需要自己维护状态和调度。我下面把这两条路线都拆开讲给出可复制的方案、代码、参数和踩坑经验。适合用 InfluxDB 存监控数据、又想在业务系统里实现告警的团队参考也适合刚接触 Flux 和时序告警的新手快速上手。2. 三条技术路线选型决定坑的数量先讲选型再讲实现。选错了方案后面写多少代码都是给自己添堵。2.1 方案一InfluxDB 原生 Check 加 NotificationRule这是最贴近创建 Check 告警这个表述的方案。InfluxDB 2.x 在 UI 的 Alerts 模块里提供了 Check、NotificationRule、Endpoint 三层能力。你编写一段 Flux 查询作为检查逻辑设置阈值等级Warning、Critical数据库会按照设定周期执行这段查询把检查结果写入内部的_monitoring桶。然后由 NotificationRule 订阅这些检查结果在状态变化时触发通知端点把告警发到 HTTP 接口、邮件或者企业群机器人。这个方案最大的优点是配置都在界面里基本不用写 Java 代码。你只要保证 Java 侧把监控指标写入对应的 bucket剩下的阈值判断和告警通知都由 InfluxDB 完成。缺点也很明显多条件规则一旦复杂起来Flux 写起来比 SQL 难受得多而且通知端点要跟你们内部的告警平台对接通常还是得走一个 Webhook 做中转。2.2 方案二Java 定时拉取数据后在业务层判断把判断逻辑放在 Java 里InfluxDB 只承担存储加查询的角色。用 Spring Boot 的Scheduled或者 Quartz 起一个定时任务每隔一段时间向 InfluxDB 发起 Flux 查询拿回最近窗口期的指标数据在 Java 里做多条件判断命中规则就调用告警发送服务。这个方案彻底解决了复杂规则难以表达的问题。你想做连续三次超过阈值才告警只在工作日的白天才告警CPU 和内存同时超过阈值才触发这类规则在 Java 里就是一组合法和判断非常直白。缺点是你要自己处理告警幂等防止重复触发自己维护调度和失败重试整体工作量比方案一大一些。2.3 方案三Kapacitor 加 TICKscript1.x 时代的方案InfluxDB 1.x 时代告警的标准组合是 Telegraf 加 InfluxDB 加 Kapacitor。Kapacitor 用 TICKscript 描述从哪个库里读数据、做什么计算、何时告警、发到哪里当年确实能跑得非常稳。如果你现在维护的还是 InfluxDB 1.8 的老系统并且不想把读写链路大改那确实只能走这条路或者让 Java 自己判断。但 2.x 已经把 Kapacitor 的不少能力内化到了 Flux 和 Check 里新项目再单独维护一个 Kapacitor 集群运维成本和学习成本都不划算。所以我提一嘴就带过不建议你再走老路。2.4 我的选型建议直接给结论简单阈值判断用方案一需要多条件业务规则、要和内部系统联动用方案二混合场景可以两者都用InfluxDB 负责基础指标告警Java 负责复杂业务告警。我自己做过的项目里最常见的组合是方案二为主、方案一为辅。因为监控指标最终都要汇总到告警平台Java 侧判断完以后可以直接复用公司的告警通道。但如果你只是给几个系统配 CPU 内存告警方案一半小时就能搞定别杀鸡用牛刀。接下来我分别把方案一和方案二的实操细节讲透。3. 方案一实操用 InfluxDB 原生 Check 做多条件告警3.1 先在 Ubuntu 22.04 或 Windows 上装好 InfluxDB 2.x检查告警是 InfluxDB 2.0 及以后版本才有的完整能力所以你最好装 2.7 以上的版本。Ubuntu 22.04 上用.deb包最方便官方下载地址下载后执行sudo dpkg -i influxdb2-2.7.10-amd64.deb装完以后用sudo systemctl enable --now influxdb启动并设置开机自启。启动后浏览器打开http://localhost:8086会进入首次初始化页面填写用户名、密码、组织名称和初始 bucket 名平台会给你生成一个 API Token这个 Token 后面 Java 连接要用先复制好存着。Windows 上稍微简单一点直接下载官方 zip 包解压到比如C:\influxdb然后在该目录执行influxd.exe同样用浏览器访问http://localhost:8086初始化。有一点要提醒Windows 下如果开着防火墙记得放行 8086 端口否则后续 Java 客户端或外部数据上报都会连不上。还有influxd 默认在前台运行关掉窗口服务就停了生产环境如果必须跑 Windows建议用计划任务或者 NSSM 把它注册成系统服务。3.2 准备好组织、桶和 Token 的概念InfluxDB 2.x 的组织Organization是逻辑隔离单元桶Bucket是存放数据的地方Token 是访问凭证。你可以把组织理解成公司把桶理解成数据库表空间。监控数据建议单独建一个桶比如叫monitor不要和业务数据混在一起。写入和查询的 Token 权限也要注意Java 连接时用的 Token 至少要有这个桶的读写权限。初始化生成的 Operator Token 权限很大生产环境建议在 UI 的 Data 页面单独创建一个只读权限或者读写权限的 Token降低泄露后的影响范围。权限这步我们往往觉得无所谓实际排查问题的时候经常栽在 Token 权限不够导致检查任务读写不了数据后面我会专门讲。3.3 用 Flux 写多条件阈值检查逻辑这一步是整个方案一最费脑子的地方。假设你的监控数据按照host_status这个 measurement 写入里面有cpu、mem、conn三个字段分别代表 CPU 使用率、内存使用率和连接数。现在要判断CPU 大于 85 并且内存大于 90或者连接数大于 5000。Flux 里处理多字段的常用办法是pivot把字段转成列然后再做组合条件过滤。示例如下from(bucket: monitor) | range(start: -1m) | filter(fn: (r) r._measurement host_status and (r._field cpu or r._field mem or r._field conn)) | aggregateWindow(every: 30s, fn: max) | pivot(rowKey: [_time], columnKey: [_field], valueColumn: _value) | filter(fn: (r) (r.cpu 85.0 and r.mem 90.0) or r.conn 5000.0) | map(fn: (r) ({r with _value: 1.0, _time: r._time}))我解释一下每一步的意图。range定义检查窗口这里取最近 1 分钟filter先把需要的字段筛出来别把无关字段拉回来增加计算量aggregateWindow按 30 秒做一次聚合取窗口内的最大值这样能过滤瞬时抖动pivot把cpu、mem、conn变成列方便后续一次判断多个字段最后的filter才是真正的多条件阈值逻辑。这里很关键的一点是最后加了一个map把_value强制设为 1.0。原因在于 Threshold Check 天然是拿记录里的_value和阈值做比较的如果_value是 cpu 的百分比数值那还能用但如果你的条件命中并不想依赖某个原始字段干脆统一输出一个固定值配合阈值大于 0.5这种写法就能保证命中的记录一定触发告警。3.4 创建 Check、NotificationRule 和 EndpointFlux 查询在 UI 的 Data Explorer 里确认能查出结果以后就可以创建 Check。打开 Alerts 页面选择 Checks新建一个 Threshold Check把上面这段 Flux 粘进查询编辑区周期设置成 1 分钟。阈值设置那里可以配置一个阈值比如_value 0.5设为 Critical也可以再加一个 Warning 阈值。保存成功以后系统会按周期执行这个检查。这里要强调一下 Check 和 NotificationRule 的关系。Check 只是判断判断结果会写入内部_monitoring桶但它本身不发消息。真正发消息要靠 NotificationRule所以还要去 Notification Rules 里新建一条规则选择你刚创建的 Check状态变化选项建议选任何状态变化避免只在变为 Critical 时发而恢复时不发。然后绑定一个 EndpointEndpoint 就是通知目的地。Endpoint 支持 HTTP 和邮件两种最常见的方式。我一般用 HTTP 方式指向 Java 写的一个接收告警的接口或者指向团队用的告警机器人。配置时字段很简单填一个 URL 即可InfluxDB 会以 JSON 格式 POST 消息到这个地址。如果你想直接发邮件Endpoint 里填 SMTP 服务器、端口、发件人和收件人注意多数邮件服务对 SMTP 有连接数和频率限制告警频率太高会被封发件账号。3.5 Java 侧只负责写数据不参与判断方案一的 Java 代码量大减。你要做的就是把监控数据写进monitor桶其他交给 InfluxDB。Java 写入推荐用官方客户端influxdb-client-java在 pom 里加依赖dependency groupIdcom.influxdb/groupId artifactIdinfluxdb-client-java/artifactId version7.2.0/version /dependency连接和写数据也很简单InfluxDBClient client InfluxDBClientFactory.create( http://127.0.0.1:8086, my-token.toCharArray(), my-org, monitor ); WriteApiBlocking writeApi client.getWriteApiBlocking(); Point point Point.measurement(host_status) .addTag(host, web-01) .addField(cpu, 92.5) .addField(mem, 95.1) .addField(conn, 5200) .time(Instant.now(), WritePrecision.S); writeApi.writePoint(point);这套代码里值得留意的点有两个。第一是WritePrecision.S如果你的时序数据在秒级精度就够就不用传纳秒传纳秒会导致存储膨胀。第二是 Tag 不宜放动态高基数数据比如把每次调用里的请求 ID 放进 Tag会造成 Tag 数量爆炸严重影响查询性能。字段名和 Tag 名最好先规划好后面写 Check 时就不会改来改去。4. 方案二实操Java 做多条件阈值判断并发告警方案二更接近大多数业务团队的编码习惯我展开讲完整链路。4.1 Java 客户端接入 InfluxDB 并初始化连接无论是方案一还是方案二Java 连接 InfluxDB 的姿势是一样的。用官方客户端会比较省心它内部自带连接池和自适应压缩不用自己封装 HTTP 层。初始化时建议把连接对象声明为 Spring Bean整个应用复用同一个连接而不是每次查询都新建一个客户端。Configuration public class InfluxDbConfig { Bean public InfluxDBClient influxDBClient() { return InfluxDBClientFactory.create( http://127.0.0.1:8086, my-token.toCharArray(), my-org, monitor ); } Bean public QueryApi queryApi(InfluxDBClient client) { return client.getQueryApi(); } }连接地址、Token、组织这些配置别硬编码放到application.yml里通过ConfigurationProperties注入后面换环境就不用改代码了。有一点要注意Token 属于敏感信息提交到 git 仓库是大忌我见过不止一次把 Token 写死在代码里然后整个仓库公开的事故这种问题比告警漏发还难处理。4.2 定时拉取最近时间窗口的监控指标在 Java 里实现周期任务优先用 Spring Boot 自带的Scheduled不需要引入额外组件。核心是写一个 Flux 查询把最近一分钟的数据取回来。注意窗口要对齐比如你每 1 分钟检查一次range(start: -1m)取的就是上一次检查到现在的数据如果任务延迟了窗口会漂移建议用固定窗口填充方式。Component public class MonitorScheduler { private final QueryApi queryApi; public MonitorScheduler(QueryApi queryApi) { this.queryApi queryApi; } Scheduled(fixedRate 60000, initialDelay 5000) public void checkMetrics() { String flux from(bucket: \monitor\)\n | range(start: -1m)\n | filter(fn: (r) r._measurement \host_status\)\n | pivot(rowKey: [\_time\], columnKey: [\_field\], valueColumn: \_value\)\n; ListFluxTable tables queryApi.query(flux); // 对表格里的每个记录做判断 } }这里用pivot的好处和方案一里说的一样把字段变成列后一条记录里可以同时拿到 cpu、mem、conn 三个值判断逻辑写起来和写普通对象一样自然不用来回查 map。需要注意pivot之后记录的时间列可能变成_time原来的_start、_stop都还在取数时只关注_time和字段列就行。4.3 多条件阈值判断逻辑与参数设计取回数据以后真正的判断逻辑就很常规了。我习惯把规则参数单独抽象出来而不是在判断代码里写死数字。举个例子规则是CPU 大于 85 且内存大于 90并且连接数大于 5000那对应的判断大致是private boolean match(MetricsRecord r) { if (r.cpu null || r.mem null || r.conn null) { return false; } return r.cpu 85.0 r.mem 90.0 r.conn 5000; }字段为 null 的情况必须处理。InfluxDB 的表结构是稀疏的某些时刻可能只有 cpu 和 mem 有数据conn 字段根本没有记录pivot之后这一列就是 null直接做比较会得到 false 或抛空指针。所以先判空再判断尽量让结果为「数据不完整」时保持静默不要因为缺字段就误告警这是我在线上踩过最多的坑。如果规则更多建议把阈值抽成一个配置对象用枚举定义比较方式避免每次改阈值都要重新发版public enum CompareOp { GT, GTE, LT, LTE } public class ThresholdRule { private String field; private double value; private CompareOp op; public boolean matches(MetricsRecord r) { Double actual r.getValue(field); if (actual null) { return false; } return switch (op) { case GT - actual value; case GTE - actual value; case LT - actual value; case LTE - actual value; }; } }用这种结构后多条件判断就变成遍历规则集合做组合评估。单个字段的单条件规则、多个字段的与或组合规则都能用同一套模型表达。规则引擎的收益是后期维护成本显著下降新告警规则不用改代码改配置就行。4.4 状态去重与恢复通知直接调用告警接口的做法很简单但很快会遇到一个问题Scheduled每 1 分钟执行一次如果条件连续 10 分钟满足就会发 10 次告警告警渠道瞬间被淹。解决思路是在 Java 侧维护一个当前告警状态的 Map记录每条规则是否已经处于告警中只有从 false 变 true 的瞬间才发送触发消息从 true 变 false 的瞬间发送恢复消息中间持续命中保持静默。private final MapString, Boolean alertState new ConcurrentHashMap(); private void handleState(String ruleKey, boolean current) { Boolean last alertState.get(ruleKey); if (current !Boolean.TRUE.equals(last)) { alertState.put(ruleKey, true); notificationService.sendTrigger(ruleKey); } else if (!current Boolean.TRUE.equals(last)) { alertState.put(ruleKey, false); notificationService.sendRecover(ruleKey); } }这里一定要用ConcurrentHashMap。因为定时任务在多实例部署时可能被多个节点同时调度即使你只有一个节点Spring 也可能会配置多个调度线程使用普通 HashMap 并发写入轻则丢数据重则直接抛异常。如果系统是多实例部署状态 Map 只存在于本机会带来重复告警问题这种场景建议把状态存 Redis 或者数据库用 SETNX 带上过期时间做分布式锁。4.5 接入邮件和 Webhook 通知通知这块我建议优先走 Webhook把告警消息封装成 JSON发给公司内部的统一告警平台。这种方式最灵活也方便后续扩展成短信、电话告警。Java 侧用 JDK 11 自带的HttpClient就能满足需求不需要额外依赖public void sendWebhook(String url, String title, String message) { String body {\title\:\ escape(title) \,\message\:\ escape(message) \}; HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .header(Content-Type, application/json) .timeout(Duration.ofSeconds(5)) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); try { client.send(request, HttpResponse.BodyHandlers.ofString()); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); // 这里要记录日志但不能影响下一次调度 } }一定要做超时控制。告警服务如果因为网络问题无法访问HttpClient默认会一直等下去导致定时任务线程被占满整个监控循环停摆。超时时间 3 到 5 秒足够失败时记录日志然后让下一次调度重试。邮件方式可以用 Spring Mail但要注意邮件服务器经常有发送频率限制不建议高频告警直接走邮件否则发件账号很快会被封掉邮件网关阻塞还会反过来拖垮应用线程。5. 多条件判断的规则设计细节很多团队代码写完了告警还是不好用问题大多出在规则设计上。这一节我把容易忽略的细节集中讲一下。5.1 与、或、边界条件的优先级多条件判断里最常见的错误是优先级不清。比如需求是CPU 大于 85 且内存大于 90或者连接数大于 5000翻译成程序语言一定是(cpu 85 mem 90) || conn 5000初学者经常写成cpu 85 mem 90 || conn 5000虽然 Java 里优先级高于||结果碰巧一样但可读性很差而且一旦再加条件就乱套。边界条件也有讲究。如果需求写超过 85% 告警那应该用还是通常严格按字面意思用但实际监控里 85.0 和 85.0001 差别不大加上浮点计算误差我一般建议用并把阈值调成 85.0避免因浮点误差漏告警。整数型字段如连接数用比较没有浮点问题随意。5.2 连续 N 次满足条件才告警的实现单次数值的毛刺特别容易误报比如 CPU 瞬间冲到 95% 但下一秒又恢复正常这种情况大部分团队是不想收到告警的。所以连续 N 次满足才告警是刚需。在 Java 侧实现不复杂维护一个命中次数的计数器private final MapString, Integer hitCount new ConcurrentHashMap(); private boolean shouldTrigger(String ruleKey, boolean matched) { if (matched) { int count hitCount.getOrDefault(ruleKey, 0) 1; hitCount.put(ruleKey, count); return count 3; } else { hitCount.put(ruleKey, 0); return false; } }这里的逻辑是每次命中加 1连续命中 3 次才认为应该进入告警状态不命中立刻清零。需要注意hitCount和alertState是两回事hitCount决定是否触发alertState决定是否已经触发过。两者配合才能实现连续 3 分钟异常才会告警且告警不重复发送恢复后才允许再次触发。5.3 不同指标的时间窗口对齐问题当你判断当前 CPU 和内存同时高时必须保证 cpu 和 mem 的值来自同一时间窗口。如果 CPU 取的是最近 1 分钟的最大值内存取的是最近 5 分钟的平均值两个数不是同一个时间粒度的判断结果就没有意义。解决办法是统一窗口。Java 查询时用同一个range(start: -1m)聚合同样用aggregateWindow(every: 30s, fn: max)让各字段在相同窗口上聚合。InfluxDB 的聚合是分组在时间桶里的两个字段只要在同一个aggregateWindow里处理时间桶就天然对齐。如果不做聚合直接pivot要留意两条记录的_time可能相差几十毫秒条件是同时超过阈值的话这种微小的错位也有可能造成误判。5.4 告警风暴与冷却控制告警风暴指的是短时间内大量告警消息刷屏。根源通常是规则设置得太敏感或者阈值触达后没有冷却时间。Java 侧可以在状态去重之外再加一个冷却时间比如某条规则触发告警后10 分钟内不再发送同类型告警就算状态变成 normal 又变成 critical 也不理。冷却时间和状态去重要分清楚。状态去重是只在状态跃迁时发送冷却时间是状态跃迁事件本身被抑制。我常用的策略是连续命中判定交给hitCount状态跃迁控制交给alertState再加一个lastAlertTime存最近一次发送时间如果当前时间减去lastAlertTime小于冷却窗口即使状态刚才还没告警过也先不发。等冷却期过了以后再重新判断是否触发这样能有效避免告警抖动。6. 常见问题排查与避坑实录最后把我在实操里遇到的问题和排查思路整理成速查表每一条都是真实踩过的坑。问题现象可能原因解决办法Check 一直不执行检查任务被禁用或周期设置过长在 Alerts 页面打开 Check确认任务状态是 ActiveCheck 执行了但没写结果Token 权限不足给 Token 增加_monitoring桶的读写权限Check 有结果但通知没发出NotificationRule 没启用或状态变化条件不匹配检查规则是否 Active状态变化选 any告警重复发送忘记了状态去重逻辑用alertStateMap 做状态跃迁控制连续 3 次判断都命中但没告警hitCount计数器没持久化服务重启被清零把计数器状态放到 Redis查询结果为空range窗口内无数据或字段被过滤掉在 Data Explorer 中先手动确认查询结果Java 查询超时Flux 查询跨了太大的时间范围缩小range给查询 API 设置 timeout阈值边界告警漏报浮点比较精度问题使用并再输出时对值做四舍五入另外有几个值得单独展开的提醒。第一个是 Check 查询的返回结构。阈值型 Check 要求查询结果包含_value字段并且阈值是基于_value判断的如果你的自定义字段名不叫_value一定要用map重新赋值我就是因为改了字段名忘记映射导致 Check 一直判定成 normal。在 UI 里 Check 编辑页有 Run Check 按钮手动执行能直接看到返回结果测试通过以后再启用自动调度。第二个是时区问题。InfluxDB 使用 UTC 时间存储而我们的告警规则经常是按北京时间或者本地时间定义的。如果在 Java 侧判断晚上 22 点到早上 6 点之间不告警直接用LocalDateTime.now()跟存储时间比会有 8 小时偏差。建议在写入时统一把时间换算成 UTC 纳秒判断时明确指定时区或者干脆所有规则都基于 UTC 时间配置避免踩坑。第三个是写入性能。InfluxDB 写入接口对批量写入友好逐条写入容易把磁盘和 CPU 打满。Java 侧可以用官方客户端自带的WriteApi异步批量写入默认按批次刷新比每次调用writePoint快很多。要注意批量写入在没有成功回调前异常是异步抛出的日志里要专门记录写入失败的记录否则数据悄悄丢了都发现不了。第四个也是很多人忽略的问题告警系统的可观测性。监控系统本身也是要监控的。InfluxDB 的 Check 任务如果因为查询报错连续失败默认不会有任何通知你的告警链路可能已经断了却完全无感知。我建议额外写一个心跳告警每分钟往桶里写一个固定值再配一条规则判断心跳值是否正常心跳停了立刻告警这样整个链路至少能保证出了问题会有人知道。个人使用感受和最后的经验做了这么多监控告警项目我最深的感受是工具再花哨规则设计永远是第一位的。原生 Check 帮你省了轮询代码但多条件规则最终还是得落到 Flux 或者 Java 里表达Java 轮询万能但状态管理、幂等、时间对齐这些细节没有一定经验很难一次到位。最后分享一个小技巧无论选哪条路线先在测试环境手工构造一批临界值数据把阈值边界、窗口聚合、告警恢复全部测一遍再上生产。很多人急着上线跳过测试结果生产环境第一次真实告警就把通知渠道打爆或者漏报严重故障。监控告警这事稳比快重要得多。