Java Profilovanie v Hardened Kubernetes: Keď Security Blokuje Tvoj Debugger
Profilovanie v hardened clusteri je ako debugovanie cez klucovu dierku. “Potrebujem sprofilovať tento produkčný problém ale nič nefunguje.” Problém: hardened Kubernetes prostredia blokujú system cally ktoré profilery potrebujú. Tu je ako získať profilovacie dáta bez kompromitácie bezpečnosti.
Prostredie: Java 17+, Kubernetes s PodSecurityStandards, seccomp profiles, read-only root filesystem
Problém
Všetko Je Zablokované
# Pokus 1: async-profiler
java -agentpath:/profiler/libasyncProfiler.so ...
# Error: perf_event_open failed: Operation not permitted
# Pokus 2: JFR s perf events
jcmd <pid> JFR.start settings=profile
# Warning: Native profiling not available (requires elevated privileges)
# Pokus 3: Pripoj debugger
jdb -attach 5005
# Error: ptrace operation not permitted
# Pokus 4: eBPF-based profilovanie
./profile -p <pid>
# Error: bpf() syscall blocked by seccomp
# Čo blokuje všetko?
Security Vrstvy
# Vrstva 1: seccomp profil blokuje syscalls
apiVersion: v1
kind: Pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # Blokuje perf_event_open, ptrace, bpf
# Vrstva 2: Capabilities dropped
containers:
- name: app
securityContext:
capabilities:
drop: ["ALL"]
# CAP_SYS_PTRACE, CAP_PERFMON, CAP_BPF všetky dropped
# Vrstva 3: Read-only filesystem
readOnlyRootFilesystem: true
# Nemôže zapisovať profiler output alebo temp súbory
# Vrstva 4: Non-root user
runAsNonRoot: true
runAsUser: 1000
# Mnoho profilerov predpokladá root prístup
Čo Stále Funguje
JFR Bez Native Profilovania
# JFR funguje aj v hardened prostrediach!
# Používa JVM-interné samplovanie, nie perf_event_open
# Spusti nahrávanie (cez jcmd alebo JMX)
jcmd <pid> JFR.start duration=60s filename=/tmp/recording.jfr
# Alebo cez JVM flags pri štarte
java -XX:StartFlightRecording=duration=60s,filename=/tmp/recording.jfr ...
# Kľúčový insight: JFR "profile" setting používa perf events
# Ale "default" setting používa pure JVM samplovanie
jcmd <pid> JFR.start settings=default filename=/tmp/recording.jfr
Získanie JFR Dát z Containera
# Problém: readOnlyRootFilesystem blokuje /tmp zápisy
# Riešenie 1: Zapisuj do emptyDir volume
volumes:
- name: profiler-output
emptyDir: {}
volumeMounts:
- name: profiler-output
mountPath: /profiler-data
# Potom:
jcmd <pid> JFR.start filename=/profiler-data/recording.jfr
# Skopíruj von:
kubectl cp pod-name:/profiler-data/recording.jfr ./recording.jfr
# Riešenie 2: Streamuj na stdout (šikovný hack)
jcmd <pid> JFR.dump name=1 filename=/dev/stdout | base64 > recording.b64
JVM Vstavané Samplovanie
// ThreadMXBean samplovanie - nepotrebuje špeciálne permissions
import java.lang.management.ThreadMXBean;
import java.lang.management.ManagementFactory;
public class SimpleSampler {
public static void sample(int durationSeconds, int intervalMs) {
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
Map<String, Integer> stackCounts = new HashMap<>();
long end = System.currentTimeMillis() + (durationSeconds * 1000L);
while (System.currentTimeMillis() < end) {
for (ThreadInfo ti : tmx.dumpAllThreads(false, false)) {
String stack = Arrays.stream(ti.getStackTrace())
.limit(10)
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n"));
stackCounts.merge(stack, 1, Integer::sum);
}
Thread.sleep(intervalMs);
}
// Output ako jednoduchý flame graph formát
stackCounts.forEach((stack, count) ->
System.out.println(stack.replace("\n", ";") + " " + count));
}
}
Riešenia
Možnosť 1: Sidecar Profiler s Elevated Permissions
# Pridaj privileged sidecar len pre profilovanie
# Deploynutý len keď potrebné, odstránený po
apiVersion: v1
kind: Pod
spec:
shareProcessNamespace: true # Sidecar vidí procesy hlavného containera
containers:
- name: app
image: my-app:latest
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
- name: profiler
image: async-profiler:latest
securityContext:
capabilities:
add: ["SYS_PTRACE", "PERFMON"] # Len čo treba
command: ["sleep", "infinity"]
volumeMounts:
- name: profiler-output
mountPath: /output
# Profiluj zo sidecar:
# kubectl exec -it pod-name -c profiler -- \
# /profiler/profiler.sh -d 30 -f /output/flamegraph.html <pid>
Možnosť 2: Ephemeral Debug Container
# Kubernetes 1.23+ podporuje ephemeral containers
kubectl debug pod-name -it --image=async-profiler:latest \
--target=app \
--profile=sysadmin # Pridá potrebné capabilities
# Vnútri debug containera:
/profiler/profiler.sh -d 30 -f /tmp/flamegraph.html 1
Možnosť 3: Pre-Konfigurované JFR pri Štarte
# Nakonfiguruj JFR v deployment - nepotrebné runtime attachment
containers:
- name: app
image: my-app:latest
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:StartFlightRecording=
disk=true,
dumponexit=true,
filename=/profiler-data/recording.jfr,
maxsize=100m,
settings=default
volumeMounts:
- name: profiler-output
mountPath: /profiler-data
Možnosť 4: JMX Remote Access
# Zapni JMX pre remote profilovacie nástroje
containers:
- name: app
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.rmi.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Djava.rmi.server.hostname=127.0.0.1
ports:
- containerPort: 9010
name: jmx
# Port forward a pripoj sa cez VisualVM/JMC:
# kubectl port-forward pod-name 9010:9010
# jmc # Pripoj na localhost:9010
Možnosť 5: Continuous Profiling Service
# Použi Pyroscope alebo podobné pre always-on profilovanie
# Agent používa JFR pod kapotou - nepotrebuje elevated permissions
containers:
- name: app
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-javaagent:/pyroscope/pyroscope.jar
-Dpyroscope.serverAddress=http://pyroscope.monitoring:4040
-Dpyroscope.applicationName=my-app
-Dpyroscope.profilingInterval=10ms
Analýza Bez Plného Profilera
Thread Dump Analýza
# Thread dumpy vždy fungujú
jcmd <pid> Thread.print > threads.txt
# Alebo cez kill signál
kill -3 <pid> # Output na stderr
# Viacero thread dumpov + analýza
for i in {1..10}; do
jcmd <pid> Thread.print >> threads.txt
sleep 1
done
# Analyzuj s fastthread.io alebo podobné
Heap Dump pre Memory Problémy
# Heap dumpy fungujú bez špeciálnych permissions
jcmd <pid> GC.heap_dump /profiler-data/heap.hprof
# Analyzuj s Eclipse MAT, VisualVM, alebo jhat
GC Log Analýza
# Zapni detailné GC logovanie pri štarte
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-Xlog:gc*=info:file=/profiler-data/gc.log:time,uptime,level,tags
# Analyzuj s GCViewer, GCEasy, alebo gceasy.io
Checklist
## Java Profilovanie v Hardened K8s
### Pred Deploymentom
- [ ] Zapni JFR pri štarte s -XX:StartFlightRecording
- [ ] Nakonfiguruj JMX remote access
- [ ] Pridaj emptyDir volume pre profiler output
- [ ] Zahrň profilovací agent do image (Pyroscope, atď.)
### Počas Incidentu
- [ ] Skús jcmd JFR.start (funguje bez privilégií)
- [ ] Zozbieraj thread dumpy (vždy funguje)
- [ ] Použi kubectl debug pre ephemeral container
- [ ] Port-forward JMX a použi remote nástroje
### Ak Potrebuješ Native Profilovanie
- [ ] Deploy profiler sidecar s SYS_PTRACE
- [ ] Použi shareProcessNamespace: true
- [ ] Odstráň sidecar po dokončení profilovania
Záver
Lekcia: hardened security je dobrá, ale plánuj observabilitu pred deployom. JFR a JMX fungujú bez elevated privileges - použi ich. Keď potrebuješ native profilovanie, použi cielenú eskaláciu privilégií (sidecar containers) namiesto spúšťania všetkého privileged.
Kľúčové nástroje fungujúce v hardened prostrediach:
- JFR s default settings (pure JVM samplovanie)
- Thread dumpy (vždy fungujú)
- JMX pre remote monitoring
- Continuous profilery ako Pyroscope ktoré používajú JFR
Súvisiace články
- Java Native Memory OOMKilled - Memory debugging
- eBPF Run-Queue Latency - System-level profilovanie
Súvisiace články
Go cgo DNS Resolution Thread Explózia: Keď net.LookupHost Spawne Tisíce Threadov
Go aplikácia má zrazu 10,000 threadov konzumujúcich všetku pamäť. Príčina: cgo-based DNS resolution blokujúce v pomalých DNS prostrediach, obchádzajúce Go's goroutine scheduler.
Java OOMKilled So Stabilným Heapom: Native Memory, Direct Buffers a glibc Arenas
Heap metriky vyzerajú dobre, GC je spokojný, ale kontajner stále umiera. Vinník: native memory z direct buffers, JNI a glibc memory allocator fragmentácia.
JVM Metaspace OOM v Kubernetes: Prečo MaxMetaspaceSize Nestačí
Pod OOMKilled napriek nastavenému MaxMetaspaceSize. Príčina: Metaspace rastie mimo heap, container memory limit nepočíta s tým, a triedy sa neuvoľňujú.
etcd Watch Replay Búrky: Keď Obrovské ConfigMapy Zabíjajú Control Plane
Apiserver je 'náhodne pomalý'. Príčina: veľké, často aktualizované ConfigMapy spúšťajú watch compaction, čo spôsobuje simultánny relist tisícov kontrolérov.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.