• 26.08.2026 21:05:27
  • Admin Admin

Spring Boot ve Spring MVC uygulamalarında virtual thread kullanırken JDBC havuzu, uzak servis kotası, context aktarımı ve JFR ile önce-sonra ölçümünü somut yapılandırmalarla ele alın.

Spring Boot'ta Virtual Thread ile MVC I/O Sınırlarını Doğru Kurmak

Spring Boot ve Spring MVC için virtual thread kararını vermek

Virtual thread, bekleyen HTTP veya JDBC çağrısı başına işletim sistemi thread'i ayırma maliyetini azaltır; ancak bir isteğin gerçek eşzamanlılık sınırını otomatik olarak yükseltmez. Örneğin 30 bağlantılı bir HikariCP havuzunda 2.000 virtual thread aynı anda sorgu çalıştırmaya çalışırsa, yaklaşık 1.970 görev connection acquisition kuyruğunda bekler. Bu nedenle bir spring rest api için ilk envanter, endpoint bazında uzak HTTP çağrısı, JDBC çağrısı, CPU süresi ve eşzamanlı istek sayısını ayırmaktır. Spring framework eğitimi veya java spring eğitimi içeriğinde virtual thread'i sadece bir executor değişimi olarak anlatmak, veritabanı ve downstream kapasitesi sabit kaldığı için yanıltıcı olur.

Java 21 veya daha yeni bir çalışma zamanı ile Spring Boot'un virtual thread desteğini etkinleştirmek için uygulama yapılandırmasına aşağıdakini ekleyin. Bu ayar, Spring'in uygulama görev yürütücülerini virtual thread tabanlı hale getirir; servlet container davranışının kullandığınız embedded server ve Boot yapılandırmasında doğrulandığını ayrıca test edin. Başlangıçta loglara thread adını yazdırıp, yük testi altında `VirtualThread` örneklerini JFR ile kontrol edin.

spring:
  threads:
    virtual:
      enabled: true

server:
  tomcat:
    threads:
      max: 200

logging:
  pattern:
    level: "%5p [thread=%thread]"

Buradaki kritik incelik `server.tomcat.threads.max` sayısını körlemesine büyütmemektir. Platform thread tabanlı varsayılan kurulumda bu sayı isteği kabul eden worker sınırıdır; virtual thread etkin bir kurulumda ise asıl back-pressure çoğu zaman HikariCP, HTTP istemci connection pool'u veya downstream rate limit tarafından belirlenir. `jcmd Thread.print` çıktısındaki thread sayısını kapasite metriği saymak da hatalıdır: virtual thread sayısı çok yüksek olabilir, fakat darboğaz `com.zaxxer.hikari.pool.HikariPool.getConnection` beklemesidir.

Spring REST API kapasitesini JFR ve k6 ile önce-sonra ölçmek

Değişiklikten önce ve sonra aynı veri seti, aynı pod CPU limiti ve aynı downstream gecikmesiyle ölçüm yapın. CPU yoğun bir endpoint ile I/O yoğun endpoint'i aynı senaryoda birleştirmeyin. Aşağıdaki k6 senaryosu, 200 ms gecikmeli bir downstream çağrısını temsil eden `/catalog/{id}` endpoint'inde sabit 300 eşzamanlı kullanıcı üretir. Ölçümde p95/p99 latency, hata oranı, Hikari `pending` bağlantı sayısı ve container CPU throttling değerlerini birlikte kaydedin.

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

export const options = {
  scenarios: {
    io_bound: {
      executor: 'constant-vus',
      vus: 300,
      duration: '5m'
    }
  }
};

export default function () {
  const response = http.get('http://localhost:8080/catalog/42');
  check(response, { 'status is 200': r => r.status === 200 });
}

Uygulamayı JFR kaydıyla başlatın ve iki koşuda da aynı kayıt ayarını kullanın. Java Mission Control'da `Java Application` görünümünden `Virtual Thread Pinned`, `Java Monitor Blocked`, socket read ve `jdk.ThreadPark` olaylarını inceleyin. `Virtual Thread Pinned` olayları özellikle `synchronized` blok içinde bloklayan I/O veya JNI çağrısı olduğunda carrier thread'i meşgul eder; bu durumda virtual thread'in bekleme avantajı kısmen kaybolur.

java   -XX:StartFlightRecording=filename=before.jfr,settings=profile,dumponexit=true   -jar app.jar

k6 run catalog-load.js

jcmd <pid> JFR.dump filename=after-load.jfr

