菜单
文档面包屑箭头 Grafana Loki面包屑箭头 社区面包屑箭头 Loki 改进文档 (LID)面包屑箭头 0003:租户内用户查询公平性
开源

0003:租户内用户查询公平性

作者: Christian Haudum ( christian.haudum@grafana.com)

日期 02/2023

赞助者: @chaudum @owen-d

类型:功能

状态:已接受

相关问题/PR

来自邮件列表的讨论


背景

查询调度器(简称 scheduler)是 Loki 的一个组件,它将来自查询前端(简称 frontend)的请求(子查询)分发给 querier worker,以确保租户之间的执行公平性。

通过为每个租户维护独立的 FIFO 队列,并将适当数量的 querier worker 分配给这些队列,调度器确保单个租户不会影响其他所有租户的查询能力。

组件图

scheduler-component-diagram.plantuml

时序图

scheduler-sequence-diagram.plantuml

问题陈述

尽管 Loki 默认构建为多租户系统,但在某些用例中,Loki 安装可能只有一个非常大的单个租户,例如 Grafana Cloud 中为客户提供的专用 Loki 单元。

然而,可能有很多不同的用户使用同一个租户来查询日志,例如从 Grafana 或通过 CLI 或 HTTP API 访问 Loki 的用户。这可能导致不同用户查询之间的争用,因为他们都共享同一个租户。

虽然调度器队列的当前实现允许租户之间的 QoS 保证,但它不考虑单个租户内个体用户之间的 QoS 保证。

也就是说,Loki 没有个体用户的概念。

目标

以下提案的主要目标是阐述如何改进调度器组件,使其不仅能保证租户之间的 QoS,还能保证租户内参与者(用户)之间的 QoS,且无需更改 frontend、scheduler 和 queriers 的部署模型。这也应包括对队列结构的更改,使其易于扩展以进行未来的调度改进。

非目标 (可选)

虽然更改和扩展调度器也需要用户可见的 API 更改,但公共 API 不属于本文讨论的范围。

提案

提案 0:不做任何改动

更改调度机制的替代方案是通过多个租户和多租户查询来处理 QoS 控制。

优点

  • 保持调度器现有简洁性
  • 无需开发时间

缺点

  • 虽然那种按租户进行的划分对一些潜在客户来说可能可行,但对其他人来说可能难以实现。

提案 1:为调度器添加固定第二层

当前的调度器实现方式是为每个租户维护一个独立的 FIFO 队列。当请求(子查询)入队时,调度器将其放入该租户的现有队列中。如果队列尚不存在,则先创建它,然后将连接的 querier worker 重新分配给可用的租户队列。每个 querier worker 从分配的队列中循环进行轮询。

现在,请求不再直接入队到租户队列并从中拉取,而是入队到每个用户的队列中,然后租户队列从分配给该租户队列的用户队列中循环进行轮询。

组件图

scheduler-proposal-1-component-diagram.plantuml

与当前实现类似,调度器根据 X-Scope-OrgID 请求头(或请求上下文中的等效键)对请求进行入队,但同时也会考虑第二个键(例如 X-Scope-UserID)。这构成了一个固定的两层层次结构,其中租户与用户之间的关系是一对多关系。然而,这样做有一个缺点,即用户的概念(Loki 中尚不存在)会渗透到调度器领域。

优点

  • 相对容易实现

缺点

  • 不可扩展
  • 泄露领域知识

提案 2:完全分层调度器

此提案与提案 1类似,但不同之处在于没有固定的层级,层级可以任意嵌套。

组件图

scheduler-proposal-2-component-diagram.plantuml

控制哪些 querier worker 连接到哪些根队列(也称为租户队列)的 RequestQueue 的实现可以保持原样。然而,租户和用户的概念被放弃,取而代之的是分层参与者的概念,这可以表示为标识符切片。请注意,这不会放弃 Loki 中贯穿始终的租户概念(由 X-Scope-OrgID 请求头和/或请求上下文表示)。

标识符示例

Go
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"}

更普遍地来说

Go
actorN := []string{"L0 Queue", "L1 Queue", "L2 Queue", ... "Ln Queue"}

L0 队列(根队列)需要能够处理 worker 连接,因此与其叶队列相比需要附加功能。

以下代码片段旨在展示队列简化的递归结构。

Go
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"}
  • 无需更改调度器实现即可扩展队列层次结构
  • 实现不依赖其领域以外的知识

缺点

  • 比固定层级实现更复杂
  • 每个队列都会带来内存开销

提案 3:每个租户多个子队列

另一种将用户概念排除在 Loki 之外,同时仍提供一定查询公平性保证的选项是,简单地将请求分片到租户队列内的多个子队列中。分片大小可以是一个按租户设置,以适应不同的租户大小。

这类似于提案 1,都在于添加另一个固定层级的子队列。但不同之处在于,在这种情况下,单个查询请求会被分配一个随机标识符并进行哈希。当查询被拆分时,子请求保持相同的哈希标识符。哈希的模数定义了请求将被入队到租户的哪个子队列。

优点

  • 与用户无关的按请求 QoS 控制

缺点

  • 个体用户的请求仍然可能影响其他用户
  • 不可扩展

替代方案

按请求进行分片仍可通过提案 2 实现,方法是在层次结构中添加请求哈希作为附加层。

Go
actor := []string{"tenant", "user", "request_hash"}

共识

提案 2 将被实施。