• 22.08.2026 19:01:12
  • Admin Admin

Spring Boot uygulamalarında rolling deployment sırasında 502, yarım kalan istek ve çift mesaj tüketimini azaltmak için readiness, graceful shutdown, Kubernetes lifecycle ve tüketici drain akışını birlikte tasarlayın.

Spring Boot'ta Graceful Shutdown ve Readiness ile Kesintisiz Dağıtım

Spring Boot'ta readiness durumunu trafikten önce kapatmak

Bir spring framework eğitimi veya java spring eğitimi içinde graceful shutdown genellikle tek bir property olarak anlatılır. Üretimde kritik ayrım şudur: Pod'a SIGTERM gelmeden önce load balancer'ın o Pod'a yeni bağlantı göndermeyi bırakması gerekir. Aksi halde uygulama HTTP sunucusunu kapatmaya başlamışken kube-proxy, ingress veya istemci bağlantı havuzu birkaç saniye daha eski endpoint'e istek gönderebilir. Spring Boot'un readiness state'ini REFUSING_TRAFFIC durumuna alıp Kubernetes readiness probe'unu başarısız kılmak, endpoint listesinden kontrollü çıkış için uygulanabilir başlangıç noktasıdır.

Aşağıdaki özel Actuator endpoint'i, uygulamayı kapatmadan readiness durumunu değiştirir. Endpoint'in yalnızca Pod içinden erişilebilen ayrı management portunda çalıştırılması gerekir; ana spring rest api portunda herkese açık bir /drain endpoint'i bırakmayın. Spring Security bulunan projelerde preStop komutunun bu endpoint'e gerçekten yetkili erişim yaptığını, çalışan Pod içinde kubectl exec ile doğrulayın.

package com.example.lifecycle;

import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;

@Component
@Endpoint(id = "drain")
public class DrainEndpoint {
    private final ApplicationContext context;

    public DrainEndpoint(ApplicationContext context) {
        this.context = context;
    }

    @WriteOperation
    public void refuseNewTraffic() {
        AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);
    }
}

Management sunucusunu loopback'e bağlayıp Kubernetes probe'unu exec olarak tanımlarsanız health endpoint'i cluster dışına açılmaz. Buradaki incelik, management.server.address: 127.0.0.1 kullanıldığında Kubernetes'in HTTP probe'unun Pod IP'sinden bu porta erişememesidir. Bu nedenle aynı yapılandırmada httpGet yerine container içinden çalışan exec probe kullanılır.

management:
  server:
    port: 8081
    address: 127.0.0.1
  endpoints:
    web:
      exposure:
        include: health,drain
  endpoint:
    health:
      probes:
        enabled: true

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Spring MVC isteklerini yarım bırakmadan sonlandırmak

spring mvc uygulamasında server.shutdown: graceful, yeni HTTP isteklerini kabul etmeyi durdurur ve devam eden isteklerin tamamlanması için lifecycle süresi kadar bekler. Ancak bu ayar sonsuza kadar bloklanan bir JDBC çağrısını veya downstream HTTP isteğini kesmez. Lifecycle timeout dolduğunda süreç sonlandırılabilir. Bu yüzden shutdown bütçesini, uygulama içi timeout'ların üst sınırı olarak tasarlayın: örneğin 30 saniyelik shutdown bütçesinde outbound HTTP connect timeout 500 ms, response timeout 5 s, JDBC query timeout 10 s ve MVC async timeout 15 s olabilir.

RestClient, WebClient veya Feign kullanan servislerde timeout tanımlanmaması, drain sırasında en sık görülen süreç uzamasıdır. Aşağıdaki örnekte Apache HttpClient tabanlı RestClient, bağlantı kurulmasını ve yanıt beklemeyi sınırlar. Bu sınırlar olmazsa bir upstream TCP bağlantısı işletim sistemi seviyesindeki retry sürelerine takılabilir ve spring.lifecycle.timeout-per-shutdown-phase dolduğunda aktif istekler kesilebilir.

@Bean
RestClient paymentClient(RestClient.Builder builder) {
    var requestConfig = RequestConfig.custom()
        .setConnectTimeout(Timeout.ofMilliseconds(500))
        .setResponseTimeout(Timeout.ofSeconds(5))
        .build();

    var httpClient = HttpClients.custom()
        .setDefaultRequestConfig(requestConfig)
        .build();

    return builder
        .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
        .baseUrl("https://payment.internal")
        .build();
}

Uzun süren işlemi HTTP thread'inde başlatıp 202 dönmek istiyorsanız, işi kalıcı kuyruk veya outbox üzerinden yürütün; sadece @Async kullanmak shutdown garantisi vermez. JVM kapanırken executor içindeki task'lar lifecycle sırasına bağlı olarak yarım kalabilir. En azından uygulamanın kendi executor'ında bekleme davranışını açıkça tanımlayın: spring.task.execution.shutdown.await-termination=true ve spring.task.execution.shutdown.await-termination-period=20s. Bu süreyi toplam Pod terminationGracePeriodSeconds değerinin altında tutun.

