• 30.08.2026 09:03:39
  • Admin Admin

Spring Boot servislerinde rolling deployment sırasında 502, kesilen istek ve yinelenen POST üretmeden trafik tahliyesini tasarlayın. Readiness, Spring MVC aktif istek sayacı, Spring Security koruması ve k6 doğrulamasını uygulayın.

Spring Boot'ta Graceful Shutdown ve Kubernetes Trafik Tahliyesi

Spring Boot'ta readiness ve graceful shutdown sıralaması

Bir Pod'a SIGTERM gelmesi, yük dengeleyicinin o Pod'a yeni bağlantı göndermeyi aynı anda bıraktığı anlamına gelmez. Kubernetes EndpointSlice güncellemesi, ingress veya servis mesh uç nokta önbelleği ve istemcideki keep-alive bağlantıları farklı zamanlarda etkilenir. Bu nedenle doğru sıra önce Pod'u REFUSING_TRAFFIC durumuna almak, EndpointSlice'tan çıkmasını beklemek, ardından uygulamayı kapatmaktır. Spring Boot'un graceful shutdown modu kabul edilmiş servlet isteklerinin tamamlanmasını bekler; fakat readiness önceden false olmazsa bu bekleme penceresinde yeni istek kabul edilebilir.

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 40s

management:
  endpoint:
    health:
      probes:
        enabled: true
        add-additional-paths: true
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

Bu yapılandırma ile Kubernetes readiness probe'u /actuator/health/readiness adresini, aynı uygulama portundan erişim gerekiyorsa ek olarak /readyz adresini kullanabilir. timeout-per-shutdown-phase değeri rastgele seçilmemelidir: en uzun meşru HTTP isteği, proxy boşaltma gecikmesi ve kapanış payının toplamından büyük olmalıdır. Örneğin 20 saniyelik rapor üretimi yapan bir spring rest api için 10 saniyelik tahliye beklemesi ve 40 saniyelik uygulama kapanış penceresi, 30 saniyelik tek bir genel timeout'tan daha denetlenebilirdir.

Bu ayrım spring framework eğitimi veya java spring eğitimi içeriklerinde çoğu zaman atlanır: liveness sadece süreç kilitlenmiş mi sorusunu yanıtlamalıdır. Veritabanının kısa süreli erişilememesini liveness'a bağlamak Pod'un tekrar tekrar öldürülmesine yol açar. Bağımlılık erişilebilirliği ve trafik kabul kararı readiness grubunda değerlendirilmelidir.

Spring MVC aktif isteklerini async edge case'i ile ölçmek

Graceful shutdown süresini tahmin etmek için yalnızca http.server.requests histogramına bakmak yeterli değildir; histogram tamamlanmış istekleri gösterir. Drain başladıktan sonra kaç isteğin halen işlendiğini bir Micrometer gauge olarak yayınlayın. Spring MVC'de kritik incelik şudur: finally bloğunda sayaç azaltmak, Callable, DeferredResult veya servlet async kullanan uçlarda isteği gerçekten bitmeden sayacı sıfırlar. Bu nedenle AsyncListener kaydedilmelidir.

@Component
public final class InFlightRequestFilter extends OncePerRequestFilter {
    private static final String COUNTED = InFlightRequestFilter.class.getName() + ".counted";
    private final AtomicInteger inFlight = new AtomicInteger();

    InFlightRequestFilter(MeterRegistry registry) {
        Gauge.builder("app.http.inflight", inFlight, AtomicInteger::get)
             .description("Requests not completed at the servlet boundary")
             .register(registry);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        if (request.getAttribute(COUNTED) == null) {
            request.setAttribute(COUNTED, Boolean.TRUE);
            inFlight.incrementAndGet();
        }
        try {
            chain.doFilter(request, response);
        } finally {
            if (!request.isAsyncStarted()) {
                inFlight.decrementAndGet();
            } else {
                request.getAsyncContext().addListener(new AsyncListener() {
                    public void onComplete(AsyncEvent e) { inFlight.decrementAndGet(); }
                    public void onTimeout(AsyncEvent e) { inFlight.decrementAndGet(); }
                    public void onError(AsyncEvent e) { inFlight.decrementAndGet(); }
                    public void onStartAsync(AsyncEvent e) { e.getAsyncContext().addListener(this); }
                });
            }
        }
    }
}

Bu filtre async redispatch'te çift sayım yapmamak için request attribute kullanır. Ancak uygulamanızda filtre sırası, Spring Security filtrelerinden önce veya sonra bilinçli seçilmelidir: kimlik doğrulama dahil tüm bağlantıları saymak istiyorsanız önce, sadece iş mantığına ulaşan çağrıları saymak istiyorsanız sonra yerleştirin. Prometheus'ta drain anını izlemek için app_http_inflight ile birlikte sum(rate(http_server_requests_seconds_count{status=~"5.."}[1m])) sorgusunu aynı zaman ekseninde kullanın.

