Kubernetes'te Spring Boot: İmaj, JVM Ayarları ve Probe'lar
'Konteynere koyduk, Kubernetes'e attık, pod'lar sürekli yeniden başlıyor.' Spring Boot uygulamasını Kubernetes'te sağlıklı çalıştırmak için katmanlı ve küçük imaj, CDS ile hızlı başlangıç, JVM bellek ve CPU ayarları, doğru kurgulanmış startup/readiness/liveness probe'ları, graceful shutdown ve yapılandırma yönetimini örnek Dockerfile ve manifest ile anlatıyoruz.
"Uygulamayı konteynere koyduk, Kubernetes'e attık; pod'lar sürekli yeniden başlıyor, dağıtımda beş dakika hata alıyoruz, bellek limitini üç kere artırdık." Bu cümle, Kubernetes'in değil, Spring Boot uygulamasının Kubernetes için hazırlanmamış olmasının belirtisidir. JVM'in kaynak modeli, uygulamanın başlangıç süresi ve sağlık sinyalleri konteyner dünyasına göre ayarlanmadığında, orkestratör iyi niyetle yanlış kararlar verir: yavaş başlayan pod'u öldürür, hazır olmayan pod'a trafik gönderir, belleği aşan pod'u sessizce keser.
Bu yazıda Spring Boot uygulamasını Kubernetes'te sağlıklı çalıştırmanın beş başlığını ele alıyoruz: küçük ve katmanlı imaj, CDS ile hızlı başlangıç, JVM bellek ve CPU ayarları, doğru kurgulanmış startup/readiness/liveness probe'ları ve graceful shutdown, yapılandırma ve ölçekleme. Örnek Dockerfile ve manifest ile.
1. İmaj: küçük, katmanlı, root olmayan
Tek katmanlı 300 MB'lık "fat jar" imajı her sürümde tamamen yeniden yüklenir; kod bir satır değişse bile. Spring Boot'un katmanlı jar desteği, bağımlılıkları (nadiren değişen) ve uygulama kodunu (her sürüm değişen) ayrı imaj katmanlarına koyar; registry ve düğümler yalnızca değişen katmanı çeker.
# 1. aşama: derleme
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . .
RUN ./mvnw -q -DskipTests package
# 2. aşama: katmanlara ayırma ve CDS arşivi
FROM eclipse-temurin:21-jre AS layers
WORKDIR /app
COPY --from=build /src/target/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted
# CDS: uygulama bir kez "eğitim" modunda başlatılır, sınıf arşivi üretilir (başlangıç %30–50 kısalır)
RUN cd extracted && java -XX:ArchiveClassesAtExit=/app/app.jsa -Dspring.context.exit=onRefresh \
-cp "dependencies/*:spring-boot-loader/*:snapshot-dependencies/*:application/*" org.springframework.boot.loader.launch.JarLauncher || true
# 3. aşama: çalışma imajı
FROM eclipse-temurin:21-jre
RUN useradd -r -u 1001 app
WORKDIR /app
COPY --from=layers --chown=app /app/extracted/dependencies/ ./
COPY --from=layers --chown=app /app/extracted/spring-boot-loader/ ./
COPY --from=layers --chown=app /app/extracted/snapshot-dependencies/ ./
COPY --from=layers --chown=app /app/extracted/application/ ./
COPY --from=layers --chown=app /app/app.jsa ./
USER app
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:SharedArchiveFile=app.jsa -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Xlog:gc:stdout"
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Alternatif: mvn spring-boot:build-image ile Buildpacks aynı katmanlamayı ve CDS'i (BP_JVM_CDS_ENABLED=true) Dockerfile yazmadan yapar. Her iki yolda da imaj etiketi sürüm numarası ya da commit hash'idir; latest üretimde yasaktır.
2. JVM: bellek ve CPU'nun konteyner modeli
GC seçimi yazısında ayrıntılandırdığımız kuralın özeti: heap konteyner limitinin en fazla %75'i; sabit -Xmx yerine MaxRAMPercentage; metaspace, thread ve native bellek için pay bırakılır. Kubernetes tarafında bellek için requests = limits (JVM belleği geri vermez, dalgalanma yoktur).
CPU daha az bilinir ve daha çok sorun çıkarır. JVM, cgroup CPU kotasını işlemci sayısı olarak görür: limits.cpu: 500m → JVM tek çekirdekli makine sanır → JIT derleme ve GC tek thread'le çalışır → başlangıç 3 kat yavaşlar, ilk dakikalarda gecikme yüksektir ve CPU throttling probe zaman aşımlarını tetikler. Öneri: CPU için requests tanımlayın (zamanlama için), limits'i ya koymayın ya da isteğin 2–4 katı verin; alternatif olarak -XX:ActiveProcessorCount=2 ile JVM'e gerçek paralelliği söyleyin.
3. Probe'lar: üç soru, üç cevap
Probe | Sorusu | Spring Boot ucu | Başarısızlıkta Kubernetes ne yapar | Ne kontrol etmeli |
|---|---|---|---|---|
startup | Uygulama açıldı mı? |
| Bekler; eşik aşılırsa yeniden başlatır | Yalnızca "process ayakta"; yavaş JVM için yüksek eşik |
readiness | Trafik alabilir mi? |
| Service'ten çıkarır (yeniden başlatmaz) | Bağımlılıklar: DB havuzu, önbellek, gerekli dış servis |
liveness | Kilitlendi mi, yeniden başlatmalı mı? |
| Konteyneri öldürür ve yeniden başlatır | Yalnızca uygulamanın iç durumu; asla DB ya da dış servis |
En yıkıcı hata, liveness'ın veritabanını kontrol etmesidir: DB 30 saniye yavaşlar, tüm pod'lar liveness'ı kaçırır, Kubernetes hepsini aynı anda yeniden başlatır, DB'ye bağlantı fırtınası gelir — küçük bir yavaşlık tam kesintiye dönüşür. Liveness "process sağlıklı mı", readiness "şu an iş yapabilir mi" sorusudur; bağımlılıklar readiness'ta yaşar.
apiVersion: apps/v1
kind: Deployment
metadata: { name: siparis-servisi }
spec:
replicas: 3
strategy:
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } # her an tam kapasite
template:
spec:
terminationGracePeriodSeconds: 45 # graceful shutdown + preStop'tan uzun
containers:
- name: app
image: registry.acme.com/siparis-servisi:2026.11.24-a1b2c3
ports: [{ containerPort: 8080 }]
resources:
requests: { cpu: "1", memory: "1536Mi" }
limits: { memory: "1536Mi" } # CPU limiti yok: JIT ve GC boğulmasın
env:
- { name: SPRING_PROFILES_ACTIVE, value: prod }
- { name: SPRING_CONFIG_IMPORT, value: "optional:configtree:/etc/config/" }
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 5
failureThreshold: 36 # 3 dakikaya kadar başlangıç toleransı
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
exec: { command: ["sh", "-c", "sleep 5"] } # endpoint listesinden çıkarılma yayılsın
volumeMounts:
- { name: config, mountPath: /etc/config, readOnly: true }
volumes:
- name: config
secret: { secretName: siparis-servisi-config }# application.yaml
server:
shutdown: graceful # Spring Boot 3.4+ varsayılan; açık yazmak zarar vermez
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # devam eden istekler için bekleme
management:
endpoint:
health:
probes:
enabled: true # Kubernetes'te otomatik açılır; açıkça yazmak belgeleme sağlar
group:
readiness:
include: readinessState, db, redis # trafik alabilmek için gerekenler
liveness:
include: livenessState # yalnızca uygulamanın kendisiGraceful shutdown akışı
Pod silinirken Kubernetes iki şeyi aynı anda yapar: SIGTERM gönderir ve pod'u Service'ten çıkarır. Endpoint listesinin tüm düğümlere yayılması birkaç saniye sürer; bu sırada gelen istekler kapanmakta olan pod'a düşer. preStop uykusu bu pencereyi kapatır; ardından Spring Boot yeni bağlantı kabul etmeyi durdurur, devam eden istekleri 30 saniyeye kadar bitirir, Kafka tüketicilerini ve havuzları kapatır. terminationGracePeriodSeconds bu toplamdan uzun olmalıdır; kısa olursa SIGKILL gelir ve yarım istekler kaybolur.
4. Yapılandırma ve gizli bilgiler
Ortama özgü hiçbir değer imajda olmaz: aynı imaj test, staging ve üretimde koşar. ConfigMap ve Secret dosya olarak bağlanır ve spring.config.import=configtree:/etc/config/ ile her dosya bir özellik olur; ortam değişkeni de olur ama uzun listelerde dosya daha temizdir. Gizli bilgiler için External Secrets Operator ya da Vault entegrasyonu; Secret'ları Git'e düz metin yazan bir proje, güvenli değildir.
5. Ölçekleme ve kullanılabilirlik
HPA: CPU yüzdesi JVM'de yanıltıcıdır (GC ve JIT dalgalanır); istek hızı, kuyruk derinliği ya da p95 gecikme gibi özel metrikler daha iyi sinyaldir.
PodDisruptionBudget: Düğüm bakımında en az bir replikanın ayakta kalması için
minAvailable: 1(ya da %50).Anti-affinity: Üç replikanın aynı düğüme düşmesini engelleyin.
Sanal thread'ler: Thread başına yığın maliyetini kaldırdığı için aynı bellekle daha fazla eşzamanlılık;
spring.threads.virtual.enabled=trueile küçük pod'larda belirgin kazanç.Loglar: Yalnızca stdout'a, JSON biçiminde; dosya logu ve log rotasyonu konteynerde yoktur.
Sahada sık yapılan hatalar
Startup probe olmadan liveness: JVM 40 saniyede açılır, liveness 30. saniyede öldürür → CrashLoopBackOff. Startup probe'un toleransı uzun olmalı.
Liveness'ta DB kontrolü: Yukarıdaki zincirleme yeniden başlatma.
Sabit
-Xmx= bellek limiti: OOMKilled, heap dump yok.Düşük CPU limiti: Yavaş başlangıç, throttling, probe zaman aşımı.
Kısa
terminationGracePeriodSeconds: Dağıtımda 5xx dalgası.Root kullanıcı ve
latestetiketi: Güvenlik ve tekrarlanabilirlik sorunu.
Sık sorulan sorular
Başlangıç süresini daha da kısaltmak için GraalVM native image mı?
CDS ile 40 saniyeden 12–15 saniyeye inen başlangıç çoğu kurumsal servis için yeterlidir. Native image milisaniyelere iner ama derleme süresi, yansıma kısıtları ve kütüphane uyumluluğu bedeli vardır; ölçek-sıfır ve fonksiyon senaryolarına saklayın.
Konteyner başına bir JVM mi, büyük pod mu?
Küçük ve çok pod: yatay ölçek, hızlı geri alma ve hata izolasyonu. 8 GB'lık tek dev pod yerine 1,5–2 GB'lık dört pod; JVM'in başlangıç yükü CDS ile makul kalır.
Sidecar'lar (service mesh) probe'ları etkiler mi?
Evet: mesh proxy'si uygulamadan önce hazır olmalıdır ve kapanışta uygulamadan sonra kapanmalıdır; aksi hâlde başlangıçta DB'ye erişilemez, kapanışta istekler düşer. Mesh'in kendi sıralama ayarları (holdApplicationUntilProxyStarts gibi) kullanılır.
Sonuç
Kubernetes'te sağlıklı Spring Boot: katmanlı ve root olmayan imaj, CDS ile kısa başlangıç, yüzdeyle verilen heap ve boğulmayan CPU, bağımlılıkları readiness'ta, kendi durumunu liveness'ta kontrol eden probe'lar, uzun toleranslı startup probe, preStop ve graceful shutdown ile kesintisiz dağıtım. Bu ayarlar bir kez şablona konur ve her servis aynı şablonla doğar.
Uygulamalarınızı Kubernetes'e taşımak ya da mevcut dağıtımınızın kararlılığını artırmak için bakım ve destek ile kurumsal uygulama geliştirme hizmetlerimize göz atabilirsiniz.