Load Balancing
Audience: operators running multiple EventMesh instances, and anyone wondering how traffic spreads across a cluster without peer forwarding. Part of the offset / load-balancing / frame design trio; see also Offset management.
Architectural position
Balancing happens at the session-assignment layer (the
recommendentry point), not at the pull/dispatch layer.
Each instance pulls — passively, on demand — only for the clients it serves. No peer-to-peer pull, no forwarding. First balance which client belongs to which instance (the session layer); each instance's pull volume then roughly follows — instead of "pull equally everywhere, then balance the dispatch".
The full-stickiness model
Old model (retired): New model (full stickiness):
PartitionOwnership (partition%n) × no partition ownership
ClusterCoordinator (cross-instance fwd) × no forwarding
HttpForwarder (HTTP forward to peer) × no forwarding
ClusterSubscriptionStore (cluster subs) × local subscriptions
enableCluster keeps only:
ClusterMembership (heartbeat + load metrics) feeds /session/recommend scoring
The forwarding classes (PartitionOwnership / ClusterCoordinator /
HttpForwarder) are retained but unused (reserve + tests).
Zero client burden — instances self-report load
Instances collect their own load metrics locally (LoadMeter); clients
never report anything.
| Metric | Source |
|---|---|
activeSessions | SessionRouter sinks / subscribeSinks size |
inflowBytes/s | UniIngressService publish byte counters (bucketed per clientId → per-client traffic profile) |
outflowBytes/s | SSE / poll egress byte counters |
cpuLoad | OperatingSystemMXBean |
These are written with the existing 5 s heartbeat into
/em/instances/<id> = <ts>|<addr>|<activeSessions>|<byteRate>|<cpuLoad>.
The balancing loop
① client first connects to any instance
│
② GET /session/recommend?clientId=xxx
│ → that instance reads the cluster-wide /em/instances/ (all instances + load)
│ → score = activeSessions×w1 + byteRate×w2 + cpuLoad×w3
│ → overload feedback: cpuLoad>0.8 or inflow>5MB/s → score += 10000 (yield)
│ → big-client spreading: check the client's existing session spread; prefer not-yet-saturated instances
│
③ returns the recommended instanceUrl
│
④ POST /session/open → {sessionId, agentId, instanceUrl}
│ → the SDK pins subsequent turn/close to that instance
│
⑤ POST /events/subscribe → {subscriptionId, instanceUrl}
│ → the SDK pins subsequent poll/ack to that instance
│
⑥ all further requests go straight to that instance
│ → instances pull only for their own clients; no peers, no forwarding
│
⑦ failure / instance unreachable → the SDK re-fetches /session/recommend for another instance
Session-granularity stickiness
/session/openand/events/subscribeboth returninstanceUrl.- SDK
SessionHandle.instanceUrl→StreamingSessionusesclient.withBaseUrl(instanceUrl)to pin subsequent turn/close. - SDK
subscribe→capturePollInstancesetspollBaseUrl; subsequent poll+ack go to that instance. advertisedAddrdefaults to empty (single-instance / tests / LB-compatible); set-Deventmesh.http.advertisedAddr=host:portto enable pinning.
The costs (accepted trade-offs)
- No forwarding ⇒ subscribers must be sticky (otherwise a poll landing on a different instance sees nothing) — guaranteed by subscribe→instanceUrl.
- Multiple subscribers of one topic spread across instances ⇒ each instance pulls every partition (MQ read amplification: N instances = N reads) — the price paid to eliminate the forwarding hop and global coordination complexity.
- A single session whose traffic exceeds one instance's ceiling cannot be split → the instance rate-limits/rejects — a known boundary with no workaround.
Where the code lives
| Piece | Location |
|---|---|
| Load meter | eventmesh-runtime/.../ingress/LoadMeter.java |
| Cluster membership / heartbeat | eventmesh-runtime/.../cluster/ClusterMembership.java |
| Recommend scoring | UniIngressService (/session/recommend) |
| Session routing | eventmesh-runtime/.../session/SessionRouter.java, Matchmaker.java |
| SDK pinning | eventmesh-sdks/.../cloudevents/ (capturePollInstance, SessionHandle) |
| Tests | ClusterMembershipLoadTest, LoadMeterTest, LoadBalancingScoringTest |