Loki 大规模生产部署伸缩指南:Query Scheduler、内存 Ballast 与远程规则评估
Loki 大规模生产部署伸缩指南:Query Scheduler、内存 Ballast 与远程规则评估
当 Loki 集群的日志体量持续增长、单一进程难以承载时,合理的伸缩策略是核心运维课题。本文基于 docs/sources/operations/scalability.md 整理出三套生产级扩容方案:将查询调度队列独立为 Query Scheduler 进程、利用内存 ballast 降低 GC 开销、以及把规则评估从 ruler 外部化到独立的 query-frontend/querier 池。读完本文,你将掌握这些方案对应的配置项、CLI 参数、源码实现位置与可观测指标,可直接用于规划自己的 Loki 生产部署。
从单进程到按角色分片的部署模式
Loki 默认以单二进制模式运行(-target=all),所有组件共处于一个进程内,便于本地验证与小规模使用。但当日志量上升、需要扩容时,官方建议按角色拆分多个 Loki 进程:ingester、distributor、querier、query-frontend、ruler 等各自独立部署、独立伸缩,而非继续压榨单个进程。
这一设计在源码中体现得很直接:Loki 根配置 Config.Target 支持逗号分隔的组件列表,默认值为 all(见 pkg/loki/loki.go 与 -target 标志注册,L154-L159),-list-targets 可打印全部可用的 target。在 pkg/loki/modules.go 中可以看到 query-scheduler 与 query-scheduler-ring 均作为独立 target 存在,说明调度器既可以作为独立进程启动,也可以单独托管其 ring 管理。
仓库中的 production/ksonnet 目录包含 Grafonnet 格式的 .libsonnet 配置,演示了如何为各组件单独配置资源并按其负载特征伸缩,是理解"按角色分片"生产形态的参考实现。
将查询队列独立出来:Separate Query Scheduler
query-frontend 内部维护着一个内存队列,用于在查询被执行前对请求做排队、切分与调度。在日志量大、查询并发高的场景下,这个队列会成为瓶颈,且队列位于 frontend 进程内意味着你无法横向扩展多个 query-frontend。解决办法是把队列移出为独立的 Query Scheduler 进程(与 Grafana Mimir 的 query-scheduler 思路一致),这样 frontend 只负责接收请求并转发给调度器,querier 通过 worker 从调度器拉取并执行查询,两者之间由调度器完成排队与分发。
启用独立调度器后,需要同时给 frontend 和 querier 中的 worker 配置调度器地址。querier 侧的配置入口不是 querier 块,而是 frontend_worker 块——该块配置的是运行在 querier 进程内、负责从调度器拉取并执行查询的 worker。
使用 CLI 标志:
# query-frontend 进程
-frontend.scheduler-address=<scheduler-host:port>
# querier 进程
-querier.scheduler-address=<scheduler-host:port>
使用配置文件(等价写法):
frontend:
scheduler_address: <scheduler-host:port>
frontend_worker:
scheduler_address: <scheduler-host:port>
注意:querier 的
scheduler_address必须配置在frontend_worker块下,而不是querier块下。同时,不允许同时配置 frontend 地址与 scheduler 地址,二者只能选其一。
调度器进程本身通过 -target=query-scheduler 启动。例如使用 Loki Docker 镜像:
docker run grafana/loki:latest \
-config.file=/etc/loki/config.yaml \
-target=query-scheduler \
-server.http-listen-port=8009 \
-server.grpc-listen-port=9009
该命令启动一个仅运行 Query Scheduler 的进程,HTTP 端口 8009、gRPC 端口 9009,frontend 与 querier 通过 gRPC 与之通信。
调度器在源码层面提供了若干关键队列参数(见 pkg/scheduler/scheduler.go):
max_outstanding_requests_per_tenant(-query-scheduler.max-outstanding-requests-per-tenant,默认32000):每个租户在调度器上允许的未完成请求数上限,超限请求将以 HTTP 429 拒绝,可用于防止单一租户打爆队列;max_queue_hierarchy_levels(-query-scheduler.max-queue-hierarchy-levels,默认3):分层队列的最大嵌套层数,0表示禁用分层队列;querier_forget_delay(-query-scheduler.querier-forget-delay,默认0):querier 未发送优雅关闭通知即断开时,调度器在租户分片内保留该 querier 的时长,配合 shuffle-sharding 可缩小故障爆炸半径。
使用 hash ring 发现调度器
除了在 frontend 和 querier 上静态配置 scheduler_address,还可以让多个 Query Scheduler 实例把自己注册进一个 hash ring,querier 与 query-frontend 通过 ring 动态发现可用的调度器,免去维护固定地址列表的工作。
启用方式(YAML 与 CLI 标志二选一):
query_scheduler:
use_scheduler_ring: true
-query-scheduler.use-scheduler-ring
注意:
use_scheduler_ring需要在查询调度器、querier、query-frontend 各自的配置中都设置为 true(三者共用一份配置文件时只需设置一次),因为 querier 和 frontend 只有在自己配置为 true 时才去读 ring。
ring 需要一个键值存储后端。多数部署无需为调度器 ring 单独配置:scheduler_ring 块会继承 common.ring 的存储,且只要配置了 memberlist 段,Loki 的全部 ring 都会使用 memberlist。如需单独指定调度器 ring 的存储,可以使用 scheduler_ring 块:
query_scheduler:
use_scheduler_ring: true
scheduler_ring:
kvstore:
store: memberlist
还有一条便捷规则:如果整个配置中既没有 frontend 地址、也没有 scheduler 地址、也没有 downstream URL,Loki 会自动为你启用调度器 ring——对应标志的注册说明中"若配置中任何位置都不存在 frontend_address 或 scheduler_address,Loki 会将该值置为 true"(见 pkg/scheduler/scheduler.go)。
参与 ring 的每个组件都会在 /scheduler/ring 端点暴露 ring 状态,可用它确认所有调度器都已按预期注册。该端点的注册逻辑位于 pkg/loki/modules.go:initQuerySchedulerRing 在 UseSchedulerRing 为 false 时直接跳过 ring 初始化,否则将调度器 ring 的监听端口设为 gRPC 监听端口,并同时挂载到主 HTTP server 与 internal server 上。
源码层面对该 ring 有两个值得注意的硬性约束(pkg/scheduler/scheduler.go):token 数量与副本因子是固定值,不可修改(修改会直接报错),因为调度器 ring 的 token 与副本设计已由实现内部决定;同时,启用 ring 后调度器必须以 ServerMode 初始化 ring manager(L146-L152),否则启动即失败。
使用内存 ballast 优化垃圾回收
在 CPU 受限的环境中,频繁的垃圾回收(GC)会持续抢占应用运行所需的 CPU 资源,成为显著的性能损耗点。内存 ballast 是一种缓解手段:预先分配一块额外但不被使用的虚拟内存,人为抬高"活跃堆空间"的体量。Go 的 GC 由堆空间增长触发,被抬高的堆基数使得堆增长的相对比例变小,从而降低 GC 触发频率。
配置项为 ballast_bytes,对应 CLI 标志 -config.ballast-bytes:
# 顶层配置
ballast_bytes: 1073741824 # 1 GiB 示例
-config.ballast-bytes=1073741824
该配置在源码中的定义与说明非常完整(pkg/loki/loki.go 与 L170-L175):
- 数值表示保留为 ballast 的虚拟内存字节数,默认
0(即不启用); - ballast 越大,GC 次数越少,以"更大的堆"换取更低的 CPU 开销;
- 因为 ballast 内存从不被读取,所以不会消耗物理内存;
- 但它会被统计为活跃内存,会扭曲内存相关指标,监控告警时需要把这一点纳入考量。
实践中建议根据实例的堆内存上限按比例预留(常见做法是预留总内存的 20%~50% 量级),并通过观察 GC 频率与 CPU 使用率逐步调整。
远程规则评估(Remote rule evaluation)
为什么需要外部化规则评估
默认情况下,ruler 组件内置了一个查询引擎来执行告警规则与预计算规则(recording rules)。当规则复杂或需要周期性处理大量数据时,内嵌引擎会成为瓶颈——它的查询执行是单线程的,规则不会被切分(sharding)或像普通 Loki 查询那样被加速,性能问题通常表现为:
- recording rules 产出的指标出现空洞(gaps);
- 告警漏发;
loki_prometheus_rule_group_iterations_missed_total指标出现非零值(应针对该指标配置告警以便及时发现)。
该功能最初由设计提案 LID-0002 提出,其中包含了指导实现的设计决策。
方案:独立 query-frontend/querier 池执行规则
query-frontend 组件存在的意义正是切分、调度并加速查询。将规则评估从 ruler 中外部化——ruler 只作为 gRPC 客户端,把 LogQL 查询发给独立的 query-frontend,由一组专用 querier 执行——可以显著提升规则评估性能、减少漏执行次数。
官方还给出了一条明确的部署建议:为规则评估单独部署一套 query-frontend 与 querier 池,与处理临时查询(adhoc queries,来自 Grafana、logcli 或 API)的现有池隔离。理由是规则用于产出指标与告警,直接关系到服务的可靠运行,应当获得优先保障;若与临时查询共用同一池,规则会与临时查询同等排队竞争,性能表现不可预测。
启用远程规则评估的配置:
ruler:
evaluation:
mode: remote
query_frontend:
address: dns:///<query-frontend-service>:<grpc-port>
要点:
mode: remote使 ruler 变成 query-frontend 的 gRPC 客户端,大部分查询工作量被外部化,ruler 自身资源占用会大幅下降;- 地址使用
dns:///前缀时,请求会在所有 query-frontend IP 间自动负载均衡(客户端侧 DNS 服务发现),这是官方推荐写法; - 如果连接 query-frontend 需要 TLS,在
query_frontend下设置tls_enabled: true并配置配套的 TLS 选项; - 执行失败的查询不会被重试。
配置项的源码定义位于 pkg/ruler/evaluator_remote.go:QueryFrontendConfig 由 address(对应 CLI 标志 -ruler.evaluation.query-frontend.address,注释明确要求"必须是 DNS 地址(dns:/// 前缀)以启用客户端侧负载均衡")与内联的 gRPC client 配置组成;DialQueryFrontend 负责创建并初始化 httpgrpc.HTTPClient(L166-L167)。在 pkg/loki/modules.go 中,ruler 初始化会校验 evaluation 配置合法性、按 mode 分支初始化,并在远程模式下 dial query-frontend,非法 mode 会直接报错。
用 max_jitter 缓解规则并发冲突
当大量规则在同一时刻触发评估时,会产生显著的资源争抢。max_jitter 用于在每次规则评估前加入一段有界的随机延迟,把规则执行在时间上摊开。它同时适用于本地与远程两种评估模式:
ruler:
evaluation:
max_jitter: 5s
源码中该选项对应 -ruler.evaluation.max-jitter,默认 0(禁用),且注释特别说明 jitter 对同一条规则是确定性的(按规则稳定计算,不会让同一条规则每次延迟都不同),见 pkg/ruler/evaluator.go。
限制与可观测性
远程规则评估可通过以下两个限额进行调优(两者都既可在全局 limits_config 中设置,也可通过 runtime 配置文件按租户覆盖):
ruler_remote_evaluation_timeout:单次规则评估的最大允许执行时长。默认继承querier.query-timeout的值(见 pkg/validation/limits.go);ruler_remote_evaluation_max_response_size:query-frontend 通过 gRPC 返回给 ruler 的响应体最大字节数,0表示不限(默认)。
需要留意的是,max_jitter 是全局唯一设置(位于 ruler.evaluation 下),不能作为按租户的 limit 配置。
远程规则评估暴露了以下指标(详见 docs/sources/operations/scalability.md):
| 指标 | 类型 | 含义 |
|---|---|---|
loki_ruler_remote_eval_request_duration_seconds |
histogram | 规则评估耗时 |
loki_ruler_remote_eval_response_bytes |
histogram | 规则评估响应的字节数 |
loki_ruler_remote_eval_response_samples |
histogram | 规则评估响应中的样本数 |
loki_ruler_remote_eval_success_total |
counter | 成功的规则评估次数 |
loki_ruler_remote_eval_failure_total |
counter | 失败的规则评估次数(带失败原因) |
这些指标均按租户维度暴露(per-tenant),因此在多租户集群中需要特别关注指标基数,避免租户数量增长导致指标规模失控。
小结
Loki 的规模化路径可以归纳为三条主线:按角色拆分进程以获得独立伸缩能力;把查询调度队列独立为 Query Scheduler 并用 hash ring 做服务发现,从而支撑多个 query-frontend;将规则评估外部化到专用 query-frontend/querier 池,配合 max_jitter 与限额配置保障规则执行的可靠性与优先级。在此之上,内存 ballast 提供了一种低成本的 GC 优化手段。三者结合源码中的实现细节(队列参数、ring 约束、jitter 语义、限额默认值),即可为持续增长的日志量设计出可预测、可观测的生产部署形态。