Load balancers¶
A load balancer fronts a pool of VMs selected by label and brokers each connection to one healthy backend. It gives you a single stable name for a set of interchangeable VMs - a stateless web tier, a group of read replicas - so a client connects to the load balancer and never has to know which VM it lands on.
Balancing is per-connection: the broker picks a healthy backend at the moment
a connection is established (broker-time), so a long-lived listener spreads its
connections across the pool. Load balancers are managed through the otherix lb
command group (aliased loadbalancer) and are addressed by name.
For how a client reaches a VM through the control plane with no public IP per VM, see the Ingress concept.
Label your backends¶
VMs join a load balancer's pool by their labels. Set them when you create the VM:
otherix vm create web01 \
--image-url https://example.com/noble.img \
--arch arm64 \
--network net-app \
--label app=web \
--label tier=frontend
Repeat --label k=v for each label. Create every VM that should serve the pool
with the same selectable label (here app=web).
Labels are set at create time
A VM's labels are fixed when the VM is created. A VM must be created with a label to be selectable by a load balancer - there is no way to add the label afterwards, so plan the labels before you launch the pool.
Create a load balancer¶
The minimum a load balancer needs is a traffic port and a selector:
--port(required, 1..65535) is the guest TCP port that traffic is sent to - the port your service listens on inside each VM.--selector(required, one or morek=vterms) is the label match. Every VM whose labels satisfy every term joins the pool. Comma-separate multiple terms:--selector app=web,tier=frontend.
Adding health checks¶
Health-check flags turn on active probing so a broken backend is taken out of rotation. All are optional:
otherix lb create web-lb \
--port 80 \
--selector app=web \
--health-port 8080 \
--health-interval 5 \
--health-healthy-threshold 2 \
--health-unhealthy-threshold 2
| Flag | Meaning | Default |
|---|---|---|
--health-port |
TCP port to probe. Lets you health-check a /health port distinct from the traffic port. |
follows --port |
--health-interval |
Seconds between probes (1..300). | 10 |
--health-timeout |
Per-probe timeout in seconds (1..60). | 2 |
--health-healthy-threshold |
Consecutive successful probes before an unhealthy backend rejoins (1..10). | 2 |
--health-unhealthy-threshold |
Consecutive failed probes before a backend is dropped (1..10). | 3 |
--health-port follows --port unless you set it, so a service that serves both
traffic and its health check on the same port needs no health-port flag.
How health checks work¶
The owning agent actively TCP-probes each backend's health port on the configured interval. The pool served to connections is filtered by the verdicts:
- A confirmed-unhealthy backend is subtracted from the pool. After a backend
fails
--health-unhealthy-thresholdprobes in a row it is removed from rotation and no new connections are brokered to it. - A recovering backend rejoins. Once a dropped backend passes
--health-healthy-thresholdprobes in a row it is put back into rotation. - A still-warming backend is served. A backend that has not yet been probed (no verdict) is kept in the pool. Load balancing fails toward serving: a problem in the health pipeline never darks a load balancer that is otherwise serving traffic.
- When every backend is confirmed unhealthy, a connect is refused. With no eligible backend the broker returns an error rather than sending traffic to a known-bad VM.
The health port can differ from the traffic port, which is the point of
--health-port - probe a lightweight /health endpoint on port 8080 while
serving traffic on port 80.
See pool status¶
otherix lb list shows each load balancer's rolled-up health and backend count:
NAME PORT SELECTOR STATUS TARGETS
web-lb 80 app=web healthy 3/3
api-lb 8443 app=api,tier=edge degraded 2/3
db-ro 5432 role=replica unhealthy 0/2
cache 6379 app=cache no_backends 0/0
The STATUS column reports the rolled-up state of the pool:
healthy- every backend is passing its health check.degraded- the pool is serving but not fully healthy: some backends are down, or some are still warming up. A partly-healthy or still-warming pool readsdegraded, notunhealthy.unhealthy- there are backends, but every one of them is confirmed unhealthy. A connect is refused in this state.no_backends- the selector currently matches no VMs.
TARGETS is <healthy>/<total> - how many backends are in rotation out of how
many the selector matches.
For per-backend detail, use get:
This lists each backend VM with its health (true, false, or unknown for a
warming backend that has no verdict yet) and the time it was last probed, so you
can see exactly which VM is dragging the pool down.
Connect through it¶
otherix lb connect opens a local listener that balances each connection
over the healthy pool:
Then point any client at the local address:
-L, --listen host:port sets the local listener address. Because each accepted
connection is brokered independently to a healthy backend, a long-lived listener
load-balances on its own - every new connection re-brokers and may land on a
different VM.
Publish a public port¶
lb connect is the private path: a client runs otherix lb connect and the CLI
brokers a credentialed session. A published port is the opposite - a
persistent public L4 (TCP) listener that accepts plain traffic with no
Otherix tooling and no per-connection credential, and forwards it into the pool.
It is the analog of an AWS Network Load Balancer: gateway-role nodes are the data
plane, the control plane stays out of the path, and the listener is bound on every
gateway node (connect to any of them).
Prerequisite: gateway-role nodes¶
The public listener runs on nodes that hold the gateway role. Give at least one node the role before you publish:
See Ingress gateways for the gateway role in full. You also
need the loadbalancer:publish permission (in addition to normal LB management).
Publish on create or update¶
Add --publish (with an optional explicit public port) to expose the pool:
otherix lb create web-lb \
--port 8080 \ # backend port inside each VM
--selector app=web \
--publish --publish-port 30080 # public TCP port on the gateways
--publishturns on the public listener.--publish-portor--source-cidrimply it, so--publishalone is only needed when you want the published port to default to--port.--publish-port(1..65535) is the public port. It must be unique across the cluster. Omit it to reuse the backend--port.--source-cidr(repeatable) restricts the listener to client source ranges. With no--source-cidrthe port is open to every source that can reach it (published means public). A non-empty allowlist fails closed: a client outside every range is dropped with no data.- Published ports are TCP only in this release.
Change or remove the public listener later with lb update:
otherix lb update web-lb --publish-port 30080 # publish / change the port
otherix lb update web-lb --source-cidr 203.0.113.0/24 # replace the client allowlist
otherix lb update web-lb --no-publish # remove the public listener
--source-cidr on update replaces the whole allowlist (an empty list opens the
port to all). --no-publish clears the published port, protocol, and CIDRs.
Find the public address¶
The listener binds on every gateway node, so the endpoint is the set of gateway
addresses on the published port. otherix lb get shows exactly which gateways are
bound and the address to connect to:
name: web-lb
port: 8080
selector: app=web
published_port: 30080
protocol: tcp
source_cidrs: 203.0.113.0/24
listeners:
- node: node-3 address: 10.77.0.3:30080 bound=true
- node: node-5 address: 10.77.0.5:30080 bound=false error=listen tcp :30080: bind: address already in use
...
Each listeners entry is one gateway's observed status: node (the gateway name),
address (the host:published_port to connect to), bound (whether it is
currently accepting), and an error when a gateway cannot bind. Connect a plain
client to the address of any bound=true gateway:
For a single stable public endpoint, front the gateway addresses with your own DNS
round-robin or an external load balancer - Otherix does not allocate a cluster-wide
virtual IP (the NLB model: the gateways themselves are the endpoints). The listener
status is observed state, so it appears in lb get (text) and -o json, but not in
the -o yaml manifest, which stays a round-trippable desired-state spec.
Published port vs lb connect
Use a published port for external clients that speak plain TCP and cannot
run Otherix tooling. Use lb connect when the client can authenticate and
you do not want a public listener - it brokers a credentialed session and needs
no gateway role or public port.
Update and delete¶
otherix lb update changes a load balancer in place. --selector replaces the
whole selector (it is not additive), and the --port / --health-* flags work
as on create:
otherix lb update web-lb --selector app=web,tier=frontend # replace the selector
otherix lb update web-lb --port 8080 # change the traffic port
otherix lb update web-lb --health-port 9000 --health-interval 3
Delete removes the load balancer (the backing VMs are untouched):
Declarative manifest¶
A load balancer can be described declaratively and applied with
otherix create -f (see Declarative manifests):
apiVersion: otherix/v1
kind: LoadBalancer
metadata:
name: web-lb
spec:
port: 80 # guest TCP port (required)
selector: # required, one or more entries; VMs selected by these labels
app: web
publishedPort: 30080 # optional; public TCP port on gateway nodes (omit = no public listener)
protocol: tcp # optional; only tcp is supported
sourceCIDRs: # optional; client-IP allowlist (omit/empty = open to all)
- 203.0.113.0/24
healthCheck: # optional; omitted keys take server defaults
port: 8080 # optional; omit = follow spec.port
intervalSeconds: 10
timeoutSeconds: 2
healthyThreshold: 2
unhealthyThreshold: 3
The whole healthCheck block is optional, and any key omitted inside it takes
the server default. Omitting healthCheck.port makes the probe follow
spec.port, exactly like the --health-port flag. otherix lb get web-lb -o
yaml projects the live load balancer back into this same apply-ready shape, so
the manifest round-trips.
Use cases¶
- Stateless web pool. Front a set of identical web VMs (
--label app=web) with one load balancer on port 80 and let connections spread across them; scale by launching more VMs with the same label. - Read-replica pool. Balance read traffic over a set of database read
replicas (
--label role=replica) so no single replica takes every query. - Separate health port. Probe a dedicated
/healthport (--health-port 8080) while serving application traffic on another port, so the health signal reflects real readiness rather than just an open socket.