RAPPS-WP-0014-T04: record isolated target-hardware Vergabe replay

Throwaway namespace on railiance01 ran the deployed digest (3 workers,
60m/1000m CPU, 256Mi/1Gi) against fresh PostgreSQL 16 with the phased
synthetic profile: ordinary p95 0.192s, transfer p95 0.076s, zero errors.
Raw cgroup/node/Prometheus samples, manifests, driver and a bounded
recommendation retained; namespace torn down; T03 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 8340@bnt-lap001
Assistant-Session: 2e192b3d-b3e8-4403-94d4-d55a2c8b476c
This commit is contained in:
tegwick 2026-10-21 08:38:12 +02:00
parent 3851865c87
commit 1af6d828f5
26 changed files with 2590 additions and 3 deletions

View file

@ -0,0 +1,155 @@
# Vergabe target-hardware synthetic replay — 2026-10-21
Task: `RAPPS-WP-0014-T04` (feeds `CUST-WP-0071-T02`, `RESOURCE-WP-0008-T02`).
Authority: founder decision of 2026-10-21
(`the-custodian/docs/decisions/2026-10-21-cust-wp-0071-delivery-interview.md`):
isolated replay in a throwaway namespace on railiance01, reads elsewhere only.
No customer data was read or written; the live `vergabe-demo-company` deployment,
its database (`apps-pg`), quotas, policies and nodes were not modified (live
Deployment generation 4, same digest, Ready 1/1 before and after). No Secret was
read. This is synthetic sizing evidence, not human/recovery acceptance (T03).
## Deployed release (read-only capture, 2026-10-21)
| Item | Value |
| --- | --- |
| Cluster / node | context `default` → 92.205.62.239, single k3s v1.35.1 control-plane node |
| Node CPU / memory | 4 vCPU `AMD EPYC Processor (with IBPB)`, 16 GiB; requests 3605m (90%) / limits 37500m; reservation residual 395m |
| Image | `forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91` (source `c067993`, per `docs/vergabe-demo-company-binding.md`) |
| Chart / rollout | `vergabe-teilnahme-0.2.1`, one replica, Recreate, init `migrate`; Deployment revision 4 (rollout restart 2026-09-25). Helm release revision not re-read (release storage is a Secret) |
| Gunicorn | no command override → image CMD `--workers 3` (sync), default 30s timeout |
| Resources (app and migrate) | requests 60m CPU / 256Mi, limits 1000m CPU / 1Gi |
| Settings | `vergabe_teilnahme.settings.prod`, SSO via NetKingdom, `envFrom` Secret reference `vergabe-demo-env` (not read) |
| Database binding | namespace label `railiance.io/postgres-client=apps-pg`: shared CNPG `databases/apps-pg`, PostgreSQL 16, 100m/256Mi request, 1000m/1Gi limit, WAL archiving + `wal_level=logical` |
| Storage | media 5Gi and app-state 1Gi PVCs, `local-path` |
Drift vs. the 2026-10-19 local test image (`vergabe-synthetic:local-11f7c8a`,
`sha256:57da46…`): source `11f7c8a` is 16 commits ahead of deployed `c067993`;
9 application files differ (+181/−10: ausschreibungen forms/views/templates,
core views, search partial); Dockerfile, `uv.lock`, `pyproject.toml` identical.
The local runs used 2 workers; the deployed image runs 3. This replay used the
deployed digest itself, so the drift does not affect these results.
## Replay configuration (pinned)
Namespace `vergabe-sizing-20261021` (manifests in `harness/bench.yaml`,
`harness/driver.yaml`), all pods on the same node:
- **app**: deployed digest, image CMD (3 workers), init `migrate`, 60m/256Mi request,
1000m/1Gi limit, live securityContext/probes, Recreate; media on a fresh
`local-path` PVC, issue state emptyDir. Settings = harness synthetic settings
(`harness/synthetic_settings.py`: base settings, SSO off, password login).
- **db**: fresh `docker.io/library/postgres@sha256:4b7183ac…` (PostgreSQL 16),
emptyDir, 100m/256Mi request, 1000m/1Gi limit (matching apps-pg resources).
- **driver**: deployed digest running `vergabe-teilnahme/scripts/synthetic_pilot.py`
`runtime: remote` (commit `dca6683`; executed copy and config retained, digests
`c8da867f…` / `9045f2c7…` match `raw/result.json.gz`), 100m request/1000m limit.
In-cluster HTTP via ClusterIP Service — chosen over port-forward so latency
excludes the WSL/SSH tunnel. Restart deletes the app pod through a namespaced
Role (pods get/list/delete) and waits for a different ready pod.
- NetworkPolicy: ingress/egress within the namespace, DNS, API server only;
preflight confirmed the live Service was unreachable.
- Profile `config/synthetic-pilot-phases.json` unchanged except runtime fields
(`synthetic-pilot-target-remote.json`): 2 users, 100 seed tenders, 1 MiB
documents, 30s idle, 90s ordinary (0.2s think), 30s burst (0 think), restart,
60s recovery; 11 named operations including edits.
- Sampling: 1s cgroup v2 `cpu.stat`/`memory.current`/`cpu.pressure` inside app,
db and driver containers plus node-wide `/proc/stat` (`harness/sampler.sh`);
`kubectl top` every 15s; Prometheus (cadvisor) per-namespace CPU/throttling.
The sampler shell itself costs ~11–12m per container and is included.
Timeline (UTC): namespace created 06:26; pre-run idle baseline 06:27:14–06:29:26;
harness idle 06:29:31–06:30:01; ordinary 06:30:01–06:31:32; burst –06:32:03;
restart –06:32:22 (18.98s to ready incl. migrate); recovery –06:33:23; post
idle –06:34:28; namespace deletion requested 06:35:25, completed 06:36:18; namespace and its PV verified absent (`raw/teardown.txt`).
## Results
Driver: complete, `provisional_pass: true`, `measurement_complete: true`, no gaps.
| Phase | Ordinary req / err / p95 | Transfer req / err / p95 |
| --- | --- | --- |
| ordinary | 1903 / 0 / 0.142s | 422 / 0 / 0.085s |
| burst | 697 / 0 / 0.192s | 154 / 0 / 0.069s |
| recovery | 1075 / 0 / 0.214s | 238 / 0 / 0.073s |
| **all** | **3675 / 0 / 0.192s** | **814 / 0 / 0.076s** |
Per operation p95 (all phases): list 0.262s, search 0.255s, detail 0.146s,
creates/edits 0.059–0.066s, upload 0.088s, download 0.050s; login 1.27s and
login form 0.62s (n=6, first requests after idle). ~24.7 requests/s closed loop —
far above two human users' pace.
| Window | App CPU mean / p95 / peak | Throttled periods | App memory peak | DB CPU mean / p95 | Node busy mean (free mean / free p5) |
| --- | --- | --- | --- | --- | --- |
| pre-run baseline | 12m / 15m / 16m | 0% | 166 MiB | 22m / 66m | 1348m (2652m / 1499m) |
| ordinary | 711m / 845m / 1003m | 20.4% | 403 MiB | 388m / 501m | 2979m (1021m / 78m) |
| burst | 953m / 1004m / 1011m | 65.8% | 482 MiB | 430m / 539m | 2806m (1194m / 278m) |
| recovery | 861m / 957m / 982m | 41.5% | 322 MiB | 335m / 399m | 2624m (1376m / 392m) |
| all load phases | 775m / 998m / 1020m | 35.7% (22.8s throttled) | 483 MiB | 341m / 500m | 2790m (1210m / 147m) |
| post | 12m | 0% | 323 MiB | 22m | 1413m (2587m) |
App memory is cgroup `memory.current` (includes page cache); no OOM, no restarts.
CPU pressure (PSI some) on the app was 15–21% of wall time under load.
Incremental PostgreSQL CPU ≈ 320m mean / ~480m p95 over its idle baseline, on
the isolated DB only (live `apps-pg` stayed at ~8m during the window). Driver
added ~84m mean.
Competing workloads (Prometheus, workload window): all containers 1622m mean, of
which the replay namespace 915m; others led by databases 121m, state-hub 120m,
telemetry 102m, forgejo 78m (max 385m), activity-core 59m, core-hub 43m; live
vergabe ~0m. Node-wide busy includes ~0.7 core of k3s/system outside containers.
Pre-run competing load left ~2.65 cores free on average; during the replay the
node reached near-saturation at peaks (free p5 ~150m) largely through the replay
itself (app + db + driver ≈ 1.2 cores). Requests during the run were 3865m of
4000m (replay 260m of the 395m residual, released at teardown).
## Coverage and gaps
- Representative: exact deployed image/digest, worker count, CPU/memory
requests and limits (CFS quota 100000/100000 verified), Recreate + init migrate
restart path, target hardware and real shared node contention.
- Not representative: synthetic settings instead of prod (no SSO/OIDC, insecure
cookies, no whitenoise manifest storage); no Traefik ingress/TLS/browser; DB is a
standalone PostgreSQL 16 pod, not shared CNPG `apps-pg` (WAL archiving, logical
WAL, other tenants) — the ~320m incremental DB demand would land on apps-pg's
1000m limit/100m request in production and is not proven there.
- Load is a synthetic closed loop (~25 req/s), not observed human demand; one run
with ~3.5 min of load (pinned profile), ~9 min namespace lifetime.
- Node contention was partly self-induced; contention from other tenants was
light at this hour. A busier moment (forgejo CI, backups) is not covered.
- The 60m request is a scheduling weight/reservation, not a quota; the 1000m
limit is the quota that throttled. Sampler exec overhead is in the app cgroup.
## Bounded recommendation (for RESOURCE-WP-0008-T02 / CUST-WP-0071-T02)
- **CPU limit 1000m: keep.** Under an aggressive synthetic mix it throttles
(36% of periods, 66% in burst) yet ordinary p95 stays ≤0.22s per phase — ~9×
margin to the 2s target, zero failed operations, including restart recovery.
- **CPU request 60m: keep for the two-user invited pilot**, provisionally. Even
with the node near saturation and only a 60m weight, latency stayed far inside
target; two humans generate a small fraction of this load. Raising it (e.g.
250m) is not required by these results and would consume most of the 395m
residual — an allocation-owner decision, not made here.
- **Memory limit 1Gi: keep** (peak 483 MiB incl. page cache, ~2× margin).
**Memory request 256Mi: keep, consider 512Mi** at the next allocation review,
since loaded working set (320–480 MiB) exceeds the request; node memory requests
are 61%, so this is low-risk but not urgent.
- **Database**: budget ~0.3–0.5 core incremental on apps-pg at this synthetic
rate; acceptable against its 1000m limit for a two-user pilot, unverified on the
shared cluster.
- **Revisit when**: user count/companies or corpus grow, image or worker count
changes, live p95 >1s or any 5xx, node requests >95% or sustained node free CPU
<500m outside benchmarks, apps-pg CPU >600m sustained, or the pilot moves to
railiance02/other hardware.
## Files
- `analysis.json` — per-window cgroup statistics, per-operation p95, phase results
(`harness/analyze.py raw/ analysis.json` after gunzip)
- `raw/result.json.gz` — driver output (all operations, phase UTC windows, host facts)
- `raw/{app,db,driver}-cgroup.txt.gz` — 1s cgroup/node samples (`POD` lines mark
the restart re-attach; the `exit code 137` exec termination is that restart)
- `raw/kubectl-top.txt.gz`, `raw/prometheus-{baseline,workload}.json.gz`
- `raw/windows.txt`, `raw/pod-provenance.txt`, `raw/namespace-events.txt`,
`raw/teardown.txt`, `raw/driver-stdout.txt`
- `harness/` — manifests, settings, samplers, orchestrator, executed driver/config

