0003:租户内用户查询公平性
作者: Christian Haudum ( christian.haudum@grafana.com)
日期 02/2023
赞助者: @chaudum @owen-d
类型:功能
状态:已接受
相关问题/PR
来自邮件列表的讨论
背景
查询调度器(简称 scheduler)是 Loki 的一个组件,它将来自查询前端(简称 frontend)的请求(子查询)分发给 querier worker,以确保租户之间的执行公平性。
通过为每个租户维护独立的 FIFO 队列,并将适当数量的 querier worker 分配给这些队列,调度器确保单个租户不会影响其他所有租户的查询能力。
组件图
时序图
问题陈述
尽管 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 从分配的队列中循环进行轮询。
现在,请求不再直接入队到租户队列并从中拉取,而是入队到每个用户的队列中,然后租户队列从分配给该租户队列的用户队列中循环进行轮询。
组件图
与当前实现类似,调度器根据 X-Scope-OrgID
请求头(或请求上下文中的等效键)对请求进行入队,但同时也会考虑第二个键(例如 X-Scope-UserID
)。这构成了一个固定的两层层次结构,其中租户与用户之间的关系是一对多关系。然而,这样做有一个缺点,即用户的概念(Loki 中尚不存在)会渗透到调度器领域。
优点
- 相对容易实现
缺点
- 不可扩展
- 泄露领域知识
提案 2:完全分层调度器
此提案与提案 1类似,但不同之处在于没有固定的层级,层级可以任意嵌套。
组件图
控制哪些 querier worker 连接到哪些根队列(也称为租户队列)的 RequestQueue
的实现可以保持原样。然而,租户和用户的概念被放弃,取而代之的是分层参与者的概念,这可以表示为标识符切片。请注意,这不会放弃 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"}
更普遍地来说
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"}
- 无需更改调度器实现即可扩展队列层次结构
- 实现不依赖其领域以外的知识
缺点
- 比固定层级实现更复杂
- 每个队列都会带来内存开销
提案 3:每个租户多个子队列
另一种将用户概念排除在 Loki 之外,同时仍提供一定查询公平性保证的选项是,简单地将请求分片到租户队列内的多个子队列中。分片大小可以是一个按租户设置,以适应不同的租户大小。
这类似于提案 1,都在于添加另一个固定层级的子队列。但不同之处在于,在这种情况下,单个查询请求会被分配一个随机标识符并进行哈希。当查询被拆分时,子请求保持相同的哈希标识符。哈希的模数定义了请求将被入队到租户的哪个子队列。
优点
- 与用户无关的按请求 QoS 控制
缺点
- 个体用户的请求仍然可能影响其他用户
- 不可扩展
替代方案
按请求进行分片仍可通过提案 2 实现,方法是在层次结构中添加请求哈希作为附加层。
actor := []string{"tenant", "user", "request_hash"}
共识
提案 2 将被实施。