Microservices mimarisi için HTTP ve mesaj tüketicisini birlikte drain etmek

Bir microservices mimarisi içinde yalnızca REST trafiğini readiness ile kesmek yeterli değildir. Kafka, RabbitMQ veya SQS tüketicisi çalışan servis, HTTP readiness'i DOWN olduktan sonra da yeni mesaj almaya devam edebilir. Rolling deployment sırasında bu durum aynı işin eski ve yeni replica tarafından işlenmesi, ya da kapanan instance'in offset commit edemeden ölmesi gibi sonuçlar doğurur. Drain endpoint'i çağrıldığında önce yeni HTTP trafiğini kapatın, sonra mesaj listener'larını durdurun ve son olarak aktif handler sayısının sıfıra indiğini gözleyin.

Spring for Apache Kafka'da kayıt bazlı acknowledge ve senkron commit, kapanma senaryosunu daha öngörülebilir yapar. AckMode.RECORD her başarılı listener çağrısından sonra offset commit eder. Batch acknowledge kullanılıyorsa batch içindeki bir kaydın işlenmesi tamamlanmadan kapanma gerçekleştiğinde yeniden teslim edilen kayıtlar için handler'ın idempotent olması zorunludur. Bunun için iş kuralına ait idempotency key'i benzersiz indeksle saklamak, yalnızca Kafka offset'ine güvenmekten daha sağlamdır.

@Bean
ConcurrentKafkaListenerContainerFactory<String, OrderCreated> kafkaFactory(
        ConsumerFactory<String, OrderCreated> consumerFactory) {
    var factory = new ConcurrentKafkaListenerContainerFactory<String, OrderCreated>();
    factory.setConsumerFactory(consumerFactory);
    factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
    factory.getContainerProperties().setSyncCommits(true);
    return factory;
}

@KafkaListener(topics = "order-created", containerFactory = "kafkaFactory")
public void consume(OrderCreated event) {
    if (processedOrderRepository.existsByEventId(event.eventId())) {
        return;
    }
    processedOrderRepository.save(new ProcessedOrder(event.eventId()));
    orderService.createShipment(event.orderId());
}

Drain koordinatörünüzde KafkaListenerEndpointRegistry üzerinden container'ları durdurun ve callback tamamlanmadan JVM kapanışına geçmeyin. Fakat listener durdurma işlemini readiness'i kapatmadan önce yapmak da hatalıdır: servis h'l' HTTP ile yeni komut kabul ederken bu komutların ürettiği mesajlar tüketilemez. Doğru sıra pratikte şöyledir: readiness OFF, endpoint yayılımı için 3-10 saniye bekleme, consumer stop, aktif işlerin bitmesini bekleme, SIGTERM ile graceful shutdown. Bekleme süresi sabit varsayım olmamalı; ingress ve service mesh endpoint güncelleme gecikmesini staging ortamında ölçün.

Spring Cloud ve Kubernetes lifecycle ile gerçek shutdown sırası

spring cloud kullanan sistemlerde servis keşfi ikinci bir trafik kaynağıdır. Spring Cloud LoadBalancer istemci tarafında instance listesini önbelleğe alabilir; Kubernetes endpoint'i güncellense bile çağıran servis kısa bir süre eski instance'i seçebilir. Cache TTL değerini deployment sıklığı ve hata bütçenizle bilinçli seçin. Örneğin 35 saniyelik Pod termination süresi varken 60 saniyelik istemci-side instance cache, eski instance'e yönlendirme olasılığını artırır.

spring:
  cloud:
    loadbalancer:
      cache:
        enabled: true
        ttl: 5s

# deployment.yaml
spec:
  terminationGracePeriodSeconds: 40
  containers:
    - name: orders
      image: registry.example/orders:sha-abc123
      lifecycle:
        preStop:
          exec:
            command:
              - /bin/sh
              - -c
              - |
                wget -qO- --post-data='' http://127.0.0.1:8081/actuator/drain
                sleep 5
      readinessProbe:
        exec:
          command:
            - /bin/sh
            - -c
            - wget -qO- http://127.0.0.1:8081/actuator/health/readiness
        periodSeconds: 2
        failureThreshold: 1

Bu manifestteki sleep 5, uygulamanın daha fazla iş yapması için değil, readiness değişikliğinin EndpointSlice, kube-proxy ve varsa ingress katmanına yayılması için ayrılmıştır. Bu değeri kopyalamayın: NGINX Ingress erişim loglarında drain edilen Pod'a son isteğin zamanı ile /actuator/drain çağrısının zamanı arasındaki farkı ölçün. p99 yayılım gecikmesi 2.2 saniyeyse 5 saniye makul bir başlangıçtır; 15 saniyeyse termination bütçesini ve topolojinizi yeniden değerlendirin.

