Skip to content

Commit 6b9c585

Browse files
Merge pull request #596 from lmiccini/simplified-safety
Final InstanceHA cleanup and small fixes
2 parents 797736a + 400b0a2 commit 6b9c585

9 files changed

Lines changed: 313 additions & 391 deletions

docs/instanceha_architecture.md

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ InstanceHA is a high-availability service for OpenStack that automatically detec
99
1. [Core Components](#core-components)
1010
2. [Architecture Patterns](#architecture-patterns)
1111
3. [Service Workflow](#service-workflow)
12-
4. [Critical Services Validation](#critical-services-validation)
13-
5. [Configuration System](#configuration-system)
14-
6. [Evacuation Mechanisms](#evacuation-mechanisms)
15-
7. [Fencing Agents](#fencing-agents)
16-
8. [Kubernetes Events and Prometheus Metrics](#kubernetes-events-and-prometheus-metrics)
17-
9. [Advanced Features](#advanced-features)
18-
10. [Region Handling](#region-handling)
19-
11. [Authentication](#authentication)
20-
12. [Startup Reconciliation](#startup-reconciliation)
21-
13. [Graceful Shutdown](#graceful-shutdown)
22-
14. [Security](#security)
23-
15. [Performance](#performance)
24-
16. [Testing](#testing)
25-
17. [Deployment](#deployment)
26-
18. [Configuration Options Reference](#configuration-options-reference)
12+
4. [Fencing Safety Model](#fencing-safety-model)
13+
5. [Critical Services Validation](#critical-services-validation)
14+
6. [Configuration System](#configuration-system)
15+
7. [Evacuation Mechanisms](#evacuation-mechanisms)
16+
8. [Fencing Agents](#fencing-agents)
17+
9. [Kubernetes Events and Prometheus Metrics](#kubernetes-events-and-prometheus-metrics)
18+
10. [Advanced Features](#advanced-features)
19+
11. [Region Handling](#region-handling)
20+
12. [Authentication](#authentication)
21+
13. [Startup Reconciliation](#startup-reconciliation)
22+
14. [Graceful Shutdown](#graceful-shutdown)
23+
15. [Security](#security)
24+
16. [Performance](#performance)
25+
17. [Testing](#testing)
26+
18. [Deployment](#deployment)
27+
19. [Configuration Options Reference](#configuration-options-reference)
2728

2829
---
2930

@@ -162,7 +163,7 @@ def __init__(self, config_manager: ConfigManager, cloud_client: Optional[OpenSta
162163
self._last_hash_time = 0
163164
self._previous_hash = ""
164165
self.ready = False # Readiness flag (set after first poll)
165-
self.k8s_api_reachable = False # K8s API connectivity (partition detection)
166+
self.k8s_api_reachable = True # K8s API connectivity (fail-open, see Fencing Safety Model)
166167

167168
# Caching
168169
self._host_servers_cache = {}
@@ -202,6 +203,7 @@ def __init__(self, config_manager: ConfigManager, cloud_client: Optional[OpenSta
202203
**State Management Notes**:
203204

204205
- `ready` flag starts `False` and is set to `True` after the first successful poll cycle. This drives the `/healthz` readiness probe endpoint — Kubernetes won't route traffic until the service has completed its first poll.
206+
- `k8s_api_reachable` starts `True` (fail-open). The background health check thread flips it to `False` after 3 consecutive failures. See [Fencing Safety Model](#fencing-safety-model) for the startup trade-off rationale.
205207
- `shutdown_event` is set by the SIGTERM handler to signal the main loop and all `wait()` calls to exit gracefully.
206208
- `kdump_lock` protects all kdump-related state since the UDP listener thread writes concurrently with the main poll loop.
207209
- `heartbeat_lock` similarly protects heartbeat state from concurrent UDP listener writes.
@@ -674,6 +676,93 @@ The `disabled_reason` field tracks evacuation state:
674676

675677
---
676678

679+
## Fencing Safety Model
680+
681+
InstanceHA uses a two-tier safety model to prevent false-positive fencing. Each gate answers a distinct question — there is no redundancy between them.
682+
683+
### Gate Chain
684+
685+
When `CHECK_HEARTBEAT=true` (recommended for production):
686+
687+
```
688+
Poll cycle starts
689+
690+
├─ Gate 1: K8s API reachable?
691+
│ Question: "Can we trust our own observations?"
692+
│ If no (3 consecutive failures) → skip entire cycle
693+
│ Rationale: if InstanceHA's worker node is network-isolated,
694+
│ Nova's service list may be stale — fencing would hit healthy hosts
695+
696+
├─ Gate 2: Per-host heartbeat
697+
│ Question: "Is this specific host actually down?"
698+
│ If host sending heartbeats within HEARTBEAT_TIMEOUT → skip that host
699+
│ Rationale: Nova reports host as down, but heartbeat proves the OS is alive —
700+
│ only nova-compute crashed, not the host
701+
702+
├─ Gate 3: Heartbeat cliff detection
703+
│ Question: "Did we lose visibility, or did hosts actually fail?"
704+
│ If >HEARTBEAT_CLIFF_THRESHOLD% of heartbeats vanish at once → skip ALL fencing
705+
│ Rationale: simultaneous mass heartbeat loss is more likely a listener-side
706+
│ network partition than N simultaneous host failures
707+
708+
├─ Gate 4: Kdump detection (optional, CHECK_KDUMP=true)
709+
│ Question: "Is this host crashing and dumping memory?"
710+
│ If kdump message received → fence immediately, skip power-on during recovery
711+
712+
├─ Gate 5: Global threshold
713+
│ Question: "Are too many hosts down to safely evacuate?"
714+
│ If failed percentage > THRESHOLD → skip all fencing
715+
716+
└─ Gate 6: Per-aggregate threshold
717+
Question: "Is this aggregate losing too many hosts?"
718+
If failed count > instanceha:max_failures metadata → skip hosts in that aggregate
719+
```
720+
721+
When `CHECK_HEARTBEAT=false`, gates 2 and 3 are not evaluated. A warning is logged at startup to alert operators that split-brain protection is limited to the K8s API check.
722+
723+
### Fail-Open vs Fail-Closed Semantics
724+
725+
| Gate | On error / no data | Rationale |
726+
|------|-------------------|-----------|
727+
| K8s API (startup) | Fail-open (allow fencing) | No data yet — blocking fencing on startup would silently prevent all evacuations until the health check thread runs |
728+
| K8s API (after 3 failures) | Fail-closed (block fencing) | Confirmed partition — fencing would hit healthy hosts |
729+
| Heartbeat (grace period) | Fail-open (allow fencing) | No heartbeat history in first `HEARTBEAT_TIMEOUT` seconds — cannot distinguish "no heartbeat" from "never received one" |
730+
| Heartbeat (steady state) | Fail-open (allow fencing) | No heartbeat = host is down = proceed with fencing |
731+
| Cliff detection | Fail-closed (block fencing) | Mass heartbeat loss is treated as observer failure |
732+
733+
### Startup Race Window
734+
735+
`k8s_api_reachable` initializes to `True` before the background health check thread confirms K8s API connectivity. If the K8s API is genuinely unreachable at startup, fencing could proceed for up to ~45 seconds (3 failures × 15s `K8S_API_CHECK_INTERVAL`) before the check blocks it.
736+
737+
In practice this window is smaller: the health check thread starts during `_initialize_service()` and runs its first probe almost immediately, while the main loop still needs to complete `_establish_nova_connection()` and `_reconcile_orphaned_hosts()` before its first poll. The health check typically completes its first probe before the main loop is ready to fence.
738+
739+
This is a deliberate trade-off. The alternative (`k8s_api_reachable = False` at startup) caused a deadlock: if the health check thread failed to start or was delayed, fencing was silently blocked forever with no log output indicating the problem.
740+
741+
### Fencing Decision Log
742+
743+
Every poll cycle that reaches the fencing decision point emits a single summary line:
744+
745+
```
746+
INFO Fencing decision: 3 stale, 1 heartbeat-alive, 2 to fence, cliff=False
747+
```
748+
749+
This line is emitted in all cases — including when all hosts are filtered out by heartbeat or cliff detection — so operators can diagnose fencing decisions from a single log grep.
750+
751+
### Relationship to MedIK8s
752+
753+
InstanceHA and MedIK8s (NHC/FAR/SNR) operate at different layers with no overlap:
754+
755+
| Layer | Scope | What it handles |
756+
|-------|-------|-----------------|
757+
| MedIK8s | OpenShift cluster | Kubernetes node failures, pod recovery, node remediation |
758+
| InstanceHA | EDPM data plane | Compute host fencing, VM evacuation |
759+
760+
MedIK8s watches Kubernetes Node objects. EDPM compute nodes are **not** Kubernetes nodes — they are external bare-metal hosts managed outside the cluster. MedIK8s cannot detect or remediate EDPM compute node failures; InstanceHA handles those independently.
761+
762+
MedIK8s is relevant for control plane recovery (e.g., if the node running RabbitMQ or the InstanceHA pod itself fails). InstanceHA is relevant for data plane recovery (compute host failures requiring VM evacuation).
763+
764+
---
765+
677766
## Critical Services Validation
678767

679768
**Function**: `_check_critical_services(conn, services, compute_nodes)`
@@ -1286,11 +1375,12 @@ Minimum packet size: 45 bytes (4 magic + 8 timestamp + 1 hostname + 32 HMAC).
12861375
**Configuration**: `K8S_API_CHECK_INTERVAL: 15`
12871376
12881377
**Behavior**:
1289-
1. The health check thread pings the K8s API every `K8S_API_CHECK_INTERVAL` seconds
1290-
2. Any non-5xx response (status code < 500) is treated as reachable — the API is responding, even if RBAC denies the specific request
1291-
3. After 3 consecutive failures, `k8s_api_reachable` is set to `False` and `ready` is set to `False`
1292-
4. The main poll loop checks `k8s_api_reachable` before fencing — if `False`, the entire fencing cycle is skipped
1293-
5. When connectivity restores, `k8s_api_reachable` is set back to `True`; readiness is restored after the next successful Nova poll
1378+
1. `k8s_api_reachable` initializes to `True` (fail-open) — see [Fencing Safety Model](#fencing-safety-model) for the startup trade-off rationale
1379+
2. The health check thread pings the K8s API every `K8S_API_CHECK_INTERVAL` seconds
1380+
3. Any non-5xx response (status code < 500) is treated as reachable — the API is responding, even if RBAC denies the specific request
1381+
4. After 3 consecutive failures, `k8s_api_reachable` is set to `False` and `ready` is set to `False`
1382+
5. The main poll loop checks `k8s_api_reachable` before fencing — if `False`, the entire fencing cycle is skipped
1383+
6. When connectivity restores, `k8s_api_reachable` is set back to `True`; readiness is restored after the next successful Nova poll
12941384
12951385
**Prometheus**: `instanceha_k8s_api_reachable` gauge (1=ok, 0=isolated)
12961386

0 commit comments

Comments
 (0)