Why architecture matters here

Why not just let the OS or a YARN-style scheduler sort it out? Because MPP query memory is reserved, distributed, and all-or-nothing. A hash join’s build side must fit (or spill) on every host that runs a fragment; a query that gets halfway through its memory acquisition and stalls holds locks and buffers hostage across the whole cluster. Operating-system level arbitration sees per-process pressure on one host, not the cluster-wide shape of a query; by the time a kernel OOM killer acts, it kills an impalad — every query on that host dies, and every query with a fragment on that host dies with them. Admission control exists because the only safe place to arbitrate MPP memory is before execution begins, with a cluster-wide view of the query’s footprint.

The second driver is workload isolation. A BI dashboard firing hundreds of small queries, an ETL job running six-way joins over a year of data, and a data scientist’s exploratory monster have incompatible needs; unpartitioned, the monster starves the dashboards and one bad estimate takes down all three. Pools turn this into policy: separate memory budgets, separate concurrency caps, separate queues — the dashboard pool admits many small queries with tight per-query ceilings, the ETL pool admits few with generous ones, and a runaway in one pool cannot consume the other’s budget.

The third is honest failure. Without admission control, overload manifests as random mid-flight OOM failures minutes into execution — wasted work, confusing errors, retry storms. With it, overload manifests as a queue and then a clean rejection with a stated reason (‘pool mem limit exceeded: request 48 GB, available 30 GB’) at submission time, before any resources burn. Fail fast, fail cheap, fail with a reason — the entire value proposition in one gate.

Advertisement

The architecture: every piece explained

Top row: the decision path. A query arrives at a coordinator, which parses, plans, and — crucially — computes a per-host memory estimate from statistics: hash-table sizes from cardinality estimates, scan buffers, sort and aggregation footprints, summed per host for the hosts the schedule touches. The effective admission footprint is this estimate unless overridden by MEM_LIMIT (a query option or pool default), which replaces estimation with declaration. The admission controller inside the coordinator then checks the target pool’s constraints: max concurrent running queries, aggregate pool memory across the cluster, per-host memory availability (the min of pool headroom and the host’s mem_limit headroom), and queue capacity.

Middle row: how coordinators know the cluster’s state. Each coordinator publishes its admitted queries’ reservations to a statestore topic; the statestore gossips the merged view back on its update cycle (sub-second, typically). Decisions are therefore local against a slightly stale global view — two coordinators can simultaneously admit into the last slot, briefly over-committing the pool. The design accepts this race deliberately: admission is a fast local operation with no consensus round-trip, and small transient over-admission is absorbed by memory headroom. Deployments that cannot accept it (many coordinators, tight pools) enable the admissiond — a dedicated daemon making all decisions centrally, serializing admissions at the cost of one RPC hop.

Bottom row: enforcement. Admission reserves the memory; execution enforces it — each fragment’s memory tracker enforces the per-host limit, spilling operators (hash join, sort, aggregation) to disk as they approach it, and killing the query if a non-spillable allocation would exceed it. Pools map to users and groups via configuration (fair-scheduler.xml/llama-site.xml lineage, or CDW UI); the queue is per-pool FIFO with max-queued depth and queue-timeout-ms bounding how long a query may wait before a clean rejection.

Later releases added a second admission axis: slots. Memory alone under-models CPU-bound contention, so each executor advertises admission slots (defaulting to core count) and each query consumes slots proportional to its effective parallelism (MT_DOP); a pool can exhaust slots with free memory or vice versa, and admission requires both. Alongside it, executor groups partition the cluster horizontally: a group is a set of executors sized to a workload (a small always-on group for BI, large elastic groups for batch), each group admits independently, and a query runs entirely within one group — the mechanism behind autoscaling Impala in cloud warehouses, where groups spin up when queues form and drain away when idle. Pools decide whether a query may run; groups decide where.

Impala admission control — deciding what runs before it can hurtper-pool memory and concurrency gates, enforced before a query touches an executorClient / JDBCquery submitted to a coordinatorCoordinator impaladplans query, estimates per-host memoryAdmission controllerlocal decision vs pool limitsResource poolsmem limit, max running, max queued, queue timeoutStatestore topiccluster-wide admission state gossipQueueFIFO per pool, spills to timeoutExecutorsadmitted query reserves mem_limit per hostAdmission Daemon (admissiond)optional centralized decisions, no gossip racesOps — pool sizing from workload classes, MEM_LIMIT vs estimates, queued-query metrics, rejection reasons in profilesSQLadmit?pool configpublish usageno room: enqueuesyncadmitted: runcentralized path
Impala admission control: coordinators check each planned query against per-pool memory and concurrency limits — gossiped via the statestore or centralized in admissiond — admitting, queueing, or rejecting before execution starts.
Advertisement

End-to-end flow

Follow three queries hitting a cluster at 9:00:00. The pool bi allows 20 running queries, 400 GB aggregate, no more than 8 GB per query per host. Query A — a dashboard aggregate estimated at 2 GB/host — arrives at coordinator 1: pool has 14 running, 210 GB committed; A fits every check, is admitted in microseconds, its reservation is published to the statestore, and fragments start on executors with a 2 GB/host tracker. Query B — a fat join estimated at 12 GB/host — arrives at coordinator 2 a moment later: the per-query-per-host cap is 8 GB, so B is rejected immediately, profile stating the exact reason; the analyst adds a MEM_LIMIT and better predicates, or moves to the ETL pool.

Query C — 6 GB/host, arriving as the pool hits its 20-query concurrency cap — is queued: position 1, timeout 60 s. The client blocks in the fetch call; nothing executes. At 9:00:07 a running query completes, its reservation is released and gossiped; coordinator 3 (which owns C’s queue slot) re-evaluates on the statestore update, admits C, and execution begins — total added latency, seven seconds of honest waiting instead of an OOM at minute four. Had 60 seconds elapsed first, C would return ‘admission timeout’ with queue statistics in the profile.

Now the race: coordinators 1 and 4 each admit a 30 GB query into a pool with 45 GB headroom, within the same statestore cycle — both saw 45 GB free. The pool briefly runs 15 GB over. Execution-side enforcement absorbs it: memory trackers hold each query to its limit, spillable operators spill a little earlier than they otherwise would, and the next gossip cycle corrects the accounting. The system trades momentary softness at the boundary for never paying a consensus round-trip on the hot path — with admissiond available when that trade reverses.