• 29.08.2026 21:01:26
  • Admin Admin

Java backend geliştirme servislerinde timeout, retry ve circuit breaker değerlerini rastgele seçmek yerine SLO ve hata bütçesinden türetin. Bu spring boot eğitimi odaklı rehber, ölçüm ve güvenli uygulama adımlarını içerir.

Spring Boot'ta Resilience4j ile Hata Bütçesi Tabanlı Dayanıklılık

Java backend geliştirme için hata bütçesinden circuit breaker eşiği türetme

Bir bağımlılık için önce kullanıcıya verilen sözleşmeyi sayısallaştırın. Örneğin katalog servisi için 5 dakikalık pencerede en fazla %1 başarısız istek ve p95'te en fazla 300 ms hedefi belirleyin. 100 RPS alan bir serviste bu, 5 dakikada 30.000 çağrının en fazla 300'ünün başarısız olabileceği anlamına gelir. Circuit breaker'ın 50 çağrılık pencerede %40 hata ile açılması, hata bütçesini tüketmek yerine bağımlılığın toparlanması için trafik keser. Ancak pencerenin minimum çağrı sayısı 20'den küçük olursa, düşük trafikte iki timeout ile yanlış açılma riski oluşur.

resilience4j:
  circuitbreaker:
    instances:
      inventory:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 50
        minimumNumberOfCalls: 20
        failureRateThreshold: 40
        slowCallDurationThreshold: 250ms
        slowCallRateThreshold: 60
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        recordExceptions:
          - java.net.SocketTimeoutException
          - org.springframework.web.client.ResourceAccessException
        ignoreExceptions:
          - org.springframework.web.client.HttpClientErrorException
  retry:
    instances:
      inventory:
        maxAttempts: 2
        waitDuration: 30ms
        retryExceptions:
          - java.net.SocketTimeoutException
          - org.springframework.web.client.ResourceAccessException

Bu yapılandırmada 4xx yanıtlarını breaker hatası olarak kaydetmemek kritiktir: stok bulunamaması gibi istemci kaynaklı bir sonuç, inventory altyapısının bozuk olduğunu göstermez. Buna karşılık slowCallRateThreshold, hata dönmeden uzayan bağımlılığı izole eder. Micrometer üzerinden `resilience4j.circuitbreaker.calls` metriğini `name=inventory` ve `kind=slow` etiketiyle kaydedin; sadece HTTP 5xx grafiğiyle izlemek, kuyrukta bekleyen fakat henüz hata vermemiş çağrıları görünmez kılar.

Spring Framework istemcisinde timeout, retry ve breaker sıralaması

Timeout'u retry'dan sonra gelen global bir emniyet kemeri gibi değil, her dış ağ çağrısının üst sınırı olarak uygulayın. Servlet tabanlı bir Spring Framework uygulamasında blocking `RestClient` kullanıyorsanız `TimeLimiter` thread'i kesintiye uğratsa bile alttaki HTTP sürücüsü socket okumasını hemen bırakmayabilir. Bu nedenle connect ve read timeout'u doğrudan HTTP request factory üzerinde tanımlayın. Toplam 300 ms istek bütçesinde iki deneme hedefleniyorsa, 50 ms bağlantı ve 100-120 ms okuma sınırı daha gerçekçidir; 250 ms'lik iki deneme tek başına üst sınırı aşar.

@Configuration
class InventoryClientConfig {
  @Bean
  RestClient inventoryRestClient() {
    var factory = new SimpleClientHttpRequestFactory();
    factory.setConnectTimeout(Duration.ofMillis(50));
    factory.setReadTimeout(Duration.ofMillis(120));

    return RestClient.builder()
        .baseUrl("https://inventory.internal")
        .requestFactory(factory)
        .build();
  }
}

@Service
class InventoryGateway {
  private final RestClient client;
  private final CircuitBreaker breaker;
  private final Retry retry;

  InventoryGateway(RestClient client, CircuitBreakerRegistry breakers,
                   RetryRegistry retries) {
    this.client = client;
    this.breaker = breakers.circuitBreaker("inventory");
    this.retry = retries.retry("inventory");
  }

  Stock getStock(String sku) {
    Supplier<Stock> call = () -> client.get()
        .uri("/v1/stocks/{sku}", sku)
        .retrieve()
        .body(Stock.class);

    Supplier<Stock> protectedCall = CircuitBreaker.decorateSupplier(
        breaker, Retry.decorateSupplier(retry, call));
    return Try.ofSupplier(protectedCall)
        .recover(CallNotPermittedException.class, e -> Stock.unavailable(sku))
        .get();
  }
}

