Michal S created NIFI-16120:
-------------------------------

             Summary: 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.10.0, 2.5.0
            Reporter: Michal S


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