Testowy klaster OKD (OpenShift)
Poniższy wpis bazuje na konfiguracji klastra przeprowadzonej wg. wpisu https://itadmin.vblog.ovh/klaster-okd-openshift-na-maszynach-wirtualnych-proxmox-opis-instalacji/ oraz konfiguracji serwera NFS opisanej na stronie https://itadmin.vblog.ovh/klaster-okd-openshift-na-maszynach-wirtualnych-proxmox-przygotowanie-maszyny-storage-nfs/
Opis projektu OpenShift : java-memory-eater
Projekt ma na celu kontrolowane wywołanie częstego błędu w aplikacjach java: OutOfMemoryError poprzez cykliczne pożeranie pamięci RAM. Bazuje na obrazie Docker-a eclipse-temurin:17-jdk-alpine . Dodatkowo projekt wykorzystuje configmapę w standardowej postaci key – value. POD aplikacji w tym przykładzie może maksymalnie wykorzystać ~1.8 GB pamięci RAM bez ustawionych limitów OpenShift lub ~ 1 GB pamięci RAM z ustawionymi limitami pamięci RAM.
ConfigMap to obiekt w Kubernetes/OpenShift, który przechowuje dane konfiguracyjne w postaci par klucz–wartość.
- Dane te mogą być następnie udostępniane do podów jako:
- zmienne środowiskowe (
envw kontenerze), - albo pliki w wolumenie (
volumeMount).
- zmienne środowiskowe (
Dzięki temu nie musisz wbudowywać konfiguracji w obraz Dockera — aplikacja może być konfigurowana dynamicznie przez administratorów DevOps bez rekompilacji.
Utworzenie nowego projektu OpenShift o nazwie java-memory-eater
Najpierw należy przygotować kod aplikacji Java , która w pętli zwiększa zużycie pamięci. Aplikacja korzysta z 3 zmiennych środowiskowych przesłanych przez configmapę.
SLEEP_BEFORE_START – ile sekund czekać na starcie zanim ruszy nieskończona pętla zjadająca pamięć
OBJECT_SIZE_MB – ile jednorazowo ma zostać zajęte pamięci
LOOP_TIME – ile ma trwać pauza pomiędzy kolejnym wykonaniem pętli zjadającej pamięć
Jeśli przyjąć, że OBJECT_SIZE_MB= 20 oraz LOOP_TIME = 5 to pętla wykona się 85 razy zjadając 1700 MB pamięci RAM przy założeniu, że rozmiar sterty ustawiony w Dockerfile wynosi odpowiednio
ENV JAVA_XMS=512m , ENV JAVA_XMX=1800m. Można więc przez ponad 7 minut obserwować jak rośnie zużycie pamięci i doprowadza w końcu do wywalenia się poda błędem CrashLoopBackOff
public class MemoryEater {
public static void main(String[] args) {
int sleepBeforeStart = getEnvAsInt("SLEEP_BEFORE_START", 10);
int objectSizeMB = getEnvAsInt("OBJECT_SIZE_MB", 10);
int loopTimeSec = getEnvAsInt("LOOP_TIME", 1); // czas między kolejnymi krokami w sekundach
System.out.println("Konfiguracja:");
System.out.println(" - SLEEP_BEFORE_START = " + sleepBeforeStart + "s");
System.out.println(" - OBJECT_SIZE_MB = " + objectSizeMB + " MB");
System.out.println(" - LOOP_TIME = " + loopTimeSec + "s");
try {
Thread.sleep(sleepBeforeStart * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
java.util.List<byte[]> memoryLeak = new java.util.ArrayList<>();
while (true) {
try {
byte[] block = new byte[objectSizeMB * 1024 * 1024];
memoryLeak.add(block);
int blocks = memoryLeak.size();
int totalMB = blocks * objectSizeMB;
System.out.println(
"Dodano blok " + objectSizeMB + "MB, rozmiar listy = " + blocks + ", Suma RAM: " + totalMB + " MB"
);
Thread.sleep(loopTimeSec * 1000L);
} catch (OutOfMemoryError e) {
System.err.println("💥 OutOfMemoryError złapany!");
e.printStackTrace();
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Koniec działania MemoryEater.");
}
private static int getEnvAsInt(String name, int defaultValue) {
String value = System.getenv(name);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
System.err.println("Niepoprawna wartość dla " + name + ": " + value + " (używam domyślnej " + defaultValue + ")");
}
}
return defaultValue;
}
}Następnie należy przygotować odpowiedni Dockerfile, który posłuży do zbudowania obrazu aplikacji. Będą w nim parametry, które kontrolują rozmiar sterty (heap) w Javie czyli tej części pamięci, z której korzystają obiekty aplikacji.
-Xms512m
Xms = initial heap size (rozmiar początkowy sterty).
512m = 512 megabajtów.
JVM przy starcie od razu zarezerwuje 512 MB RAM na heap.
Dzięki temu:
nie musi dynamicznie powiększać sterty od małych wartości (co spowalnia GC),
start aplikacji może być stabilniejszy przy dużych systemach.
-Xmx1800m
Xmx = maximum heap size (maksymalny rozmiar sterty).
1800m = 1800 megabajtów (~1.8 GB).
To jest twardy limit sterty w JVM.
Jeśli aplikacja spróbuje zaalokować więcej obiektów niż się zmieści to wywoła to błąd java.lang.OutOfMemoryError: Java heap space.
FROM eclipse-temurin:17-jdk-alpine
RUN apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Europe/Warsaw /etc/localtime \
&& echo "Europe/Warsaw" > /etc/timezone
WORKDIR /app
COPY MemoryEater.java /app
RUN javac MemoryEater.java
ENV JAVA_XMS=512m
ENV JAVA_XMX=1800m
CMD ["sh", "-c", "java -Xms$JAVA_XMS -Xmx$JAVA_XMX MemoryEater"]Teraz możemy przystąpić do utworzenia projektu o nazwie java-memory-eater oraz build configa i image stream-a o wskazanej dowolnej nazwie np. java-memory-eater-build
oc new-project java-memory-eater # stworzenie build nowego obrazu bazującego na Dockerfile oc new-build --strategy=docker --binary --name=java-memory-eater-build
Następny krokiem jest zbudowanie obrazu czyli builda o nazwie java-memory-eater-build
cd /home/bastuser/repo/java-memory-eater oc start-build java-memory-eater-build --from-dir=. --follow
Weryfikujemy czy powyższy build wykonał się poprawnie.
# sprawdzenie dostępnych buildów w projekcie
oc get builds
NAME TYPE FROM STATUS STARTED DURATION
java-memory-eater-build-1 Docker Binary Failed (ManageDockerfileFailed) About a minute ago 3s
java-memory-eater-build-2 Docker Binary Failed (GenericBuildFailed) About a minute ago 6s
java-memory-eater-build-3 Docker Binary Complete 36 seconds ago 18s
# przejrzenie logów z builda
oc logs -f build/java-memory-eater-build-2
Receiving source from STDIN as archive ...
time="2025-10-03T07:03:08Z" level=info msg="Not using native diff for overlay, this may cause degraded performance for building images: kernel has CONFIG_OVERLAY_FS_REDIRECT_DIR enabled"
I1003 07:03:08.215302 1 defaults.go:112] Defaulting to storage driver "overlay" with options [mountopt=metacopy=on].
Caching blobs under "/var/cache/blobs".
error: build error: no FROM image in Dockerfile
# ostatnie zdarzenia w projekcie gdzie widać szczegóły powstawania builda w projekcie
oc get events --sort-by='{.lastTimestamp}' -n java-memory-eaterDodanie configmapy do projektu
Tworzymy obiekt configmap o nazwie cm-java-memory-eater przechowujący zmienne dla aplikacji java-memory-eater
oc create configmap cm-java-memory-eater \ --from-literal=SLEEP_BEFORE_START=5 \ --from-literal=OBJECT_SIZE_MB=20 \ --from-literal=LOOP_TIME=5 # sprawdzamy czy istnieje taki obiekt cm oc get cm NAME DATA AGE cm-java-memory-eater 3 3s # szczegółu obiektu cm cm-java-memory-eater oc describe cm/cm-java-memory-eater Name: cm-java-memory-eater Namespace: java-memory-eater Labels: <none> Annotations: <none> Data ==== LOOP_TIME: ---- 5 OBJECT_SIZE_MB: ---- 20 SLEEP_BEFORE_START: ---- 5 BinaryData ==== Events: <none>
Utworzenie aplikacji OpenShift
Teraz możemy utworzyć aplikację na bazie istniejącego builda jako deployment-config do którego możemy dodać pvc oraz configmap.
# aplikacja czyli pody mają się zaczynąc od java-memoryeater-app oc new-app java-memory-eater-build:latest --name=java-memory-eater-app --as-deployment-config
Podłączenie configmap-y do deploymentconfig.
oc set env dc/java-memory-eater-app --from=configmap/cm-java-memory-eater Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+ deploymentconfig.apps.openshift.io/java-memory-eater-app updated #weryfikacja oc set env dc/java-memory-eater-app --list Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+ # deploymentconfigs/java-memory-eater-app, container java-memory-eater-build # LOOP_TIME from configmap cm-java-memory-eater, key LOOP_TIME # OBJECT_SIZE_MB from configmap cm-java-memory-eater, key OBJECT_SIZE_MB # SLEEP_BEFORE_START from configmap cm-java-memory-eater, key SLEEP_BEFORE_START
Restart aplikacji aby dostała nowe ustawienia środowiskowe z configmap.
oc rollout latest dc/java-memory-eater-app Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+ Warning: apps.openshift.io/v1 DeploymentRequest is deprecated in v4.14+, unavailable in v4.10000+ deploymentconfig.apps.openshift.io/java-memory-eater-app rolled out
Monitorowanie logów POD-a
Z poziomu konsoli tekstowej oc logs -f <nazwa poda>
oc logs -f java-memory-eater-app-3-sw2vm
Konfiguracja:
- SLEEP_BEFORE_START = 5s
- OBJECT_SIZE_MB = 20 MB
- LEAK_DURATION_SEC = 120s
Dodano blok 20MB, rozmiar listy = 1, Suma RAM: 20 MB
Dodano blok 20MB, rozmiar listy = 2, Suma RAM: 40 MB
Dodano blok 20MB, rozmiar listy = 3, Suma RAM: 60 MB
...
💥 OutOfMemoryError złapany!
java.lang.OutOfMemoryError: Java heap space
at MemoryEater.main(MemoryEater.java:23)
Koniec działania MemoryEater.Z poziomu konsoli graficznej jako Developer – projekt – pod – zakładka Logs

Monitorowanie wykorzystanie pamięci przez POD-a
Z poziomu konsoli tekstowej
# sprawdzamy które pody wykorzystują najwięcej zasobów na klastrze oc adm top pods NAME CPU(cores) MEMORY(bytes) java-memory-eater-app-3-8c9xb 54m 2268Mi # sprawdzamy które pody wykorzystują najwięcej zasobów na klastrze w konkretnym projekcie oc adm top pods -n java-memory-eater NAME CPU(cores) MEMORY(bytes) java-memory-eater-app-3-8c9xb 54m 2268Mi # sprawdzamy które pody wykorzystują najwięcej zasobów na klastrze w konkretnym projekcie (w pętli) watch -n 2 oc adm top pods -n java-memory-eater # sprawdzamy na którego workera na klastrze trafił pod oc get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES java-memory-eater-app-3-8c9xb 1/1 Running 3 (42s ago) 5m48s 10.129.2.15 compute-3 <none> <none> java-memory-eater-app-3-deploy 0/1 Completed 0 5m49s 10.129.2.14 compute-3 <none> <none> java-memory-eater-build-3-build 0/1 Completed 0 22m 10.129.2.9 compute-3 <none> <none> # sprawdzamy zuzyćie zasobów na workerze w klastrze oc adm top nodes NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% compute-1 202m 5% 3510Mi 23% compute-2 230m 6% 3350Mi 22% compute-3 66m 1% 2477Mi 16% control-plane-1 373m 10% 5533Mi 37% control-plane-2 316m 9% 4046Mi 27% control-plane-3 281m 8% 3866Mi 26%
Z poziomu konsoli graficznej jako Developer – projekt – pod

Ustawianie limitów dla pamięci RAM i CPU
Celem tego zadania jest ustawienie niższych limitów dla pamięci np. 1 GB RAM dla POD-a za pomocą mechanizmu limitów zasobów dostępnych w OpenShift.
- Requests
Definicja: Minimalna ilość zasobów, którą kontener potrzebuje do działania.
Rola: Scheduler K8s/OS używa tego, żeby zdecydować, na którym node uruchomić poda.
Przykład:
Jeśli pod ma requests.memory=128Mi, scheduler szuka noda, na którym jest co najmniej 128 MiB wolnej pamięci „zagwarantowanej”.
👉 requests = gwarancja, że aplikacja dostanie co najmniej tyle.
- Limits
Definicja: Maksymalna ilość zasobów, którą kontener może zużyć.
Rola: To jest twardy sufit – kontener nie może użyć więcej.
Dla pamięci (limits.memory):
Jeśli aplikacja spróbuje użyć więcej niż limit → kubelet ubije kontener (OOMKilled).
Dla CPU (limits.cpu):
Proces nie zostanie ubity, ale będzie dławiony (throttling) przez CFS (Completely Fair Scheduler) w jądrze.
👉 limits = sufit, powyżej którego kontener nie może pójść.
🔹 Różne scenariusze
1️⃣ Brak requests i limits
Pod działa jako BestEffort.
Może używać wszystkiego, co jest wolne, ale nie ma żadnych gwarancji.
Najczęściej pierwszy do zabicia przy presji pamięci na nodzie.
2️⃣ Tylko limits ustawione
Scheduler nie wie, ile „naprawdę” pod potrzebuje.
Może wrzucić kilka podów na jeden node, bo myśli, że każdy jest lekki → w praktyce węzeł się przepełnia i zaczynają się OOM’y.
Klasa QoS: Burstable.
3️⃣ Tylko requests ustawione
Scheduler rezerwuje miejsce, ale brak limitu = kontener może „zajeść” cały node.
Ryzykowne w środowiskach multi-tenantowych.
4️⃣ Ustawione i requests, i limits
Najbardziej przewidywalne.
Scheduler rezerwuje requests.
Kernel/Kubelet pilnuje, żeby nie przekroczyć limits.
Klasa QoS zależy od tego, czy wartości są równe:
requests = limits → QoS Guaranteed (najwyższy priorytet, najmniejsza szansa OOMKill).
requests < limits → QoS Burstable (może używać więcej niż gwarantowane, do limitu).
# ustawiamy parametry, że pod nie moze zużyc w trakcie swojej pracy wiecej niż 1 GB pamięci RAM oc set resources dc/java-memory-eater-app --limits=memory=1024Mi,cpu=200m --requests=memory=256Mi,cpu=100m -n java-memory-eater # restart poda oc rollout latest dc/java-memory-eater-app
Sprawdzenie jakie limity RAM / CPU obowiązują danego POD-a. Sprawdzamy sekcję Limits i Requests.
oc describe pod java-memory-eater-app-5-mbw8b
Name: java-memory-eater-app-5-mbw8b
Namespace: java-memory-eater
Priority: 0
Service Account: default
Node: compute-3/192.168.40.63
Start Time: Fri, 03 Oct 2025 12:19:07 +0200
Labels: deployment=java-memory-eater-app-5
deploymentconfig=java-memory-eater-app
Annotations: k8s.ovn.org/pod-networks:
{"default":{"ip_addresses":["10.129.2.45/23"],"mac_address":"0a:58:0a:81:02:2d","gateway_ips":["10.129.2.1"],"routes":[{"dest":"10.128.0.0...
k8s.v1.cni.cncf.io/network-status:
[{
"name": "ovn-kubernetes",
"interface": "eth0",
"ips": [
"10.129.2.45"
],
"mac": "0a:58:0a:81:02:2d",
"default": true,
"dns": {}
}]
openshift.io/deployment-config.latest-version: 5
openshift.io/deployment-config.name: java-memory-eater-app
openshift.io/deployment.name: java-memory-eater-app-5
openshift.io/generated-by: OpenShiftNewApp
openshift.io/scc: restricted-v2
seccomp.security.alpha.kubernetes.io/pod: runtime/default
Status: Running
SeccompProfile: RuntimeDefault
IP: 10.129.2.45
IPs:
IP: 10.129.2.45
Controlled By: ReplicationController/java-memory-eater-app-5
Containers:
java-memory-eater-build:
Container ID: cri-o://a1f1214cdd88d641ea40bd7bf87d2ba5e30726ffcf44f0868e4dc1b0d25d3461
Image: image-registry.openshift-image-registry.svc:5000/java-memory-eater/java-memory-eater-build@sha256:b7c529aade50284c8135989a3cdb5150b557d48f980941a9fc60e0ab112d7709
Image ID: 3b3b978ac3f97932d21d3c0fb9f4f1877ba25bdda40583225f0b929cfd24a08e
Port: <none>
Host Port: <none>
State: Running
Started: Fri, 03 Oct 2025 12:19:07 +0200
Ready: True
Restart Count: 0
Limits:
cpu: 200m
memory: 1Gi
Requests:
cpu: 100m
memory: 256Mi
Environment:
LOOP_TIME: <set to the key 'LOOP_TIME' of config map 'cm-java-memory-eater'> Optional: false
OBJECT_SIZE_MB: <set to the key 'OBJECT_SIZE_MB' of config map 'cm-java-memory-eater'> Optional: false
SLEEP_BEFORE_START: <set to the key 'SLEEP_BEFORE_START' of config map 'cm-java-memory-eater'> Optional: false
...Z takimi limitami jak powyżej POD zostanie zrestartowany przy ok. 800 MB zajętości pamięci RAM.
Dlaczego ~800 MB, a nie 1024 MB?
JVM to nie tylko heap (-Xmx).
Mamy też:
Metaspace (klasy, refleksja) – zwykle dziesiątki MB.
Thread stacks (po 1 MB na wątek).
Native memory (alokacje off-heap, biblioteki JNI, GC metadata).
Realnie heap (-Xmx) musi być zapasowo niższy niż limit poda, bo inaczej proces zostanie zabity przez kubelet zanim heap dojdzie do sufitów.
W tym przypadku: kubelet ubija proces przy ~800 MB heap + reszta natywna ~ 1 GB.
oc logs -f java-memory-eater-app-5-mbw8b Konfiguracja: - SLEEP_BEFORE_START = 5s - OBJECT_SIZE_MB = 20 MB - LOOP_TIME = 5s Dodano blok 20MB, rozmiar listy = 1, Suma RAM: 20 MB Dodano blok 20MB, rozmiar listy = 2, Suma RAM: 40 MB Dodano blok 20MB, rozmiar listy = 3, Suma RAM: 60 MB ... Dodano blok 20MB, rozmiar listy = 37, Suma RAM: 740 MB Dodano blok 20MB, rozmiar listy = 38, Suma RAM: 760 MB Dodano blok 20MB, rozmiar listy = 39, Suma RAM: 780 MB Dodano blok 20MB, rozmiar listy = 40, Suma RAM: 800 MB

Poprawiony kod Dockerfile z obsługą SnakeYAML do parsowania plików yml oraz prawidłowym czasem wewnątrz poda.
Dla zapewnienia długiej dostępności warto pobrać plik snakeyaml-2.2.jar na dysk.
cd /home/bastuser/repo/java-hello-application-yml wget https://repo1.maven.org/maven2/org/yaml/snakeyaml/2.2/snakeyaml-2.2.jar
FROM eclipse-temurin:17-jdk-alpine
RUN apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Europe/Warsaw /etc/localtime \
&& echo "Europe/Warsaw" > /etc/timezone
WORKDIR /app
# Skopiuj źródła i bibliotekę
COPY JavaHello.java /app
COPY snakeyaml-2.2.jar /app
# Kompilacja z biblioteką w classpath
RUN javac -cp snakeyaml-2.2.jar JavaHello.java
# Uruchomienie z biblioteką w classpath
CMD ["java", "-cp", ".:snakeyaml-2.2.jar", "JavaHello"]Wykonanie nowego buildconfiga, nowego imagestream i nowego builda oraz deployment configu
cd /home/bastuser/repo/java-hello-application-yml oc new-build --strategy=docker --binary --name=java-hello-yaml-build oc start-build java-hello-yaml-build --from-dir=. --follow oc new-app java-hello-yaml-build:latest --name=java-hello-app-yaml --as-deployment-config # zamontowanie configmapy do nowego dc java-hello-app-yaml oc set volume dc/java-hello-app-yaml \ --add \ --name=config-volume \ --mount-path=/config \ --configmap-name=cm-java-hello # zamontowanie volumeny PVC do dc java-hello-app-yaml oc set volume dc/java-hello-app-yaml \ --add \ --name=downloads \ --mount-path=/downloads \ --claim-name=java-hello-pvc # restart poda oc rollout latest dc/java-hello-app-yaml
Sprawdzenie poprawności konfiguracji i działania poda aplikacji
# sprawdzamy logi poda oc logs -f java-hello-app-yaml-10-dq7rk Witaj Zbychu dziś jest 2025-10-02 16:48:27 Witaj Zbychu dziś jest 2025-10-02 16:48:37 # oc get pods, sprawdzamy nazwę poda i wchodzimy do niego oc exec -it java-hello-app-yaml-10-dq7rk sh # w środku poda /app $ cat /config/application.yml appsettings: user_name: "Zbychu" loop_time: "10" file_path: "/downloads/status.txt" /app $ ls -la /downloads/status.txt -rw-r--r-- 1 1000710000 root 14256 Oct 2 16:30 /downloads/status.txt
Usunięcie zawartości projektu oraz samego projektu
# usunięcie zawartości bez usuwania projketu oc delete all --all -n java-hello pod "java-hello-app-yaml-10-deploy" deleted pod "java-hello-app-yaml-10-dq7rk" deleted pod "java-hello-app-yaml-7-deploy" deleted pod "java-hello-app-yaml-8-deploy" deleted pod "java-hello-app-yaml-9-deploy" deleted pod "java-hello-build-1-build" deleted pod "java-hello-yaml-build-1-build" deleted pod "java-hello-yaml-build-2-build" deleted pod "java-hello-yaml-build-3-build" deleted pod "java-hello-yaml-build-4-build" deleted replicationcontroller "java-hello-app-yaml-1" deleted replicationcontroller "java-hello-app-yaml-10" deleted replicationcontroller "java-hello-app-yaml-2" deleted replicationcontroller "java-hello-app-yaml-3" deleted replicationcontroller "java-hello-app-yaml-4" deleted replicationcontroller "java-hello-app-yaml-5" deleted replicationcontroller "java-hello-app-yaml-6" deleted replicationcontroller "java-hello-app-yaml-7" deleted replicationcontroller "java-hello-app-yaml-8" deleted replicationcontroller "java-hello-app-yaml-9" deleted Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+ deploymentconfig.apps.openshift.io "java-hello-app-yaml" deleted buildconfig.build.openshift.io "java-hello-build" deleted buildconfig.build.openshift.io "java-hello-yaml-build" deleted imagestream.image.openshift.io "java-hello-build" deleted imagestream.image.openshift.io "java-hello-yaml-build" deleted # usuniecie wszystkich configmap oc delete configmap --all -n java-hello # usuniecie wszystkich secretow oc delete secret --all -n java-hello #usuniecie wszystkich pvc oc delete pvc --all -n java-hello # usunięcie całęgo projektu oc delete project java-hello project.project.openshift.io "java-hello" deleted