View file

@ -0,0 +1,923 @@
{
"windows_utc": {
"baseline_pre": [
"2026-10-21T06:27:14.447540Z",
"2026-10-21T06:29:26.175778Z"
],
"harness_idle": [
"2026-10-21T06:29:31.522662Z",
"2026-10-21T06:30:01.522800Z"
],
"ordinary": [
"2026-10-21T06:30:01.523056Z",
"2026-10-21T06:31:32.589152Z"
],
"burst": [
"2026-10-21T06:31:32.589194Z",
"2026-10-21T06:32:03.275085Z"
],
"restart": [
"2026-10-21T06:32:03.275117Z",
"2026-10-21T06:32:22.256585Z"
],
"recovery": [
"2026-10-21T06:32:22.256609Z",
"2026-10-21T06:33:23.057045Z"
],
"workload_all_load_phases": [
"2026-10-21T06:30:01.523056Z",
"2026-10-21T06:33:23.057045Z"
],
"post": [
"2026-10-21T06:33:22.885983Z",
"2026-10-21T06:34:28.753426Z"
]
},
"cgroup": {
"app": {
"pods": [
"vergabe-app-579448bf49-2fsn2",
"vergabe-app-579448bf49-plp4j"
],
"baseline_pre": {
"samples": 126,
"seconds": 128.0,
"cpu_mean_m": 12.2,
"cpu_p95_m": 14.7,
"cpu_peak_m": 16.2,
"cfs_periods": 317,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 165.6,
"mem_mean_mib": 165.2,
"node_busy_mean_m": 1348,
"node_busy_p95_m": 2501,
"node_free_mean_m": 2652,
"node_free_p5_m": 1499,
"load1_max": 4.91
},
"harness_idle": {
"samples": 28,
"seconds": 28.4,
"cpu_mean_m": 11.5,
"cpu_p95_m": 13.4,
"cpu_peak_m": 13.6,
"cfs_periods": 76,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 165.3,
"mem_mean_mib": 165.1,
"node_busy_mean_m": 1538,
"node_busy_p95_m": 2800,
"node_free_mean_m": 2462,
"node_free_p5_m": 1200,
"load1_max": 2.91
},
"ordinary": {
"samples": 88,
"seconds": 90.2,
"cpu_mean_m": 711.4,
"cpu_p95_m": 845.4,
"cpu_peak_m": 1003.2,
"cfs_periods": 902,
"cfs_throttled_periods": 184,
"throttle_ratio": 0.204,
"throttled_seconds": 4.85,
"psi_cpu_some_share": 0.15,
"mem_peak_mib": 402.5,
"mem_mean_mib": 295.2,
"node_busy_mean_m": 2979,
"node_busy_p95_m": 3922,
"node_free_mean_m": 1021,
"node_free_p5_m": 78,
"load1_max": 7.83
},
"burst": {
"samples": 29,
"seconds": 29.9,
"cpu_mean_m": 952.9,
"cpu_p95_m": 1003.7,
"cpu_peak_m": 1011.1,
"cfs_periods": 298,
"cfs_throttled_periods": 196,
"throttle_ratio": 0.658,
"throttled_seconds": 8.94,
"psi_cpu_some_share": 0.211,
"mem_peak_mib": 481.8,
"mem_mean_mib": 443.4,
"node_busy_mean_m": 2806,
"node_busy_p95_m": 3722,
"node_free_mean_m": 1194,
"node_free_p5_m": 278,
"load1_max": 8.19
},
"restart": {
"samples": 10,
"seconds": 10.3,
"cpu_mean_m": 403.3,
"cpu_p95_m": 1019.7,
"cpu_peak_m": 1019.7,
"cfs_periods": 54,
"cfs_throttled_periods": 39,
"throttle_ratio": 0.722,
"throttled_seconds": 2.37,
"psi_cpu_some_share": 0.235,
"mem_peak_mib": 483.3,
"mem_mean_mib": 154.0,
"node_busy_mean_m": 1908,
"node_busy_p95_m": 3359,
"node_free_mean_m": 2092,
"node_free_p5_m": 641,
"load1_max": 7.84
},
"recovery": {
"samples": 59,
"seconds": 60.6,
"cpu_mean_m": 860.6,
"cpu_p95_m": 956.8,
"cpu_peak_m": 982.3,
"cfs_periods": 605,
"cfs_throttled_periods": 251,
"throttle_ratio": 0.415,
"throttled_seconds": 6.17,
"psi_cpu_some_share": 0.152,
"mem_peak_mib": 322.2,
"mem_mean_mib": 251.0,
"node_busy_mean_m": 2624,
"node_busy_p95_m": 3608,
"node_free_mean_m": 1376,
"node_free_p5_m": 392,
"load1_max": 6.66
},
"workload_all_load_phases": {
"samples": 189,
"seconds": 194.0,
"cpu_mean_m": 775.3,
"cpu_p95_m": 998.3,
"cpu_peak_m": 1019.7,
"cfs_periods": 1889,
"cfs_throttled_periods": 675,
"throttle_ratio": 0.357,
"throttled_seconds": 22.77,
"psi_cpu_some_share": 0.166,
"mem_peak_mib": 483.3,
"mem_mean_mib": 295.7,
"node_busy_mean_m": 2790,
"node_busy_p95_m": 3853,
"node_free_mean_m": 1210,
"node_free_p5_m": 147,
"load1_max": 8.19
},
"post": {
"samples": 63,
"seconds": 63.9,
"cpu_mean_m": 12.3,
"cpu_p95_m": 14.3,
"cpu_peak_m": 15.1,
"cfs_periods": 161,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.001,
"mem_peak_mib": 322.5,
"mem_mean_mib": 322.2,
"node_busy_mean_m": 1413,
"node_busy_p95_m": 2414,
"node_free_mean_m": 2587,
"node_free_p5_m": 1586,
"load1_max": 5.75
}
},
"db": {
"pods": [
"vergabe-db-7c4bffdd48-w4jz4"
],
"baseline_pre": {
"samples": 127,
"seconds": 128.5,
"cpu_mean_m": 21.5,
"cpu_p95_m": 65.7,
"cpu_peak_m": 71.7,
"cfs_periods": 333,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 91.3,
"mem_mean_mib": 90.4,
"node_busy_mean_m": 1346,
"node_busy_p95_m": 2448,
"node_free_mean_m": 2654,
"node_free_p5_m": 1552,
"load1_max": 4.91
},
"harness_idle": {
"samples": 29,
"seconds": 29.3,
"cpu_mean_m": 20.9,
"cpu_p95_m": 64.5,
"cpu_peak_m": 64.9,
"cfs_periods": 75,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.003,
"mem_peak_mib": 93.0,
"mem_mean_mib": 92.7,
"node_busy_mean_m": 1522,
"node_busy_p95_m": 2680,
"node_free_mean_m": 2478,
"node_free_p5_m": 1320,
"load1_max": 3.15
},
"ordinary": {
"samples": 88,
"seconds": 89.9,
"cpu_mean_m": 387.8,
"cpu_p95_m": 501.1,
"cpu_peak_m": 532.2,
"cfs_periods": 892,
"cfs_throttled_periods": 2,
"throttle_ratio": 0.002,
"throttled_seconds": 0.02,
"psi_cpu_some_share": 0.067,
"mem_peak_mib": 101.4,
"mem_mean_mib": 95.7,
"node_busy_mean_m": 2974,
"node_busy_p95_m": 3895,
"node_free_mean_m": 1026,
"node_free_p5_m": 105,
"load1_max": 8.19
},
"burst": {
"samples": 29,
"seconds": 29.6,
"cpu_mean_m": 430.1,
"cpu_p95_m": 539.1,
"cpu_peak_m": 544.4,
"cfs_periods": 293,
"cfs_throttled_periods": 2,
"throttle_ratio": 0.007,
"throttled_seconds": 0.06,
"psi_cpu_some_share": 0.062,
"mem_peak_mib": 113.1,
"mem_mean_mib": 97.1,
"node_busy_mean_m": 2798,
"node_busy_p95_m": 3664,
"node_free_mean_m": 1202,
"node_free_p5_m": 336,
"load1_max": 8.19
},
"restart": {
"samples": 18,
"seconds": 18.3,
"cpu_mean_m": 24.3,
"cpu_p95_m": 67.7,
"cpu_peak_m": 67.7,
"cfs_periods": 55,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.009,
"mem_peak_mib": 103.3,
"mem_mean_mib": 101.4,
"node_busy_mean_m": 2650,
"node_busy_p95_m": 3961,
"node_free_mean_m": 1350,
"node_free_p5_m": 39,
"load1_max": 7.84
},
"recovery": {
"samples": 58,
"seconds": 58.9,
"cpu_mean_m": 335.0,
"cpu_p95_m": 398.7,
"cpu_peak_m": 408.1,
"cfs_periods": 581,
"cfs_throttled_periods": 1,
"throttle_ratio": 0.002,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.041,
"mem_peak_mib": 111.3,
"mem_mean_mib": 106.9,
"node_busy_mean_m": 2621,
"node_busy_p95_m": 3524,
"node_free_mean_m": 1379,
"node_free_p5_m": 476,
"load1_max": 6.66
},
"workload_all_load_phases": {
"samples": 196,
"seconds": 199.8,
"cpu_mean_m": 340.8,
"cpu_p95_m": 500.0,
"cpu_peak_m": 544.4,
"cfs_periods": 1837,
"cfs_throttled_periods": 5,
"throttle_ratio": 0.003,
"throttled_seconds": 0.08,
"psi_cpu_some_share": 0.052,
"mem_peak_mib": 113.1,
"mem_mean_mib": 99.8,
"node_busy_mean_m": 2813,
"node_busy_p95_m": 3894,
"node_free_mean_m": 1187,
"node_free_p5_m": 106,
"load1_max": 8.19
},
"post": {
"samples": 64,
"seconds": 64.8,
"cpu_mean_m": 21.7,
"cpu_p95_m": 65.1,
"cpu_peak_m": 79.6,
"cfs_periods": 161,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 101.3,
"mem_mean_mib": 98.1,
"node_busy_mean_m": 1420,
"node_busy_p95_m": 2509,
"node_free_mean_m": 2580,
"node_free_p5_m": 1491,
"load1_max": 5.75
}
},
"driver": {
"pods": [
"synthetic-driver"
],
"baseline_pre": {
"samples": 126,
"seconds": 127.9,
"cpu_mean_m": 11.6,
"cpu_p95_m": 13.2,
"cpu_peak_m": 13.7,
"cfs_periods": 267,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 6.6,
"mem_mean_mib": 6.3,
"node_busy_mean_m": 1348,
"node_busy_p95_m": 2510,
"node_free_mean_m": 2652,
"node_free_p5_m": 1490,
"load1_max": 4.91
},
"harness_idle": {
"samples": 28,
"seconds": 28.4,
"cpu_mean_m": 11.6,
"cpu_p95_m": 13.1,
"cpu_peak_m": 13.1,
"cfs_periods": 59,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 63.3,
"mem_mean_mib": 63.0,
"node_busy_mean_m": 1537,
"node_busy_p95_m": 2829,
"node_free_mean_m": 2463,
"node_free_p5_m": 1171,
"load1_max": 2.91
},
"ordinary": {
"samples": 88,
"seconds": 90.0,
"cpu_mean_m": 91.7,
"cpu_p95_m": 117.2,
"cpu_peak_m": 124.8,
"cfs_periods": 891,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.029,
"mem_peak_mib": 75.9,
"mem_mean_mib": 72.4,
"node_busy_mean_m": 2977,
"node_busy_p95_m": 3912,
"node_free_mean_m": 1023,
"node_free_p5_m": 88,
"load1_max": 7.83
},
"burst": {
"samples": 29,
"seconds": 29.6,
"cpu_mean_m": 98.5,
"cpu_p95_m": 124.8,
"cpu_peak_m": 129.3,
"cfs_periods": 289,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.026,
"mem_peak_mib": 78.4,
"mem_mean_mib": 76.4,
"node_busy_mean_m": 2790,
"node_busy_p95_m": 3387,
"node_free_mean_m": 1210,
"node_free_p5_m": 613,
"load1_max": 8.19
},
"restart": {
"samples": 18,
"seconds": 18.3,
"cpu_mean_m": 22.4,
"cpu_p95_m": 25.5,
"cpu_peak_m": 25.5,
"cfs_periods": 112,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.009,
"mem_peak_mib": 73.7,
"mem_mean_mib": 71.3,
"node_busy_mean_m": 2650,
"node_busy_p95_m": 3990,
"node_free_mean_m": 1350,
"node_free_p5_m": 10,
"load1_max": 7.84
},
"recovery": {
"samples": 59,
"seconds": 60.1,
"cpu_mean_m": 82.2,
"cpu_p95_m": 95.4,
"cpu_peak_m": 139.7,
"cfs_periods": 594,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.02,
"mem_peak_mib": 80.9,
"mem_mean_mib": 77.2,
"node_busy_mean_m": 2635,
"node_busy_p95_m": 3540,
"node_free_mean_m": 1365,
"node_free_p5_m": 460,
"load1_max": 6.66
},
"workload_all_load_phases": {
"samples": 197,
"seconds": 201.1,
"cpu_mean_m": 84.4,
"cpu_p95_m": 117.7,
"cpu_peak_m": 200.8,
"cfs_periods": 1911,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.024,
"mem_peak_mib": 80.9,
"mem_mean_mib": 74.4,
"node_busy_mean_m": 2820,
"node_busy_p95_m": 3912,
"node_free_mean_m": 1180,
"node_free_p5_m": 88,
"load1_max": 8.19
},
"post": {
"samples": 64,
"seconds": 64.9,
"cpu_mean_m": 16.3,
"cpu_p95_m": 13.2,
"cpu_peak_m": 317.3,
"cfs_periods": 137,
"cfs_throttled_periods": 0,
"throttle_ratio": 0.0,
"throttled_seconds": 0.0,
"psi_cpu_some_share": 0.002,
"mem_peak_mib": 79.1,
"mem_mean_mib": 19.2,
"node_busy_mean_m": 1423,
"node_busy_p95_m": 2535,
"node_free_mean_m": 2577,
"node_free_p5_m": 1465,
"load1_max": 5.75
}
}
},
"per_operation": {
"burst/detail": {
"n": 77,
"errors": 0,
"p95_s": 0.1132,
"max_s": 0.1706
},
"burst/document-download": {
"n": 77,
"errors": 0,
"p95_s": 0.0447,
"max_s": 0.0592
},
"burst/document-upload": {
"n": 77,
"errors": 0,
"p95_s": 0.075,
"max_s": 0.1055
},
"burst/list": {
"n": 77,
"errors": 0,
"p95_s": 0.2974,
"max_s": 0.3233
},
"burst/login": {
"n": 2,
"errors": 0,
"p95_s": 1.1775,
"max_s": 1.1775
},
"burst/login-form": {
"n": 2,
"errors": 0,
"p95_s": 0.0281,
"max_s": 0.0281
},
"burst/lot-create": {
"n": 77,
"errors": 0,
"p95_s": 0.0581,
"max_s": 0.0971
},
"burst/lot-edit": {
"n": 77,
"errors": 0,
"p95_s": 0.0536,
"max_s": 0.0743
},
"burst/search": {
"n": 77,
"errors": 0,
"p95_s": 0.2652,
"max_s": 0.3359
},
"burst/task-create": {
"n": 77,
"errors": 0,
"p95_s": 0.0599,
"max_s": 0.1778
},
"burst/task-edit": {
"n": 77,
"errors": 0,
"p95_s": 0.0597,
"max_s": 0.0759
},
"burst/tender-create": {
"n": 77,
"errors": 0,
"p95_s": 0.0772,
"max_s": 0.0845
},
"burst/tender-edit": {
"n": 77,
"errors": 0,
"p95_s": 0.0623,
"max_s": 0.0732
},
"ordinary/detail": {
"n": 211,
"errors": 0,
"p95_s": 0.1758,
"max_s": 0.3877
},
"ordinary/document-download": {
"n": 211,
"errors": 0,
"p95_s": 0.0537,
"max_s": 0.11
},
"ordinary/document-upload": {
"n": 211,
"errors": 0,
"p95_s": 0.1051,
"max_s": 0.4418
},
"ordinary/list": {
"n": 211,
"errors": 0,
"p95_s": 0.194,
"max_s": 0.3779
},
"ordinary/login": {
"n": 2,
"errors": 0,
"p95_s": 1.2574,
"max_s": 1.2574
},
"ordinary/login-form": {
"n": 2,
"errors": 0,
"p95_s": 0.3288,
"max_s": 0.3288
},
"ordinary/lot-create": {
"n": 211,
"errors": 0,
"p95_s": 0.0696,
"max_s": 0.1603
},
"ordinary/lot-edit": {
"n": 211,
"errors": 0,
"p95_s": 0.0763,
"max_s": 0.1017
},
"ordinary/search": {
"n": 211,
"errors": 0,
"p95_s": 0.2294,
"max_s": 0.6217
},
"ordinary/task-create": {
"n": 211,
"errors": 0,
"p95_s": 0.0697,
"max_s": 0.1338
},
"ordinary/task-edit": {
"n": 211,
"errors": 0,
"p95_s": 0.0686,
"max_s": 0.1146
},
"ordinary/tender-create": {
"n": 211,
"errors": 0,
"p95_s": 0.0626,
"max_s": 0.1187
},
"ordinary/tender-edit": {
"n": 211,
"errors": 0,
"p95_s": 0.0633,
"max_s": 0.1171
},
"recovery/detail": {
"n": 119,
"errors": 0,
"p95_s": 0.131,
"max_s": 0.1379
},
"recovery/document-download": {
"n": 119,
"errors": 0,
"p95_s": 0.0475,
"max_s": 0.0818
},
"recovery/document-upload": {
"n": 119,
"errors": 0,
"p95_s": 0.08,
"max_s": 0.1587
},
"recovery/list": {
"n": 119,
"errors": 0,
"p95_s": 0.3302,
"max_s": 0.3757
},
"recovery/login": {
"n": 2,
"errors": 0,
"p95_s": 1.2653,
"max_s": 1.2653
},
"recovery/login-form": {
"n": 2,
"errors": 0,
"p95_s": 0.6157,
"max_s": 0.6157
},
"recovery/lot-create": {
"n": 119,
"errors": 0,
"p95_s": 0.0526,
"max_s": 0.0945
},
"recovery/lot-edit": {
"n": 119,
"errors": 0,
"p95_s": 0.0602,
"max_s": 0.1186
},
"recovery/search": {
"n": 119,
"errors": 0,
"p95_s": 0.3193,
"max_s": 0.4237
},
"recovery/task-create": {
"n": 119,
"errors": 0,
"p95_s": 0.0558,
"max_s": 0.0888
},
"recovery/task-edit": {
"n": 119,
"errors": 0,
"p95_s": 0.0577,
"max_s": 0.0959
},
"recovery/tender-create": {
"n": 119,
"errors": 0,
"p95_s": 0.0489,
"max_s": 0.0661
},
"recovery/tender-edit": {
"n": 119,
"errors": 0,
"p95_s": 0.0556,
"max_s": 0.0753
}
},
"per_operation_all_phases": {
"detail": {
"n": 407,
"errors": 0,
"p95_s": 0.1463
},
"document-download": {
"n": 407,
"errors": 0,
"p95_s": 0.0498
},
"document-upload": {
"n": 407,
"errors": 0,
"p95_s": 0.0878
},
"list": {
"n": 407,
"errors": 0,
"p95_s": 0.2624
},
"login": {
"n": 6,
"errors": 0,
"p95_s": 1.2653
},
"login-form": {
"n": 6,
"errors": 0,
"p95_s": 0.6157
},
"lot-create": {
"n": 407,
"errors": 0,
"p95_s": 0.0613
},
"lot-edit": {
"n": 407,
"errors": 0,
"p95_s": 0.0621
},
"search": {
"n": 407,
"errors": 0,
"p95_s": 0.2553
},
"task-create": {
"n": 407,
"errors": 0,
"p95_s": 0.0621
},
"task-edit": {
"n": 407,
"errors": 0,
"p95_s": 0.0657
},
"tender-create": {
"n": 407,
"errors": 0,
"p95_s": 0.0661
},
"tender-edit": {
"n": 407,
"errors": 0,
"p95_s": 0.0587
}
},
"summary": {
"ordinary": {
"requests": 3675,
"errors": 0,
"p95_seconds": 0.19182809395715594
},
"document_transfer": {
"requests": 814,
"errors": 0,
"p95_seconds": 0.07621732295956463
},
"complete": true,
"provisional_pass": true,
"measurement_complete": true,
"measurement_gaps": [],
"startup_seconds": 0.04661138809751719,
"duration_seconds": 201.53947580303065,
"completed_iterations": {
"ordinary": [
106,
105
],
"burst": [
39,
38
],
"recovery": [
59,
60
]
},
"host": {
"logical_cpus": 4,
"affinity_cpus": 4,
"cpu_model": "AMD EPYC Processor (with IBPB)",
"memory_total": "MemTotal: 16377032 kB",
"cgroup_cpu_max": "100000 100000",
"app_cpu_quota": 1.0
}
},
"phase_results": [
{
"name": "ordinary",
"kind": "load",
"seconds": 91.06608834001236,
"started_at": "2026-10-21T06:30:01.523056+00:00",
"finished_at": "2026-10-21T06:31:32.589152+00:00",
"expected_requests": 2325,
"actual_requests": 2325,
"complete": true,
"ordinary": {
"requests": 1903,
"errors": 0,
"p95_seconds": 0.14162223995663226
},
"document_transfer": {
"requests": 422,
"errors": 0,
"p95_seconds": 0.08520571992266923
}
},
{
"name": "burst",
"kind": "load",
"seconds": 30.685886312043294,
"started_at": "2026-10-21T06:31:32.589194+00:00",
"finished_at": "2026-10-21T06:32:03.275085+00:00",
"expected_requests": 851,
"actual_requests": 851,
"complete": true,
"ordinary": {
"requests": 697,
"errors": 0,
"p95_seconds": 0.19206618797034025
},
"document_transfer": {
"requests": 154,
"errors": 0,
"p95_seconds": 0.06853019201662391
}
},
{
"name": "restart",
"kind": "restart",
"seconds": 18.981462653027847,
"started_at": "2026-10-21T06:32:03.275117+00:00",
"finished_at": "2026-10-21T06:32:22.256585+00:00",
"ready": true
},
{
"name": "recovery",
"kind": "load",
"seconds": 60.80042844603304,
"started_at": "2026-10-21T06:32:22.256609+00:00",
"finished_at": "2026-10-21T06:33:23.057045+00:00",
"expected_requests": 1313,
"actual_requests": 1313,
"complete": true,
"ordinary": {
"requests": 1075,
"errors": 0,
"p95_seconds": 0.21410848898813128
},
"document_transfer": {
"requests": 238,
"errors": 0,
"p95_seconds": 0.07255190098658204
}
}
]
}