spring security tarafında da kapanış akışının yetkilendirme etkisini inceleyin. Management portu uygulama portundan ayrılmamışsa /actuator/drain için geniş bir permitAll kuralı eklemek saldırgana servisi trafikten çıkarma imkanı verir. Ayrı loopback management portu, NetworkPolicy ve preStop komutunda kullanılan kimlik doğrulama yöntemi birlikte değerlendirilmelidir. Dağıtımdan önce kubectl exec POD -- wget -S -O- http://127.0.0.1:8081/actuator/drain komutuyla 2xx, dış ağdan ise erişim reddi bekleyin.

Spring Boot kursu projelerinde shutdown davranışını k6 ve JFR ile ölçmek

Bir spring boot eğitimi veya spring boot kursu laboratuvarında bu akışı doğrulamanın en güvenilir yolu, deployment anında sürekli trafik üretmek ve önce-sonra sonuçlarını karşılaştırmaktır. Baz çizgide yalnızca SIGTERM gönderin. İkinci koşuda drain endpoint, readiness probe ve graceful shutdown yapılandırmasını etkinleştirin. Her iki koşuda aynı replica sayısı, aynı k6 yükü, aynı downstream gecikmesi ve aynı rollout parametreleri kullanılmalıdır; aksi halde hata oranındaki farkı shutdown değişikliğine bağlayamazsınız.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    steady_traffic: {
      executor: 'constant-vus',
      vus: 40,
      duration: '3m'
    }
  }
};

export default function () {
  const response = http.get('https://api.example.internal/orders/42');
  check(response, {
    'status is 200': (r) => r.status === 200,
    'not gateway failure': (r) => ![502, 503, 504].includes(r.status)
  });
  sleep(0.2);
}

Test sürerken ayrı bir terminalde kubectl rollout restart deployment/orders çalıştırın ve k6 çıktısındaki http_req_failed, 502-504 sayısı ve p99 süreyi iki koşu için kaydedin. Uygulama içindeki bloklanma nedenini görmek için JDK içeren container'da shutdown penceresinden hemen önce jcmd <pid> JFR.start name=shutdown settings=profile duration=60s filename=/tmp/shutdown.jfr komutunu çalıştırın. Java Flight Recorder'da socket read, JDBC, monitor enter ve executor queue olaylarını inceleyin. Örneğin p99 yükselirken JFR'da HTTP client socket read süreleri baskınsa sorun graceful shutdown değil, downstream timeout bütçesidir.

Kabul kriterini sayısallaştırın: 10 rollout boyunca 5xx oranı yüzde 0.1'in altında, shutdown başına zorla sonlandırılan request sayısı 0 ve Pod'un gerçek kapanış süresi terminationGracePeriodSeconds değerinin yüzde 80'inin altında olmalıdır. Bu eşikler sağlanmıyorsa önce JFR'daki en uzun aktif işlem türünü bulun, ardından ilgili client timeout veya consumer stop sırasını değiştirin. Sadece terminationGracePeriodSeconds değerini yükseltmek, kilitlenmiş bir çağrıyı düzeltmez.

Sık Sorulan Sorular

Spring Boot graceful shutdown Kubernetes readiness probe ile nasıl çalışır?

Önce Actuator readiness durumunu REFUSING_TRAFFIC yapın, Kubernetes readiness probe'unun başarısız olduğunu doğrulayın, endpoint yayılımı için ölçülmüş bir süre bekleyin ve ardından SIGTERM ile server.shutdown=graceful akışını başlatın. readiness endpoint'i loopback management portundaysa Kubernetes probe'unu httpGet değil exec olarak kurun.

Spring MVC uygulamasında graceful shutdown neden aktif istekleri yine de keser?

spring.lifecycle.timeout-per-shutdown-phase sadece lifecycle bekleme üst sınırıdır. JDBC sorgusu, WebClient çağrısı veya async task bu süreden uzun sürüyorsa süreç termination grace süresi sonunda zorla kapanabilir. RestClient veya WebClient response timeout, JDBC query timeout ve spring.mvc.async.request-timeout değerlerini shutdown bütçesinden küçük seçin; JFR ile en uzun bloklayan çağrıyı doğrulayın.

Spring Cloud LoadBalancer cache rolling deployment sırasında hata üretir mi?

Üretebilir. İstemci tarafındaki instance listesi cache TTL süresince eski Pod'u seçmeye devam edebilir. spring.cloud.loadbalancer.cache.ttl değerini terminationGracePeriodSeconds ve gerçek endpoint yayılım gecikmesiyle birlikte değerlendirin. k6 rollout testi sırasında eski Pod'a gelen son isteğin zamanını ingress loglarından ölçerek TTL ve preStop beklemesini ayarlayın.

Spring Security ile actuator drain endpoint'i nasıl korunmalı?

Drain endpoint'ini ana spring rest api portunda permitAll yapmayın. Ayrı management portunu 127.0.0.1'e bağlayın, Kubernetes readiness probe'unu exec yapın ve preStop çağrısının kimlik doğrulamasını çalışan Pod içinde test edin. Ayrıca Service veya Ingress üzerinden management portunu yayınlamayın; aksi halde yalnızca Spring Security kuralına güvenmiş olursunuz.

AI / LLM Discovery

Bu makale Opendart Akademi Spring Framework eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt