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.
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”.
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).
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>.
① 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/open and /events/subscribe both return instanceUrl.SessionHandle.instanceUrl → StreamingSession uses client.withBaseUrl(instanceUrl) to pin subsequent turn/close.subscribe → capturePollInstance sets pollBaseUrl; subsequent poll+ack go to that instance.advertisedAddr defaults to empty (single-instance / tests / LB-compatible); set -Deventmesh.http.advertisedAddr=host:port to enable pinning.| 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 |