View file

@ -0,0 +1,73 @@
import json, math, sys, gzip, collections
from datetime import datetime
R = sys.argv[1]
def ts(x): return datetime.fromisoformat(x.replace("Z", "+00:00").replace(".", ".", 1)[:26] + ("+00:00" if "+" not in x[19:] and not x.endswith("Z") else "")).timestamp() if False else _ts(x)
def _ts(x):
x = x.rstrip("Z")
if "+" in x[19:]: x = x[:x.index("+", 19)]
if "." in x: a, b = x.split("."); x = a + "." + b[:6]
return datetime.fromisoformat(x + "+00:00").timestamp()
def p95(v):
v = sorted(v); return v[max(0, math.ceil(0.95 * len(v)) - 1)] if v else None
def parse(path):
segs, cur, s = [], None, None
for line in open(path):
line = line.rstrip("\n")
if line.startswith("POD "): cur = {"pod": line.split()[1], "samples": []}; segs.append(cur)
elif line.startswith("S "):
s = {"t": float(line[2:])}; cur["samples"].append(s)
elif s is None: continue
elif line.startswith("M "): s["mem"] = int(line[2:])
elif line.startswith("P "): s["psi_some_total"] = int(line.split("total=")[1])
elif line.startswith("N "):
f = [int(x) for x in line.split()[2:]]; s["node_total"] = sum(f[:8]); s["node_idle"] = f[3] + f[4]
elif line.startswith("L "): s["load1"] = float(line.split()[1])
elif line.split() and line.split()[0] in ("usage_usec","nr_periods","nr_throttled","throttled_usec"):
k, v = line.split(); s[k] = int(v)
for seg in segs: seg["samples"] = [x for x in seg["samples"] if "mem" in x and "node_total" in x and "usage_usec" in x]
return segs
def window(segs, a, b, cores=4):
rates, mems, thr_p, per_p, thr_us, nodebusy, psi, load = [], [], 0, 0, 0, [], 0, []
span = 0; used = 0
for seg in segs:
xs = [x for x in seg["samples"] if a <= x["t"] <= b]
for x in xs: mems.append(x["mem"]); load.append(x["load1"])
for p, q in zip(xs, xs[1:]):
dt = q["t"] - p["t"]
if dt <= 0 or dt > 5: continue
du = q["usage_usec"] - p["usage_usec"]; used += du; span += dt
rates.append(du / 1e3 / dt)
per_p += q.get("nr_periods",0) - p.get("nr_periods",0); thr_p += q.get("nr_throttled",0) - p.get("nr_throttled",0)
thr_us += q.get("throttled_usec",0) - p.get("throttled_usec",0)
psi += q["psi_some_total"] - p["psi_some_total"]
tot = q["node_total"] - p["node_total"]; idle = q["node_idle"] - p["node_idle"]
if tot > 0: nodebusy.append(cores * (tot - idle) / tot * 1000)
if not rates: return None
return {"samples": len(rates), "seconds": round(span,1), "cpu_mean_m": round(used/1e3/span,1), "cpu_p95_m": round(p95(rates),1), "cpu_peak_m": round(max(rates),1),
"cfs_periods": per_p, "cfs_throttled_periods": thr_p, "throttle_ratio": round(thr_p/per_p,3) if per_p else None, "throttled_seconds": round(thr_us/1e6,2),
"psi_cpu_some_share": round(psi/1e6/span,3), "mem_peak_mib": round(max(mems)/2**20,1), "mem_mean_mib": round(sum(mems)/len(mems)/2**20,1),
"node_busy_mean_m": round(sum(nodebusy)/len(nodebusy)), "node_busy_p95_m": round(p95(nodebusy)), "node_free_mean_m": round(cores*1000-sum(nodebusy)/len(nodebusy)),
"node_free_p5_m": round(cores*1000-p95(nodebusy)), "load1_max": max(load)}
win = {}
for line in open(f"{R}/windows.txt"):
k, *v = line.split(); win[k] = v
res = json.load(open(f"{R}/result.json"))
W = {"baseline_pre": (_ts(win["baseline_start"][0]), _ts(win["driver_start"][0])),
"harness_idle": (_ts(res["idle_window"]["started_at"]), _ts(res["idle_window"]["finished_at"]))}
for p in res["phase_results"]: W[p["name"]] = (_ts(p["started_at"]), _ts(p["finished_at"]))
loads = [p for p in res["phase_results"] if p["kind"] == "load"]
W["workload_all_load_phases"] = (_ts(loads[0]["started_at"]), _ts(loads[-1]["finished_at"]))
W["post"] = (_ts(win["driver_exit"][1]), _ts(win["post_end"][0]))
out = {"windows_utc": {k: [datetime.utcfromtimestamp(a).isoformat()+"Z", datetime.utcfromtimestamp(b).isoformat()+"Z"] for k,(a,b) in W.items()}, "cgroup": {}}
for name in ("app", "db", "driver"):
segs = parse(f"{R}/{name}-cgroup.txt")
out["cgroup"][name] = {"pods": [s["pod"] for s in segs], **{k: window(segs, a, b) for k, (a, b) in W.items() if k != "workload_all_load_phases" or True}}
ops = collections.defaultdict(list)
for r in res["operations"]: ops[(r["phase"], r["operation"])].append(r)
out["per_operation"] = {f"{ph}/{op}": {"n": len(v), "errors": sum(not x["ok"] for x in v), "p95_s": round(p95([x["seconds"] for x in v]),4), "max_s": round(max(x["seconds"] for x in v),4)} for (ph, op), v in sorted(ops.items())}
allops = collections.defaultdict(list)
for r in res["operations"]: allops[r["operation"]].append(r)
out["per_operation_all_phases"] = {op: {"n": len(v), "errors": sum(not x["ok"] for x in v), "p95_s": round(p95([x["seconds"] for x in v]),4)} for op, v in sorted(allops.items())}
out["summary"] = {k: res[k] for k in ("ordinary","document_transfer","complete","provisional_pass","measurement_complete","measurement_gaps","startup_seconds","duration_seconds","completed_iterations","host")}
out["phase_results"] = [{k: v for k, v in p.items() if k != "operations"} for p in res["phase_results"]]
json.dump(out, open(sys.argv[2], "w"), indent=1)

