审查Thanos部署
基于在GKE上运行约20个单租户Thanos堆栈(位于全局查询器后,Thanos 0.42,缓存 0.35+)的现场规则。事实来源,包含数据和推理:
- 架构、性能和FinOps:https://wiki.jdelgado.fr/monitoring/lgtm/thanos/
- 缓存:https://wiki.jdelgado.fr/monitoring/lgtm/thanos-cache/
- 读取并发性:https://wiki.jdelgado.fr/monitoring/lgtm/thanos-query-concurrency/
完整规则集(R1-R102,标志、默认值、指标、锚点)在 references/rules.md 中。在得出结论之前,先阅读变更涉及的每个组件的部分。
审查流程
- 记下Thanos版本(镜像标签)。标志和默认值会变化:对照它检查每个标志(规则R92)。
- 对于包装图表(bitnami
thanos、本地 umbrella 图表、手写的extraFlags/args),读取渲染后的Manifest(每个堆栈helm template)上的标志,而不是values上的:各个层会相互覆盖,有些标志只存在于模板中。 - 部署特定的名称(用于修剪的标签堆栈、租户标签、集群或数据中心标签)来自配置本身。当差异未显示它们时,请索要GitOps仓库,而不是假设名称。
- 对于每个存在的组件,先走查其下方的清单,然后走查其在
references/rules.md中的部分。 - 当其四个标志中的任何一个被设置或保留默认值时,使用实际值计算并发不变量。
- 根据组件实际运行方式(CronJob、Spot实例、Pending状态的Pod)检查告警规则。
- 按照下面的输出格式报告。
按组件的清单
并发不变量(查询前端 + 查询器 + 存储网关)。 --query-range.max-query-parallelism (默认14) x min(selectors, --query.max-concurrent-select) (默认4)必须保持在 min(--store.grpc.series-max-concurrency, --query.max-concurrent) (默认均为20)以下。默认值已经破坏了它(56 vs 20)。一旦并行度单独达到网关上限,一个查询就会在每个堆栈上使网关饱和。将 --query.max-concurrent-select 提升到远高于4会移除上限:乘数变成原始的selector数量。设计用于绑定存储网关RSS(在OOMKill后设置)的网关是一个内存边界,请保留它。该不变量在宽查询时起作用:测量的查询平均有1.5个选择器,网关饱和来自历史分片上的几个并发查询,而不是一个宽查询。全局查询器通过 StoreAPI 访问区域查询器,这会绕过它们的 PromQL 网关。同时修复两个乘数,包括全局前端,在整个查询器池上计算 --query.max-concurrent,并注释每个乘数及其必须遵守的下游上限。
查询器。
- Partial response 默认开启(
--query.partial-response默认true):缺少标志并不是安全的默认值。它会丢弃有错误的存储,而不是慢的存储:--store.response-timeout默认为0ms(禁用),因此一个慢存储会导致整个查询在--query.timeout(默认2m)处失败,而不是被丢弃。 - 在由 ruler 或 alerting 使用的查询器上启用 partial response 可能会静默丢失告警,并且 partial responses 永远不会被缓存。仅在人工 Grafana 数据源上按请求启用它。
--selector-label绝不能是模板默认值:一个摄取了两个该标签值的堆栈会导致第二个值返回空(静默数据丢失)。每个堆栈都需要显式选择,且标签必须是查询实际携带的单值标签。检查每个设置了该标志的堆栈,看它实际摄取了什么。--grpc-compression在 v0.43.0 之后已弃用。其替代品,--endpoint.sd-config中每个端点的client_config.compression,除非该端点设置了其自己的tls_config(例如enabled: false),否则会被忽略:否则全局标志仍然有效。
存储网关。
- 绝不要提高
--store.grpc.series-max-concurrency来治疗饱和:网关会排队,不会拒绝。饱和表现为超时,大多数分钟远低于上限,提高它会在峰值期间移除唯一的界限。在查询前端慢日志中找到客户端。 --store.limits.request-series=2000000是一个复制过来的值,永远不会触发(测量到的最大调用是200000个系列)。首先测量thanos_bucket_store_series_data_touched_bucket{data_type="series"}。thanos_bucket_store_queries_dropped_total为0证明不了什么:它只计算默认值为0(关闭)的限制器。- 时间分片必须重叠;背靠背的
--min-time/--max-time界限保证在副本缺失时出现盲窗。--min-time是最旧的界限。一个正的--max-time(例如3h)不是上限:该分片会服务到当前时间,覆盖 Receive。 - 节点启动磁盘上没有
emptyDir:索引标头下载会使其饱和(30分钟恢复)。专用 PVC 并配置预置 IOPS/吞吐量;对于 Hyperdisk Balanced,容量不购买性能。 - 将
--sync-block-duration降低到 15m 默认值以下,在缓存桶后面几乎不产生成本(只有iter调用依赖它,整个集群测量约1美元/月):不值得专门发布,在下一次计划重启时修复它。 - 保持懒惰读取器,绝不要懒惰索引标头下载(p99 ~224 秒)。在默认的 5m 空闲超时下,每次空闲后的第一次查询都要支付完整的重载;隐藏标志
--store.index-header-lazy-reader-idle-timeout提高到一个工作日(10h)后,在一个堆栈上将加载时间减少了84%,代价是常驻内存。隐藏标志:升级时重新检查。
缓存。
- 在 Thanos < v0.35.0 上跨租户共享缓存桶:键冲突,存储网关保持
Ready状态且不提供历史数据。Redisprefix可以在任何版本上隔离。单租户金丝雀无法捕获此问题。 - 索引缓存和缓存桶位于同一个 Redis 上:分离
db(索引db: 1)。 - 不要根据内存缓存大小来调整共享缓存的大小(它们测量的是 Pod 运行时间)。根据 TTL 窗口内的工作集大小来调整(滚动 >= 24 小时子查询的 P95,并带有显式步长),除以测量的分片副本之间的重叠,绝不是 Pod 数量。副本加载相同的块但缓存不同的块:2个副本测量值为1.4,而不是2。
- 缓存桶子范围存活 24 小时(
chunk_subrange_ttl);Redisexpiration不控制它们。 - 查询前端缓存文件解析严格:没有
ttl,没有enabled_items,TTL 是config:下的expiration。未知键:组件拒绝启动。 - Sentinel 后的 Redis:Sentinel 服务 +
master_name,否则故障转移后写入会发送到旧主节点。
Receive。
- 保持按需运行,使用专用的优先级类:这是唯一保存尚未上传数据的组件。其他所有组件都可以使用 Spot 实例。
- 根据峰值活跃序列数来设定内存限制。
head_series_limit仅在共享多租户 Receive 或启用了复制时才有用:对于一个副本,429 会阻塞每个写入的租户,而 OOM 仅会导致重启和 WAL 重放。如果没有可访问的meta_monitoring_url,它就不起作用。请求限制返回 413(Prometheus 丢弃数据),序列限制返回 429(重试)。 - 当池大小将发生变化时,使用 Ketama 哈希环,而不是
hashmod。 - 限制 Go 堆:
GOMEMLIMIT来自 Pod 内存限制,或--enable-auto-gomemlimit(默认关闭,容器限制的 0.9)。Go 自 1.25 起从 cgroup 设置 GOMAXPROCS,但从不设置 GOMEMLIMIT。Receive OOM 意味着在再次服务之前要进行 WAL 重放。
Compactor。
- 作为 CronJob:
concurrencyPolicy: Forbid是必须的(两个 Compactor 同时操作同一个桶会损坏块)。 thanos_compact_todo_*指标仅在启用--wait时注册:在 CronJob 模式下,针对它们的告警永远不会触发。标准的ThanosCompactIsDown在 23 小时/24 小时内都会触发。替换为 kube-state-metrics CronJob 规则和存储网关thanos_blocks_meta_synced积压代理。任何针对 Compactor 指标(thanos_compact_group_compactions_failures_total...)的告警在 CronJob 模式下永远不会触发:两次运行之间没有抓取目标。- 绝不要在 Thanos 桶上设置桶生命周期规则或归档存储类:只有 Compactor 知道块依赖关系;重试循环会使 API 账单增加 10 倍,抵消了节省。
- 保留层级与存储网关服务的内容保持一致。
横切关注点。
- 一个堆栈的所有组件放在一个可用区内:跨可用区流量会对数据路径上的每个箭头收费。在一个堆栈上设置可用区分布约束是一个成本错误,一个需要刻意权衡的例外情况。
- 针对 StatefulSet 期望就绪副本数的存储就绪告警,需设置
for: 30m:up == 0对于驱逐的 Pending Pod 永远不会触发,而for: 5m会在正常的 Spot 恢复过程中引发警报。
诊断饱和
使用 references/rules.md 中的过程 D1-D10。D1 的核心:in_flight = throughput x duration (Little's law),按 Pod 测量;持续时间通常占主导地位(现场测量为 x190 vs x4.5)。使用 max_over_time(...[$__interval:1m]) 读取网关面板,延迟读取 p999/p9999,集群大小使用 count(group by (pod) (up == 1))。
输出格式
发现项按严重程度排序:数据丢失或静默错误结果,然后是中断风险(组件拒绝启动的配置算在这里),然后是成本,最后是运维整洁性。每个发现项:
- 字段或标志,带有行号
- 问题及其具体后果
- 修复方法,作为确切的值或代码片段
- 来自
references/rules.md的带有锚点的 wiki URL(规则 ID 是内部的,不要包含它们)