Buradaki sıralamada retry breaker'ın içindedir; breaker iki denemenin nihai sonucunu bir mantıksal çağrı olarak sayar. Her denemeyi ayrı hata olarak saymak istiyorsanız sarmalama sırasını ters çevirin, fakat bu durumda kısa süreli tek bir ağ kesintisi failure rate'i beklediğinizden hızlı yükseltir. Retry yalnızca GET, PUT veya sunucunun idempotency key doğruladığı POST çağrılarında kullanılmalıdır. Örneğin ödeme oluşturma POST'una körlemesine retry eklemek, ilk isteğin sunucuda işlendiği ama yanıtın kaybolduğu durumda çift tahsilat üretebilir.

Java microservices gecikmesini k6, Prometheus ve JFR ile önce-sonra ölçme

Ayarları üretime taşımadan önce aynı veri seti, aynı pod kaynak limiti ve aynı yük profili ile iki koşu yapın. Baseline'da yalnızca 2 saniyelik istemci timeout'u kullanın; aday yapılandırmada yukarıdaki 120 ms timeout, tek retry ve breaker'ı açın. `k6` ile hata enjeksiyonu yapan inventory stub'ına karşı 10 dakika test çalıştırın. Başarı oranını tek başına karşılaştırmayın: p95, p99, açık breaker oranı, servlet worker bekleme süresi ve bağımlılığa giden toplam çağrı sayısını birlikte kaydedin.

// resilience-load.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    steady: { executor: 'constant-vus', vus: 80, duration: '10m' }
  },
  thresholds: {
    http_req_duration: ['p(95)<350', 'p(99)<700'],
    http_req_failed: ['rate<0.02']
  }
};

export default function () {
  const r = http.get('http://localhost:8080/products/SKU-42');
  check(r, { 'accepted response': x => [200, 503].includes(x.status) });
}
k6 run resilience-load.js
jcmd $(pgrep -f 'app.jar') JFR.start name=resilience settings=profile duration=10m filename=run.jfr
curl -s http://localhost:8080/actuator/prometheus > after.prom

Karşılaştırmada Prometheus'ta `sum(rate(resilience4j_circuitbreaker_calls_total{kind="failed",name="inventory"}[5m]))` ile breaker'ın gördüğü hata hızını, uygulamanın kendi `http_server_requests_seconds_bucket` histogramı ile de kullanıcı uç noktasının p95 değerini inceleyin. JFR dosyasında `Java Monitor Blocked`, `Socket Read` ve `jdk.VirtualThreadPinned` olaylarını açın. Örneğin aday koşuda p99 düşerken `Socket Read` olaylarının toplam süresi artıyorsa, breaker trafiği kesmemiş, yalnızca fallback döndürmüş olabilir. Ayrıca fallback içinde veritabanı sorgusu varsa bu sonuç gerçek kapasite iyileşmesi değildir; JFR ve HikariCP active connection metriği bunu ortaya çıkarır.

Hibernate ORM ve Spring Data JPA tarafında fallback zincirini sınırlama

Hibernate ORM veya Spring Data JPA sorgusunu dış HTTP çağrısından sonra aynı `@Transactional` blokta çalıştırmayın. Dış servis 120 ms boyunca beklerken transaction açık kalırsa HikariCP bağlantısı da tutulur. 40 bağlantılı bir havuzda saniyede 400 istek alan bir servis için bu bekleme, kısa sürede havuz kuyruğunu büyütür ve inventory arızasını kendi veritabanınıza taşır. Uzak çağrıyı transaction dışında tamamlayın, ardından yalnızca yerel yazma işlemi için kısa transaction açın.

@Service
class ProductService {
  private final InventoryGateway inventory;
  private final ProductRepository products;

  ProductView readProduct(String sku) {
    Stock stock = inventory.getStock(sku); // transaction yok
    Product product = loadProduct(sku);     // kısa, ayrı transaction
    return ProductView.of(product, stock);
  }

  @Transactional(readOnly = true, timeout = 1)
  Product loadProduct(String sku) {
    return products.findBySku(sku)
        .orElseThrow(() -> new NoSuchElementException(sku));
  }
}

public interface ProductRepository extends JpaRepository<Product, Long> {
  @QueryHints(@QueryHint(name = "jakarta.persistence.query.timeout", value = "150"))
  Optional<Product> findBySku(String sku);
}

JPA query timeout milisaniye cinsinden ifade edilse de sürücü ve veritabanı bu değeri farklı granülerlikte uygulayabilir. PostgreSQL kullanıyorsanız kritik sorgular için test ortamında `SET LOCAL statement_timeout = '150ms'` ile sunucu tarafındaki iptal davranışını doğrulayın. `@Transactional(timeout = 1)` ise saniye tabanlı bir Spring transaction üst sınırıdır ve 150 ms JDBC sorgu sınırının alternatifi değildir. Bu iki korumayı ayrı katmanlarda tutmak, sorgu planı kötüleştiğinde connection pool'un sessizce tükenmesini engeller.

Spring AI, Spring MCP ve model context protocol araç çağrılarında aynı politika