View file

@ -0,0 +1,125 @@
apiVersion: v1
kind: Namespace
metadata:
name: vergabe-sizing-20261021
labels:
railiance.io/purpose: synthetic-sizing
railiance.io/task: rapps-wp-0014-t04
railiance.io/ephemeral: "true"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: bench-media, namespace: vergabe-sizing-20261021}
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources: {requests: {storage: 5Gi}}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: vergabe-db, namespace: vergabe-sizing-20261021}
spec:
replicas: 1
strategy: {type: Recreate}
selector: {matchLabels: {app: vergabe-sizing-db}}
template:
metadata: {labels: {app: vergabe-sizing-db}}
spec:
automountServiceAccountToken: false
containers:
- name: postgres
image: docker.io/library/postgres@sha256:4b7183ac05f8ef417db21fd72d71047a4238340c261d3cc3ddb6d579ab5071ae
imagePullPolicy: IfNotPresent
env:
- {name: POSTGRES_DB, value: synthetic}
- {name: POSTGRES_USER, value: synthetic}
- {name: POSTGRES_PASSWORD, value: synthetic-local-only}
- {name: PGDATA, value: /var/lib/postgresql/data/pgdata}
ports: [{containerPort: 5432}]
readinessProbe: {exec: {command: [pg_isready, -U, synthetic]}, periodSeconds: 5}
resources: {requests: {cpu: 100m, memory: 256Mi}, limits: {cpu: "1", memory: 1Gi}}
volumeMounts: [{name: data, mountPath: /var/lib/postgresql/data}]
volumes: [{name: data, emptyDir: {}}]
---
apiVersion: v1
kind: Service
metadata: {name: vergabe-db, namespace: vergabe-sizing-20261021}
spec: {selector: {app: vergabe-sizing-db}, ports: [{port: 5432, targetPort: 5432}]}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: vergabe-app, namespace: vergabe-sizing-20261021}
spec:
replicas: 1
strategy: {type: Recreate}
selector: {matchLabels: {app: vergabe-sizing-app}}
template:
metadata: {labels: {app: vergabe-sizing-app}}
spec:
automountServiceAccountToken: false
securityContext: {fsGroup: 999, runAsGroup: 999, runAsNonRoot: true, runAsUser: 999}
initContainers:
- name: migrate
image: forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91
imagePullPolicy: IfNotPresent
command: [python, manage.py, migrate, --noinput]
env: &env
- {name: DJANGO_SETTINGS_MODULE, value: synthetic_settings}
- {name: PYTHONPATH, value: /synthetic}
- {name: SECRET_KEY, value: synthetic-disposable-not-a-deployment-key}
- {name: DATABASE_URL, value: "postgres://synthetic:synthetic-local-only@vergabe-db:5432/synthetic"}
resources: &res {requests: {cpu: 60m, memory: 256Mi}, limits: {cpu: "1", memory: 1Gi}}
securityContext: &sc {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}, readOnlyRootFilesystem: false}
volumeMounts: [{name: settings, mountPath: /synthetic}]
containers:
- name: app
image: forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91
imagePullPolicy: IfNotPresent
env: *env
ports: [{containerPort: 8000, name: http}]
readinessProbe: {httpGet: {path: /health/, port: 8000, httpHeaders: [{name: Host, value: vergabe-app}]}, initialDelaySeconds: 5, periodSeconds: 10, timeoutSeconds: 5, failureThreshold: 3}
livenessProbe: {httpGet: {path: /health/, port: 8000, httpHeaders: [{name: Host, value: vergabe-app}]}, initialDelaySeconds: 30, periodSeconds: 30, timeoutSeconds: 5, failureThreshold: 3}
resources: *res
securityContext: *sc
volumeMounts:
- {name: settings, mountPath: /synthetic}
- {name: media, mountPath: /app/media}
- {name: app-state, mountPath: /app/.issue-facade}
volumes:
- {name: settings, configMap: {name: synthetic-settings}}
- {name: media, persistentVolumeClaim: {claimName: bench-media}}
- {name: app-state, emptyDir: {}}
---
apiVersion: v1
kind: Service
metadata: {name: vergabe-app, namespace: vergabe-sizing-20261021}
spec: {selector: {app: vergabe-sizing-app}, ports: [{port: 80, targetPort: 8000}]}
---
apiVersion: v1
kind: ServiceAccount
metadata: {name: synthetic-driver, namespace: vergabe-sizing-20261021}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: synthetic-driver-restart, namespace: vergabe-sizing-20261021}
rules: [{apiGroups: [""], resources: [pods], verbs: [get, list, delete]}]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: synthetic-driver-restart, namespace: vergabe-sizing-20261021}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: synthetic-driver-restart}
subjects: [{kind: ServiceAccount, name: synthetic-driver, namespace: vergabe-sizing-20261021}]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: synthetic-isolation, namespace: vergabe-sizing-20261021}
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
ingress: [{from: [{podSelector: {}}]}]
egress:
- to: [{podSelector: {}}]
- to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}, podSelector: {matchLabels: {k8s-app: kube-dns}}}]
ports: [{port: 53, protocol: UDP}, {port: 53, protocol: TCP}]
- to: [{ipBlock: {cidr: 92.205.62.239/32}}, {ipBlock: {cidr: 10.43.0.1/32}}]
ports: [{port: 6443, protocol: TCP}, {port: 443, protocol: TCP}]

