• 23.09.2026 21:05:33
  • Admin Admin

Spring Boot'ta virtual thread kullanırken JDBC havuzu, pinning ve SecurityContext aktarımını ölçerek yönetin. JFR, Micrometer ve kapasite sınırıyla Spring MVC isteklerinde kuyruk taşmasını görünür kılın.

Spring Boot'ta Virtual Thread, JDBC Kuyruğu ve Pinning Ölçümü

Spring Framework eğitimi bağlamında doğru virtual thread iş yükünü seçmek

Java 21 ile standarda giren virtual thread, bekleme ağırlıklı işleri daha ucuza eşzamanlı çalıştırır; ancak PostgreSQL bağlantısı, HTTP bağlantı havuzu veya Redis soketi üretmez. Bu nedenle bir spring framework eğitimi veya java spring eğitimi sırasında "thread sayısını artır" kuralı yerine, her istek için hangi kaynağın gerçekten kıt olduğunu çıkarın. Spring MVC uç noktanız JDBC'de ortalama 40 ms bekliyor ve HikariCP en fazla 52 bağlantı açıyorsa, 5.000 virtual thread aynı anda çalışabilse bile en fazla 52 sorgu ilerler; kalan istekler havuz kuyruğunda bekler.

Spring Boot'un otomatik yapılandırılmış görev yürütücüsünde virtual thread etkinleştirmek için aşağıdaki ayar kullanılabilir. Bu ayar özellikle @Async, zamanlayıcılar ve otomatik yapılandırılmış executor kullanan bileşenleri etkiler. HTTP istek işleme thread'lerinin de virtual olup olmadığını sürüm veya container varsayımıyla kabul etmeyin; staging ortamında /actuator/threaddump çıktısı ve JFR ile doğrulayın. Servlet container executor'ı özel olarak değiştirilmiş bir uygulamada bu özellik devre dışı kalabilir.

spring:
  threads:
    virtual:
      enabled: true

management:
  endpoints:
    web:
      exposure:
        include: threaddump,metrics,prometheus

Bir spring boot eğitimi veya spring boot kursu örneğinde ilk deneyinizi CPU ağırlıklı BCrypt, JSON serileştirme ya da büyük Excel üretimi üzerinde yapmayın. Bu işler carrier thread'i CPU'da meşgul eder ve virtual thread scheduler'ı daha fazla CPU oluşturmaz. Önce endpoint'leri APM trace'lerinden sınıflandırın: örneğin p95 süresinin en az yüzde 60'ı JDBC, uzak HTTP veya dosya I/O beklemesinden geliyorsa adaydır. OpenTelemetry span'larında db.client.duration, http.client.duration ve controller toplam süresini aynı trace üzerinde karşılaştırmak bu ayrımı doğrudan verir.

Spring MVC virtual thread geçişini JFR ve k6 ile önce-sonra ölçmek

Geçişten önce ve sonra aynı commit, aynı veritabanı veri seti, aynı HikariCP boyutu ve aynı trafik profiliyle iki ayrı koşu yapın. JDK Flight Recorder içinde jdk.VirtualThreadPinned, jdk.JavaMonitorEnter, jdk.SocketRead ve jdk.ThreadPark olaylarını inceleyin. Aşağıdaki komut kontrollü test ortamında hem JFR kaydı alır hem de pinning oluştuğunda stack trace yazar. Üretimde sürekli jdk.tracePinnedThreads kullanmak yerine kısa süreli JFR kaydı tercih edin; yoğun pinning logu uygulamanın I/O maliyetini büyütebilir.

java   -Djdk.tracePinnedThreads=full   -XX:StartFlightRecording=filename=baseline.jfr,settings=profile,dumponexit=true   -jar app.jar

jfr print --events jdk.VirtualThreadPinned baseline.jfr
jfr print --events jdk.JavaMonitorEnter baseline.jfr

Yükü sabit tutmak için k6 ile örneğin 2 dakika ısınma, 10 dakika sabit 300 VU ve 2 dakika soğuma kullanın. Sonuçları yalnızca p95 HTTP gecikmesiyle değerlendirmeyin: http_req_failed, p99, Hikari bekleyen bağlantı sayısı ve veritabanı CPU'sunu birlikte kaydedin. Virtual thread etkinleştirilince p95 düşerken hikaricp.connections.pending sürekli sıfırdan büyük kalıyorsa darboğaz scheduler değil veritabanıdır.

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

export const options = {
  stages: [
    { duration: '2m', target: 300 },
    { duration: '10m', target: 300 },
    { duration: '2m', target: 0 }
  ],
  thresholds: {
    http_req_duration: ['p(95)<800'],
    http_req_failed: ['rate<0.01']
  }
};

export default function () {
  const response = http.get('http://localhost:8080/api/orders?status=OPEN');
  check(response, { '200 döndü': (r) => r.status === 200 });
}

