Repository navigation
Read Kubernetes metadata in the background, off the threads analyzing ALS, eBPF access logs and MAL - #14135
Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
4 open findings
This is still an O(#services) scan for each resolve (including during startup whenhasSynced()is… · New The API/docs here say callers should pass “JSON paths”, but the trimming logic (ResourceTrimmer)… · New The Javadoc states that if a handler is registered after the store started, it is “first called on… · Newaddress.contains("/")will throw aNullPointerExceptionif theaddresseslist contains a null… · New
What changed in this PR
This PR shifts Kubernetes/Istio metadata lookups used by ALS/MAL/LAL from synchronous API-server calls on request threads to watch-maintained local stores, reducing tail latency and preventing thread/memory blowups when the API server is slow/unreachable.
Changes:
- Introduces
ResourceInformer+ trimming/indexing infrastructure inlibrary-kubernetes-supportand migrates Pods/Services/Endpoints/Nodes/ServiceEntries to informer-backed stores. - Updates Envoy ALS K8s/Istio registries (and MAL
K8sInfoRegistry) to resolve purely from local stores and avoid caching misses before the first sync. - Adds extensive unit tests for informer behavior, trimming correctness, and rule-field parsing; updates docs and CHANGES.
| File | Description |
|---|---|
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/k8s/ServiceNameFormatterFieldPathsTest.java | Adds tests for extracting rule-read field paths from naming rules. |
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/k8s/K8SServiceRegistryTest.java | Verifies K8s registry starts watches with needed fields and miss-caching behavior pre/post sync. |
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/istio/IstioServiceEntryRegistryTest.java | Tests ServiceEntry matching (static/DNS) and hostname resolver creation behavior. |
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/k8s/ServiceNameFormatter.java | Adds fieldPathsOf() extraction to drive store field retention from naming rules. |
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/k8s/K8SServiceRegistry.java | Switches service resolution to local stores; starts watches at boot; avoids caching before sync. |
| oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/istio/IstioServiceEntryRegistry.java | Switches to local ServiceEntry store; moves DNS resolver creation off lookup threads; avoids caching before sync. |
| oap-server/server-library/library-kubernetes-support/src/test/java/org/apache/skywalking/library/kubernetes/ResourceTrimmerTest.java | Tests trimmed copies retain the values naming rules read. |
| oap-server/server-library/library-kubernetes-support/src/test/java/org/apache/skywalking/library/kubernetes/ResourceInformerTest.java | End-to-end informer/store tests (sync, retries, watch errors, ordering, trimming). |
| oap-server/server-library/library-kubernetes-support/src/test/java/org/apache/skywalking/library/kubernetes/LookupFieldsTest.java | Validates index values and trimmed-field retention across stores. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/TrimmingItemStore.java | Adds item store wrapper to persist trimmed copies while preserving informer index behavior. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/SharedKubernetesClient.java | Tracks/halts informers at shutdown before closing the shared client. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/ResourceTrimmer.java | Implements mapping from rule bean paths to serialized JSON paths and selective field retention. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/ResourceInformer.java | Adds generic informer-backed local store with retrying start and non-blocking reads. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/KubernetesServices.java | Migrates Services to informer-backed store with trimming and watch start. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/KubernetesPods.java | Migrates Pods to informer-backed store with IP indexing and pod selection logic. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/KubernetesNodes.java | Introduces informer-backed Node address store for node-IP detection. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/KubernetesEndpoints.java | Migrates Endpoints to informer-backed store with ready-address IP indexing. |
| oap-server/server-library/library-kubernetes-support/src/main/java/org/apache/skywalking/library/kubernetes/IstioServiceEntries.java | Migrates ServiceEntries to informer-backed store and exposes change event handlers. |
| oap-server/server-library/library-kubernetes-support/pom.xml | Adds Awaitility/beanutils/log bridge test dependencies for new tests. |
| oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/k8s/K8sInfoRegistry.java | Avoids caching misses before sync and routes lookups to local stores. |
| docs/en/setup/envoy/als_setting.md | Documents required RBAC and startup “unknown until synced” behavior. |
| docs/en/concepts-and-designs/mal.md | Documents RBAC and local-store behavior for MAL K8s metadata usage. |
| docs/en/changes/changes.md | Adds release note describing the metadata lookup/stores change and implications. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
2b54561 to
6dc25d2
Compare
6dc25d2 to
3462bb8
Compare
3462bb8 to
d35a164
Compare
c39a431 to
fc68169
Compare
ef6bf9a to
e4d8253
Compare
e4d8253 to
735aaae
Compare
Updated lookup rulesThis revision changes how the Kubernetes metadata lookups retry, cache and wait. The PR description is updated to match. What changed
Lookup rules (defaults; keys in
Settings
Two reader threads serve all of it, one per lane. Each runs one read at a time, is created on demand and ends after 60 s idle (a virtual thread on JDK 25+). Refreshes are triggered by use, not by a timer. Verified against a real API server (kind, with the API server's audit log as the record of each request OAP sent):
|
… the data The lookups of pods, services, endpoints, nodes and Istio ServiceEntries behind Envoy ALS k8s-mesh, the eBPF access log and MAL made synchronous Kubernetes API calls on the gRPC handler threads, on every cache miss, cache expiry and failed call. A slow or unreachable API server held one handler thread, with the batch it was analyzing, per access log stream, until OAP ran out of memory. The same API calls are now made in the background, one at a time in each of two lanes (the first reads MAL waits for, and the rest), and only for what the lookups use. Envoy ALS and the eBPF access log never wait: an IP seen for the first time is unknown while it is read, and is found by the later lookups; an eBPF connection resolves its unknown addresses again on its later messages. A MAL retagByK8sMeta queues the reads of its pods first and waits for them up to core.default.malMetadataReadTimeout (2 seconds) in all; its lookups do not wait. Data is read again when used core.default.k8sMetadataRefreshPeriod (3 minutes) after it was written, and dropped core.default.k8sMetadataCacheTime (als=21,mal=21 minutes) after its last write. Finding nothing is read again when used core.default.k8sMetadataNotFoundRetryPeriod (1 minute) later, and the endpoints or services read are read again for a pod not in them once that old, as the pod may be new. A failed read, such as for a 403 or the API server not answering, is retried when used the refresh period later and keeps the data read before. The reads waiting at once are of distinct keys and can be limited per lane by core.default.k8sMetadataReadQueueSize (als=<size>,mal=<size>). The reads are sent without the client's retries, so a failing read holds its lane for one attempt, and the HTTP and TCP access log analyzers share their lookups. Failed and refused reads are logged as periodic summaries instead of a line per failure, and a resource the API server does not serve is not logged. Istio ServiceEntry hostname resolutions retry with a growing backoff instead of immediately, and ServiceEntries are read through the shared Kubernetes client.
735aaae to
70f22a4
Compare

Fix Kubernetes metadata lookups calling the API server on the threads analyzing the data
Why. Every IP → pod / service / Istio ServiceEntry lookup went through Guava
LoadingCaches whose loaders made synchronous Kubernetes REST calls on the caller's thread: a cluster-widepods?fieldSelector=status.podIPlist per IP, pod and service GETs, and the whole-cluster endpoints / services / nodes / ServiceEntry lists. The callers are the gRPC handler threads of Envoy ALS (k8s-mesh) and of the eBPF access log, and MAL (retagByK8sMeta, process naming) throughK8sInfoRegistry. Each cache miss, eachexpireAfterWriteexpiry, and each failed call (failures were not cached) repeated the call on that thread. When the API server is slow or unreachable, every access log stream touching an uncached IP holds a handler thread, with the batch it was analyzing, for up to the client timeout and fabric8's retries; gRPC's default executor is unbounded before JDK 25, so threads and retained batches pile up until OAP runs out of memory.Fix. The API calls, their scopes and the matching logic stay as they were; only where and when they run changes.
LookupCache(inlibrary-kubernetes-support) replaces the per-IP / per-pod caches ofK8SServiceRegistry,IstioServiceEntryRegistryandK8sInfoRegistry. A key not cached is read once in the background; a key has one pending read at most, whether it is read for the first time or again. The reads are queued to two lanes, each running one read at a time on one background thread (a platform thread that ends once idle before JDK 25, a virtual thread on JDK 25+), so a backlog never turns into a burst of concurrent API requests:K8sLookup-waitingfor the first reads MAL waits for, andK8sLookupfor every other read, including all reads again. A lookup shares any read already pending for its key, including one in the other lane.retagByK8sMetafirst queues the reads of the pods of its samples not read yet, then waits for them up tocore.default.malMetadataReadTimeoutin all, however many pods it has; a cached pod is not waited for, even while it is read again. The waiting of a pod counts from when a step first waited for it within twice the timeout, so theretagByK8sMetaof one Export over the same pods wait that long at most in all, whatever happens to their reads meanwhile (a full lane admitting them later, an entry expiring); its lookups after that never wait. Process naming reads its pod and service the same way. The existingK8sInfoRegistrylookup methods keep their names and signatures.Client.newClient(RequestConfig), sharing its connections and threads), so a failing read holds its lane for one attempt, up to the client's timeouts, instead of through fabric8's retries and backoff (up to 10 retries).K8SServiceRegistryand oneIstioServiceEntryRegistry, as they look up the same IPs with the same configuration.list pods,get services, ...) as one ERROR line a minute after the first failure and then every five minutes, with a recovery line, instead of a line per failure; a 404 of a resource the API server does not serve is not logged and ends an earlier failure of the same read. Reads not queued as a lane is full are summarized the same way at WARN.DefaultIstioClientper read; hostname resolutions use Armeria's default backoff instead ofwithMaxAttempts(3), which retried without delay once exhausted, and their failures are summarized at WARN instead of a line per attempt; the matching uses sequential streams instead of nestedparallelStreams on the common pool, and skips null and IPv6 CIDR addresses that threw.Lookup policy. Times are the defaults; keys are in
core.default.k8s-mesh(and Istio ServiceEntries), eBPF access logretagByK8sMeta, process namingK8sLookup; an eBPF connection resolves it again on its later messagesK8sLookup-waiting, then waited for up tomalMetadataReadTimeout(2000 ms) in all — the steps of one Export over the same keys together; a key not read by then gets an empty servicek8sMetadataRefreshPeriod(3 min) after it was written; a read again returning data, the same or not, writes it againk8sMetadataNotFoundRetryPeriod(1 min) after it was written; the endpoints are read again for a pod not in them once older than thatk8sMetadataRefreshPeriod(3 min) after it failedk8sMetadataRefreshPeriodlaterk8sMetadataCacheTimeals(21 min) after it was last writtenk8sMetadataCacheTimemal(21 min) after it was last writtenk8sMetadataReadQueueSize)Configuration. Each has a default in the code, for an
application.ymlwritten before it, and is written out inapplication.ymlwith the same value.malMetadataReadTimeoutSW_CORE_MAL_METADATA_READ_TIMEOUT2000(ms)retagByK8sMetaover the samples of a metric, waits in all; kept under the 5-second export timeout of the OpenTelemetry Collector's OTLP exporter, which sends the data again when it times outk8sMetadataCacheTimeSW_CORE_K8S_METADATA_CACHE_TIMEals=21,mal=21(min)k8sMetadataRefreshPeriodSW_CORE_K8S_METADATA_REFRESH_PERIOD3(min)k8sMetadataNotFoundRetryPeriodSW_CORE_K8S_METADATA_NOT_FOUND_RETRY_PERIOD1(min)k8sMetadataReadQueueSizeSW_CORE_K8S_METADATA_READ_QUEUE_SIZEals=<size>,mal=<size>A value not in its form stops OAP at boot with a message showing the form, a bare number (such as
1000) included.Docs.
als_setting.md(a policy section with the table above),mal.md,configuration-vocabulary.md; the storage e2econfig-dump.ymlexpects the new keys.Tests.
LookupCacheTest: a lookup never waits and the read serves the next lookups; one pending read per key, shared across lanes and across expiry; data used after the refresh period is read again once while lookups keep getting it, and a read again of the same data keeps it alive; an entry expires the cache time after its last write; a read again finding nothing or failing keeps the data and is retried a refresh period later; finding nothing is read again after the not-found retry period, a failed read only after the refresh period, and a read again of an entry without data writes what it finds or that it failed; a full lane refuses a key that a later lookup queues, and a timed lookup whose read is refused returns at once; a waiting lookup does not queue behind the reads nobody waits for; a timed lookup waits at most the timeout since its read was queued; each lane has its own limit; the expiry is longer than the refresh period.CachedReadTest(read again once older than the caller accepts; a 404 kept as empty; a failed read keeps what was read before; a failed first read kept so callers do not read again),KubernetesApiReadsTest(404 is nothing and not reported; 403 and other failures throw and are summarized; and, against a local HTTP server, a 503 sent once by a client configured with 10 retries),K8sMetadataReadSettingsTest(theals=…,mal=…forms, defaults of a lane left out, the cache time longer than the refresh period, a bare number typed by the config loader rejected with the form, nothing published when a setting is invalid),SerialReaderTest,FailureSummaryLoggerTest.K8SServiceRegistryTest(the endpoints read again for a pod not in them; a failed read is a failure, not finding nothing) andIstioServiceEntryRegistryTestcover the readers, the registry shared by the analyzers of one configuration, and that a lookup never waits for a read.K8sTagTest,MALExpressionExecutionTest,LALExpressionExecutionTest,K8SALSServiceMeshHTTPAnalysisTest,FieldFormatterTest,KubernetesCoordinatorTest) pass unchanged.Verified locally against a real API server (kind, with the API server's audit log recording each request OAP sent):
Envoy ALS never waits: thousands of entries over 5 streams answered in 0.2–0.6 s with the API server healthy, unresponsive (
docker pause) or refusing (403), on JDK 11 and 25; HTTP and TCP,k8s-meshalone andmx-mesh,k8s-mesh; pod and STATIC / CIDR / DNS ServiceEntry names resolved.One read per key, one request at a time per lane, nothing read again before it is due; a 403 for ServiceEntries is listed once, not once per IP.
A new pod: first lookup 1 s after it started finds it but not in the endpoints read 24 s earlier; the use 66 s later reads the endpoints again and resolves it (before: up to 3–6 minutes).
403: nothing read again at +5 s or at +75 s; read again at +186 s. Failures are one ERROR summary.
MAL with the API server unresponsive: one Export of 50 pods not read yet, through four
retagByK8sMeta, took 2.2 s (before: 5 s per pod); the same pods again 0.2 s; healthy, 41 pods not read yet 0.3 s.With a 1-minute refresh and 2-minute cache time: pods in use were read again at +66 s and +129 s and stayed cached (0.19 s with the API server paused); pods unused for 2.5 minutes had expired (2.2 s, read again).
2000 IPs not read yet over 50 streams with the API server paused, JDK 11: gRPC executor threads followed the 50 streams and did not grow on a second wave; no thread but the two readers in Kubernetes code; back to 2 after recovery.
Every invalid setting, including a bare number for
k8sMetadataReadQueueSizeork8sMetadataCacheTime, stops OAP at boot with a message showing the form.If this pull request closes/resolves/fixes an existing issue, replace the issue number. Closes #.
Update the
CHANGESlog.