View file

@ -0,0 +1,53 @@
{
"users": 2,
"workers": 3,
"iterations": 100000,
"tenders": 100,
"document_bytes": 1048576,
"think_seconds": 0.2,
"timeout_seconds": 30,
"ordinary_p95_seconds": 2.0,
"duration_seconds": 180,
"idle_seconds": 30,
"runtime": "remote",
"app_image": "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91",
"app_cpu_limit": 1.0,
"app_memory_limit": "1Gi",
"operations": [
"list",
"search",
"tender-create",
"detail",
"tender-edit",
"lot-create",
"lot-edit",
"task-create",
"task-edit",
"document-upload",
"document-download"
],
"phases": [
{
"name": "ordinary",
"kind": "load",
"duration_seconds": 90,
"think_seconds": 0.2
},
{
"name": "burst",
"kind": "load",
"duration_seconds": 30,
"think_seconds": 0
},
{
"name": "restart",
"kind": "restart"
},
{
"name": "recovery",
"kind": "load",
"duration_seconds": 60,
"think_seconds": 0.2
}
]
}

View file

@ -0,0 +1,29 @@
apiVersion: v1
kind: Pod
metadata: {name: synthetic-driver, namespace: vergabe-sizing-20261021, labels: {app: vergabe-sizing-driver}}
spec:
serviceAccountName: synthetic-driver
restartPolicy: Never
securityContext: {runAsGroup: 999, runAsNonRoot: true, runAsUser: 999}
containers:
- name: driver
image: forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91
imagePullPolicy: IfNotPresent
workingDir: /app
command: [sleep, "7200"]
env:
- {name: PYTHONPATH, value: /app}
- {name: SYNTHETIC_BASE_URL, value: "http://vergabe-app"}
- {name: SYNTHETIC_DATABASE_URL, value: "postgres://synthetic:synthetic-local-only@vergabe-db:5432/synthetic"}
- {name: SYNTHETIC_RESTART_SELECTOR, value: "app=vergabe-sizing-app"}
- {name: SYNTHETIC_SOURCE_REVISION, value: "deployed image c067993037f1cdeeb7b22d5600b51c649bc239db; driver from vergabe-teilnahme working tree"}
- {name: SYNTHETIC_DATABASE_DESCRIPTION, value: "fresh PostgreSQL 16 pod in throwaway namespace (docker.io/library/postgres:16 pinned digest)"}
- {name: SYNTHETIC_TRANSPORT_DESCRIPTION, value: "in-cluster HTTP via ClusterIP Service to Gunicorn (image CMD, 3 workers)"}
resources: {requests: {cpu: 100m, memory: 256Mi}, limits: {cpu: "1", memory: 1Gi}}
securityContext: {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}}
volumeMounts:
- {name: driver, mountPath: /driver}
- {name: out, mountPath: /out}
volumes:
- {name: driver, configMap: {name: synthetic-driver}}
- {name: out, emptyDir: {}}

