forked from lei.chen/myxce
finalize configuration replication
This commit is contained in:
parent
e1517bbf28
commit
fa66d1ba9c
30 changed files with 8867 additions and 1560 deletions
272
src/docs/cluster-config-split-analysis.md
Normal file
272
src/docs/cluster-config-split-analysis.md
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
# ClusterConfig Schema Split Analysis
|
||||
|
||||
> Analysis date: 2026-06-08. Branch: persistent_ha.
|
||||
> Source schema: `src/schema/controller.capnp:680`, struct `ClusterConfig`.
|
||||
|
||||
## Summary
|
||||
|
||||
The `ClusterConfig` struct (20 fields) mixes three distinct configuration concerns:
|
||||
cluster-wide business config, HA peer membership, and per-controller performance
|
||||
tunables. These should be split into separate objects so each can evolve
|
||||
independently without forcing a re-push of the full config blob to all standbys.
|
||||
|
||||
## Current State Discovery
|
||||
|
||||
### Peer list is already structurally separate
|
||||
|
||||
The `--peers` CLI flag (`main.c:178-210`) parses into `myconf.peers_cfg`
|
||||
(`xce_cluster_config_t`), which is a **separate struct** from the config blob's
|
||||
`cluster_config_t`. At startup, `cluster_init_phase1` uses `peers_cfg.candidates`
|
||||
to determine `my_name`, `my_port`, `my_priority`, and the peer list — all before
|
||||
the config blob is loaded (`cluster.c:865-880`). The config blob's `candidates`
|
||||
field is **not consulted** for HA peer discovery; it is carried for backward
|
||||
compatibility in the blob but superseded by `--peers`.
|
||||
|
||||
### HA tunables are hardcoded at phase1
|
||||
|
||||
`cluster_init_phase1` uses compile-time defaults for all HA timing parameters
|
||||
(`cluster.c:854-863`):
|
||||
```c
|
||||
ha_config_t ha_cfg = {
|
||||
.heartbeat_interval_ms = HA_DEFAULT_HEARTBEAT_INTERVAL_MS,
|
||||
.heartbeat_timeout_ms = HA_DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
||||
.election_timeout_ms = HA_DEFAULT_ELECTION_TIMEOUT_MS,
|
||||
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
The `heartbeatInterval` and `inactiveHeartbeatCount` fields in
|
||||
`ClusterConfig` are consumed later (e.g., `cluster.c:581-585` writes them into
|
||||
a config key-value store for runtime access), but the HA init path does not
|
||||
read them. The heartbeat timeout validation in `startup_validation.c:144` works
|
||||
against the hardcoded defaults, not the config blob values.
|
||||
|
||||
### vcpus is already deprecated
|
||||
|
||||
Field `@7 vcpus` is marked deprecated in the schema comment: "use --vcpus CLI
|
||||
flag; kept for config blob backward compatibility." `cluster_init_phase2`
|
||||
reads `c->myconf->vcpus` (set from CLI), not the blob's `vcpus` field
|
||||
(`cluster.c:1045`).
|
||||
|
||||
## Proposed Split: Three Configuration Objects
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Current ClusterConfig │
|
||||
│ (20 fields) │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
|
||||
│ ClusterConfig │ │ PeersConfig │ │ ControllerConfig │
|
||||
│ (cluster-wide) │ │ (HA membership) │ │ (per-controller) │
|
||||
└───────────────────┘ └─────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### Group 1: Cluster-Wide Business Config (keep in `ClusterConfig`)
|
||||
|
||||
These fields define the cluster's behavior, resources, and policy. They **must
|
||||
be identical** across all controllers. When modified via `OP_upgradeConfig`,
|
||||
the new blob is pushed to all standbys and committed on majority.
|
||||
|
||||
| Field | Ord | Type | Rationale |
|
||||
|-------|-----|------|-----------|
|
||||
| `clusterName` | @0 | Text | Cluster identity — must be identical cluster-wide |
|
||||
| `resources` | @8 | List(Resource) | Resource catalog — shared state across all nodes |
|
||||
| `queues` | @9 | List(QueueConf) | Queue definitions — cluster scheduling policy |
|
||||
| `ugroups` | @10 | List(GroupConfig) | User group membership — access control |
|
||||
| `hgroups` | @11 | List(GroupConfig) | Host group membership — scheduling constraints |
|
||||
| `hostConfigs` | @12 | List(HostConfig) | Per-host overrides — cluster-wide host knowledge |
|
||||
| `hproviders` | @18 | List(ScalerConfig) | Autoscaler provider config — cluster infrastructure |
|
||||
| `limits` | @19 | List(LimitConfig) | Resource limits — cluster policy enforcement |
|
||||
| `configGeneration` | @20 | UInt64 | Monotonic version counter — replication consistency |
|
||||
|
||||
**How consumed**: `cluster_init_phase2` (after election and config load)
|
||||
iterates these to populate `c->resmgr`, `c->jobmgr`, queue tables, host
|
||||
groups, etc.
|
||||
|
||||
### Group 2: HA Peer Membership (extract to `PeersConfig`)
|
||||
|
||||
These fields define **who** is in the cluster. They change independently when
|
||||
controllers are added, removed, or re-IP'd — without any change to the
|
||||
business config.
|
||||
|
||||
| Field | Ord | Type | Rationale |
|
||||
|-------|-----|------|-----------|
|
||||
| `candidates` | @2 | List(MasterCandidate) | HA peer list — changes independently of business config |
|
||||
| `port` | @1 | UInt32 | Fallback listener port — derived from candidates; per-controller in practice |
|
||||
|
||||
**Already structurally separate**: The `--peers` CLI flag populates
|
||||
`myconf.peers_cfg` (`xce_cluster_config_t` in `src/include/clusterconfig.h`),
|
||||
a struct distinct from the config blob's `cluster_config_t`. The candidates
|
||||
list in the blob is carried for backward compatibility but is not read for HA
|
||||
peer discovery at startup. Making the separation formal means:
|
||||
- `PeersConfig` travels via its own capnp message type and replication path
|
||||
- Cluster config upgrades via `OP_upgradeConfig` no longer need to carry the candidate list
|
||||
- Peer list changes (add/remove controllers) are a dedicated operation with its own quorum semantics
|
||||
|
||||
**Candidate list has special quorum requirements**: Changing the peer set is a
|
||||
Raft cluster membership change. It must not use the same
|
||||
`repl_wait_majority_ack` path as business config because the majority
|
||||
denominator itself is changing. A separate message type enables a two-phase
|
||||
membership change protocol (joint consensus) in the future.
|
||||
|
||||
**Impact of removing `candidates` from `ClusterConfig` (verified against codebase):**
|
||||
|
||||
`cluster_build_config()` (`cluster.c:575–609`) is the **only** place that reads
|
||||
`cluster_config->candidates` and `cluster_config->ncandidates` — it copies them
|
||||
into the runtime KV store as `ncandidates` and `candidate_N_{name,host,port}`.
|
||||
No other component (scheduler, apiserver, opget, sched) reads those KV keys.
|
||||
The live peer list used by `ha.c:3266–3272` and `cluster_open_listener` comes
|
||||
exclusively from `peers_cfg.candidates[]` (parsed by `--peers`). Removing
|
||||
candidates from the blob is therefore a **zero-blast-radius** change.
|
||||
|
||||
| File | Location | Change |
|
||||
|------|----------|--------|
|
||||
| `src/controller/cluster.c` | 587–606 | Remove `ncandidates` + `candidate_N_*` KV loop from `cluster_build_config()` |
|
||||
| `src/include/clusterconfig.h` | 34–35 | Remove `int ncandidates` and `master_candidate_t** candidates` from `cluster_config_t` |
|
||||
| `src/schema/controller.capnp` | 683 | Remove `candidates @2: List(MasterCandidate)` from `ClusterConfig` |
|
||||
|
||||
**`MasterCandidate` fields**:
|
||||
```
|
||||
struct MasterCandidate {
|
||||
name @0: Text; // controller identity (e.g., "ctrl-1")
|
||||
host @1: Text; // hostname or IP
|
||||
port @2: UInt32; // peer listener port
|
||||
}
|
||||
```
|
||||
|
||||
### Group 3: Per-Controller Performance Tunables (extract to `ControllerConfig`)
|
||||
|
||||
These fields configure **how** an individual controller process behaves. They
|
||||
can legitimately differ across controllers (e.g., a lower-memory node with
|
||||
fewer vcpus, or a WAN-connected standby with a longer heartbeat timeout).
|
||||
|
||||
| Field | Ord | Type | Rationale |
|
||||
|-------|-----|------|-----------|
|
||||
| `heartbeatInterval` | @3 | UInt32 | Primary heartbeat send interval — can be tuned per primary |
|
||||
| `inactiveHeartbeatCount` | @4 | UInt32 | Missed-heartbeat tolerance multiplier |
|
||||
| `fetchJobInterval` | @5 | UInt32 | Job fetch poll interval — local scheduler interaction |
|
||||
| `netioTimeout` | @6 | UInt32 | Network I/O timeout — environment-specific |
|
||||
| `vcpus` | @7 | UInt32 | **Already deprecated** — use `--vcpus` CLI flag; remove from schema |
|
||||
| `jobmgr` | @13 | HashZArrayConfig | Job manager hash table sizing (nHashSlots, delta) |
|
||||
| `workermgr` | @14 | HashZArrayConfig | Worker manager hash table sizing |
|
||||
| `qmgr` | @15 | HashZArrayConfig | Queue manager hash table sizing |
|
||||
| `resmgr` | @16 | ResmgrConfig | Resource manager hash + instance table sizing |
|
||||
| `xstrings` | @17 | HashZArrayConfig | String interning table sizing |
|
||||
|
||||
**How consumed**:
|
||||
- `jobmgr`/`workermgr`/`qmgr`/`resmgr`/`xstrings` are read once at
|
||||
`cluster_init_phase2` to construct the internal key-value stores
|
||||
(`cluster.c:803-808`). After construction, the config values are not referenced again.
|
||||
- `heartbeatInterval`/`inactiveHeartbeatCount` are read at runtime by the
|
||||
heartbeat and liveness tasks (`cluster.c:1180, 1195`).
|
||||
- `fetchJobInterval` drives the scheduler polling loop.
|
||||
- `netioTimeout` is used by the iomgr layer for TCP timeouts.
|
||||
|
||||
**Why these can differ per controller**: A controller on a high-memory machine
|
||||
can be configured with larger hash tables. A controller on a high-latency WAN
|
||||
link can have a longer netioTimeout. These differences do not affect cluster
|
||||
correctness — they only affect local process behavior.
|
||||
|
||||
**`vcpus` removal**: Already deprecated. The `--vcpus` CLI flag
|
||||
(`main.c:1045`) is the canonical source. Remove field @7 in the next
|
||||
schema revision.
|
||||
|
||||
## Fields With Dual Usage (require care)
|
||||
|
||||
### `port` (@1)
|
||||
|
||||
**Decision: Remove. No fallback bind port.**
|
||||
|
||||
`cluster_config->port` currently serves as a fallback in `cluster_open_listener`
|
||||
(`cluster.c:953–975`) when `--peers` does not specify a port for the matching
|
||||
candidate. This fallback is removed: the `--peers` port is mandatory.
|
||||
|
||||
**Affected code (all changes mechanical):**
|
||||
|
||||
| File | Location | Change |
|
||||
|------|----------|--------|
|
||||
| `src/controller/cluster.c` | 953–975 | Remove the `if (my_port == 0) { cluster_config->port }` branch; return `EINVAL` immediately if no port found in `peers_cfg` |
|
||||
| `src/controller/myconf.c` | 74 | Remove `SET_DEFAULT_IF_UNSET(c->port, 0, 9990)` |
|
||||
| `src/controller/config_repl.c` | 55 | Remove `SET_DEFAULT_IF_UNSET(c->port, 0, 9990)` |
|
||||
| `src/include/clusterconfig.h` | 33 | Remove `int port` from `cluster_config_t` |
|
||||
| `src/schema/controller.capnp` | 682 | Remove `port @1: UInt32` from `ClusterConfig` |
|
||||
|
||||
Note: `"port"` in `testsuite/test.c:124,266` is the scheduler/worker
|
||||
connect-to-primary port (via `get_setting("port")`), not the listener bind
|
||||
port. It is unaffected.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### Phase 1: Remove `port` and `candidates` from `ClusterConfig` (immediate)
|
||||
|
||||
No backward-compat transition needed for these two fields — the live code
|
||||
already ignores both (peer discovery uses `peers_cfg`; port fallback is being
|
||||
deleted). All changes are mechanical.
|
||||
|
||||
1. `src/schema/controller.capnp` — Remove `port @1` and `candidates @2` from
|
||||
`ClusterConfig`; mark or remove `MasterCandidate` struct if no longer
|
||||
referenced elsewhere
|
||||
2. `src/include/clusterconfig.h` — Remove `int port`, `int ncandidates`,
|
||||
`master_candidate_t** candidates` from `cluster_config_t`
|
||||
3. `src/controller/cluster.c` — Remove `if (my_port == 0)` fallback block in
|
||||
`cluster_open_listener` (lines 967–975); remove `ncandidates` + `candidate_N_*`
|
||||
KV loop in `cluster_build_config` (lines 587–606)
|
||||
4. `src/controller/myconf.c` and `src/controller/config_repl.c` — Remove
|
||||
`SET_DEFAULT_IF_UNSET(c->port, 0, 9990)` from `fill_default_cluster_config`
|
||||
5. Regenerate capnp bindings
|
||||
|
||||
### Phase 2: Schema split (wire-compatible)
|
||||
|
||||
1. Define `PeersConfig` and `ControllerConfig` structs in `controller.capnp`
|
||||
2. Add them as new fields in the top-level `ConfigData` union alongside
|
||||
the existing `clusterconfig @2`
|
||||
3. Regenerate capnp bindings
|
||||
4. **Keep the remaining `ClusterConfig` fields intact** during transition —
|
||||
new fields are additive
|
||||
|
||||
### Phase 3: Controller reads from new structs
|
||||
|
||||
1. `cluster_init_phase2`: read `ControllerConfig` for tunables
|
||||
2. `cluster_config_t` retains business config fields only
|
||||
3. Fall back to old `ClusterConfig` fields when new structs are absent
|
||||
(backward compatibility for existing `cluster.bin` files)
|
||||
|
||||
### Phase 4: Remove superseded tunables from `ClusterConfig`
|
||||
|
||||
1. Remove `heartbeatInterval`, `inactiveHeartbeatCount`, `fetchJobInterval`,
|
||||
`netioTimeout`, `vcpus` from `ClusterConfig`
|
||||
2. Remove `jobmgr`, `workermgr`, `qmgr`, `resmgr`, `xstrings` from
|
||||
`ClusterConfig`
|
||||
|
||||
### Phase 5 (future): Peer list replication
|
||||
|
||||
1. Define `OPCode_peersUpdate` / `OPCode_peersUpdateAck` for peer list
|
||||
replication with joint-consensus semantics
|
||||
2. Cluster membership changes become a dedicated operation with its own
|
||||
quorum protocol
|
||||
|
||||
## Effort and Risk
|
||||
|
||||
| Phase | Effort | Risk | Justification |
|
||||
|-------|--------|------|---------------|
|
||||
| 1 — Remove port + candidates | XS (½d) | Low | No transition needed; dead code removal + schema field removal |
|
||||
| 2 — Schema split | S (1d) | Low | Additive capnp change; old fields preserved |
|
||||
| 3 — Read from new structs | M (2-3d) | Medium | Touches cluster_init paths; backward compat needed |
|
||||
| 4 — Remove superseded tunables | S (1d) | Low | Mechanical removal after Phase 3 is validated |
|
||||
| 5 — Peer replication | L (1-2wk) | High | New protocol; joint consensus; extensive testing |
|
||||
|
||||
## References
|
||||
|
||||
- `src/schema/controller.capnp:680-700` — `ClusterConfig` definition
|
||||
- `src/schema/controller.capnp:253` — `ConfigData.clusterconfig @2`
|
||||
- `src/controller/main.c:178-213` — `--peers` CLI parsing into `peers_cfg`
|
||||
- `src/controller/cluster.c:854-880` — `ha_config_t` hardcoded defaults + priority derivation
|
||||
- `src/controller/cluster.c:945-977` — port derivation from candidates vs config blob
|
||||
- `src/controller/cluster.c:803-808` — internal manager construction
|
||||
- `src/controller/ha.h:134` — `ha_config_t.heartbeat_interval_ms` default via `ha_init`
|
||||
- `src/controller/startup_validation.c:133-163` — heartbeat config validation
|
||||
- `src/include/clusterconfig.h` — `xce_cluster_config_t` (separate peers struct)
|
||||
Loading…
Add table
Add a link
Reference in a new issue