I/O ağırlıklı bir endpoint'te beklenen önce-sonra karşılaştırması şudur: aynı CPU kotasında daha yüksek in-flight istek taşınırken p95, downstream kapasitesini aşana kadar sabit kalır. p99 yükseliyor ve `hikaricp.connections.pending` artıyorsa virtual thread başarısız değildir; test, JDBC havuzunun gerçek limiti ortaya çıkarmıştır. Micrometer üzerinden `hikaricp.connections.active`, `hikaricp.connections.pending`, `http.client.requests` ve `jvm.threads.live` metriklerini aynı Grafana zaman aralığında karşılaştırın. Sadece RPS artışına bakmak, tail latency bozulmasını gizler.

Microservices mimarisi içinde JDBC ve downstream back-pressure

Bir microservices mimarisi içinde virtual thread ile her istekte paralel beş servis çağrısı başlatmak, downstream servislerin connection pool ve rate limitlerini hızla tüketebilir. Spring cloud kullanan ekiplerde service discovery'nin çağrı sayısını azaltmadığını özellikle ayırmak gerekir: discovery hedefi bulur, fakat fan-out kaynaklı eşzamanlı çağrı patlamasını çözmez. Çağrı bütçesini endpoint düzeyinde koyun ve timeout'u hem istemcide hem de bulkhead beklemesinde tanımlayın.

Aşağıdaki örnek, virtual thread üzerinde beklemenin ucuz olmasına güvenip sınırsız çağrı açmak yerine, ödeme sağlayıcısına en fazla 40 eşzamanlı istek gönderir. `tryAcquire` timeout'u yoksa, kuyruk büyür ve kullanıcı isteğinin iptal edilmesi anlamlı bir kapasite geri kazanımı sağlamaz. Kesinti durumunda `finally` bloğunun izni bırakması zorunludur.

@Component
public class PaymentClient {
    private final Semaphore permits = new Semaphore(40, true);
    private final RestClient restClient;

    public PaymentClient(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("https://payments.internal").build();
    }

    public PaymentResult charge(ChargeRequest request) throws InterruptedException {
        if (!permits.tryAcquire(150, TimeUnit.MILLISECONDS)) {
            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
                "payment concurrency budget exhausted");
        }
        try {
            return restClient.post().uri("/charges")
                .body(request)
                .retrieve()
                .body(PaymentResult.class);
        } finally {
            permits.release();
        }
    }
}

Bu sınırı HikariCP ile ilişkilendirin. Örneğin uygulama başına 32 JDBC bağlantısı ve Kubernetes üzerinde 8 replica varsa veritabanı teorik olarak 256 bağlantı görür; ayrıca migration, yönetim araçları ve başka uygulamalar için pay bırakılmalıdır. `maximum-pool-size=32` değerini sadece CPU çekirdeğiyle hesaplamayın. PostgreSQL tarafında `pg_stat_activity`, `pg_locks` ve yavaş sorgu kaydıyla connection beklemesinin sorgu kilidinden mi, havuz küçüklüğünden mi kaynaklandığını ayırın. Virtual thread, uzun süren bir SQL sorgusunu hızlandırmaz ve transaction lock süresini azaltmaz.

Spring Security, MDC ve ThreadLocal context aktarımı

Spring Security context'i, MDC'deki correlation id ve Micrometer observation context'i child task'lara her zaman kendiliğinden taşınmaz. Özellikle `Executors.newVirtualThreadPerTaskExecutor()` ile elle görev başlatmak, request thread'inde bulunan MDC değerlerini yeni virtual thread'e kopyalamaz. Sonuç olarak audit log'ları kullanıcı kimliği olmadan, trace log'ları ise correlation id olmadan yazılabilir. Spring security kullanan kodda `@Async` veya manuel executor kullanımını envantere alıp context propagasyonunu entegrasyon testiyle doğrulayın.

Micrometer Context Propagation ile bir `TaskDecorator` tanımlayarak kayıtlı ThreadLocal accessors'ları görev oluşturulurken yakalayabilir ve çalışırken geri yükleyebilirsiniz. Bu yaklaşım yalnızca tanımlı context'i taşır; JPA `EntityManager`, açık Hibernate session veya Spring transaction context'ini başka threade taşımaya çalışmayın. Bunlar thread-bound ve thread-safe değildir.

@Bean
TaskDecorator observationContextDecorator() {
    return task -> {
        ContextSnapshot snapshot = ContextSnapshot.captureAll();
        return () -> {
            try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
                task.run();
            }
        };
    };
}