View file

@ -0,0 +1,15 @@
#!/bin/bash
NS=vergabe-sizing-20261021; W=$SCRATCH; R=$W/raw; STOP=$R/stop; rm -f $STOP
$SCRATCH/sampler.sh app=vergabe-sizing-app app $R/app-cgroup.txt $STOP &
$SCRATCH/sampler.sh app=vergabe-sizing-db postgres $R/db-cgroup.txt $STOP &
$SCRATCH/sampler.sh app=vergabe-sizing-driver driver $R/driver-cgroup.txt $STOP &
$SCRATCH/top.sh $R/kubectl-top.txt $STOP &
echo "baseline_start $(date -u +%FT%T.%NZ)" > $R/windows.txt
sleep 120
echo "driver_start $(date -u +%FT%T.%NZ)" >> $R/windows.txt
kubectl -n $NS exec synthetic-driver -- python /driver/synthetic_pilot.py --config /driver/config.json --output /out/result.json > $R/driver-stdout.txt 2>&1
echo "driver_exit $? $(date -u +%FT%T.%NZ)" >> $R/windows.txt
sleep 60
echo "post_end $(date -u +%FT%T.%NZ)" >> $R/windows.txt
touch $STOP; sleep 3; wait
echo done

View file

@ -0,0 +1,25 @@
import json, subprocess, sys, urllib.parse
start, end, out = sys.argv[1], sys.argv[2], sys.argv[3]
P = "/api/v1/namespaces/telemetry/services/telemetry-prometheus:9090/proxy/api/v1/query_range?"
def q(expr, step="15s"):
url = P + urllib.parse.urlencode({"query": expr, "start": start, "end": end, "step": step})
return json.loads(subprocess.check_output(["kubectl", "get", "--raw", url]))
res = {"window": [start, end], "queries": {}}
for name, expr in {
"cpu_by_namespace_cores": 'sum by (namespace) (rate(container_cpu_usage_seconds_total{container!="",image!=""}[1m]))',
"throttled_ratio_by_namespace": 'sum by (namespace) (rate(container_cpu_cfs_throttled_periods_total{container!=""}[1m])) / sum by (namespace) (rate(container_cpu_cfs_periods_total{container!=""}[1m]))',
"memory_working_set_by_namespace_bytes": 'sum by (namespace) (container_memory_working_set_bytes{container!="",image!=""})',
"apps_pg_cpu_cores": 'sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="databases",container!="",image!=""}[1m]))',
"live_vergabe_cpu_cores": 'sum (rate(container_cpu_usage_seconds_total{namespace="vergabe-demo-company",container!="",image!=""}[1m]))',
}.items():
r = q(expr)
res["queries"][name] = {"expr": expr, "result": r["data"]["result"]}
json.dump(res, open(out, "w"))
# summary: mean per namespace cpu
s = {}
for series in res["queries"]["cpu_by_namespace_cores"]["result"]:
v = [float(x[1]) for x in series["values"]]
s[series["metric"].get("namespace", "?")] = (round(1000*sum(v)/len(v)), round(1000*max(v)))
tot = sum(a for a, b in s.values())
print("container_total_mean_m", tot)
for k, v in sorted(s.items(), key=lambda kv: -kv[1][0])[:15]: print(k, "mean_m", v[0], "max_m", v[1])

