Skip to main content
Version: v1.9 🚧

Koord-Queue Observability

Introduction​

Koord-Queue exposes four complementary observability surfaces: Prometheus metrics, a Grafana dashboard, an aggregated visibility API for querying the contents of a queue, and an HTTP debugging API that reports the internal state of the quota cache. In addition, the QueueUnit status, the Queue status and Kubernetes events carry the information that is needed to explain an individual scheduling decision.

SurfaceProvided byEnabled by
Prometheus metrics on TCP port 10259koord-queueAlways on
Grafana dashboardRepository artefacts pkg/dashboard/dashboard.json and pkg/dashboard/exporter.yamlImported manually
Visibility API, group visibility.koord-queue.x-k8s.iokoord-queueHelm value controller.enableVisibilityServer=true
Debugging HTTP API on TCP port 19876koord-queueFlag --enableApiHandler=true
QueueUnit status, Queue status and eventskoord-queue and koord-queue-controllersAlways on

Prometheus Metrics​

The koord-queue process starts an HTTP server on port 10259 that serves /metrics. The server is started unconditionally and before leader election, so every replica answers on that port. The metrics that describe queueing activity, however, are produced only by the replica that holds the lease, because the controller and the scheduler run only there; the other replicas serve the Go collector and the client-go metrics alone. The Helm chart declares neither a containerPort nor a Service for this port, therefore scraping has to target the pods directly, for example through a PodMonitor, or the port has to be forwarded for an ad-hoc inspection:

$ kubectl -n koord-queue port-forward deployment/koord-queue 10259:10259
$ curl -s http://127.0.0.1:10259/metrics | grep -E '^queueunits_|^job_'

Exported Metrics​

The metrics that the current implementation actually populates are the following.

MetricTypeLabelsDescription
dequeued_quota_usage_by_quotaGaugequota, resourceAmount of resources held by dequeued jobs, aggregated per quota and resource name.
dequeued_quota_usage_by_namespaceGaugenamespace, resourceThe same aggregation, per namespace.
job_scheduling_algorithm_latencyHistogramnoneLatency of the filter phase of a scheduling cycle, in milliseconds. Buckets grow exponentially from 0.01 by a factor of two, fifteen times.
job_schedule_attemptsCounterqueue, resultNumber of scheduling attempts per queue, labelled with the outcome.
queueunits_by_job_typeGaugenamespace, typeNumber of QueueUnits per namespace and job type. Raised when a QueueUnit is added and lowered when it is deleted.
rest_client_request_duration_secondsHistogramverb, hostLatency of the requests that the component sends to the API server.
rest_client_requests_totalCountercode, method, hostNumber of requests sent to the API server, by response code.

The process also exports the standard Go collector metrics, among them process_cpu_seconds_total and process_resident_memory_bytes.

Three further families are declared in the code but are never populated by the current implementation, so they do not appear in the output at all: queueunits_in_active_queue, queueunits_in_backoff_queue and queueunits_coming_rate_by_job_type.

Every metric that carries labels is exported only once its label combination has been observed at least once. A freshly started controller that has not seen a queue yet therefore serves only job_scheduling_algorithm_latency, the client-go metrics and the Go collector metrics, while dequeued_quota_usage_by_quota, job_schedule_attempts and queueunits_by_job_type appear as soon as queues exist, jobs are admitted and QueueUnits are created. An empty result is consequently not by itself an indication of a broken installation.

Note that the metric job_scheduling_e2e_latency is referenced by the Grafana dashboard described below but is not exported by the current implementation, so the corresponding panel remains empty.

Grafana Dashboard​

The repository ships a ready-made dashboard together with the scrape configuration that it expects.

ArtefactPurpose
pkg/dashboard/dashboard.jsonDashboard AckKoordQueue, uid VhwsfAqVz, with panels for quota usage, dequeued resources per quota and namespace, job counts, per-user job counts and arrival rate, per-queue job counts, scheduling throughput, scheduling latency, component resource usage and API server request latency.
pkg/dashboard/exporter.yamlPrometheus scrape configuration with job_name: koord-queue-exporter, metrics_path: /metrics and target 127.0.0.1:10259.

Every panel filters by the label job="koord-queue-exporter". The scrape job must therefore carry that name, otherwise the dashboard shows no data. In a Kubernetes deployment the target is a pod rather than 127.0.0.1, so the scrape configuration has to be adapted, for example to a PodMonitor whose scrape job is named koord-queue-exporter. The dashboard was authored for an internal Grafana instance; the data source has to be selected again after the import, and the panel titles are in Chinese.

Four panels stay empty with the current implementation because the metrics they query are not exported: the end-to-end scheduling latency panel, which queries job_scheduling_e2e_latency, and the panels that query queueunits_in_active_queue, queueunits_in_backoff_queue and queueunits_coming_rate_by_job_type.

Visibility API​