Spring Security ile drain endpoint'ini kontrollü açmak

Kubernetes preStop hook'unun çağıracağı drain endpoint'ini herkese açık bırakmak, herhangi bir istemcinin Pod'u trafikten düşürmesine izin verir. Aşağıdaki örnek uygulama içi bir paylaşımlı sır kullanır; üretimde sırrı Kubernetes Secret ile enjekte edin, endpoint'i NetworkPolicy ile yalnızca Pod ağına sınırlayın ve erişim denetimini denetim kaydına yazın. Daha merkezi ortamlarda aynı kontrol Spring Security resource server ile kısa ömürlü platform JWT'si üzerinden yapılabilir.

@RestController
@RequestMapping("/internal")
final class DrainController {
    private final ApplicationEventPublisher events;
    private final byte[] expectedToken;

    DrainController(ApplicationEventPublisher events,
                    @Value("${platform.drain-token}") String token) {
        this.events = events;
        this.expectedToken = token.getBytes(StandardCharsets.UTF_8);
    }

    @PostMapping("/drain")
    ResponseEntity<Void> drain(@RequestHeader("X-Drain-Token") String token) {
        if (!MessageDigest.isEqual(expectedToken,
                token.getBytes(StandardCharsets.UTF_8))) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }
        AvailabilityChangeEvent.publish(events, this, ReadinessState.REFUSING_TRAFFIC);
        return ResponseEntity.accepted().build();
    }
}

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.ignoringRequestMatchers("/internal/drain"))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health/**").permitAll()
            .requestMatchers("/internal/drain").permitAll()
            .anyRequest().authenticated())
        .build();
}

Buradaki permitAll(), drain çağrısının güvenli olduğu anlamına gelmez; doğrulama controller içinde yapılmaktadır. Sabit zamanlı MessageDigest.isEqual karşılaştırması, sır uzunluğu ve eşleşen önek üzerinden zaman farkı sızdırma riskini azaltır. Spring Security kullanan sistemlerde daha iyi bir seçenek, bu endpoint'i ayrı bir management portuna taşımak değil, ayrı bir platform kimliği ve Kubernetes NetworkPolicy ile katmanlamaktır; ayrı port, yanlış Service tanımı yüzünden probe'un uygulamayı değil boş bir portu denetlemesi gibi operasyonel hatalar üretir.

Kubernetes preStop, Spring Cloud ve microservices mimarisi

Aşağıdaki manifestte preStop önce readiness'i kapatır, ardından 15 saniye EndpointSlice, ingress ve servis keşfi yayılımı için bekler. Sonrasında Kubernetes SIGTERM gönderir ve Spring Boot kabul edilmiş istekleri 40 saniyeye kadar tamamlar. terminationGracePeriodSeconds, preStop beklemesi ile uygulama kapanış süresinin toplamından büyük olmazsa Kubernetes SIGKILL gönderir; burada alt sınır 55 saniyedir.

spec:
  terminationGracePeriodSeconds: 60
  containers:
    - name: orders
      image: registry.example/orders:build-20260830
      readinessProbe:
        httpGet:
          path: /actuator/health/readiness
          port: 8080
        periodSeconds: 3
        failureThreshold: 1
      lifecycle:
        preStop:
          exec:
            command:
              - /bin/sh
              - -c
              - >
                curl --fail --silent --show-error -X POST
                -H "X-Drain-Token: ${DRAIN_TOKEN}"
                http://127.0.0.1:8080/internal/drain
                && sleep 15

microservices mimarisi içinde Spring Cloud Gateway veya başka bir edge proxy kullanılıyorsa, yalnızca Pod readiness'ine güvenmeyin. Gateway'in discovery tabanlı route listesi ve HTTP connection pool'u eski Pod'a açık keep-alive bağlantı taşıyabilir. Rolling deployment testinde gateway access loglarında drain başlangıcından sonraki 20 saniye boyunca eski Pod'a giden istek sayısını etiketleyin. POST çağrılarında Gateway Retry filtresini körlemesine açmak yerine, yalnızca idempotency anahtarıyla korunan operasyonları yeniden deneyin; aksi halde tahliye sırasında zaman aşımına uğrayan ama sunucuda tamamlanan sipariş oluşturma çağrısı ikinci kez çalışabilir.

spring cloud kullanılan ortamda servis keşfi kaydının silinmesi ile Kubernetes readiness'in false olması iki ayrı sinyaldir. Uygulama hem Kubernetes Service hem de discovery client üzerinden çağrılıyorsa, rollout kabul kriterinize ikisini de ekleyin: drain sonrası yeni isteklerin EndpointSlice'ta ve discovery kayıtlarında sıfıra yakınlaması ölçülmelidir.

k6 ile önce-sonra kesinti ve gecikme karşılaştırması

Bu tasarımı yalnızca başarılı deployment kaydıyla doğrulamayın. Aynı container imajı, aynı replikasyon sayısı ve aynı trafik profiliyle iki rollout çalıştırın: önce preStop ve graceful shutdown kapalıyken, sonra açıkken. k6 sonucunda karşılaştırılacak metrikler http_req_failed, HTTP 502/503 sayısı, p95 gecikme ve POST yanıtlarındaki iş anahtarı tekrarlarıdır. Amaç p95'i soyut biçimde düşürmek değil, rollout zaman aralığında bağlantı kesilmesinin ve çift işlenmenin ölçülebilir şekilde sıfıra yaklaşmasıdır.

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

export const options = {
  scenarios: {
    steady_orders: {
      executor: 'constant-arrival-rate',
      rate: 80,
      timeUnit: '1s',
      duration: '6m',
      preAllocatedVUs: 30,
      maxVUs: 150
    }
  },
  thresholds: {
    http_req_failed: ['rate<0.001'],
    http_req_duration: ['p(95)<400']
  }
};

export default function () {
  const key = `${__VU}-${__ITER}`;
  const res = http.post(`${__ENV.BASE_URL}/orders`,
    JSON.stringify({ sku: 'book-42', quantity: 1 }),
    { headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key } });
  check(res, { '2xx or accepted': r => r.status === 201 || r.status === 202 });
  sleep(0.05);
}

Testin 60. saniyesinde kubectl rollout restart deployment/orders çalıştırın ve sonuçları Prometheus ile ilişkilendirin. Profiling için Java Flight Recorder kaydını rollout boyunca alın: jcmd <pid> JFR.start name=rollout settings=profile duration=6m filename=/tmp/rollout.jfr. JFR'da uzun GC pause, bloklanan JDBC çağrısı veya TLS handshake görülürse timeout'u büyütmek sorunu gizler; örneğin connection pool tükenmesi varsa HikariCP bekleme süreleri ve pool boyutu ayrıca düzeltilmelidir. Bu ölçüm disiplini, spring boot eğitimi veya spring boot kursu pratiğinde rollout davranışını uygulama kodundan bağımsız bir altyapı varsayımı olmaktan çıkarır.

Sık Sorulan Sorular

Spring Boot graceful shutdown Kubernetes'te neden tek başına 502 hatalarını bitirmez?

Graceful shutdown kabul edilmiş servlet isteklerini bekler, fakat SIGTERM anında ingress, EndpointSlice ve istemci keep-alive bağlantıları eski Pod'u kısa süre daha hedefleyebilir. Önce readiness'i REFUSING_TRAFFIC yapın, preStop içinde yayılım için ölçülmüş bir süre bekleyin, sonra SIGTERM ile uygulamayı kapatın. k6 ile rollout penceresindeki 502/503 oranını önce-sonra karşılaştırın.

Spring MVC async endpoint'lerinde aktif istek sayısı nasıl doğru ölçülür?

OncePerRequestFilter içindeki finally bloğu async iş başlatıldığında erken çalışır. HttpServletRequest.isAsyncStarted() kontrolünden sonra AsyncListener ekleyin ve sayacı onComplete, onTimeout ve onError olaylarında azaltın. Redispatch kaynaklı çift sayımı request attribute ile engelleyin; sonucu Micrometer Gauge olarak app.http.inflight adıyla yayınlayın.

Spring Security ile Kubernetes preStop drain endpoint'i nasıl korunur?

Endpoint'i dış trafiğe açık genel bir actuator operasyonu yapmayın. Kubernetes Secret'tan gelen token veya kısa ömürlü platform JWT'si doğrulayın, endpoint'i NetworkPolicy ile sınırlandırın ve çağrıları audit loga yazın. Paylaşımlı sır kullanılıyorsa MessageDigest.isEqual ile sabit zamanlı byte karşılaştırması yapın; yalnızca string equals kullanmak sır eşleşme önekine dair zaman farkı üretebilir.

Spring Cloud Gateway rollout sırasında POST isteklerini yeniden denemeli mi?

Hayır, idempotency kanıtı yoksa Retry filtresi POST'u yeniden denememelidir. Gateway timeout yaşasa bile eski Pod işlemi tamamlamış olabilir. Her mutasyon çağrısında Idempotency-Key saklayıp aynı anahtar için ilk sonucu döndürün; ardından k6 testinde her anahtarın veritabanında yalnızca bir iş kaydı oluşturduğunu kontrol edin.

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