Add kubernetes-deployment: probes, shutdown, JVM ergonomics, HPA

Companion code for "Deploying Spring Boot 4 on Kubernetes: Probes, Graceful
Shutdown, Limits and JVM Ergonomics". A dependency outage under three
probe-group setups, a rolling restart under load four ways (three runs
each), the JVM's ergonomic choices for nine pod shapes, one GC-heavy load
under five CPU limits with throttling counters, and an HPA driven by a
Micrometer gauge through prometheus-adapter. Measured on k3s v1.36.4.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
This commit is contained in:
2026-09-11 17:12:10 +00:00
co-authored by Claude Opus 5
parent 644da9e65e
commit a065696478
72 changed files with 28415 additions and 2 deletions
@@ -0,0 +1,60 @@
# 3. JVM ergonomics: what the JVM decides from your pod spec
[← 2. Probes](02-probes.md) · [Index](../README.md) · Next: [4. CPU limits and GC →](04-cpu-limits-and-gc.md)
With no JVM options at all, the JVM reads the container's cgroup limits and picks a processor
count, a garbage collector, a heap size and thread counts. [`demo-ergonomics.sh`](../scripts/demo-ergonomics.sh)
runs `java -XX:+PrintFlagsFinal -version` in a pod per resource shape
([`jvm-ergonomics.txt`](output/jvm-ergonomics.txt)):
```
cpu.req cpu.lim mem.req mem.lim | CPUs GC MaxHeap PGCThr CGCThr JITThr
100m none 256Mi 2Gi | 2 G1GC 512M 2 1 2
100m 500m 256Mi 512Mi | 1 SerialGC 128M 0 0 2
100m 1 256Mi 1Gi | 1 SerialGC 256M 0 0 2
100m 1 256Mi 2Gi | 1 SerialGC 512M 0 0 2
100m 1500m 256Mi 2Gi | 2 G1GC 512M 2 1 2
100m 2 256Mi 1Gi | 2 SerialGC 256M 0 0 2
100m 2 256Mi 1700Mi | 2 SerialGC 426M 0 0 2
100m 2 256Mi 1800Mi | 2 G1GC 450M 2 1 2
100m 2 256Mi 4Gi | 2 G1GC 1024M 2 1 2
```
## The rules the table follows
- **CPUs = the limit, rounded up.** 500m → 1, 1500m → 2. With no limit, the JVM sees every CPU on
the node - here 2, on a production node perhaps 64.
- **Requests are ignored.** Every row has a 100m request. JDK 19 removed CPU shares from the
calculation (JDK-8281181), so on a modern JDK `requests.cpu` affects scheduling and nothing the
JVM decides.
- **G1 needs 2 CPUs *and* about 1792 MB.** Below either, the JVM is not a "server-class machine"
and picks SerialGC: a single-threaded, stop-the-world collector. 2 CPUs with 1700Mi → Serial;
1800Mi → G1. A typical Spring Boot pod sized 512Mi-1Gi therefore **runs SerialGC
without anyone having chosen it**.
- **Heap = 25 % of the memory limit** (`MaxRAMPercentage=25`). A 512Mi pod gets a 128 MB heap and
384 MB for metaspace, code cache, thread stacks and native memory - usually far more than they use. `-XX:MaxRAMPercentage=75` is the usual
correction; the manifests here set it through `JAVA_TOOL_OPTIONS`.
- **1500m gives G1 two parallel GC threads on one and a half CPUs of quota.** That combination is
what [chapter 4](04-cpu-limits-and-gc.md) measures.
## cgroup v1 here, v2 in production
The lab node is cgroup v1 (a limitation of the VM it ran in; Kubernetes 1.35+ refuses v1 unless
`failCgroupV1=false`). JVM container detection works the same on both. The files differ:
| | cgroup v1 | cgroup v2 |
|---|---|---|
| CPU limit | `cpu/cpu.cfs_quota_us` / `cpu.cfs_period_us` | `cpu.max` (`150000 100000`) |
| throttling | `cpu/cpu.stat`: `nr_throttled`, `throttled_time` (ns) | `cpu.stat`: `nr_throttled`, `throttled_usec` |
| memory limit | `memory/memory.limit_in_bytes` | `memory.max` |
[`JvmController`](../src/main/java/com/ankurm/k8s/diag/JvmController.java) reads either.
`java -XshowSettings:system -version` prints what the JVM detected, including the provider:
```
Operating System Metrics:
Provider: cgroupv1
Effective CPU Count: 2
CPU Period: 100000us
CPU Quota: 150000us
```