The visibility API is an aggregated API group that reports what a queue or a quota currently holds. It is served by the koord-queue process on port 8082 and is registered through the APIService v1alpha1.visibility.koord-queue.x-k8s.io. Enabling it creates a Service named koord-queue-visibility-server in the koord-queue namespace, which selects pods that carry the labels control-plane: koord-queue and koord-queue-leader: "true". The leader label is applied by the process that wins the leader election, so the service always routes to the replica that holds the scheduling state.

$ helm upgrade koord-queue koordinator-sh/koord-queue --version 1.8.0 \
--namespace koord-queue --reuse \
--set controller.enableVisibilityServer=true

$ kubectl get apiservice v1alpha1.visibility.koord-queue.x-k8s.io
NAME SERVICE AVAILABLE AGE
v1alpha1.visibility.koord-queue.x-k8s.io koord-queue/koord-queue-visibility-server True 1m

Endpoints​

RequestDescription
/apis/visibility.koord-queue.x-k8s.io/v1alpha1/queues/{queue}/queueunitsThe QueueUnits that the named queue currently holds in memory, which are in practice the units that are still enqueued. Returns NotFound when the queue does not exist.
/apis/visibility.koord-queue.x-k8s.io/v1alpha1/elasticquotas/{quota}/queueunitsThe QueueUnits that are mapped to the named quota. Supports the query parameter phase, for example ?phase=Enqueued.

Each item reports the namespace and name of the QueueUnit, the quota and queue it belongs to, its request and its total resource requirement, its phase, and the number of running and pending pods.

$ kubectl get --raw "/apis/visibility.koord-queue.x-k8s.io/v1alpha1/queues/team-a/queueunits" | jq .
{
"kind": "QueueUnitsSummary",
"apiVersion": "visibility.koord-queue.x-k8s.io/v1alpha1",
"items": [
{
"metadata": { "name": "my-job-blocked", "namespace": "default" },
"quotaName": "team-a",
"queueName": "team-a",
"request": { "cpu": "4", "memory": "8Gi" },
"resources": { "cpu": "4", "memory": "8Gi" },
"phase": "Enqueued",
"podState": { "pending": 0, "running": 0 }
}
]
}

$ kubectl get --raw "/apis/visibility.koord-queue.x-k8s.io/v1alpha1/elasticquotas/team-a/queueunits?phase=Enqueued" | jq '.items | length'

The API accepts the query parameters queue, phase, offset and limit. Only phase is evaluated, and only by the per-quota endpoint; offset and limit are accepted but currently have no effect. The collection resources queues and elasticquotas are registered for discovery purposes and do not implement a list operation.

Leader Election​

The queue controller, the debugging API and the visibility server all start only after the process has acquired the leader election lease, which is a Lease named example-lease in the default namespace. Two consequences are worth knowing when diagnosing an installation:

  • A pod that is Running and ready is not necessarily serving anything yet. Acquisition can take up to the fifteen second lease duration after a restart, and until then the visibility Service has no ready endpoint and the APIService reports Available=False with the reason MissingEndpoints.
  • The lease name is generic. Any other component in the cluster that uses a lease with the same name in the default namespace, for example another installation of the same queuing system, prevents this installation from becoming the leader. Check the holder identity of default/example-lease when the component appears to be running but does nothing.
$ kubectl -n default get lease example-lease -o jsonpath='{.spec.holderIdentity}{"\n"}'
$ kubectl -n koord-queue logs deployment/koord-queue -c controller | grep -m1 "became leader"

Debugging HTTP API​

The flag --enableApiHandler=true starts an additional HTTP server on port 19876. It is intended for interactive diagnosis and is not exposed through a Service.

$ kubectl -n koord-queue port-forward deployment/koord-queue 19876:19876
EndpointContent
GET /apis/v1/elasticquotaPer-quota view of the in-memory cache of the ElasticQuotaV2 plugin, keyed by quota name: Count, Max, Min, Used, SelfUsed, ChildrenUsed, GuaranteedUsed, SelfGuaranteedUsed, ChildrenGuaranteedUsed, and Items, the list of QueueUnits held in the reserve cache with their resource, priority, creation timestamp and whether they are in the reserve cache. Resource quantities are reported in milli-units, so five cores appear as 5000. An empty object is returned while no quota is known.
GET /apis/v1/queueReserved for per-queue debugging information. The response is a map with one key per queue, and every value is null, because the queue implementations of the current release provide no data. With the queues team-a and team-b the response is {"team-a":null,"team-b":null}.
GET /apis/v1/userquotaReserved for per-user quota debugging information, with the same shape and the same limitation.

The per-quota endpoint is the most useful one when the accounting of a quota has to be reconciled with the ElasticQuota objects in the cluster: it reports what the plugin believes to be reserved, which is the value that the filter phase compares against min and max.

Inspecting an Individual Job​

The QueueUnit status is the primary source of information about a single job.

