|
| 1 | +# Namespace Capacity Model |
| 2 | + |
| 3 | +This document records the per-instance resource footprint, the resulting namespace quota |
| 4 | +headroom, and the rationale for HPA `maxReplicas` and scale-up policy choices. |
| 5 | + |
| 6 | +## Background |
| 7 | + |
| 8 | +Shared OpenShift namespaces (e.g. `fd34fb-test`) apply a `ResourceQuota` that is |
| 9 | +shared across every instance deployed to that namespace. When HPAs across multiple |
| 10 | +instances scale up simultaneously they can collectively exhaust the quota ceiling, |
| 11 | +causing new pods to stay `Pending` (`FailedScheduling`) and rollouts to stall. |
| 12 | + |
| 13 | +Observed quota on `fd34fb-test` (as of 2026-07-17): |
| 14 | + |
| 15 | +| Resource | Used | Limit | % | |
| 16 | +|---|---|---|---| |
| 17 | +| `requests.cpu` | 3230m | 4000m | 80.75% | |
| 18 | +| `requests.memory` | 5736Mi | 16384Mi | 35% | |
| 19 | +| `requests.storage` | 64Gi | 64Gi | 100% | |
| 20 | + |
| 21 | +CPU is the binding constraint. Storage exhaustion is addressed separately (PVC sizing). |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Per-pod resource footprint |
| 26 | + |
| 27 | +Resources are **request** values, which count against the namespace `ResourceQuota`. |
| 28 | +Limits are not shown here; see individual deployment manifests. |
| 29 | + |
| 30 | +### backend-services pod |
| 31 | + |
| 32 | +| Container | CPU req | Memory req | |
| 33 | +|---|---|---| |
| 34 | +| `backend-services` | 100m | 1Gi (1024Mi) | |
| 35 | +| `logrotate` | 10m | 32Mi | |
| 36 | +| `promtail` | 50m | 64Mi | |
| 37 | +| **Pod total** | **160m** | **1120Mi** | |
| 38 | + |
| 39 | +### temporal-worker pod |
| 40 | + |
| 41 | +| Container | CPU req | Memory req | |
| 42 | +|---|---|---| |
| 43 | +| `temporal-worker` | 100m | 768Mi | |
| 44 | +| `logrotate` | 10m | 32Mi | |
| 45 | +| `promtail` | 50m | 64Mi | |
| 46 | +| **Pod total** | **160m** | **864Mi** | |
| 47 | + |
| 48 | +### temporal-server pod (2 replicas, no HPA — fixed cost) |
| 49 | + |
| 50 | +| Container | CPU req | Memory req | |
| 51 | +|---|---|---| |
| 52 | +| `temporal` | 250m | 512Mi | |
| 53 | +| `promtail` | 50m | 32Mi | |
| 54 | +| **Pod total** | **300m** | **544Mi** | |
| 55 | + |
| 56 | +### temporal-ui pod (1 replica, no HPA — fixed cost) |
| 57 | + |
| 58 | +| Container | CPU req | Memory req | |
| 59 | +|---|---|---| |
| 60 | +| `temporal-ui` | 50m | 128Mi | |
| 61 | +| **Pod total** | **50m** | **128Mi** | |
| 62 | + |
| 63 | +### frontend pod |
| 64 | + |
| 65 | +| Container | CPU req | Memory req | |
| 66 | +|---|---|---| |
| 67 | +| `frontend` | 50m | 128Mi | |
| 68 | +| `promtail` | 50m | 32Mi | |
| 69 | +| **Pod total** | **100m** | **160Mi** | |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Per-instance footprint |
| 74 | + |
| 75 | +### At minimum replicas (steady-state floor) |
| 76 | + |
| 77 | +| Deployment | Replicas | CPU | Memory | |
| 78 | +|---|---|---|---| |
| 79 | +| backend-services | 2 | 320m | 2240Mi | |
| 80 | +| temporal-worker | 2 | 320m | 1728Mi | |
| 81 | +| temporal-server | 2 (fixed) | 600m | 1088Mi | |
| 82 | +| temporal-ui | 1 (fixed) | 50m | 128Mi | |
| 83 | +| frontend | 2 | 200m | 320Mi | |
| 84 | +| **Instance total (min)** | **9** | **1490m** | **5504Mi** | |
| 85 | + |
| 86 | +### At maximum replicas (HPA ceiling) |
| 87 | + |
| 88 | +| Deployment | Replicas | CPU | Memory | |
| 89 | +|---|---|---|---| |
| 90 | +| backend-services | 3 | 480m | 3360Mi | |
| 91 | +| temporal-worker | 3 | 480m | 2592Mi | |
| 92 | +| temporal-server | 2 (fixed) | 600m | 1088Mi | |
| 93 | +| temporal-ui | 1 (fixed) | 50m | 128Mi | |
| 94 | +| frontend | 3 | 300m | 480Mi | |
| 95 | +| **Instance total (max)** | **12** | **1910m** | **7648Mi** | |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## Namespace headroom model (fd34fb-test: 4000m CPU / 16384Mi memory) |
| 100 | + |
| 101 | +| Scenario | CPU used | CPU % | Memory used | Memory % | |
| 102 | +|---|---|---|---|---| |
| 103 | +| 1 instance @ min | 1490m | 37% | 5504Mi | 34% | |
| 104 | +| 1 instance @ max | 1910m | 48% | 7648Mi | 47% | |
| 105 | +| **2 instances @ min** | **2980m** | **75%** | **11008Mi** | **67%** | |
| 106 | +| 2 instances @ max | 3820m | 96% | 15296Mi | 93% | |
| 107 | +| 3 instances @ min | 4470m | **112% — EXCEEDS CPU QUOTA** | 16512Mi | **101% — EXCEEDS MEM QUOTA** | |
| 108 | + |
| 109 | +**Supported configuration: 2 instances per shared namespace.** |
| 110 | + |
| 111 | +- At **min replicas** both instances consume **75% CPU and 67% memory**, leaving headroom |
| 112 | + for rolling-update surge pods during deployments. |
| 113 | +- At **max replicas** both instances consume **96% CPU and 93% memory**. This worst case |
| 114 | + requires both instances to be at full HPA ceiling simultaneously. Under correctly-sized |
| 115 | + requests (backend ~72% at steady state, worker ~73%), the memory HPA metric will not |
| 116 | + trigger scale-out at normal load — scale-out will only occur under genuine CPU load spikes. |
| 117 | + The max scenario is therefore only expected during benchmark or load-test runs. |
| 118 | +- **3 instances cannot fit** within either the CPU or memory quota even at minimum replicas. |
| 119 | + |
| 120 | +> **Rolling-update surge budget:** each rolling deployment adds up to 1 extra pod |
| 121 | +> (`maxSurge: 1`). During a deploy, a worst-case surge across all 5 HPA-managed deployments |
| 122 | +> in 2 instances is 10 extra pods ≈ 1400m additional CPU requests. Because the base |
| 123 | +> is at 2980m (75%), there is only 1020m spare — enough for a controlled 2-at-a-time |
| 124 | +> deploy but not all 5 services simultaneously. Stagger rollouts when possible. |
| 125 | +
|
| 126 | +--- |
| 127 | + |
| 128 | +## HPA settings rationale |
| 129 | + |
| 130 | +### maxReplicas reduction |
| 131 | + |
| 132 | +Previous values (backend 5, worker 4, frontend 4) allowed a single instance to reach |
| 133 | +2490m CPU. With two instances at peak, aggregate demand was 4980m — 24% above the 4000m |
| 134 | +quota ceiling, which caused new pods to remain `Pending`. |
| 135 | + |
| 136 | +New values (all capped at 3) reduce the per-instance peak to 1910m. Two-instance |
| 137 | +aggregate at peak: 3820m (96%). |
| 138 | + |
| 139 | +### Target utilization raised to 80% |
| 140 | + |
| 141 | +Previous targets (CPU 70–75%) caused HPAs to trigger scale-outs at relatively low actual |
| 142 | +load, consuming namespace headroom that neighbour instances needed. Raising to 80% means |
| 143 | +scale-out only when pods are genuinely busy. |
| 144 | + |
| 145 | +### Scale-up stabilization windows extended |
| 146 | + |
| 147 | +| HPA | Old window | New window | Rationale | |
| 148 | +|---|---|---|---| |
| 149 | +| backend-services | 60 s | 120 s | Prevents reacting to sub-minute CPU spikes (e.g. auth token validation bursts) | |
| 150 | +| temporal-worker | 120 s | 180 s | Workers handle long-running activities; short-lived load spikes do not warrant a new pod | |
| 151 | +| frontend | 60 s | 120 s | Static nginx; load spikes are almost always cache misses, not sustained | |
| 152 | + |
| 153 | +### Scale-up rate limited to 1 pod / 60 s |
| 154 | + |
| 155 | +Previous backend-services and frontend policies allowed doubling pod count every 30 s |
| 156 | +(100% per 30 s). Replacing with `max(50% per 60 s, 1 pod per 60 s)` makes scaling |
| 157 | +additive at one pod per minute, keeping aggregate namespace CPU growth predictable. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## Database connection headroom |
| 162 | + |
| 163 | +With `maxReplicas: 3` for both backend-services and temporal-worker: |
| 164 | + |
| 165 | +| Service | Max pods | DB_POOL_MAX | Max connections | |
| 166 | +|---|---|---|---| |
| 167 | +| backend-services | 3 | 10 | 30 | |
| 168 | +| temporal-worker | 3 | 3 | 9 | |
| 169 | +| **Total** | | | **39** | |
| 170 | + |
| 171 | +This is well within the Crunchy Postgres default `max_connections = 100`, leaving |
| 172 | +headroom for migrations, monitoring tools, and pgAdmin. |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Storage quota (separate concern) |
| 177 | + |
| 178 | +The `fd34fb-test` `storage-quota` of 64Gi is 100% consumed independently of compute |
| 179 | +scaling. Root causes include: |
| 180 | + |
| 181 | +- CrunchyDB WAL and backup PVCs (default `PG_BACKUP_STORAGE_SIZE=10Gi`) |
| 182 | +- MinIO document storage PVCs |
| 183 | + |
| 184 | +See `docs-md/operations/ENVIRONMENT_CONFIGURATION.md` for `PG_BACKUP_STORAGE_SIZE` |
| 185 | +guidance. Reduce to `2Gi` for short-lived test instances. Storage quota exhaustion |
| 186 | +is an operational concern and does not interact with HPA behaviour. |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## Monitoring commands |
| 191 | + |
| 192 | +```bash |
| 193 | +# Quota utilisation |
| 194 | +oc describe resourcequota -n <namespace> |
| 195 | + |
| 196 | +# HPA status across all instances |
| 197 | +oc get hpa -n <namespace> |
| 198 | + |
| 199 | +# Detailed HPA events (shows scale decisions) |
| 200 | +oc describe hpa <instance>-backend-services -n <namespace> |
| 201 | +oc describe hpa <instance>-temporal-worker -n <namespace> |
| 202 | +oc describe hpa <instance>-frontend -n <namespace> |
| 203 | + |
| 204 | +# Current pod CPU/memory vs requests |
| 205 | +oc adm top pods -n <namespace> --containers |
| 206 | +``` |
0 commit comments