背景
Impala 集群用来控制查询并发量的功能称为 Admission Control(准入控制),本文主要翻译admission-controller.h 中的注释,以解释 Impala 准入控制(并发控制)的运行机制。
基本名词解释:
- Statestore 是 Impala 的两个 Master 节点之一(另一个是 Catalog Server,与本文无关),负责信息的收集和广播。不同主题的信息以(topic)作为区分。
- Coordinator 和 Executor 是 Impala Daemon (impalad,即 Impala的 worker 节点) 的两种角色。Coordinator负责接收和调度查询请求,Executor 负责执行查询计算。一个 Impala 集群可以有多个 Coordinator 和多个 Executor。一个 impalad 也可以同时是 Coordinator 和 Executor。
- AdmissionController是Coordinator中实现准入控制的模块
概要
AdmissionController 基于可用的集群资源来限制请求(即查询或DML)。集群资源由一个或多个资源池(resource pool)来配置。请求可能被允许立即执行、排队等待稍后执行或拒绝(排队后也可能拒绝)。
资源池可以配置最大并发查询数、能占用的集群总内存量、队列大小、机器级别的最大最小内存限制,以及是否依据这个限制来调整 mem_limit 查询选项等。如果已经有过多的查询在跑,或者没有充足的可用内存,查询请求就会开始排队。如果队列长度达到配置的上限,后续到达的查询请求会被拒绝。队列中的排队时长也有超时配置,超时后将被拒绝。
运行模式
准入控制有两种运行模式:
- 分布式模式:每个Coordinator独立执行准入控制,基于 statestore 分发的其他Coordinator的决策信息(即其它Coordinator准入了什么,满足最终一致)。 这是传统的 Impala 设置。
- 准入服务模式:Coordinator配置了 --admission_service_host 以将它们的准入请求发送到 admissiond,admissiond根据集群负载的完整视图执行所有准入控制。这种模式目前还是实验性的。
在分布式模式下,多个 Coordinator 充当 AdmissionController(准入控制器),因此需要在 impalad 之间共享集群状态才能做准入决策。每个Coordinator都会维护各个resource pool和各台机器的资源信息。 除了一些BE特定的统计信息(即同为executor时的执行信息)之外,其它一些本地准入统计信息通过名为 impala-request-queue 的 statestore topic在集群中传播。 具体地说,Coordinator通过Statestore更新发送准入统计信息,反映已知负载。所有参与的executor发送statestore更新,反映它们正在执行的最新负载(比如已预留的内存大小)。
当resource pool的统计信息发生变化时,对应的 <impalad, pool> 会在 statestore 心跳间隔作为topic update发送。来自其他 impalad 的topic update会用于更新(重新计算)每个resource pool的聚合统计信息。 由于resource pool的统计信息仅在 statestore 心跳上更新(默认100ms),并且所有决策都是使用缓存的状态做出的,所以聚合后的resource pool统计信息实际上只是估计值。 因此,准入或排队的请求可能多于配置的阈值,这些阈值实际上只是软限制。
内存限制
每个resource pool可配置所有请求可“预留”的最大内存。虽然 Impala 在准入时还没有真正“预留”内存(即 Impala 还不能保证查询的内存能被预留,有可能发生过度准入,以至于多个查询认为它们已经预留了需要的内存),AdmissionController使用多个 metrics 来估计可用内存,并仅在它认为可用内存够用时才准入。 未来的工作将实现真正的预留,但这工作量较大,并且将涉及AdmissionController之外的更改。
查询选项 MEM_LIMIT(显式设置或通过默认值设置)指定查询准入所需的内存,这是每台机器的内存阈值。如果没设内存限制,则使用查询计划中的每台机器的估计值作为内存限制,并使用查询中各个operator的最大初始预留内存作为其下限。 使用的最终mem limit也受到resource pool配置的最大/最小内存值的限制,并且可以选择不在 MEM_LIMIT 查询选项上强制执行这些限制(如果未配置这两个最大/最小限制,则planning估计出的内存值将不会作为MEM_LIMIT使用,而仅用于做出准入决定。此外,planning估计的内存值将不再以operator的最大初始预留内存作下限)。
注:英语水平有限,上面这段翻译出来比较拗口,原文如下:
The memory required for admission for a request is specified as the query option MEM_LIMIT (either explicitly or via a default value). This is a per-host value. If there is no memory limit, the per-host estimate from planning is used instead as a memory limit and a lower bound is enforced on it based on the largest initial reservation of the query. The final memory limit used is also clamped by the max/min memory limits configured for the pool with an option to not enforce these limits on the MEM_LIMIT query option (If both these max/min limits are not configured, then the estimates from planning are not used as a memory limit and are only used for making admission decisions. Moreover the estimates will no longer have a lower bound based on the largest initial reservation).
准入条件
所有请求必须满足以下四个条件才能被准入:
- 当前资源池的配置有效
- 这个资源池中必须有足够的内存资源可供请求使用。为资源池配置的最大内存资源指定了该池中所有正在执行的查询可预留的集群范围内的总内存。因此,为该请求在所有executor中预留的总内存,加上已经接受的请求必须小于或等于指定的最大内存资源。
- 所有参与的executor必须有足够的可用内存。每个 impalad 都有进程级别的内存限制,这是可以在该executor预留的最大内存。
3b.(可选)当使用executor group(见下文)并考虑非默认executor group时,当前运行的查询数必须低于所有参与的executor配置的最大值。 - 最终使用的机器级别的内存限制可以容纳最大的初始预留内存空间。
资源信息追踪
为了根据以上条件进行准入控制,AdmissionController会考虑每台机器和每个资源池的以下内容:
- Mem Reserved:所有executor汇报的已预留的内存量,来自 statestore topic update。发送的值来自 UpdateMemTrackerStats() 中的资源池MemTracker,它反映了已开始执行的Fragment预留的内存。对于正在执行且具有mem_limit的查询,该值被视为其预留的内存,因为它可能会消耗到该限制。没有mem_limit的查询则使用当前消耗的内存值(参见 MemTracker::GetPoolMemReserved())。每个pool和每台机器的聚合信息在 UpdateClusterAggregates() 中计算。一旦所有更新都被完全分发和聚合,这个状态就会提供足够的信息来让任何 impalad 做出准入决定。然而,这需要等待所有准入的请求启动所有远程fragment,并且等待更新后的状态通过 statestore 分发回来。
- Mem Admitted:此 impalad 的AdmissionController已准许的请求所需的内存量(即准入中使用的值,mem_limit或估计值)。当请求被准入和释放时,pool和host级别的counter都会相应更新(注意:不是通过 statestore,所以没有延迟,但这不考虑来自其他 impalads 准入的请求的内存)。
- Num Admitted:已被准入并因此被视为当前正在运行的查询数。请注意,目前没有与预留内存汇报等效的方法,即executor不报告当前正在执行的查询的实际数量 (IMPALA-8762未解决该问题)。这可以防止将多个Coordinator与executor group一起使用。
如前所述,“预留”和“准入”内存追踪机制都有不同的优点和缺点。 “预留”内存追踪机制在稳定状态下运行良好,即当有足够的时间来分发更新的时候。 当只有一个Coordinator时,“准入”内存追踪机制可以完美地工作(也许有少量Coordinator时也可以合理地工作)。 预留和准入的内存的最大值用于做出准入决定,这在Coordinator较少时效果很好,或者如果请求分布在不同 impalad 上,并且提交率足够低以让新状态能够由 statestore 及时更新时效果很好。
查询释放
当查询完成时,必须使用“ReleaseQuery”和“ReleaseQueryBackends”方法从AdmissionController明确释放它们。 这些方法释放已准入的内存并减少资源池的已准入查询数。
在传统的分布式准入控制模式中,要求查询的所有后端必须通过’ReleaseQueryBackends’释放,然后使用’ReleaseQuery’释放查询。 这是可以保证的,因为Coordinator和AdmissionController在同一个进程中运行。
在admission control service模式中,在coordinators和admissiond之间rpc失败的情况下,允许更多的灵活性来保持容错。 在这种情况下,通过两个不变量确保正确的资源追踪:
- 正在使用的资源的聚合值始终与“running_queries_”的内容相匹配
- 任何查询最终都会从“running_queries_”中删除并释放其所有资源 ,不管发生任何失败。
有几个容错案例需要考虑:
- ReleaseQuery rpc 失败:Coordinator通过心跳 rpc 定期发送已注册查询 id 的列表,允许AdmissionController清理不在该列表中的任何查询。
- Coordinator失败:admission control service使用 statestore 来检测Coordinator何时从集群成员中删除,并释放在该coordinator上运行的所有查询。
- RelaseQueryBackends rpc 失败:当 ReleaseQuery 最终被调用时(如上面所保证的),它将自动释放任何剩余的backend。
释放backend会释放该backend使用的准入内存并减少对应机器上运行的查询数。释放查询不会释放任何准入的内存,它只会减少资源池中正在运行的查询的数量。
Executor Group
集群中的executor可以组成executor group。每个executor只能在一个group中。一个资源池可以有多个与之关联的executor group。每个executor group都属于一个资源池,并且只会为来自该资源池的请求提供服务。对应关系为一个资源池对应多个executor group,一个executor group对应多个executor。
在启动期间未指定executor group名称的executor会自动添加到名为 DEFAULT_EXECUTOR_GROUP_NAME 的默认组中。默认的executor group不限制每台机器的查询并发数,因此可以由多个coordinator做准入控制。
executor group通过其名称隐式映射到资源池。资源池中的查询可以在 以pool名称开头(并以“-”分隔)的所有executor group上运行。例如,名称为“q1”的pool中的查询可以在以“q1-”开头的所有executor group上运行。如果找不到与资源池匹配的executor group,并且默认的executor group不为空,则使用默认组。
除了前面描述的检查之外,executor group的并发受限于可以在executor上并发运行的最大查询数 (-admission_control_slots)。执行额外的检查以确保组中的每个executor都有一个可用的slot来运行查询。AdmissionController会在statestore更新中带上已准入的每个executor的查询数。
为了找到可以运行查询的executor group,AdmissionController在初始准入尝试期间或在 DequeueLoop() 中调用 FindGroupToAdmitOrReject()。如果集群成员发生变化,它会(重新)计算所有executor group的调度,然后用这些调度来接受查询。准入尝试总是以相同的顺序进行,以便executor group被依次填满。特别的是,我们不会尝试平衡executor group之间的查询。
举例:没有executor group的情况
考虑一个 10 节点的集群,每个节点有 100gb,资源池“q1”配置有 500gb 的总内存和 40gb 作为最大单机内存限制。
MEM_LIMIT 设置为 50gb 并计划在所有后端运行的查询请求由 SubmitForAdmission() 在一个安静的集群上接收。根据pool配置,MEM_LIMIT被调整为 40gb ,用于此查询以及在准入之前需要通过的任何后续检查。
FindGroupToAdmitOrReject() 计算默认executor group的调度,并在调用CanAdmitRequest() 之前执行拒绝测试,这会检查正在运行的查询数量,然后调用 HasAvailableMemResources() 来检查内存资源。
HasAvailableMemResources() 首先使用 PoolStats::EffectiveMemReserved() 检查请求是否有足够的内存(这是pool的 agg_mem_reserved_ 和 local_mem_admitted_ 的最大值,参见上面的#1),然后通过 hosts_stats_ 中的 mem_reserved 和 mem_admitted最大值检查每个executor上是否有足够的内存 (参见上面的#2),最后检查用于此查询的内存限制是否可以容纳其最大的初始预留内存。
在这种情况下,有充足的资源可用,因此 CanAdmitRequest() 返回 true。调用 PoolStats::Admit() 来更新 q1 的 PoolStats:它首先更新 agg_num_running_ 和 local_mem_admitted_,它们可立即用于传入的准入请求,然后它更新发送到 statestore 的结构 (local_stats_) 中的 num_admitted_running。
UpdateHostStats() 会更新所有相关executor的已准入内存(存储在 host_stats_ 中)。然后 SubmitForAdmission() 返回到 ClientRequestState。
如果另一个相同的查询请求被发到这个Coordinator,它将被拒绝,因为 q1 的 local_mem_admitted_ 已经是 400gb。如果该请求同时发送给另一个 Coordinator,它会被接受,因为前一个Coordinator尚未传播所有更新。下一个 statestore 更新将包含此后端上 q1 的 num_admitted_running 的最新值。
当远程Fragment开始在远程 impalad 上执行时,它们的pool MemTracker将反映更新后的预留内存量(在 local_stats_.backend_mem_reserved 中由 UpdateMemTrackerStats() 设置),来自这些 impalad 的下一个statestore更新将包含更新后的值。当接收到 statestore 更新时(在statestore subscriber回调函数 UpdatePoolStats() 中),传入的各个后端、各个pool的 mem_reserved 值被聚合到 PoolStats::agg_mem_reserved_(每个pool上所有主机的聚合)和 backend_mem_reserved_(每个主机上的所有pool的聚合)。
在这之后,新传入的准入请求都具有做出正确决定所需的最新状态。
举例:有executor group的情况
考虑一个具有单一Coordinator和 2 个executor group,“default-pool-group-1”和“default-pool-group-2”的集群(本例中每个组的executor数量无关紧要)。两个executor group都能为默认资源池提供服务。假设每个executor只有一个admission slot,即为所有executor设置 --admission_control_slots=1。
现有一个设置了 mt_dop=1 的查询请求通过 SubmitForAdmission() 提交,于是调用 FindGroupToAdmitOrReject() 。从那里我们调用 ComputeGroupScheduleStates() 尝试两个executor group的调度。然后我们执行拒绝测试,然后为每个调度调用 CanAdmitRequest()。
executor group按字典序顺序处理,因此我们首先尝试进入“default-pool-group-1”组。 CanAdmitRequest() 调用 HasAvailableSlots() 以检查组中的所有executor是否有充足的可用slot以容纳新查询。如果确实适合,就准入成功。
该查询被准入后,对应的executor group中的每个executor会根据查询实际的并行度更新“slots_in_use”。当第二个查询到达而第一个查询仍在运行时,我们执行相同的步骤。具体地说,我们仍是计算两个组的调度,并首先考虑进入 default-pool-group-1。但是,HasAvailableSlots() 中的检查现在失败了,接下来我们将考虑组 default-pool-group-2。对于该组,检查成功并准入查询,增加组 default-pool-group-2 中每个executor的 num_admitted 计数器。
查询排队
一旦pool中的资源被消耗殆尽,每个接收请求的Coordinator将开始排队。虽然每个队列内部是 FIFO(先进先出)调度,但AdmissionController之间的排队请求没有总排序,不能保证提交给不同Coordinator的请求的 FIFO 调度。
当资源变得可用时,节点之间没有同步协调用于确定哪些能出列和接受请求。相反,我们使用一种简单的启发式方法来尝试使一些请求出队,这些请求与在每个单独的AdmissionController等待的请求数与所有AdmissionController(即 impalad)中排队的请求总数成比例。这限制了因大量资源同时可用而导致的过度准入。
当同一Coordinator上的多个pool中有请求排队时,AdmissionController简单地遍历 pool_stats_ 中的池并尝试从每个池中出队。这对于 max_requests 限制没有问题,但对于基于内存的准入来说是不公平的,因为pool的迭代顺序实际上定义了pool间的优先级。跨队列的请求可能会在特定主机上竞争相同的资源,即上面基于内存的准入描述中的#2。请注意,pool 的 max_mem_resources (#1) 未满足。
查询取消
一个通过SubmitForAdmission()提交的形为<schedule, admit_outcome>的准入请求可以通过将“admit_outcome”设置为 AdmissionOutcome::CANCELLED 来主动取消。 这由 SubmitForAdmission() 和 DequeueLoop() 异步处理。
资源池配置机制
资源池配置文件的路径由启动参数 “fair_scheduler_allocation_path” 和 “llama_site_path”指定。配置的格式基于 yarn 和 llama 并添加了 Impala 特定的内容。 有文件监视服务(file monitoring service)会监视这些文件的更改。在处理新查询时,将这些更改传播到 Impala。 有关详细信息,请参阅 RequestPoolService 类。
总结
以上就是adminssion-controller.h里关于AdmissionController的类注释,里面提到了很多函数名和变量名,需要结合代码来细看,这里仅提供翻译,节省看英文的时间。