FieldDescription
status.phasePosition in the lifecycle. Enqueued means waiting, Reserved means quota is held while admission checks run, Dequeued means the job has been released.
status.messageHuman-readable reason for the current phase, including the message produced by the filter phase and whether a preemption attempt succeeded.
status.attemptsNumber of scheduling attempts. A steadily growing counter with an unchanged phase indicates that the job does not fit into the available quota.
status.admissionsPer pod set: the number of admitted replicas, the number of running replicas, the reserved resources and, when set, the reclaimState written by preemption.
status.podStateNumber of running and pending pods of the job.
status.lastUpdateTime and status.lastAllocateTimeTimestamps used by the backoff logic and by the reclaim protect time.
# Full status of a single job
$ kubectl get queueunit my-job-blocked -n default -o yaml

# Compact view of the fields that matter most during triage
$ kubectl get queueunit -n default -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ATTEMPTS:.status.attempts,PRIORITY:.spec.priority,MESSAGE:.status.message'

# The quota, and therefore the queue, of a unit is taken from its labels; spec.queue is empty for units
# that a job extension created
$ kubectl get queueunit -n default \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.quota\.scheduling\.koordinator\.sh/name}{"\t"}{.status.phase}{"\n"}{end}'

The Queue object reports the ordering that the queue publishes to users in status.queueItemDetails, a map from queue kind to the ordered list of items with their namespace, name, priority and position. Only the units that are waiting in the queue are listed; a unit that has been dequeued disappears from the list, so an empty value on an idle queue is expected. The list is refreshed by a periodic task per queue, every fifteen seconds by default. Two annotations control it: koord-queue/queue-items-refresh-interval sets the refresh interval, for example 30s, and koord-queue/disable-show-queue-items stops the periodic task. Stopping the task does not clear the field, so the value that was published last remains visible until publication resumes.

$ kubectl -n koord-queue get queue team-a -o jsonpath='{.status.queueItemDetails}' | jq .

Events​

Koord-Queue records events on the QueueUnit and on the Queue.

ReasonTypeObjectMeaning
ScheduledNormalQueueUnitThe unit has been dequeued successfully.
FailedSchedulingNormal or WarningQueueUnitThe unit could not be admitted. The message carries the reason reported by the filter phase and the outcome of a preemption attempt.
PreemptedWarningQueueUnitThe unit has been selected as a victim by queue-level preemption and is waiting for the job extension to reclaim its resources.
ReclaimedNormalQueueUnitThe resources of a victim have been reclaimed and the unit is schedulable again.
QueueNotFoundWarningQueueUnitA queue name was derived from the labels of the unit, but no Queue with that name exists. Recorded at most once per unit, and the bookkeeping is cleared as soon as the unit is mapped to a queue. A unit for which no quota label exists at all produces no event and simply stays in the pending list.
AddQueueFailWarningQueueA queue could not be added to the scheduling loop, for example because its policy is not supported.
DeactivatedNormalJobThe job was suspended and its resources reclaimed because the QueueUnit was deactivated.
MaximumExecutionTimeExceededWarningJobThe job exceeded spec.maximumExecutionTimeSeconds.
$ kubectl get events -n default --field-selector involvedObject.name=my-job-blocked \
-o custom-columns='LAST:.lastTimestamp,TYPE:.type,REASON:.reason,MESSAGE:.message'

Logs​

The chart starts the koord-queue container with --v=4. The verbosity that is useful for diagnosis is lower and produces considerably less output:

LevelContent
--v=1Queue construction with its policy, the value of wait-for-pods-running and max-depth; the plugin configuration that was loaded; preemption rounds with the list of victims.
--v=2Preemption decisions, including victims that were skipped because they are inside the reclaim protect time; insertion into and removal from the assumed set with the reason; clearing of the internal bookkeeping after a reclaim.
--v=4Full request and response payloads of the visibility API.
$ kubectl -n koord-queue logs deployment/koord-queue -c controller --tail=200 -f
$ kubectl -n koord-queue logs deployment/koord-queue-controllers -c manager --tail=200 -f

Reference​

ConcernLocation in koord-queue
Metric definitionspkg/metrics/metrics.go
Metrics HTTP servercmd/main.go
Dashboard and scrape configurationpkg/dashboard/dashboard.json, pkg/dashboard/exporter.yaml
Visibility API types and storagepkg/visibility/apis/v1alpha1/types.go, pkg/visibility/apis/restapi/storage.go
Visibility server bootstrappkg/visibility/server.go
Debugging HTTP APIcmd/app/server/apihandler.go, cmd/app/server/apimethod.go, pkg/framework/plugins/elasticquotav1alpha1/api_handler.go
Event emissionpkg/scheduler/scheduler.go, pkg/controller/eventhandler.go, pkg/queue/queuepolicies/schedulingqueuev2/preempt.go

What's Next​