ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Thanos Sidecar 实践:打通 Prometheus 本地数据与对象存储的持久化归档

Thanos Sidecar 实践:打通 Prometheus 本地数据与对象存储的持久化归档 这个系列写到第七篇才开始聊thanos-sidecar说实话拖到今天是有原因的。前六篇我们把Prometheus高可用、对象存储选型、Thanos Querier和Store Gateway的骨架搭完之后很多同学拿到手的只是一套“能查冷数据却查不到新数据”的半成品。标题里的“返璞归真”其实就落在这里——监控系统折腾到最后还是要回到最朴素的问题Prometheus本地磁盘上的数据怎么才能安全地、可查询地落到对象存储里变成真正能长期留存的历史资产。thanos-sidecar就是那个把本地数据和远端存储连接起来的角色。这篇不聊玄乎的架构图直接跟着我的部署路径走一遍sidecar为什么会存在、部署前要动Prometheus哪些参数、YAML里每个关键参数到底在干什么、怎么确认它真的在干活以及我在真实环境里踩过的几个坑。适合已经搭好Thanos查询端、准备把Prometheus接入持久化存储的读者。1. 先想清楚sidecar到底解决了什么问题1.1 前面搭好的架构还缺什么如果你已经按本系列前几篇的内容部署了Thanos Querier、Store Gateway和对象存储你会发现一个很尴尬的状态对象存储桶建好了Store Gateway也起来了Querier也能连上Store Gateway了但桶里面是空的。Querier面对Store Gateway时永远只能返回“查无数据”。原因很简单——没有任何东西把Prometheus的数据搬进桶里。Prometheus默认把数据写在自己的本地磁盘按2小时一个TSDB block的节奏滚动落盘。本地盘容量有限retention时间到了就会删旧数据这对长期监控留存来说是致命的。而单纯的远程写方案又容易丢标签、丢样本还得额外维护一套远端接收端的写入链路。thanos-sidecar的思路完全不一样它不改变Prometheus的写入方式而是像一个坐在副驾驶的助手蹲在Prometheus数据目录旁边发现新的block封口完成后就把它推到对象存储。Prometheus还是那个Prometheus写入行为零改动数据却多了一条可靠的长期归档通道。1.2 sidecar亮出来的一长串工作清单很多人以为sidecar只是“上传器”实际不完全对。它在Thanos体系里的工作至少包括四条监听Prometheus TSDB数据目录检测到完整的block2小时写满并封口后把整个block目录上传到对象存储把Prometheus自带的reload接口封装成HTTP服务Querier在需要刷新数据分布时可以通过sidecar触发Prometheus重新加载配置或做其他协调动作对外暴露StoreAPIgRPC接口让Querier能直接读sidecar访问到的全部本地数据——包括还没封口上传的近期样本通过gRPC健康检查和信息流向Querier传递本地StoreAPI的元数据、时间范围和外部标签参与全局查询拓扑的构建。这里最容易被忽略的是第三条。Prometheus当前正在写入的block是不能上传的但这个时间段的数据又是监控查询最关心的“现在发生了什么”。sidecar通过StoreAPI把这类“热数据”直接暴露给Querier才让整个全局查询闭环真正成立。所以sidecar既是“长期数据的搬运工”也是“近期数据的直连窗口”缺了它新数据查询和旧数据归档就断成两截了。1.3 sidecar和Store Gateway的分工别再搞混我见过不少人在这一步把架构理解反了以为sidecar负责从对象存储读数据。实际上sidecar只写不读除了本地从对象存储读数据的是Store Gateway。两者一写一读各管一头组件数据来源核心职责对外接口thanos-sidecarPrometheus本地数据目录上传新block、暴露本地热数据、触发reloadStoreAPIgRPC、HTTP管理接口Store Gateway对象存储读取桶中的历史block、建立索引、响应查询StoreAPIgRPCsidecar对对象存储桶的依赖集中在写入侧Store Gateway对桶的依赖集中在读取侧。你如果发现Querier能连Store Gateway但查不到数据问题往往在桶里本来就没数据也就是sidecar这一环还没打通反过来如果桶里有数据但Querier查不到那就该去查Store Gateway的索引和轮询状态了。两个组件的排查方向完全不同先把这条线画清楚后边排错才不会被带偏。2. 部署前必须动Prometheus两个参数和一组标签2.1 TSDB的block参数必须保持对称sidecar上传的基本单位是2小时一个的TSDB block。Prometheus本身对block的压缩和保留有一套默认逻辑但如果你想让它和Thanos协同工作就必须显式锁定block的最小和最大时长。我在所有接入Thanos的Prometheus上都会加上这两个启动参数--storage.tsdb.min-block-duration2h --storage.tsdb.max-block-duration2h为什么不加不行Prometheus的TSDB在做compaction时可能把多个小block合并成大block或者在特定条件下产生不规则的block边界。如果block时长上下限不一致TSDB会自己决定压缩节奏sidecar上传的block可能是1小时、2小时甚至6小时的混合状态。Thanos的Store Gateway在H2到H5的索引逻辑里对block边界是敏感的边界不一致轻则导致某段时间查不全重则让Blockmeta校验都过不了。两个参数写成同一个值等于告诉TSDB“别折腾就按标准2小时出块”这样sidecar拿到的每个block都是干净的、可预期的。另一个常被忽略的是本地保留时间。sidecar上传block后不会主动删本地的blockPrometheus的本地清理完全由retention策略控制。生产上我建议设置--storage.tsdb.retention.time3d --storage.tsdb.retention.size50GB至少保证本地能留下两到三天的数据这样即使对象存储或sidecar临时出问题本地还有可回溯的底账。如果retention小于2小时可能出现block没来得及封口上传就被删掉的极端情况那Sidecar配置得再对也没用。2.2 external_labels这几个键值直接影响全局查询正确性这个我在前几篇也提过但配合sidecar再强调一遍。Prometheus里的external_labels会被写进每个block的元数据sidecar上传block时也会把这份标签带上。Thanos Querier在合并多套Prometheus数据时靠的就是这些标签来区分数据来源。我习惯至少放这几个键global: external_labels: cluster: prod-1 replica: 0 prometheus: prom-prod-1为什么要这个想象你有两套Prometheus监控两个可用区如果不用external_labels区分两套数据的job和instance标签完全一样Querier合并查询时会把完全不同的两台服务器的数据当成同一台机器来聚合报警和图表都会出现不可预料的“串数据”。加上cluster、replica之后每套Prometheus的数据就有了独立的身份标识Querier能正确地按标签分组建模Store Gateway和sidecar的元数据也能正确匹配。另外提醒一下external_labels一旦定下来就尽量不要改。因为block上传后标签是留在对象存储里的历史block的标签不会随Prometheus配置变动而变动。改了标签新老block之间的join关系就断了跨时间对比查询会出问题。我就吃过这个亏上生产前想清楚别图省事随便起名字。2.3 确认lifecycle接口是开的sidecar要触发Prometheus reload靠的是Prometheus自带的/-/reload接口。默认情况下这个接口是关的必须在Prometheus启动参数里显式打开--web.enable-lifecycle如果这个参数没加sidecar启动后虽然日志可能显示connected但一旦Querier尝试通过它触发reload就会收到404或被静默拒绝。这个坑很隐蔽因为平时不影响数据上传只有做Tombstones清理或配置动态加载时才突然冒出来。我的建议是别管用不用得上统一加上成本为零后边少踩一个暗坑。2.4 对象存储侧其实只做了三件小事sidecar对接对象存储的配置文件很简单但有三件小事要提前确认桶要提前建好sidecar默认不会自动建桶。我遇到过好几回配置写得完全正确结果侧car日志一直报“bucket not found”就是桶忘了建密钥权限至少要有写入和列举权限。只给读权限sidecar上传时会直接403网络要能通到对象存储的endpoint。如果集群节点在VPC内记得走内网endpoint公网endpoint传输又慢又容易断而且流量费用也可能成为隐患。对象存储配置我用的是兼容S3的通用写法放到ConfigMap里后面sidecar挂载进来就行type: S3 config: bucket: thanos-data endpoint: s3.region.example.com region: your-region access_key: your-access-key secret_key: your-secret-key insecure: false如果你的对象存储是其他协议类型比如阿里云OSS、腾讯COS、华为OBS只需改对应的type和config块里的endpoint即可sidecar的对接逻辑是一样的。3. 侧car部署伴生容器还是独立Deployment3.1 架构选型我为什么坚持Prometheus同Pod伴生sidecar要读取Prometheus的数据目录最直接的方式就是把两个容器放进同一个Pod共享同一个数据卷。这样做有几个天然好处不需要额外把Prometheus数据目录暴露成网络存储避免NFS等共享存储的IO延迟sidecar和Prometheus的生命周期完全同步Pod滚动更新时两个容器一起重建不会出现sidecar读着一半数据目录被卸载的脏状态网络栈共用sidecar访问本机Prometheus的localhost:9090即可不需要走Service和DNS少一层网络故障因素。独立Deployment部署sidecar的玩法我也试过前提是Prometheus数据目录放在共享存储上sidecar再挂载同一份。结果发现性能和稳定性都不如伴生模式共享存储的IO抖动反而影响了Prometheus本身的写入。所以我只推荐一种方案sidecar作为Prometheus Pod的第二个容器通过同一个PVC挂载数据目录。如果你手头是多副本的Prometheus StatefulSet这条路走起来几乎是无痛的——只需要在StatefulSet的容器列表里追加一个容器。3.2 一个可以直接抄的StatefulSet片段下面是一个关键片段省略了namespace和标签等环境相关字段重点关注容器、挂载和参数这三块spec: serviceName: prometheus-headless replicas: 2 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - name: prometheus image: prom/prometheus:v2.45.0 args: - --config.file/etc/prometheus/prometheus.yml - --storage.tsdb.path/var/prometheus/data - --storage.tsdb.min-block-duration2h - --storage.tsdb.max-block-duration2h - --storage.tsdb.retention.time3d - --web.enable-lifecycle ports: - containerPort: 9090 name: http volumeMounts: - name: data mountPath: /var/prometheus - name: thanos-sidecar image: quay.io/thanos/thanos:v0.33.0 args: - sidecar - --tsdb.path/var/prometheus/data - --objstore.config-file/etc/thanos/objstore.yml - --prometheus.urlhttp://127.0.0.1:9090 - --http-address0.0.0.0:19191 - --grpc-address0.0.0.0:19190 ports: - containerPort: 19191 name: http - containerPort: 19190 name: grpc volumeMounts: - name: data mountPath: /var/prometheus - name: thanos-config mountPath: /etc/thanos readinessProbe: httpGet: path: /-/ready port: 19191 initialDelaySeconds: 10 periodSeconds: 30 resources: requests: memory: 128Mi cpu: 100m limits: memory: 256Mi cpu: 500m volumes: - name: thanos-config configMap: name: thanos-sidecar-config volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 100Gi几个要注意的细节--tsdb.path必须和Prometheus容器里实际的--storage.tsdb.path指向同一目录。看起来是废话但很多人改过Prometheus的数据目录后忘了同步sidecar参数导致sidecar盯着一个空目录看一天一条上传日志都没有探针用/-/ready而不是/-/healthy。/-/healthy只看进程存活/-/ready会额外检查sidecar的gRPC服务是否就绪更适合作为readiness探针资源限制别给太小。sidecar内存占用和block数量正相关block多了需要建立上传索引128Mi作为request、256Mi作为limit是我实测比较稳的组合。如果你的Prometheus数据量大可以放宽到512Mi。3.3 参数释义表部署时盯着这几个就够了参数作用我的建议--tsdb.pathPrometheus数据目录路径和Prometheus的storage.tsdb.path保持一致--objstore.config-file对象存储配置文件路径用ConfigMap挂载别写死到镜像--prometheus.url本机Prometheus地址伴生模式下用127.0.0.1--http-address管理接口和探针端口固定用19191--grpc-addressStoreAPI服务端口固定用19190Querier连的就是这个--reloader相关控制sidecar对Prometheus配置的监控我建议保留默认开启--shipper.upload-compacted是否上传本地的compacted block默认false不要随便开为什么--shipper.upload-compacted不要随便开因为Prometheus本地TSDB会自己做compaction把小块合并成大块。默认情况下sidecar只上传原始block已经compacted过的block留给Thanos Compactor去统一处理。如果这里开了可能导致同一个block被本地上传一遍、又被Compactor处理一遍对象存储里出现重复数据查询时出现数据重叠甚至label冲突。除非你完全禁用Prometheus本地compaction需要额外参数配合否则这个开关保持默认即可。3.4 让Querier找到sidecarsidecar部署完Querier并不会自动发现它。需要在Querier的启动参数里用--store指向每个sidecar的gRPC地址。我有两套做法测试环境直接写IP--store172.20.10.30:19190简单直接适合验证生产环境用headless Service加上DNS SRV记录--storednssrv_grpc._tcp.thanos-sidecar.monitoring.svc.cluster.localQuerier启动时会动态解析出所有Pod地址Pod重建换IP也不用改配置。用第二种方式时需要在Kubernetes里给sidecar Pod定义一个headless Service端口命名一定要带grpc协议前缀比如名字叫grpc这样才能被DNS SRV发现机制正确识别。我第一次配置时端口名字起了thanos-grpcDNS SRV解析出来的服务名对不上Querier一直报store连接失败后来把端口名改成grpc就好了。4. 验证三步曲sidecar是“活着”还是“干着活”4.1 先看日志和指标别急着开UI部署完新组件我从来不先去看Querier界面因为那是最滞后的信号。第一步永远是看sidecar日志。正常情况下sidecar启动后会立刻扫描本地数据目录把已经封口但还没上传的block挨个上传日志里会出现类似这样的条目msguploaded block block01H2K9JSMWVK0JZ1A6VZ40ASM1 msguploaded block block01H2K9JSMWVK0JZ1A6VZ40ASM2如果你刚部署完Prometheus本地还没形成完整的2小时block那前几个小时内没有上传日志是正常的。如果超过半天了一条uploaded block都没有那基本可以判定配置层面有问题按第5节的排查思路走。日志之外sidecar还暴露了一组很实用的Prometheus指标。虽然sidecar本身不是Prometheus实例但它把自己的运行指标挂在了--http-address上可以直接抓取thanos_sidecar_uploads_total thanos_sidecar_uploaded_bytes_total thanos_sidecar_remote_uploads_total只要thanos_sidecar_uploads_total这个metric的值在持续增长就说明上传链路是通的。这个比看日志更适合做持续监控我在Grafana里的“Thanos Sidecar状态”面板就挂了这几个指标一分钟刷新一次上传卡住能立刻看出来。4.2 再钻到存储桶里看看数据长什么样日志说上传了那是sidecar单方面的说法。要确认数据真正落到了桶里我一般直接用管理员工具看一眼thanos tools bucket inspect --objstore.config-fileobjstore.yml这个命令会列出桶里所有的block以及每个block的时间范围、样本数、文件大小。如果能看到block列表并且时间范围和Prometheus本地数据对得上那sidecar的数据链路就算彻底打通了。对于在线检查也可以跑一个只统计不下载的命令thanos tools bucket ls --objstore.config-fileobjstore.yml“上传成功”和“可查询”中间还隔着一个Store Gateway。你还要确认Store Gateway确实连接了同一个桶并且能正常加载这些block的索引。Store Gateway日志里会有加载block的信息也可以观察它的thanos_bucket_store_blocks_loaded指标——这个数字应该随着上传持续增长。很多人卡在“桶里有数据但Querier查不到”查到最后发现Store Gateway配置里指向了另一个桶两边的bucket名字差一个字母你说气不气。4.3 Querier全局查询验证新数据和老数据都要有最后一步才是打开Querier的查询界面验证。我习惯做两个测试查询一个正在产生的指标比如up。如果Querier能返回当前值说明sidecar的StoreAPI路径工作正常热数据直连查询一个至少几天前的指标比如某个容器的CPU使用率时间范围拉长到一周。如果能返回历史曲线说明Store Gateway从对象存储读数据的路径也通了。还可以在Querier的Store页面上看到当前连接的store列表sidecar和StoreGateway应该都在且状态为healthy。如果sidecar在列表中但查询超时多半是gRPC连接不稳定优先检查Pod到Querier之间的网络策略以及gRPC端口在Service里是否正确暴露。5. 实际维护踩坑记录五个问题一次讲清5.1 坑一sidecar一条uploaded日志都没有这个坑我帮同事排过一次当时查了快两个小时。现象是sidecar正常启动探针正常日志也没有报错但就是一条uploaded block都没有桶里空空如也。最后定位到根因很简单Prometheus容器里改了数据目录的挂载路径从/prometheus改成了/var/prometheus但sidecar的--tsdb.path还停留在旧的/prometheus。sidecar扫到一个不存在的目录反而什么错都不报因为Prometheus还没生成任何block之前它就是空目录TSDB也是从空目录开始扫描的。排查这类问题的正确姿势# 进入sidecar容器确认路径是否存在且挂载的是同一个卷 ls -l /var/prometheus/data # 查看Prometheus容器里的TSDB路径 ls -l /prometheus/data两边看到的内容不一致就不用再看别的了先把--tsdb.path对齐。这个坑提醒我改Prometheus的存储路径时记得sidecar的参数是单独配的不会自动跟着改。5.2 坑二sidecar就绪探针经常失败Kubernetes里如果配了readinessProbe探针失败会导致Pod从Service的endpoint里被摘除Querier那边就会出现“store节点间歇性消失”。我踩过一次现象是Querier查询时好时坏仔细看才发现sidecar的/-/ready探针偶尔返回500。原因有两个Prometheus正在执行reload或TSDB正在compact大文件时sidecar同步调用Prometheus的接口响应变慢探针超时sidecar启动初期需要扫描和上传大量历史blockgRPC服务还没完全就绪探针过早请求被拒。解法也简单readinessProbe: httpGet: path: /-/ready port: 19191 initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3把initialDelaySeconds拉长给sidecar足够的时间做启动扫描timeoutSeconds提到5秒基本能覆盖大多数偶发卡顿。5.3 坑三Querier报grpc连接错误但sidecar明明活着Querier报错的时候不要先去折腾sidecar先确认你连的是不是对的那个端口。sidecar有两个端口19191是HTTP管理端19190才是gRPC StoreAPI端。Querier的--store却只能写gRPC端口。我第一次配置的时候把--store写成了19191gRPC握手直接失败报错信息里只提了connection error我排查了好多轮才发现是端口写错了。另一个常见问题是如果sidecar Pod没暴露19190端口到Service里Querier通过DNS SRV解析时根本找不到这个端口。检查Service定义时确保有ports: - name: grpc port: 19190 targetPort: 19190名字带grpc前缀是DNS SRV自动发现的约定不要省掉。5.4 坑四上传了桶里有数据Querier却查不到历史数据这类问题基本要把矛头指向Store Gateway。sidecar负责“写入”Store Gateway负资“读取”写入通路正常不代表读取通路正常。我记得有一次帮客户排查sidecar日志、桶内inspect都显示数据齐全Querier一查历史就空。绕了一大圈最后打开Store Gateway的启动参数发现--objstore.config-file指向的桶名和sidecar差了一个环境后缀比如sidecar写的是thanos-data-prodStoreGateway配的是thanos-data-prod-2。这种低级错误最浪费生命。另外还要注意Store Gateway的索引刷新机制。它不会实时扫描桶的所有变化默认是按周期轮询加载新block。刚上传的block可能要等几分钟才能被Store Gateway加载查询时如果时间范围覆盖到了还未索引的blockQuerier会暂时返回空。别一查不到就急着重启Store Gateway等一个刷新周期再试通常就出来了。5.5 坑五时间戳和时区相关的隐雷sidecar和对象存储在处理时间上有个容易忽略的细节block的时间戳信息默认是UTC而TSDB内部保存的时间戳也是UTC自1970年的毫秒数。如果Prometheus所在节点的系统时间和对象存储服务端时间不同步上传后block的minTime和maxTime出现偏移Store Gateway加载block时可能因为时间重叠产生奇怪的查询结果甚至block被判定为损坏直接跳过。更麻烦的情况是集群节点时钟漂移严重两个Prometheus副本之间的时间差超过了几分钟Querier按时间对齐时会看到同一指标在两个时间点都有值图表出现“毛刺”。这个不是sidecar独有但一旦接入对象存储后问题会被历史数据放大——因为桶里的block都是按绝对时间写死的后期没法通过查询端配置去修正。5.6 关于数据安全再啰嗦一句sidecar只负责上传不负责删除。这里的“不负责删除”有两层意思它不会因为上传成功就去删Prometheus本地的block本地数据保留完全听Prometheus的retention安排它不会主动删除对象存储里的block对象存储里的数据条目的删除只能由你手动控制或者由Thanos Compactor按保留策略来处理。所以如果Prometheus因为retention策略把本地块删了对象存储里的副本还在你不需要过度紧张。反过来也一样如果对象存储里的block被误删了侧car也不会自动去补传因为本地对应的block可能早就被retention清理了。生产环境里对象存储的备份策略一定要单独做不要指望sidecar兜底。最后再分享一个部署后的习惯搭配sidecar的整个Thanos体系版本一致性很关键。我见过有同学sidecar用v0.28Querier却升到v0.33gRPC握手时遇到协议不兼容的报错反查半天。Thanos各组件之间的gRPC版本兼容性总体做得不错但没必要在版本一致性上给自己埋雷。我现在的习惯是全家桶Querier、Store Gateway、Sidecar、Compactor统一锁一个版本号升级时一起升测试环境跑完两天再动生产。还有一个加分项sidecar的--tsdb.path目录我建议定期做一次磁盘空间趋势分析。因为本地Prometheus保留3天数据又要等block封口上传磁盘占用会比原来预期的更大一些。如果数据量增长特别快100Gi的PVC可能撑不到container的滚动更新周期。提前在Grafana里挂一个PVC使用率面板比哪天真写满了再扩容省心得多。到这儿sidecar这一环算是彻底通了。下一步我建议优化Store Gateway的缓存策略和Compactor的压缩保留周期那才是Thanos真正发挥长期运维价值的重头戏。下篇接着聊。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进