ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Loki 查询调度器中的查询公平性(Query Fairness):LID 0003 设计与实现解析

Loki 查询调度器中的查询公平性(Query Fairness):LID 0003 设计与实现解析 Loki 查询调度器中的查询公平性Query FairnessLID 0003 设计与实现解析【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki导读本文基于 Grafana Loki 仓库中的设计文档 LID 0003Query fairness across users within tenantsLoki Improvement Document剖析查询调度器query scheduler如何从租户间 QoS 隔离演进到租户内用户间 QoS 隔离的设计决策。文章会完整还原文档提出的 4 个备选方案从什么都不做到全层级队列调度并结合当前仓库中pkg/scheduler与pkg/queue的实际实现说明最终落地的层级队列TreeQueue机制、Loki-Actor-Path请求头、max-queue-hierarchy-levels配置项如何协同工作。读完本文你将掌握 Loki 查询调度架构的核心链路理解单租户大集群下多用户公平调度的设计权衡并能直接对照源码定位每一层实现。背景Loki 的查询调度器如何保证租户间公平Loki 采用查询前端query frontend 查询调度器scheduler 查询器querier的拆分部署模型前端接收用户查询将其拆分为若干子查询sub-query调度器负责把子查询分发给 querier 的 worker 执行。调度器是保证多租户 QoS服务质量的关键组件它为每个租户维护独立的 FIFO 队列互不干扰通过为队列分配合理数量的 querier worker避免单个租户耗尽全部查询能力而拖垮其他租户每个 querier worker 采用 **round-robin轮询**方式从被分配的租户队列中拉取请求。该组件在仓库中的实现位于 pkg/scheduler/scheduler.go其中Scheduler结构体持有核心的requestQueue *queue.RequestQueue见 pkg/scheduler/scheduler.go#L54-L90并通过两个 gRPC 双向流接口完成对接FrontendLoop处理与前端的长连接接收ENQUEUE入队、CANCEL取消消息见 pkg/scheduler/scheduler.go#L240-L309QuerierLoop处理与 querier 的连接通过requestQueue.Dequeue(...)循环拉取请求分发给 worker见 pkg/scheduler/scheduler.go#L424-L493。下图源自本仓库docs/sources/community/lids/下的 PlantUML 源文件展示了当前调度器的组件结构前端通过 RoundTripper 接入 Scheduler 的 FrontendLoop请求落入各自 Tenant QueueQuerierLoop 与 Worker 建立连接后按队列消费。对应的时序图则展示了查询从前端发出、经调度器入队、再由 querier 取走的完整消息交互问题陈述租户内的用户竞争虽然 Loki 默认是多租户系统但现实中存在单一超大租户的部署形态例如 Grafana Cloud 中为每个客户提供的专用 Loki 单元dedicated Loki cell。在这种场景下可能有大量不同用户通过 Grafana、CLI 或 HTTP API 访问共享同一个租户查询日志导致用户之间互相争抢查询资源。现有调度器只在租户之间提供 QoS 保障无法在单个租户内的不同用户之间进行隔离。而 Loki 本身并没有用户user这一概念——请求只按X-Scope-OrgID头或请求上下文中的等价键区分租户。这正是 LID 0003 要解决的核心矛盾。设计目标与边界文档明确了本次设计的目标在不改变前端、调度器、querier 部署模型的前提下让调度器不仅能保障租户间 QoS也能保障租户内不同执行者actor之间的 QoS队列结构要易于扩展为未来的调度改进留出空间。同时文档明确将公开 API 变更排除在讨论范围之外——虽然调度器的改动需要伴随面向用户的 API 变化但那是另一个话题。备选方案从什么都不做到全层级队列文档系统评估了 4 个候选方案最终选定 Proposal 2 实施。下面逐一还原。Proposal 0什么都不做Do nothing不修改调度机制转而依靠多租户 跨租户查询来间接实现 QoS 控制。优点调度器保持简单零开发成本缺点租户拆分方案对部分客户可行但对另一些客户如既有单租户架构并不具备可行性。Proposal 1为调度器增加固定二级结构当前实现中请求直接进入对应租户的队列querier 从租户队列轮询消费。Proposal 1 的做法是在租户队列下再增加一层用户队列请求先进入用户队列租户队列再以轮询方式从分配给它的用户队列中取请求。入队时调度器依然根据X-Scope-OrgID头确定租户同时引入第二个键如X-Scope-UserID确定用户从而构成一个固定的两级层级租户对用户是一对多关系。优点实现相对简单缺点不可扩展且用户这一概念被泄漏进调度器领域Loki 本身没有用户概念。Proposal 2全层级调度器最终被采纳与 Proposal 1 类似但没有固定层级层级可以任意嵌套。RequestQueue控制 querier worker 与根队列即租户队列的连接关系这部分实现保持不变但租户用户的概念被替换为层级化的 actor执行者一个 actor 就是一个标识符切片slice of identifiers。注意这并不意味着在整个 Loki 中取消租户概念X-Scope-OrgID头依旧存在只是调度器内部不再依赖固定的两级语义。actorA : []string{tenant_a, user_1} actorB : []string{tenant_b, user_2} actorC : []string{tenant_b, user_3, service_foo} actorD : []string{tenant_b, user_3, service_bar}更一般地一个 actor 可表示为actorN : []string{L0 Queue, L1 Queue, L2 Queue, ... Ln Queue}其中 **L0 队列根队列**因为需要承载 worker 连接比其叶子队列具备额外功能。文档给出的简化递归队列结构如下type Request interface{} type Queue interface { Deqeue(actor []string) Request Enqueue(r Request, actor []string) error } // RequestQueue implements Queue type RequestQueue struct { queriers map[string]*querier rootQueues map[string]*RootQueue } // RootQueue implements Queue type RootQueue struct { queriers map[string]*querier leafs map[string]*LeafQueue ch chan Request } // LeafQueue implements Queue type LeafQueue struct { leafs map[string]*LeafQueue ch chan Request }优点向后兼容租户可表示为[]string{tenantID}行为与现状一致队列层级可在不修改调度器实现的前提下扩展实现不需要调度器领域之外的任何知识用户概念不泄漏进调度器缺点实现比固定层级复杂每个队列都有内存开销。Proposal 3每租户多个子队列分片另一种思路不让用户概念进入 Loki而是在租户队列内将请求分片到多个子队列。分片数量可以是 per-tenant 配置以适配不同规模的租户。该方案与 Proposal 1 类似都增加一层固定子队列区别在于单个查询请求被赋予一个随机标识符并做哈希查询拆分后各子请求保持同一哈希标识符用哈希对分片数取模决定进入租户的哪个子队列。优点与用户无关的 per-request QoS 控制缺点单个用户的请求仍可能影响其他用户不可扩展。备选扩展按请求分片也可以借由 Proposal 2 实现——只需把请求哈希作为层级中的一个附加层actor : []string{tenant, user, request_hash}共识最终采用 Proposal 2文档结论明确Proposal 2全层级调度器将被实施。接下来看它在当前仓库中的落地形态。实现落地TreeQueue 与 QueuePath在 pkg/queue/treequeue.go 中可以看到 Proposal 2 的工程实现TreeQueue是一个层级队列实现每个子队列与本地队列拥有同等的被选中机会公平轮询。type QueuePath []string // TreeQueue is an hierarchical queue implementation where each sub-queue // has the same guarantees to be chosen from. // Each queue has also a local queue, which gets chosen with equal preference as the sub-queues. type TreeQueue struct { // local queue ch RequestChannel // index of where this item is located in the mapping pos QueueIndex // index of the sub-queues current QueueIndex // mapping for sub-queues mapping *Mapping[*TreeQueue] // name of the queue name string // maximum queue size of the local queue size int }关键方法add依据路径递归创建/查找子队列Dequeue则实现本地队列 所有子队列的公平轮询选择先尝试本地队列再按Mapping轮询各子队列核心代码见 pkg/queue/treequeue.go#L42-L111。文档中actor 是标识符切片的设计在代码里对应QueuePath []string类型Mapping内部维护子队列索引StartIndexWithLocalQueue保证了本地队列与子队列被同等对待。层级路径如何从请求流入队列Loki-Actor-PathProposal 2 的actor 路径在实际请求链路中通过 HTTP 头传递。仓库中定义了lokihttpreq.LokiActorPathHeader承载 actor 路径的请求头lokihttpreq.LokiActorPathDelimiter路径元素分隔符。请求从前端流入调度器时frontend_scheduler_worker会把从请求头解析出的 actor 路径放进 gRPC 消息的QueuePath字段见 pkg/lokifrontend/frontend/v2/frontend_scheduler_worker.go#L290。调度器入队时读取该路径并做深度校验见 pkg/scheduler/scheduler.go#L383-L398var queuePath []string if s.cfg.MaxQueueHierarchyLevels 0 { queuePath msg.QueuePath if len(queuePath) s.cfg.MaxQueueHierarchyLevels { // 返回错误期望的队列层级超过 max-queue-hierarchy-levels 允许的最大深度 } } return s.requestQueue.Enqueue(req.tenantID, queuePath, req, ...)即在入队前调度器会检查路径深度是否超过配置上限超过则直接拒绝该请求。而在查询执行侧pkg/engine/engine_query.go与pkg/querier/queryrange/codec.go等模块会在 HTTP 请求、查询请求元数据之间传递该 actor 路径头保证子查询延续父查询的调度身份见 pkg/querier/queryrange/codec.go#L715-L716。关键配置项与运行时参数调度器在 pkg/scheduler/scheduler.go#L107-L132 中注册了以下核心配置配置项YAML命令行参数默认值说明max_outstanding_requests_per_tenant-query-scheduler.max-outstanding-requests-per-tenant32000每个租户在每个调度器上的最大未完成请求数超限请求以 HTTP 429 返回max_queue_hierarchy_levels-query-scheduler.max-queue-hierarchy-levels3层级队列的最大嵌套深度0 表示禁用层级队列querier_forget_delay-query-scheduler.querier-forget-delay0querier 未通知优雅关闭即断连时调度器将其保留在租户分片中的时长shuffle-sharding 下用于缩小爆炸半径use_scheduler_ring-query-scheduler.use-scheduler-ringfalse是否让调度器加入 hash ring 做选主与副本管理其中与本文主题最相关的是max_queue_hierarchy_levels它直接决定 actor 路径可以嵌套多深。例如在典型的三级语义[tenant, user, request_hash]下需要将其设置为3。当设置为0时层级队列被禁用调度器退化为经典的按租户 FIFO 队列行为兼容旧部署。调度器 ring 的num-tokens固定为 1、replication-factor 固定为 2见 pkg/scheduler/scheduler.go#L44-L49不可修改。测试与验证仓库为上述机制提供了充分的测试保障pkg/queue/treequeue_test.go验证不同深度的路径如[l0,l1,l3]、[c,c0,c00]在树中的递归创建、公平轮询与空队列清理行为pkg/queue/dequeue_qos_test.go验证层级队列在不同负载下的 QoS 表现pkg/scheduler/scheduler_test.go覆盖调度器整体入队/出队/取消流程。设计取舍总结维度Proposal 0Proposal 1Proposal 2采纳Proposal 3公平粒度租户租户用户固定两级任意层级 actor租户哈希分片子队列是否引入用户概念否是泄漏进调度器否actor 即路径切片否可扩展性—差强无需改调度器即可加深层级差实现复杂度零低中每队列有内存开销中LID 0003 的核心洞见是与其在调度器里硬编码租户/用户两级语义不如把调度身份抽象为任意深度的路径切片——这样既保住了租户隔离的向后兼容性租户即单元素路径又为未来按服务、按请求哈希等更细粒度调度预留了无限扩展空间。如果你正在运营一个单租户但多用户的大型 Loki 集群理解这条设计脉络将直接帮助你正确配置max-queue-hierarchy-levels并预判调度行为。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表