View file

@ -0,0 +1,16 @@
#!/bin/bash
# usage: sampler.sh <label-selector> <container> <outfile> <stopfile>
NS=vergabe-sizing-20261021; SEL=$1; C=$2; OUT=$3; STOP=$4
LOOP='while :; do echo "S $(date +%s.%N)"; cat /sys/fs/cgroup/cpu.stat; echo "M $(cat /sys/fs/cgroup/memory.current)"; echo "P $(head -1 /sys/fs/cgroup/cpu.pressure)"; echo "N $(head -1 /proc/stat)"; echo "L $(cat /proc/loadavg)"; sleep 1; done'
while [ ! -e "$STOP" ]; do
POD=$(kubectl -n $NS get pod -l "$SEL" --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [ -n "$POD" ]; then
echo "POD $POD $(date +%s.%N)" >> "$OUT"
timeout 1500 kubectl -n $NS exec "$POD" -c "$C" -- sh -c "$LOOP" >> "$OUT" 2>>"$OUT.err" &
PID=$!
while kill -0 $PID 2>/dev/null && [ ! -e "$STOP" ]; do sleep 1; done
kill $PID 2>/dev/null
echo "DETACH $(date +%s.%N)" >> "$OUT"
fi
sleep 0.5
done

View file

@ -0,0 +1,12 @@
from vergabe_teilnahme.settings.base import *
DEBUG = False
ALLOWED_HOSTS = ['vergabe-app']
NETKINGDOM_ENABLED = False
MEDIA_ROOT = '/app/media'
ISSUE_FACADE_LOCAL_DB = '/app/.issue-facade/issues.db'
ISSUE_FACADE_GITEA = None
FORCE_SCRIPT_NAME = None
MEDIA_URL = "/media/"
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False

View file

@ -0,0 +1,3 @@
#!/bin/bash
OUT=$1; STOP=$2
while [ ! -e "$STOP" ]; do echo "T $(date -u +%s)"; kubectl top pods -A --no-headers 2>&1; kubectl top node --no-headers 2>&1; sleep 15; done >> $OUT

View file

@ -0,0 +1 @@
{"ordinary": {"requests": 3675, "errors": 0, "p95_seconds": 0.19182809395715594}, "document_transfer": {"requests": 814, "errors": 0, "p95_seconds": 0.07621732295956463}, "complete": true, "provisional_pass": true}

View file

@ -0,0 +1,36 @@
T R O M
<nil> Scheduled vergabe-app-579448bf49-2fsn2 Successfully assigned vergabe-sizing-20261021/vergabe-app-579448bf49-2fsn2 to 239.62.205.92.host.secureserver.net
<nil> Scheduled vergabe-db-7c4bffdd48-w4jz4 Successfully assigned vergabe-sizing-20261021/vergabe-db-7c4bffdd48-w4jz4 to 239.62.205.92.host.secureserver.net
<nil> Scheduled synthetic-driver Successfully assigned vergabe-sizing-20261021/synthetic-driver to 239.62.205.92.host.secureserver.net
<nil> Scheduled vergabe-app-579448bf49-plp4j Successfully assigned vergabe-sizing-20261021/vergabe-app-579448bf49-plp4j to 239.62.205.92.host.secureserver.net
2026-10-21T06:26:14Z ScalingReplicaSet vergabe-db Scaled up replica set vergabe-db-7c4bffdd48 from 0 to 1
2026-10-21T06:26:14Z SuccessfulCreate vergabe-db-7c4bffdd48 Created pod: vergabe-db-7c4bffdd48-w4jz4
2026-10-21T06:26:14Z WaitForFirstConsumer bench-media waiting for first consumer to be created before binding
2026-10-21T06:26:16Z SuccessfulCreate vergabe-app-579448bf49 Created pod: vergabe-app-579448bf49-2fsn2
2026-10-21T06:26:16Z ScalingReplicaSet vergabe-app Scaled up replica set vergabe-app-579448bf49 from 0 to 1
2026-10-21T06:26:16Z Pulled vergabe-db-7c4bffdd48-w4jz4 Container image "docker.io/library/postgres@sha256:4b7183ac05f8ef417db21fd72d71047a4238340c261d3cc3ddb6d579ab5071ae" already present on machine and can be accessed by the pod
2026-10-21T06:26:16Z Created vergabe-db-7c4bffdd48-w4jz4 Container created
2026-10-21T06:26:16Z Provisioning bench-media External provisioner is provisioning volume for claim "vergabe-sizing-20261021/bench-media"
2026-10-21T06:26:16Z Started vergabe-db-7c4bffdd48-w4jz4 Container started
2026-10-21T06:26:19Z Unhealthy vergabe-db-7c4bffdd48-w4jz4 Readiness probe failed: /var/run/postgresql:5432 - no response
2026-10-21T06:26:21Z Created synthetic-driver Container created
2026-10-21T06:26:21Z ExternalProvisioning bench-media Waiting for a volume to be created either by the external provisioner 'rancher.io/local-path' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.
2026-10-21T06:26:21Z Pulled synthetic-driver Container image "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91" already present on machine and can be accessed by the pod
2026-10-21T06:26:22Z Started synthetic-driver Container started
2026-10-21T06:26:25Z ProvisioningSucceeded bench-media Successfully provisioned volume pvc-a23ccb84-ecd8-47af-9832-6d8b09c48e38
2026-10-21T06:26:26Z Started vergabe-app-579448bf49-2fsn2 Container started
2026-10-21T06:26:26Z Created vergabe-app-579448bf49-2fsn2 Container created
2026-10-21T06:26:26Z Pulled vergabe-app-579448bf49-2fsn2 Container image "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91" already present on machine and can be accessed by the pod
2026-10-21T06:26:32Z Started vergabe-app-579448bf49-2fsn2 Container started
2026-10-21T06:26:32Z Created vergabe-app-579448bf49-2fsn2 Container created
2026-10-21T06:26:32Z Pulled vergabe-app-579448bf49-2fsn2 Container image "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91" already present on machine and can be accessed by the pod
2026-10-21T06:32:03Z SuccessfulCreate vergabe-app-579448bf49 Created pod: vergabe-app-579448bf49-plp4j
2026-10-21T06:32:03Z Killing vergabe-app-579448bf49-2fsn2 Stopping container app
2026-10-21T06:32:04Z Started vergabe-app-579448bf49-plp4j Container started
2026-10-21T06:32:04Z Created vergabe-app-579448bf49-plp4j Container created
2026-10-21T06:32:04Z Pulled vergabe-app-579448bf49-plp4j Container image "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91" already present on machine and can be accessed by the pod
2026-10-21T06:32:04Z Unhealthy vergabe-app-579448bf49-2fsn2 Readiness probe failed: Get "http://10.42.0.96:8000/health/": dial tcp 10.42.0.96:8000: connect: connection refused
2026-10-21T06:32:09Z Started vergabe-app-579448bf49-plp4j Container started
2026-10-21T06:32:09Z Created vergabe-app-579448bf49-plp4j Container created
2026-10-21T06:32:09Z Pulled vergabe-app-579448bf49-plp4j Container image "forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91" already present on machine and can be accessed by the pod

View file

@ -0,0 +1,4 @@
synthetic-driver driver forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91 0 {} ['running']
vergabe-app-579448bf49-plp4j migrate forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91 0 {} ['terminated']
vergabe-app-579448bf49-plp4j app forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91 0 {} ['running']
vergabe-db-7c4bffdd48-w4jz4 postgres docker.io/library/postgres@sha256:4b7183ac05f8ef417db21fd72d71047a4238340c261d3cc3ddb6d579ab5071ae 0 {} ['running']

View file

@ -0,0 +1,7 @@
delete_requested 2026-10-21T06:35:25Z
namespace "vergabe-sizing-20261021" deleted
delete_returned 2026-10-21T06:36:18Z
Error from server (NotFound): namespaces "vergabe-sizing-20261021" not found
Error from server (NotFound): persistentvolumes "pvc-a23ccb84-ecd8-47af-9832-6d8b09c48e38" not found
0
live generation=4 image=forgejo.coulomb.social/coulomb/vergabe-teilnahme@sha256:a26444f59c259698159c69ccb96f73dc648a261ece4c86bb2037a9d977870d91 ready=1

View file

@ -0,0 +1,4 @@
baseline_start 2026-10-21T06:27:14.447540583Z
driver_start 2026-10-21T06:29:26.175778371Z
driver_exit 0 2026-10-21T06:33:22.885983171Z
post_end 2026-10-21T06:34:28.753426744Z