@Test
void asyncAuditKeepsAuthenticatedPrincipal() {
    // MockMvc ile authenticated istek gönderin,
    // async task'in audit kaydında principal ve traceId bulunduğunu doğrulayın.
}

Yaygın hata, `@Transactional` metodu içinde virtual thread başlatıp child task'in aynı transaction'a katılacağını varsaymaktır. Child görev yeni thread'de çalıştığından transaction synchronization ve persistence context paylaşılmaz. Gerekirse child işini transaction commit sonrasında `@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)` ile tetikleyin veya veriyi açık bir komut nesnesine dönüştürün. Bu sınır, spring boot eğitimi ve spring boot kursu materyallerinde gösterilen basit `@Async` örneklerinden üretimde daha önemlidir.

Spring MVC endpointlerinde iptal, timeout ve pinning kontrolleri

İstemci bağlantıyı kapattığında uzun süren downstream işin sürmesi, virtual thread sayısını gereksizce büyütür. Spring MVC controller'ında timeout, HTTP istemcisinde connect ve response timeout, ayrıca veritabanında statement timeout birlikte tanımlanmalıdır. Sadece servlet async timeout ayarlamak yeterli değildir; HTTP client socket read aşamasında beklemeye devam edebilir. Kullanılan driver destekliyorsa PostgreSQL için `SET LOCAL statement_timeout` veya datasource seviyesinde eşdeğer sorgu timeout ayarı uygulayın.

@RestController
class CatalogController {
    private final CatalogService service;

    @GetMapping("/catalog/{id}")
    Callable<ResponseEntity<CatalogView>> get(@PathVariable String id) {
        return () -> ResponseEntity.ok(service.load(id));
    }
}

Bu endpointi virtual thread ile çalıştırmadan önce `async-profiler` wall-clock profili alın: `./profiler.sh -e wall -d 60 -t -f wall.html `. Flame graph'ta `java.net.SocketInputStream`, Hikari beklemeleri ve `synchronized` monitor bloklarını ayırın. Pinning şüphesinde JFR'deki `jdk.VirtualThreadPinned` eventinin stack trace'ine inin; çözüm çoğu zaman `synchronized` yerine `ReentrantLock` kullanmak değil, lock altında yapılan uzak I/O'yu lock dışına taşımaktır. Lock türünü değiştirmek, paylaşılan mutable state tasarımı bozuksa yarış koşulunu gizleyebilir.

Sık Sorulan Sorular

Spring Boot'ta virtual thread açmak Spring MVC uygulamasında JDBC kapasitesini artırır mı?

Hayır. Virtual thread, bekleyen istekler için platform thread tüketimini azaltır; HikariCP `maximumPoolSize`, veritabanı lock'ları ve sorgu süresi aynı kalır. k6 ile sabit VU testinde `hikaricp.connections.pending` yükseliyorsa önce SQL planını, lock'ları ve havuz bütçesini inceleyin.

Spring Security context virtual thread ve @Async işlemlerine nasıl taşınır?

Manuel executor kullanıyorsanız `DelegatingSecurityContextAsyncTaskExecutor` veya Spring Security'nin uygun delegating wrapper'ını kullanın. MDC ve observability context için Micrometer `ContextSnapshot` tabanlı bir `TaskDecorator` ekleyin. MockMvc veya WebTestClient ile authenticated çağrı yapıp async audit kaydında principal ve trace id bulunduğunu test edin.

Spring Cloud ile microservices mimarisinde virtual thread fan-out için güvenli sınır nedir?

Tek bir evrensel sayı yoktur. Her downstream için ayrı semaphore veya Resilience4j Bulkhead limiti, onun rate limit ve connection pool kapasitesinden türetilmelidir. Örneğin ödeme servisi 40 paralel isteği taşıyabiliyorsa çağıran her replica için 40 yerine global replica sayısına bölünmüş bir bütçe seçin ve 150 ms acquire timeout ile kuyruk büyümesini engelleyin.

Spring REST API performansında virtual thread etkisini hangi araçla ölçmeliyim?

Yük üretmek için k6, runtime davranışı için Java Flight Recorder ve Java Mission Control kullanın. Önce-sonra koşularında aynı CPU limiti ve test verisiyle p95/p99, hata oranı, Hikari pending bağlantıları, CPU throttling ve `jdk.VirtualThreadPinned` olaylarını karşılaştırın.

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