Prometheus 数据别再硬扛了:告警继续跑,长期监控交给 ClickHouse
Prometheus 负责采集、短期查询和告警,ClickHouse 负责长期存储与大范围分析。两者接起来,真正麻烦的不是“能不能写进去”,而是数据能不能补回来、重复样本怎么处理、写入积压会不会失控。
下面把架构、Remote Write、表结构、容量估算、故障演练和上线验收串起来讲。配置只是起点,参数要按目标版本和真实负载验证,不能抄完就上生产。
告警归告警,历史归历史
Prometheus 继续抓取 exporter 和应用指标,保存近期数据,执行规则并触发告警。ClickHouse 保存更长时间的指标,负责跨天、跨月查询和多维分析。
我不会让 ClickHouse 接管紧急告警。ClickHouse、网络或远程写入层出问题时,Prometheus 本地的抓取和告警最好还能正常工作。监控系统的职责不是把所有东西塞进一个库,而是让故障发生时,关键链路仍然可用。
数据链路大致如下:
应用 / Exporter
|
v
Prometheus
| \
| +--> Alertmanager
|
+--> Remote Write 接收层
|
+--> 持久化队列 / 消息队列
|
v
ClickHouse 写入端
|
v
原始表 / 聚合表Prometheus 的 remote_write 发的是远程写入协议数据,不是普通 JSON。ClickHouse 也不是 Prometheus Remote Write 的原生接收端,中间必须有协议接收和数据转换层。
可以使用经过验证的 Remote Write 接收组件,也可以由团队维护适配服务,再通过持久化队列交给 ClickHouse 消费端。无论选哪种,都要核对具体版本对 Remote Write、直方图、Exemplars、Native Histograms、重试和写入确认的支持范围。文档里一句“兼容 Prometheus”,还不能说明这条链路适合生产。
我会特别关注接收层什么时候返回成功。如果数据还在进程内存里,接收层就先返回 2xx,接着服务崩溃,这批数据仍然可能丢。比较稳妥的做法,是确认前先写入持久化队列,或者使用经过验证的本地 WAL,并测试进程重启和磁盘故障。
缓冲能争取时间,但不能把故障变没。每个缓冲区都有容量,积压也有恢复成本。
远程写入,最怕“假成功”
Prometheus 配置可以从下面这个起点开始:
global:
scrape_interval: 15s
evaluation_interval: 15s
remote_write:
- url: "http://metrics-gateway.example.com/api/v1/write"
remote_timeout: 30s
queue_config:
capacity: 20000
max_shards: 16
min_shards: 2
max_samples_per_send: 2000
batch_send_deadline: 5s
min_backoff: 100ms
max_backoff: 10surl 指向 Remote Write 接收层,不是 ClickHouse 的 HTTP 端口。队列参数是起点,不能不压测就照搬。
max_shards 增大后,并发发送能力通常会上升,Prometheus 侧的连接和内存开销也会跟着增加,接收端压力也会变大。只调发送端,就像把水龙头开大,却不看后面的管径。
远端短时不可用时,Prometheus 会通过本地 WAL 保存待发送数据,但 WAL 不是无限容量的长期队列。故障持续太久、磁盘空间不足,或者数据超出了可恢复窗口,都可能造成远端数据缺口。
可以从这些指标开始观察,具体名称以部署版本为准:
prometheus_remote_storage_samples_pending
prometheus_remote_storage_samples_failed_total
prometheus_remote_storage_samples_retried_total
prometheus_remote_storage_shards
prometheus_remote_storage_queue_highest_sent_timestamp_seconds我会看 Pending 是否持续增长,失败和重试是否突然上升,最高发送时间与当前时间的差距是否变大。只看 Prometheus 的 up 不够:抓取可能正常,远程写入却已经落后半小时。
先算样本,再谈机器
容量规划从样本速率开始。假设有 2 万个抓取目标,每个目标平均暴露 500 条时序,抓取间隔为 15 秒:
每秒样本数 = 20,000 × 500 ÷ 15
≈ 666,667 samples/s
每天样本数 = 666,667 × 86,400
≈ 576 亿 samples/day这是平均值,不是峰值。服务批量重启、发布、定时任务集中启动,都可能让短时间流量明显高于日常水平。
我会先收集每个 Prometheus 实例的样本速率、时序数量、标签分布和峰值写入量,再补上保留周期、副本数、磁盘冗余和查询并发。最好用生产标签分布或脱敏后的真实样本做压测。标签长度、类型和查询方式不同,压缩效果会差很多,只按“每行大约多少字节”估算,结果通常不够可靠。
长期存储也不等于所有原始样本永久保留。常见做法是近期留细粒度数据,较长周期保存按分钟或更长窗口聚合的数据。聚合精度要从排障需求倒推:如果需要定位 30 秒内的抖动,把数据全部压成 5 分钟均值就不合适。
表结构别照抄
一个原始指标表可以按“一条样本一行”建模:
CREATE TABLE metrics_raw
(
ts DateTime64(3, 'UTC'),
tenant LowCardinality(String),
metric LowCardinality(String),
service LowCardinality(String),
series_hash UInt64,
value Float64,
labels Map(String, String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (tenant, metric, service, ts, series_hash)
TTL ts + INTERVAL 180 DAY DELETE;这份表结构只适合做起点。排序键要跟常用过滤条件走,最终方案要拿真实查询和真实写入负载验证。
时间统一用 UTC,业务时区转换放在查询层。tenant、service、cluster 这类稳定且常用的过滤条件,可以从标签里提取成明确的列。job、app、service_name 等叫法不一致的字段,最好在写入层统一映射,避免每条查询都要猜字段。
高基数标签要管住。request_id、用户 ID、完整 URL、异常堆栈可能让时序数量快速膨胀。不是标签越多,排障就越清楚。写入前要限制标签数量和长度,对确实需要保存的高基数数据明确保留周期,并观察带来的存储成本。
分区也别切得太细。按月还是按天,要看数据规模和清理需求;按小时甚至更细地切分,可能制造大量小分区和合并压力。TTL 也不代表到期时数据立刻从磁盘消失,要结合实际合并行为观察。
series_hash 可以由接收层按规范化后的指标名和标签计算,用来标识一条时序。但有这个字段不代表自动去重。HA 副本、远程写入重试和消息队列重复消费都可能产生重复样本。去重策略需要结合表引擎、写入方式和查询方式验证。单纯换成 ReplacingMergeTree,并不能保证查询时拿到的结果始终已去重。
写入要稳,积压要看得见
ClickHouse 更适合批量写入。如果每收到少量样本就发一次 INSERT,容易产生大量小 Part,后台合并忙起来后,写入和查询都可能受影响。
接收层至少要能按批次聚合数据,设置内存上限、超时重试、退避策略和队列容量,并暴露消费延迟、失败数、重试数和队列长度。
队列满了以后怎么办,也得提前定好:限速、暂停消费还是拒收?如果不设上限,数据会不断堆积;如果静默丢弃,故障期间最需要的历史样本可能刚好消失。
还要明确每一段的确认点:Prometheus 收到成功响应,代表数据到了哪里?消息队列确认,代表数据是否已经写进 ClickHouse?ClickHouse 写入后,消费位点什么时候提交?只有这些问题说得清,故障时才能判断数据在哪里、能不能重放、会不会重复。
我不建议轻易承诺端到端 exactly-once。消费者可能在“数据已写入、位点尚未提交”时中断,恢复后又写一遍。更现实的做法是明确重复风险,保证链路可以重放,再通过写入或查询设计处理重复样本。
Counter 不是平均数
ClickHouse 里的样本值都是数值,但不能因此对所有指标直接 avg(value)。
Gauge 表示某一时刻的状态,例如可用内存、队列长度,可以按场景求平均、最大值或分位数:
SELECT
toStartOfMinute(ts) AS minute,
avg(value) AS avg_available_bytes
FROM metrics_raw
WHERE metric = 'node_memory_MemAvailable_bytes'
AND service = 'checkout'
AND ts >= now() - INTERVAL 24 HOUR
GROUP BY minute
ORDER BY minute;Counter 是累积计数器,进程重启时会归零。对 http_requests_total 直接求平均通常没有业务意义,应该计算相邻采样点的增量,再换算成速率,并处理计数器重置。PromQL 的 rate() 不能直接替换成 SQL 的 avg()。
Histogram 也要按语义处理。经典直方图会拆成 _bucket、_sum 和 _count 等时序,计算分位数要正确累积 bucket。Native Histograms 能否完整保存,要看接收层、数据模型和 ClickHouse 写入端是否都支持。
从 PromQL 迁移历史分析 SQL 时,我会拿同一批原始样本对账。固定指标、标签和时间窗口,统一处理边界采样、缺失样本、标签映射和计数器重置,再比较结果。监控数据不只是要“查得出来”,还得确保查出来的数字讲得通。
上线前,把故障主动做出来
生产验证不能只看 ClickHouse 能不能写入。要覆盖写入容量、数据完整性、故障恢复、查询性能和告警隔离,并记录实际测试结果。
贴近生产压测。 用真实或脱敏后的标签分布,覆盖日常和峰值写入量。观察 ClickHouse CPU、磁盘吞吐、写入延迟、Part 数量和后台合并。再模拟服务批量重启、定时任务集中启动和标签数量突增。
拿固定样本对账。 选一批测试时序和时间窗口,在 Prometheus 与 ClickHouse 分别统计样本数、首末时间、缺失区间和重复率。比较之前统一抓取间隔、时区、HA 副本和时间边界,否则统计口径不同,结果就没有可比性。
停掉链路的一部分。 短时停止 ClickHouse,观察接收层队列和 Prometheus Pending 是否按预期增长;恢复后确认积压能否消退,再重新对账。接着分别停止 Remote Write 接收层、消息队列消费者和 ClickHouse 节点,检查每一段是否都能告警。
验证重启和磁盘压力。 重启 Prometheus、接收层和消费者,确认 WAL、持久化队列和消费位点都能恢复。模拟磁盘水位接近阈值,检查告警是否触发、处理流程是否清楚。未经验证,不要手动清理队列目录。
用真实查询做并发测试。 覆盖单服务、跨服务、多标签过滤和长时间范围,记录 p50、p95、扫描行数和资源消耗。只跑一条 count(),不能说明业务查询能用。写入压测期间也要同时跑查询,看查询是否会推高写入延迟。
证明告警没有被远端绑架。 让远程存储链路不可用,确认 Prometheus 仍能抓取目标、评估本地规则并通知 Alertmanager。如果这项没过,实时告警就仍然依赖远程存储,架构边界还没守住。
验收阈值要由业务和平台团队按实际负载制定,至少写清峰值时 Pending 是否持续增长、远程写入延迟上限、恢复后的积压清理时间、样本缺失和重复容忍度、核心查询 p95,以及远程存储故障期间本地告警是否正常。
这些是验收项,不是通用的标准值。生产结论必须来自目标版本和目标负载下的实测,不能拿示例参数代替。
灰度上线,回退要能执行
上线时先挑一组低风险 Prometheus 实例做灰度。原有抓取和告警路径保持不变,只增加远程写入。灰度期间持续做样本对账,观察写入延迟、队列长度、ClickHouse 写入和历史查询。
扩大范围时分批放量。每批都确认 Pending 没有持续恶化、接收层队列没有单边增长、磁盘和 Part 数量正常,抽样数据没有明显缺口,历史查询也没有拖慢在线写入。
回退步骤要提前写好:如何关闭 Remote Write,如何保证本地告警继续工作,已有积压要不要补写,ClickHouse 故障期间消费者如何限速或暂停。回退路径要实际演练过,故障发生时才不会临时找人讨论。
数据留得住,故障才说得清
Prometheus + ClickHouse 的关键,是把实时告警和长期分析拆开,再把中间链路的边界验证清楚。Prometheus 负责采集、短期查询和告警闭环;ClickHouse 保存长期指标,承担大范围分析;接收层负责协议转换、可靠缓冲和批量写入。
系统不会因为多加了一层存储就自动变可靠。每次重试都可能带来重复,每个缓冲区都有容量,每种长期保留策略也有成本。稳定不是保证故障不会发生,而是故障发生时,我们知道数据在哪里、影响到哪里、怎样恢复。
上线前算清样本量,用真实标签分布压测;拿固定时序做对账;主动演练接收层、队列和 ClickHouse 故障;确认远程存储异常时 Prometheus 告警仍然正常。把测试结果和回退记录留档,生产验证才算闭环。
如果这篇文章对你规划监控存储有帮助,欢迎转发给正在搭建监控平台的同事,也欢迎关注 @运维躬行录。后面我会继续分享 Prometheus 远程写入、ClickHouse 指标建模和监控链路排障。
公众号:耕云躬行录
个人博客:躬行笔记