Micrometer tarafında Prometheus'a en az şu üç seriyi koyun: http_server_requests_seconds_bucket, hikaricp_connections_active ve hikaricp_connections_pending. Karşılaştırma kuralı somut olmalıdır: aynı k6 koşusunda p99 artıyor, pending bağlantı sayısı 30 saniyeden uzun sıfırdan büyük kalıyor ve aktif bağlantı sayısı havuz üst sınırına dayanıyorsa virtual thread sayısını artırmak çözüm değildir. Bu tablo, sorgu planı, indeks veya istek kabul sınırı üzerinde çalışılması gerektiğini gösterir.

Spring Boot'ta JDBC kuyruğunu sınırlamak ve backpressure uygulamak

Virtual thread ile sık yapılan hata, HikariCP kuyruğunu sınırsız bir kabul mekanizması gibi kullanmaktır. HikariCP'de 52 bağlantı ve 30 saniye connection-timeout varsa, ani yükte binlerce istek 30 saniyeye kadar heap'te request nesneleri, güvenlik bağlamı ve trace context ile yaşayabilir. Bunun yerine iş açısından kritik olmayan sorgular için uygulama sınırında 48 izin verin; dört bağlantıyı migration, health check veya yönetim işlemleri için boş bırakın.

spring:
  datasource:
    hikari:
      maximum-pool-size: 52
      connection-timeout: 3000
      validation-timeout: 1000

Aşağıdaki örnek, yalnızca pahalı rapor endpoint'inde veritabanı kapasitesine giriş kontrolü uygular. İzin 200 ms içinde alınamazsa 429 veya 503'e çevrilebilen özel hata üretilir. Kritik ayrıntı acquired bayrağıdır: kesinti veya timeout sonrası alınmamış izni serbest bırakmak semaphore sayacını hatalı biçimde artırır.

@Service
public class ReportService {
    private final Semaphore reportDbPermits = new Semaphore(48, true);
    private final ReportRepository repository;

    public ReportService(ReportRepository repository) {
        this.repository = repository;
    }

    public List<ReportRow> monthlyReport(YearMonth month) {
        boolean acquired = false;
        try {
            acquired = reportDbPermits.tryAcquire(200, TimeUnit.MILLISECONDS);
            if (!acquired) {
                throw new ReportCapacityExceededException();
            }
            return repository.findMonthlyReport(month);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Rapor isteği kesildi", e);
        } finally {
            if (acquired) {
                reportDbPermits.release();
            }
        }
    }
}

Bu sınırı her repository metoduna körlemesine eklemeyin. Aynı HTTP isteği içindeki iki nested servis çağrısı iki izin almaya çalışırsa, düşük kapasitede kendi kendine kilitlenme üretebilirsiniz. Sınırı controller-service girişinde, mesaj tüketici concurrency ayarında veya ayrı bir rapor executor'ında bir kez uygulayın. Ayrıca hikaricp.connections.timeout sayacını, uygulama tarafından reddedilen istek sayısını ve semaphore bekleme süresini ayrı metrikler olarak yayınlayın; aksi halde veritabanı yavaşlığı ile kasıtlı yük reddini aynı hata oranında karıştırırsınız.

Virtual thread pinning: synchronized bloklarda gizlenen Spring REST API sorunu

Java 21 davranışında bir virtual thread, monitor tabanlı synchronized blok içindeyken bloklayan I/O yaparsa carrier thread'e pinlenebilir. Bu yalnızca gecikme yaratmaz: sınırlı sayıdaki carrier thread bloklanınca diğer hazır virtual thread'lerin çalışması da gecikir. Aşağıdaki kodda uzak HTTP çağrısı tek bir cache monitor'u altında yapıldığı için hem mantıksal olarak tüm istekleri seri hale getirir hem de JFR'de jdk.VirtualThreadPinned olayı üretebilir.

private final Object lock = new Object();

public CustomerProfile loadProfile(String customerId) {
    synchronized (lock) {
        return partnerClient.fetch(customerId); // Ağ I/O monitor altında
    }
}

Doğru çözüm çoğu zaman synchronized yerine sadece ReentrantLock geçirmek değildir. Lock'u değiştirmek pinning davranışını iyileştirebilse de uzak çağrıyı lock altında tutmak yine tüm müşterileri seri hale getirir. Önce kritik bölümde yalnızca paylaşılan state'i okuyun, I/O'yu lock dışına çıkarın ve sonucu atomik güncelleyin. Cache stampede riski varsa anahtar bazlı tek-uçuş koordinasyonu veya Caffeine'in async cache API'si kullanın; global lock kullanmayın.

private final ConcurrentHashMap<String, CompletableFuture<CustomerProfile>> inFlight =
    new ConcurrentHashMap<>();

public CompletableFuture<CustomerProfile> loadProfile(String customerId) {
    return inFlight.computeIfAbsent(customerId, id ->
        partnerClient.fetchAsync(id)
            .whenComplete((result, error) -> inFlight.remove(id))
    );
}