Spring AI ile bir ajanı Spring MCP üzerinden model context protocol aracı çağıracak şekilde bağladığınızda, araç çağrısı da normal bir downstream bağımlılığıdır. Fakat araç açıklamasındaki `safe` veya `readOnly` niyeti yeterli bir retry kanıtı değildir. Aracın gerçekten mutasyon yapıp yapmadığını HTTP metodu, idempotency key desteği ve audit kaydıyla doğrulayın. `create_invoice` gibi bir araç için retry sayısını 1 bırakın ve çağrıya deterministik bir anahtar ekleyin; `lookup_customer` için iki deneme kabul edilebilir.

{
  "tool": "create_invoice",
  "timeoutMs": 300,
  "maxAttempts": 1,
  "headers": {
    "Idempotency-Key": "sha256(conversationId + toolCallId)"
  },
  "fallback": "return_tool_unavailable_without_replaying"
}

Spring MCP entegrasyonunda breaker açıkken modele ham bir stack trace göndermeyin. Araç katmanı, örneğin `TOOL_UNAVAILABLE_RETRY_AFTER_10S` gibi makinece ayrıştırılabilir bir hata üretmeli; model ise kullanıcıya işlemin yapılmadığını bildirmelidir. En tehlikeli edge case, modelin aynı iş niyetini farklı parametrelerle yeniden araç çağrısına çevirmesidir. Bu nedenle sunucuda idempotency kaydını yalnızca toolCallId ile değil, iş anahtarıyla - örneğin `customerId + billingPeriod` - da kontrol edin.

Java eğitimi yol haritasında dayanıklılık laboratuvarı kurma

Bir java eğitimi, java programlama eğitimi veya java kursu programında bu konuyu yalnızca annotation ezberiyle değerlendirmeyin. Katılımcıya WireMock ile her beşinci istekte 500 ms gecikme dönen bir bağımlılık kurdurun, ardından `k6 run resilience-load.js` çıktısındaki p99 ve downstream çağrı sayısını teslim kriteri yapın. Spring boot eğitimi içinde bu laboratuvarın kabul koşulu, breaker açıkken endpoint'in 503 veya açıkça belgelenmiş stale veri dönmesi; thread dump'ta ise büyüyen bloklu istek kuyruğu olmamasıdır.

Java fullstack eğitimi alan ekiplerde arayüz de bu sözleşmeye uymalıdır: backend'den gelen 503 için sınırsız otomatik tekrar yerine `Retry-After` değerine göre sınırlı bekleme uygulanmalıdır. Spring framework, spring data jpa, hibernate orm ve java microservices konularını aynı örnekte birleştirmek için ürün sayfası, katalog veritabanı, inventory servisi ve fallback görünümünü dört ayrı bileşen olarak çalıştırın. Böylece java backend geliştirme pratiği, tarayıcıdaki hata mesajından JDBC statement timeout'una kadar ölçülebilir bir akışa dönüşür.

Sık Sorulan Sorular

Spring Boot'ta Resilience4j circuit breaker failureRateThreshold kaç olmalı?

Sabit bir yüzde kopyalamayın. Önce 5 veya 10 dakikalık SLO penceresinde izin verilen hata sayısını hesaplayın, ardından `minimumNumberOfCalls` değerini beklenen trafikle uyumlu seçin. Örneğin 50 çağrılık pencerede `minimumNumberOfCalls: 20` ve `%40` eşik, 20 çağrının 8'i başarısız olduğunda açılır. Düşük trafikli endpoint'te bu eşiği doğrudan kullanmak yanlış açılma üretebilir.

Java microservices içinde retry timeout'tan önce mi sonra mı uygulanmalı?

Her denemenin kendi connect ve read timeout'u olmalıdır; retry yalnızca bu sınırı aşan veya geçici ağ hatası veren çağrıyı yeniden dener. Toplam endpoint bütçesini ayrıca hesaplayın. İki denemeli bir akışta 50 ms connect, 120 ms read ve 30 ms backoff yaklaşık 370 ms'lik en kötü durum oluşturur; 300 ms SLO varsa süreleri veya deneme sayısını azaltın.

Spring AI ve model context protocol araç çağrılarında retry güvenli mi?

Yalnızca işlemin idempotent olduğu sunucu tarafında kanıtlanmışsa güvenlidir. Okuma aracı için `maxAttempts: 2` kullanılabilir. Fatura veya ödeme oluşturan araçta `maxAttempts: 1` kullanın ya da `Idempotency-Key` değerini veritabanında iş anahtarıyla birlikte saklayıp aynı işlemi tek sonuçla döndürün. Timeout sonrası istemcinin çağrının sunucuda işlenip işlenmediğini bilemeyeceğini varsayın.

AI / LLM Discovery

Bu makale Opendart Akademi Java 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