这个系列写到第七篇,才开始聊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数据目录,检测到完整的block(2小时写满并封口)后,把整个block目录上传到对象存储;
- 把Prometheus自带的reload接口封装成HTTP服务,Querier在需要刷新数据分布时可以通过sidecar触发Prometheus重新加载配置或做其他协调动作;
- 对外暴露StoreAPI(gRPC接口),让Querier能直接读sidecar访问到的全部本地数据——包括还没封口上传的近期样本;
- 通过gRPC健康检查和信息流,向Querier传递本地StoreAPI的元数据、时间范围和外部标签,参与全局查询拓扑的构建。
这里最容易被忽略的是第三条。Prometheus当前正在写入的block是不能上传的,但这个时间段的数据又是监控查询最关心的“现在发生了什么”。sidecar通过StoreAPI把这类“热数据”直接暴露给Querier,才让整个全局查询闭环真正成立。所以sidecar既是“长期数据的搬运工”,也是“近期数据的直连窗口”,缺了它,新数据查询和旧数据归档就断成两截了。
1.3 sidecar和Store Gateway的分工,别再搞混
我见过不少人在这一步把架构理解反了:以为sidecar负责从对象存储读数据。实际上sidecar只写不读(除了本地),从对象存储读数据的是Store Gateway。两者一写一读,各管一头:
| 组件 | 数据来源 | 核心职责 | 对外接口 |
|---|---|---|---|
| thanos-sidecar | Prometheus本地数据目录 | 上传新block、暴露本地热数据、触发reload | StoreAPI(gRPC)、HTTP管理接口 |
| Store Gateway | 对象存储 | 读取桶中的历史block、建立索引、响应查询 | StoreAPI(gRPC) |
sidecar对对象存储桶的依赖集中在写入侧,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-duration=2h --storage.tsdb.max-block-duration=2h为什么不加不行?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后,不会主动删本地的block,Prometheus的本地清理完全由retention策略控制。生产上我建议设置:
--storage.tsdb.retention.time=3d --storage.tsdb.retention.size=50GB至少保证本地能留下两到三天的数据,这样即使对象存储或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部署:伴生容器还是独立Deployment
3.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-duration=2h - --storage.tsdb.max-block-duration=2h - --storage.tsdb.retention.time=3d - --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.url=http://127.0.0.1:9090 - --http-address=0.0.0.0:19191 - --grpc-address=0.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.path | Prometheus数据目录路径 | 和Prometheus的storage.tsdb.path保持一致 |
--objstore.config-file | 对象存储配置文件路径 | 用ConfigMap挂载,别写死到镜像 |
--prometheus.url | 本机Prometheus地址 | 伴生模式下用127.0.0.1 |
--http-address | 管理接口和探针端口 | 固定用19191 |
--grpc-address | StoreAPI服务端口 | 固定用19190,Querier连的就是这个 |
--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找到sidecar
sidecar部署完,Querier并不会自动发现它。需要在Querier的启动参数里,用--store指向每个sidecar的gRPC地址。我有两套做法:
- 测试环境直接写IP:
--store=172.20.10.30:19190,简单直接,适合验证; - 生产环境用headless Service加上DNS SRV记录:
--store=dnssrv+_grpc._tcp.thanos-sidecar.monitoring.svc.cluster.local,Querier启动时会动态解析出所有Pod地址,Pod重建换IP也不用改配置。
用第二种方式时,需要在Kubernetes里给sidecar Pod定义一个headless Service,端口命名一定要带grpc协议前缀(比如名字叫grpc),这样才能被DNS SRV发现机制正确识别。我第一次配置时端口名字起了thanos-grpc,DNS SRV解析出来的服务名对不上,Querier一直报store连接失败,后来把端口名改成grpc就好了。
4. 验证三步曲:sidecar是“活着”还是“干着活”
4.1 先看日志和指标,别急着开UI
部署完新组件,我从来不先去看Querier界面,因为那是最滞后的信号。第一步永远是看sidecar日志。正常情况下,sidecar启动后会立刻扫描本地数据目录,把已经封口但还没上传的block挨个上传,日志里会出现类似这样的条目:
msg="uploaded block" block=01H2K9JSMWVK0JZ1A6VZ40ASM1 msg="uploaded block" block=01H2K9JSMWVK0JZ1A6VZ40ASM2如果你刚部署完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-file=objstore.yml这个命令会列出桶里所有的block以及每个block的时间范围、样本数、文件大小。如果能看到block列表,并且时间范围和Prometheus本地数据对得上,那sidecar的数据链路就算彻底打通了。对于在线检查,也可以跑一个只统计不下载的命令:
thanos tools bucket ls --objstore.config-file=objstore.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启动初期需要扫描和上传大量历史block,gRPC服务还没完全就绪,探针过早请求被拒。
解法也简单:
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写成了19191,gRPC握手直接失败,报错信息里只提了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-prod,StoreGateway配的是thanos-data-prod-2。这种低级错误最浪费生命。
另外还要注意Store Gateway的索引刷新机制。它不会实时扫描桶的所有变化,默认是按周期轮询加载新block。刚上传的block可能要等几分钟才能被Store Gateway加载,查询时如果时间范围覆盖到了还未索引的block,Querier会暂时返回空。别一查不到就急着重启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.28,Querier却升到v0.33,gRPC握手时遇到协议不兼容的报错,反查半天。Thanos各组件之间的gRPC版本兼容性总体做得不错,但没必要在版本一致性上给自己埋雷。我现在的习惯是,全家桶(Querier、Store Gateway、Sidecar、Compactor)统一锁一个版本号,升级时一起升,测试环境跑完两天再动生产。
还有一个加分项:sidecar的--tsdb.path目录,我建议定期做一次磁盘空间趋势分析。因为本地Prometheus保留3天数据,又要等block封口上传,磁盘占用会比原来预期的更大一些。如果数据量增长特别快,100Gi的PVC可能撑不到container的滚动更新周期。提前在Grafana里挂一个PVC使用率面板,比哪天真写满了再扩容省心得多。
到这儿,sidecar这一环算是彻底通了。下一步我建议优化Store Gateway的缓存策略和Compactor的压缩保留周期,那才是Thanos真正发挥长期运维价值的重头戏。下篇接着聊。