Bu değişikliği doğrulamak için JFR'de pinning event sayısını tek başına değil, event stack trace'lerini de karşılaştırın. Stack trace uygulama kodu yerine JDBC sürücüsü, native kripto kütüphanesi veya eski bir logging appender'ını gösteriyorsa controller'ı değiştirmek fayda sağlamaz. Testte -Djdk.tracePinnedThreads=full çıktısını endpoint adı ve trace id ile eşleştirmek, sadece yük altında görünen bu tür bağımlılık kaynaklı pinning vakalarını buldurur.

Spring Security ve Spring Cloud içinde context aktarımını doğrulamak

Spring Security'nin varsayılan SecurityContextHolder stratejisi ThreadLocal tabanlıdır. Bir Spring MVC isteği içinde yeni bir virtual thread veya executor görevi başlatırsanız, kimlik bilgisinin oraya kendiliğinden taşındığını varsaymayın. Özellikle tenant filtresi, audit actor bilgisi ve OpenTelemetry MDC alanları sessizce boş kalabilir. Kendi executor'unuzu tanımlıyorsanız Spring Security'nin DelegatingSecurityContextAsyncTaskExecutor sarmalayıcısını kullanın; bu tanım Spring Boot'un otomatik executor yapılandırmasının yerine geçer.

@Bean
AsyncTaskExecutor applicationTaskExecutor() {
    SimpleAsyncTaskExecutor virtualExecutor =
        new SimpleAsyncTaskExecutor("audit-vt-");
    virtualExecutor.setVirtualThreads(true);

    return new DelegatingSecurityContextAsyncTaskExecutor(virtualExecutor);
}

@Async
public CompletableFuture<String> writeAuditRecord() {
    String principal = SecurityContextHolder.getContext()
        .getAuthentication().getName();
    return CompletableFuture.completedFuture(principal);
}

microservices mimarisi içinde bu ayrım daha görünür hale gelir: Spring Cloud LoadBalancer veya bir declarative HTTP client ile yapılan çağrıda authentication bilgisini thread-local'dan kopyalamak, token'ın HTTP header'a eklendiği anlamına gelmez. Outbound istemcide authorization header politikası açıkça tanımlı olmalı, Spring Cloud Gateway'de ise token relay yalnızca ilgili route'larda açılmalıdır. Testte @WithMockUser(username = "alice") ile @Async metodu çağırın ve audit kaydının gerçekten alice içerdiğini doğrulayın; yalnızca HTTP 200 kontrolü context kaybını yakalayamaz.

Bir spring rest api için son kabul kriteri şudur: virtual thread etkin ve devre dışı iki k6 koşusunda endpoint p95/p99, Hikari pending, JFR pinning sayısı, 429 oranı ve yetkili audit kaydı karşılaştırılır. Bu beş sinyalden biri bozuluyorsa değişikliği genel uygulamaya yaymak yerine, önce ilgili endpoint veya bağımlılık için izolasyon uygulayın. Bu yaklaşım, virtual thread'i sınırsız eşzamanlılık vaadi değil, ölçülmüş I/O beklemesi için bir yürütme modeli olarak kullanır.

Sık Sorulan Sorular

Spring Boot virtual thread HikariCP bağlantı havuzunu büyütür mü?

Hayır. Virtual thread yalnızca Java tarafındaki bekleyen iş sayısını ucuzlatır; PostgreSQL veya başka bir veritabanına açılabilecek gerçek bağlantı sayısını HikariCP maximum-pool-size ve veritabanı limitleri belirler. Prometheus'ta hikaricp_connections_pending sıfırdan büyük kalıyorsa önce sorgu süresini, indeksleri ve istek kabul sınırını inceleyin.

Spring MVC virtual thread pinning nasıl bulunur?

Kontrollü yük testi sırasında JFR'yi settings=profile ile çalıştırın ve jfr print --events jdk.VirtualThreadPinned kaydını alın. Geçici olarak -Djdk.tracePinnedThreads=full ekleyerek stack trace toplayın. synchronized altında JDBC veya HTTP çağrısı varsa I/O'yu monitor dışına taşıyın; event bağımlılık kodundaysa sürücü veya kütüphane sürümünü ayrı test edin.

Spring Security context @Async virtual thread'e nasıl aktarılır?

Kendi AsyncTaskExecutor bean'inizi SimpleAsyncTaskExecutor.setVirtualThreads(true) ile oluşturup DelegatingSecurityContextAsyncTaskExecutor ile sarın. @WithMockUser kullanan bir entegrasyon testinde @Async metodunun SecurityContextHolder üzerinden aynı principal adını okuduğunu doğrulayın. Bu aktarım, outbound HTTP isteğine token eklemez; HTTP client interceptor veya token relay politikası ayrıca gerekir.

Spring Cloud ve microservices mimarisinde virtual thread için hangi metrikler izlenmeli?

HTTP p95 ve p99, hikaricp.connections.active, hikaricp.connections.pending, connection timeout sayısı, jdk.VirtualThreadPinned event sayısı ve downstream HTTP timeout oranını aynı zaman aralığında izleyin. Sadece thread sayısı karar metriği değildir; downstream servis 200 bağlantıyla sınırlıysa binlerce virtual thread o sınırı aşamaz.

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