blob: df3597b41de6794e9245f9f002e7b2fad0a8389f [file] [view]
# 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](offset-management.md).
---
## Architectural position
> **Balancing happens at the session-assignment layer (the `recommend`
> entry 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/open` and `/events/subscribe` both return `instanceUrl`.
- SDK `SessionHandle.instanceUrl` → `StreamingSession` uses
`client.withBaseUrl(instanceUrl)` to pin subsequent turn/close.
- SDK `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.
## 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` |