[ 
https://issues.apache.org/jira/browse/NIFI-16120?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18097086#comment-18097086
 ] 

Michal S commented on NIFI-16120:
---------------------------------

Thanks — details below.

Environment (measured)
- NiFi 2.5.0, 2-node cluster on EKS, 
\{{nifi.cluster.leader.election.implementation=KubernetesLeaderElectionManager}},
 \{{nifi.state.management.provider.cluster=kubernetes-provider}}.

Flow / processor configuration (measured)
- The flow has 1,648 processors across 241 process groups, of which 103 have 
Execution Node = "Primary Node Only" (\{{executionNode=PRIMARY}}).
- Schedules of those Primary-Node-Only processors are ordinary: the majority 
are CRON at 15-second and daily intervals (\{{0 0/15 * * * ?}}, \{{0/15 * * * * 
?}}, \{{0 0 4 * * ?}}) and TIMER_DRIVEN at 3–5 minutes, with a couple at 1 
minute / 1 second; the ones we inspected use a single concurrent task.
- One integration flow alone accounts for 45 of the Primary-Node-Only 
processors; while it was enabled the cluster had 300 running processors.

Observed behavior (measured)
- On the non-leader node: ~32,000 Lease GET/min (~640/s) against the 
\{{primary-node}} lease, \{{verb=get}}, all HTTP 200 (no failures/retries). 
Attributed to the non-leader node by source IP.
- Thread dumps show \{{Timer-Driven Process Thread}} stacks in 
\{{ConnectableTask.isRunOnCluster}} → \{{FlowController.isPrimary}} → 
\{{KubernetesLeaderElectionManager.isLeader}} → \{{getLeader}} → 
\{{StandardLeaderElectionCommandProvider.findLeader}} → \{{BaseOperation.get}} 
(a live Lease GET). In one snapshot, 8–9 of 100 timer-driven threads were 
simultaneously in that live GET.
- Stopping the flow dropped the rate ~99% (to ~190/min) within about a minute. 
A second cluster with the same configuration but an idle flow stayed at 
baseline (~500/min).
- Reproduced identically with fabric8 kubernetes-client 7.3.1, 7.7.0 and 7.8.0, 
and with both the JDK and OkHttp HTTP client implementations — so it is the 
call pattern, not the client.

> KubernetesLeaderElectionManager.isLeader() does an uncached Kubernetes API 
> GET on the scheduling hot path, causing excessive lease requests
> -------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: NIFI-16120
>                 URL: https://issues.apache.org/jira/browse/NIFI-16120
>             Project: Apache NiFi
>          Issue Type: Bug
>          Components: Core Framework
>    Affects Versions: 2.5.0, 2.10.0
>            Reporter: Michal S
>            Priority: Major
>
> h2. Summary
> With 
> {{nifi.cluster.leader.election.implementation=KubernetesLeaderElectionManager}},
>  NiFi issues a live, uncached GET on the leader-election Lease on *every* 
> call to {{isLeader()}}. Because {{isLeader()}} is on the processor scheduling 
> hot path, a cluster with many "Primary Node Only" processors generates 
> hundreds of Lease GET requests per second per node against the Kubernetes API 
> server.
> h2. Affects
> Confirmed on 2.5.0. The relevant code is unchanged on current {{main}}, so 
> all 2.x releases using Kubernetes leader election are affected. Not present 
> with ZooKeeper (CuratorLeaderElectionManager), which answers isLeader() from 
> local state.
> h2. Root cause
> {{org.apache.nifi.kubernetes.leader.election.KubernetesLeaderElectionManager.isLeader(String)}}
>  calls {{getLeader(roleName)}} -> 
> {{StandardLeaderElectionCommandProvider.findLeader()}} -> 
> {{kubernetesClient.leases()...get()}} (a live API GET) on every invocation. 
> The manager already keeps a local {{roleLeaders}} map, updated by the fabric8 
> {{LeaderElector}} {{onNewLeader}} callback ({{setRoleLeader}}), but 
> {{isLeader()}} ignores it.
> Call path on the hot loop:
> {{ConnectableTask.isRunOnCluster()}} -> {{FlowController.isPrimary()}} -> 
> {{KubernetesLeaderElectionManager.isLeader()}} -> getLeader() -> live Lease 
> GET.
> This runs for every scheduling invocation of a Primary-Node-Only processor 
> (on non-primary nodes it is invoked and skipped repeatedly), so lease GET 
> volume scales with (running Primary-Node-Only processors x scheduling 
> frequency x concurrent tasks).
> h2. Impact / evidence
> On a 2-node EKS cluster with a few dozen active Primary-Node-Only processors: 
> ~32,000 Lease GET/min per non-leader node (~640/s), all HTTP 200 -> ~46M 
> Kubernetes audit events/day. Thread dumps show many "Timer-Driven Process 
> Thread" stacks in isRunOnCluster -> isPrimary -> getLeader -> 
> BaseOperation.get. Stopping the flows dropped the rate ~99% instantly; an 
> identical but idle cluster stayed at baseline. Reproduced independently of 
> fabric8 client version (7.3.1 / 7.7.0 / 7.8.0) and HTTP client implementation 
> (JDK HttpClient and OkHttp) - it is not a client issue.
> h2. Proposed fix
> Make {{isLeader()}} read the locally-tracked leader from {{roleLeaders}} 
> (already maintained by the onNewLeader callback), with a one-time live 
> fallback only before the first observation. This mirrors 
> CuratorLeaderElectionManager and removes the API GET from the hot path.
> {code:java}
> } else {
>     String leaderId = roleLeaders.get(roleName);
>     if (leaderId == null) {
>         leaderId = getLeader(roleName).orElse(null); // one-time fallback 
> before first observation
>     }
>     leader = participantId.equals(leaderId);
>     ...
> }
> {code}
> Verified: steady-state Lease GET drops from ~32k/min to baseline (a few 
> hundred/min) even under a synthetic load of 21 Primary-Node-Only processors 
> at ~12k tasks/s. A short-TTL cache on getLeader() is an alternative. I can 
> supply a pull request.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to