Scalable Controllers Pool
The Scalable Controllers Pool removes the operational limits and failure risk of relying on a single CMON controller in larger ClusterControl environments. As the number of managed clusters grows, one controller can become a capacity bottleneck and a single failure domain for cluster monitoring, orchestration, and job handling.
How it works
The pool lets multiple CMON controllers coordinate cluster ownership: controller capacity scales horizontally, and ownership moves automatically when controllers are added, removed, restarted, or become unavailable.
The pool works identically regardless of the underlying infrastructure. Controllers can run on virtual machines, bare-metal servers, or cloud-hosted instances using NFS or Vault as the shared configuration backend, or on Kubernetes using Kubernetes Secrets. The only requirement is that every controller in the pool connects to the same CMON database and shares the same configuration backend.
All pool members share a common source of truth:
- All pool members connect to the same CMON database.
- All pool members must use a shared configuration backend.
- Each controller publishes its presence and status.
- Clusters are owned by one controller at a time, and ownership can move when controllers are added, removed, restarted, or become stale.
Here's a diagram on how the controller pool works:
graph TD
subgraph "<b>User Interface & Entry</b>"
User((User/<br>Admin))
CLI[ClusterControl CLI]
GUI[ClusterControl GUI]
end
subgraph "<b>Main Controller (Role: Main)</b>"
MC[CMON Main Instance]
Proxy[cmon-proxy]
JobScheduler[Job Scheduler]
end
subgraph "<b>Controller Pool (Role: Members)</b>"
C1[CMON Controller 1]
C2[CMON Controller 2]
C3[CMON Controller <i>N</i>]
end
subgraph "<b>Shared Infrastructure</b>"
DB[(Shared CMON DB)]
Storage[(Shared Config<br/>NFS or Kubernetes Secrets)]
end
subgraph "<b>Managed Clusters</b>"
CL1[Cluster A]
CL2[Cluster B]
CL3[Cluster C]
CL4[Cluster D]
end
%% Connections
User --> CLI & GUI --> Proxy
Proxy --> JobScheduler
JobScheduler -- "assigns jobs" --> C1
JobScheduler -- "assigns jobs" --> C2
JobScheduler -- "assigns jobs" --> C3
%% Shared Access
MC & C1 & C2 & C3 -.- DB & Storage
%% Ownership
C1 -- "owns" --> CL1
C1 -- "owns" --> CL2
C2 -- "owns" --> CL3
C3 -- "owns" --> CL4
Key runtime concepts
- Controllers table: stores controller identity, endpoint, status, and heartbeat information.
- Cluster assignment table: tracks which controller owns which cluster.
- Refresh cycle: each controller refreshes status and adjusts ownership.
- Staleness threshold: if a controller stops refreshing in time, peers can reclaim its clusters.
- Fair-share ownership: cluster ownership is distributed across active controllers, subject to controller capacity limits.
Operationally, a controller only needs to manage the subset of clusters that it currently owns. When ownership changes, the controller loads or unloads cluster state accordingly.
Shared configuration requirement
Shared configuration is a hard requirement in pool mode. ClusterControl supports the following backends:
- Kubernetes Secrets for Kubernetes-native environments.
- NFS or shared filesystem for VM and non-Kubernetes deployments.
- Vault as a secure backend direction for non-Kubernetes/shared deployments.
Deployment and configuration model
Minimum prerequisites:
- All controllers must connect to the same CMON database.
- Each controller must have a unique hostname:port identity.
- All controllers must use the same shared configuration backend.
- For Kubernetes shared-config deployments, controllers are started with
--pool --k8s.
Example startup pattern for non-Kubernetes environment shared config
Example startup pattern for Kubernetes-backed shared config
In multi-VM or external-controller setups using Kubernetes Secrets as the shared configuration store, the documented pattern also includes Kubernetes API endpoint and certificate environment variables so that each controller can access the same secret store.
KUBERNETES_SERVICE_HOST=hostA
KUBERNETES_SERVICE_PORT=16443
KUBERNETES_SERVICE_CA_CERT_PATH=/etc/certs/ca.crt
KUBERNETES_CLIENT_CERT_PATH=/etc/certs/client.crt
KUBERNETES_CLIENT_KEY_PATH=/etc/certs/client.key
Non-Kubernetes deployment model
The feature is also intended for non-Kubernetes environments. In those cases, the shared configuration backend is typically a shared filesystem such as NFS, while controllers still point to the same CMON database. This makes the feature applicable to:
- Traditional VM deployments
- Physical server or bare-metal deployments
- Cloud VMs or instances outside Kubernetes
Capacity and cold-start hardening
The following settings control how many clusters a controller may own and how quickly a new or restarted controller claims them.
| Configuration key | Meaning | Default behavior described |
|---|---|---|
controllers_pool_start_intervals_to_be_stable |
Refresh cycles before a starting controller becomes active | 2 cycles |
controllers_pool_max_clusters |
Per-controller ownership cap | -2 auto, -1 unlimited, 0 inactive, >0 explicit cap |
controllers_pool_clusters_per_gb |
RAM-based multiplier for auto capacity | 10 clusters per GB |
controllers_pool_cluster_claim_step |
Clusters claimed per refresh cycle during slow start | Auto, about 20% of max capacity |
controllers_pool_soft_alarm_secs |
Warning threshold for unassigned clusters | 75 seconds |
controllers_pool_hard_alarm_secs |
Critical threshold for unassigned clusters | 150 seconds |
These controls help avoid a cold-start controller claiming too many clusters at once, starving heartbeats, and triggering reassignment storms.
Managing the Controller Pool
Enabling the controller pool
Enabling the controller pool can be done from either the GUI or the s9s CLI.
-
In the left-side navigation bar, click Controller pool. If pool mode is not yet enabled, the UI notifies you and shows a Restart CMON in pool mode button.
-
Click Restart CMON in pool mode.
-
In the Allowed controllers network field, specify the CIDR block or blocks controllers are allowed to connect from (for example,
192.168.40.0/24, 10.120.0.0/24, 10.8.0.0/24). Click Enable.Enabling pool mode adds a
granted_controllers_network_maskentry to/etc/cmon.cnf(for example,granted_controllers_network_mask=192.168.40.0/24,192.168.121.0/24). The controller you enabled pool mode on keeps its existing clusters and is assigned themainrole.
The s9s CLI adds a pool-controllers mode to manage the controller pool. To enable pool mode:
--set-pool-modeenables the controller pool.--conf-storage=nfssets the shared configuration backend. Usek8sfor Kubernetes Secrets instead. Onlynfsandk8sare allowed.--granted-network-mask=192.168.40.0/24sets the CIDR block controllers are allowed to connect from.
Adjusting controller capacity at runtime
Set the maximum number of clusters a controller may own with --set-max-clusters-capacity (CLI) or through the GUI. This is only available once the controller pool is enabled. See Capacity and cold-start hardening for what the -2/-1/0/>0 values mean.
-
In the left-side navigation bar, click Controller pool.
-
Choose a controller and click its ellipsis button (...).
-
Choose Set capacity.
-
In the Max clusters field, enter the maximum number of clusters this controller may own. Setting the capacity lower than the number of clusters currently owned causes the controller to abandon the excess clusters. To move clusters back onto this controller later, raise the capacity again.
-
Click Apply.
s9s pool-controllers \
--set-max-clusters-capacity=40 \
--controller="https://HOST:9501" \
--cmon-user=USER \
--password=PASS
--set-max-clusters-capacity=40sets this controller's capacity to 40 clusters.--controller="https://HOST:9501"targets the controller to change.--cmon-userand--passwordare your ClusterControl credentials. Omit them if they are already set in yourS9S_USER_CONFIGfile.- Add
--forceif lowering the capacity would require the controller to abandon clusters it already owns.
Starting or stopping a controller
Starting and stopping a controller works the same way as starting or stopping the cmon daemon via systemd. You can only start a stopped controller, and only stop a running one.
-
In the left-side navigation bar, click Controller pool.
-
Choose a controller and click its ellipsis button (...).
-
Choose Stop or Start.
-
Confirm the action in the prompt that appears.
--startstarts the controller'scmondaemon. Use--stopto stop a running controller instead.--controller-id SIDis the controller'sSID, not its runtimeID. See View the controller pool for the difference.
Note
A controller with the main role cannot be stopped, only controllers with the member role.
Removing a controller from the pool
Removing a controller applies only to a controller that is already stopped.
-
In the left-side navigation bar, click Controller pool.
-
Choose a controller and click its ellipsis button (...).
-
Choose Remove.
-
In the prompt, choose one of two options:
- Unregister controller only: Unregisters the controller from the pool but leaves the
cmondaemon and its installation untouched. - Unregister and uninstall controller: Unregisters the controller and uninstalls its packages, ensuring the daemon is fully stopped.
- Unregister controller only: Unregisters the controller from the pool but leaves the
--remove-controllerremoves the controller from the pool.--controller-id SIDis the controller'sSID, not its runtimeID. See View the controller pool for the difference.
Add --uninstall to also remove the cmon packages from that host:
Upgrading a controller's cmon version
-
In the left-side navigation bar, click Controller pool.
-
Choose a controller and click its ellipsis button (...).
-
Choose Upgrade.
-
Confirm the action in the prompt that appears.
The controller must be stopped before it can be upgraded.
--update-cmonupgrades the controller to the latestcmonversion available in the Severalnines public repository.--controller-id SIDis the controller'sSID, not its runtimeID. See View the controller pool for the difference.
This also works against a specific pool member when run from the main controller.
Inspecting the controller pool with the CLI
Beyond the management operations above, the s9s CLI can also inspect pool status and ownership through related cluster and node commands.
View the controller pool
This shows controller SID, ID, hostname, port, status, role, and assigned clusters.
$ s9s pool-controllers --list
SID ID HOSTNAME PORT STATUS ROLE COUNT/MAX CLUSTERS
13 14 192.168.40.10 9500 active member 2/36 [2, 9]
7 24 192.168.40.5 9500 active main 2/36 [5, 10]
11 31 192.168.40.3 9500 active member 1/38 [1]
9 32 192.168.40.4 9500 active member 1/38 [8]
The ROLE field identifies which controller is your main controller, host 192.168.40.5 in this example.
The columns show:
- SID: Static controller ID (persistent config identifier). Use this for operations like
--start,--stop,--remove-controller. - ID: Runtime/dynamic controller instance ID (can change with runtime/re-registration).
- HOSTNAME: Controller host/IP.
- PORT: Controller RPC/API port (typically
9500/9501depending setup). - STATUS: Current state (e.g.,
active,starting,stopped,restarting, etc.). - ROLE: Pool role (
main= primary config/db holder,member= regular pool member). - COUNT/MAX:
owned_clusters / max_cluster_capacityfor that controller (e.g.,2/36means owns 2 clusters, cap is 36). - CLUSTERS: Cluster IDs currently assigned to that controller (e.g.,
[2, 9]).
In the example above, the controller at 192.168.40.10 owns clusters 2 and 9, out of a capacity of 36.
Identifying ownership from the cluster perspective
Combine the result of s9s pool-controllers --list with s9s cluster --list to see which cluster names and details belong to a given controller:
$ s9s cluster --list --long --controller="https://192.168.40.10:9501" --cmon-user=admin2 --password="userP@55" --rpc-tls
ID STATE TYPE OWNER GROUP NAME COMMENT
2 STARTED postgresql_single admin2 admins pgsql-18 All nodes are operational.
9 STARTED galera admin2 admins mariadb-galera All nodes are operational.
Total: 2
For security purposes, and you do not want to expose the password, you can use the following:
$ export S9S_USER_CONFIG=/root/.s9s/s9s-controller10.conf; s9s cluster --list --long --controller="https://192.168.40.10:9501" --rpc-tls
ID STATE TYPE OWNER GROUP NAME COMMENT
2 STARTED postgresql_single admin2 admins pgsql-18 All nodes are operational.
9 STARTED galera admin2 admins mariadb-galera All nodes are operational.
Total: 2
Where the file /root/.s9s/s9s-controller10.conf contains the following contents:
[global]
controller = https://192.168.40.10:9501
cmon_user = "admin"
cmon_password = "7201231b-8f0a-4d5e-a755-991e32ce3f66"
Note that querying controller 192.168.40.10 with s9s cluster --list --long only returns clusters 2 and 9, the clusters it owns. A cluster can only be viewed, listed, or identified through the controller that currently owns it, whether that controller is specified with --controller or through the controller parameter in S9S_USER_CONFIG.
Listing the nodes of the cluster
To list the nodes owned by a specific controller, run s9s node --list --long against that controller's address.
For example, from the main controller 192.168.40.5, you can check what nodes each pool member has:
$ s9s node --list --long --controller="https://192.168.40.3:9501"
STAT VERSION CID CLUSTER HOST PORT COMMENT
ho-- 1.8.27 1 mariadb-10.5 192.168.121.22 9600 Process 'haproxy' is running.
coC- 2.5.0.22113 1 mariadb-10.5 192.168.40.3 9500 Up and running.
Po-- 2.53.5 1 mariadb-10.5 192.168.40.4 9090 Process 'prometheus' is running.
soM- 10.5.29 1 mariadb-10.5 192.168.40.60 3306 Up and running (read-write).
koM- 2.1 1 mariadb-10.5 192.168.40.60 112 Process 'keepalived' is running.
soS- 10.5.29 1 mariadb-10.5 192.168.40.61 3306 Up and running (read-only).
ho-- 1.8.27 1 mariadb-10.5 192.168.40.61 9600 Process 'haproxy' is running.
ko-- 2.1 1 mariadb-10.5 192.168.40.61 112 Process 'keepalived' is running.
Total: 8
Or check what nodes and clusters (by CID) controller host 192.168.40.4 has:
$ s9s node --list --long --controller="https://192.168.40.4:9501"
STAT VERSION CID CLUSTER HOST PORT COMMENT
coC- 2.5.0.22113 8 pgsql-14 192.168.40.4 9500 Up and running.
poM- 14.23 8 pgsql-14 192.168.40.62 5432 Up and running (read-write).
poS- 14.23 8 pgsql-14 192.168.40.66 5432 Up and running (read-only).
Total: 3
Or check what nodes and clusters (by CID) controller host 192.168.40.10 has:
$ s9s node --list --long --controller="https://192.168.40.10:9501"
STAT VERSION CID CLUSTER HOST PORT COMMENT
coC- 2.5.0.22113 2 pgsql-18 192.168.40.10 9500 Up and running.
Po-- 2.53.5 2 pgsql-18 192.168.40.3 9090 Process 'prometheus' is running.
p-S- 18.4 2 pgsql-18 192.168.40.63 5432 Shut down (read-only).
ho-- 1.8.27 2 pgsql-18 192.168.40.63 9600 Process 'haproxy' is running.
koM- 2.1 2 pgsql-18 192.168.40.63 112 Process 'keepalived' is running.
poM- 18.4 2 pgsql-18 192.168.40.64 5432 Up and running (read-write).
ho-- 1.8.27 2 pgsql-18 192.168.40.64 9600 Process 'haproxy' is running.
ko-- 2.1 2 pgsql-18 192.168.40.64 112 Process 'keepalived' is running.
coC- 2.5.0.22113 9 mariadb-galera 192.168.40.10 9500 Up and running.
Po-- 2.53.5 9 mariadb-galera 192.168.40.10 9090 Process 'prometheus' is running.
goM- 11.8.8 9 mariadb-galera 192.168.40.33 3306 Up and running (read-write).
goM- 11.8.8 9 mariadb-galera 192.168.40.34 3306 Up and running (read-write).
goM- 11.8.8 9 mariadb-galera 192.168.40.35 3306 Up and running (read-write).
Total: 13
Additionally, to see hostname/IP, node type, and database version for every node across all controllers in a single table, run the following from the main controller:
$ s9s cluster --list --all-pool --long --print-json | jq -r '
["cluster","cdt_path","ip","hostname","nodetype","version","version_comment","addresses"] as $cols
| ($cols | "| " + join(" | ") + " |"),
($cols | map("---") | "| " + join(" | ") + " |"),
( [.. | objects | select(.class_name? == "CmonClusterInfo")][]
| (.cluster_name // .cdt_path) as $cluster
| .hosts[]?
| [ $cluster,
((.cdt_path // "-") | gsub("^\\s+|\\s+$"; "")),
(.ip // "-"),
(.hostname // "-"),
(.nodetype // .class_name // "-"),
(.version // "-"),
(.version_comment // "-"),
([.configuration[]?.address] | join("<br>") | if . == "" then "-" else . end)
]
| map(tostring | gsub("\\|"; "\\|"))
| "| " + join(" | ") + " |"
)'
| cluster | cdt_path | ip | hostname | nodetype | version | version_comment | addresses |
| --- | --- | --- | --- | --- | --- | --- | --- |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.60 | 192.168.40.60 | mysql | 10.5.29-MariaDB-log | MariaDB Server | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.3 | 192.168.40.3 | controller | 2.5.0.22113 | - | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.4 | 192.168.40.4 | prometheus | 2.53.5 | - | <br><br><br><br> |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.61 | 192.168.40.61 | mysql | 10.5.29-MariaDB-log | MariaDB Server | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.61 | 192.168.40.61 | haproxy | 1.8.27 | - | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.60 | 192.168.40.60 | keepalived | 2.1 | - | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.40.61 | 192.168.40.61 | keepalived | 2.1 | - | - |
| mariadb-10.5 | / mariadb-10.5 | 192.168.121.22 | 192.168.121.22 | haproxy | 1.8.27 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.63 | 192.168.40.63 | postgres | 18.4 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.64 | 192.168.40.64 | postgres | 18.4 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.10 | 192.168.40.10 | controller | 2.5.0.22113 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.3 | 192.168.40.3 | prometheus | 2.53.5 | - | <br><br><br><br> |
| pgsql-18 | /pgsql-18 | 192.168.40.63 | 192.168.40.63 | haproxy | 1.8.27 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.64 | 192.168.40.64 | haproxy | 1.8.27 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.63 | 192.168.40.63 | keepalived | 2.1 | - | - |
| pgsql-18 | /pgsql-18 | 192.168.40.64 | 192.168.40.64 | keepalived | 2.1 | - | - |
| clickhouse | /clickhouse | 192.168.40.67 | 192.168.40.67 | clickhouse | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.68 | 192.168.40.68 | clickhouse | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.69 | 192.168.40.69 | clickhouse | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.30 | 192.168.40.30 | clickhouse_keeper | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.31 | 192.168.40.31 | clickhouse_keeper | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.32 | 192.168.40.32 | clickhouse_keeper | 25.3 | - | - |
| clickhouse | /clickhouse | 192.168.40.5 | 192.168.40.5 | controller | 2.5.0.22113 | - | - |
| clickhouse | /clickhouse | 192.168.40.10 | 192.168.40.10 | prometheus | 2.53.5 | - | <br><br><br><br> |
| pgsql-14 | /pgsql-14. | 192.168.40.66 | 192.168.40.66 | postgres | 14.23 | - | - |
| pgsql-14 | /pgsql-14. | 192.168.40.62 | 192.168.40.62 | postgres | 14.23 | - | - |
| pgsql-14 | /pgsql-14. | 192.168.40.4 | 192.168.40.4 | controller | 2.5.0.22113 | - | - |
| mariadb-galera | /mariadb-galera | 192.168.40.33 | 192.168.40.33 | galera | 11.8.8-MariaDB-log | MariaDB Server | - |
| mariadb-galera | /mariadb-galera | 192.168.40.10 | 192.168.40.10 | controller | 2.5.0.22113 | - | - |
| mariadb-galera | /mariadb-galera | 192.168.40.34 | 192.168.40.34 | galera | 11.8.8-MariaDB-log | MariaDB Server | - |
| mariadb-galera | /mariadb-galera | 192.168.40.35 | 192.168.40.35 | galera | 11.8.8-MariaDB-log | MariaDB Server | - |
| mariadb-galera | /mariadb-galera | 192.168.40.10 | 192.168.40.10 | prometheus | 2.53.5 | - | <br><br><br> |
Operational guidance
Recommended workflow
- Set up a shared CMON database.
- Choose a shared configuration backend appropriate to the environment.
- Start the initial controller with pool mode enabled.
- Add additional controllers that point to the same CMON database and shared config backend.
- Use
s9s pool-controllers --listands9s cluster --list --all-poolto verify ownership distribution. - For larger fleets, tune capacity and claim-step settings to avoid cold-start overload.
Common gotchas
- Assuming Kubernetes is required: it is not. Kubernetes is one supported model, but VMs, bare metal, and cloud-hosted servers are also valid if shared DB and shared config are in place.
- Missing shared config: pool mode requires all controllers to see the same configuration state.
- Wrong HA expectations: pool mode gives you horizontal scaling of controller capacity, but full HA/failover readiness has not shipped in the same release as pool mode for every version. Check your version's release notes before assuming both are available.
- Privilege errors on pool-wide listing: use a superuser/admin account.
- Runtime capacity changes disappearing after restart: only configuration-file values are persistent.