Spring Boot eğitimi kapsamında virtual thread kullanan servislerde JFR ile pinning tespiti, HikariCP bağlantı havuzu sınırı ve ölçülebilir kapasite deneyi tasarlayın.
Spring Boot'ta Virtual Thread Pinning Teşhisi ve Kapasite Tasarımı
Java backend geliştirme için virtual thread çalışma modeli
Virtual thread, bekleyen bir isteğin platform thread'ini işgal etmesini azaltır; ancak veritabanı bağlantısı, HTTP bağlantısı ve CPU zamanı gibi fiziksel kaynakları çoğaltmaz. Java backend geliştirme yapan ekiplerde ilk hipotez şu olmalıdır: 200 platform thread ile çalışan uygulama virtual thread sonrasında 20.000 eşzamanlı isteği kabul edebilir, fakat HikariCP 32 bağlantı sağlıyorsa aynı anda en fazla yaklaşık 32 SQL sorgusu ilerler. Bu nedenle yük testi sırasında JVM metrikleri ile havuz metriklerini birlikte kaydedin: Micrometer `jvm.threads.live`, `hikaricp.connections.active`, `hikaricp.connections.pending` ve HTTP p95 gecikmesi aynı Grafana panelinde bulunmalıdır.
Bir Spring Framework MVC uygulamasında virtual thread yürütücüsünü etkinleştirmeden önce uygulamanın gerçekten blocking I/O yaptığını doğrulayın. Salt CPU ağırlıklı JSON şifreleme, büyük koleksiyon sıralama veya görüntü işleme işlerinde virtual thread scheduler CPU çekirdeklerini aşamaz. Spring Boot yapılandırmasına aşağıdaki ayarı ekleyip, uygulamanın başlangıç logunda virtual thread executor kullandığını ve actuator metriklerinde request sayısını doğrulayın. Bu ayar, uygulama içindeki her üçüncü taraf executor'un otomatik olarak virtual thread kullanacağı anlamına gelmez; `@Async`, mesaj tüketicisi ve özel `ExecutorService` tanımları ayrıca denetlenmelidir.
spring:
threads:
virtual:
enabled: true
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
Java eğitimi, java programlama eğitimi veya java kursu içeriklerinde virtual thread genellikle 'thread başına request' olarak gösterilir. Üretimde kritik ayrım şudur: request thread'i ucuzlamış olsa bile downstream kapasitesi admission control gerektirir. Java microservices mimarisinde bir endpoint'in üç farklı servise paralel çağrı yapması, 5.000 virtual request altında 15.000 outbound socket denemesi üretebilir. Bu fan-out oranını `requests_in_flight * downstream_calls_per_request` ile hesaplayın ve her bağımlılık için ayrı bir eşzamanlılık limiti tanımlayın.
JFR ile virtual thread pinning ölçümü ve önce-sonra karşılaştırması
Pinning, virtual thread'in bir carrier platform thread'den ayrılamadığı durumdur. En sık neden, `synchronized` bloğu içinde JDBC, dosya sistemi veya uzak HTTP gibi bekleyen bir işlem çalıştırmaktır. JDK Flight Recorder'da `jdk.VirtualThreadPinned` olayını 10 ms eşiğiyle kaydedin; kısa, zararsız kilit geçişlerini gürültüye katmadan uzun beklemeleri görünür yapar. Yük testiyle aynı anda 120 saniyelik kayıt almak için çalışan pod veya VM üzerinde aşağıdaki komutu kullanın.
jcmd $PID JFR.start name=virtual-thread-baseline settings=profile filename=/tmp/virtual-thread-baseline.jfr duration=120s jdk.VirtualThreadPinned#threshold=10ms
Kaydı JDK Mission Control ile açın ve `Virtual Thread Pinned` olaylarını stack trace'e göre gruplayın. Önce aynı k6 senaryosunda dört değeri kaydedin: saniye başına istek, p50/p95/p99 gecikme, `hikaricp.connections.pending` maksimumu ve 10 ms üzeri pinning olay sayısı. Ardından sorunu giderip aynı istek gövdesi, aynı veri seti ve aynı pod CPU limitiyle testi tekrarlayın. Sadece ortalama gecikmeyi kıyaslamak yanıltıcıdır; carrier thread'ler kilitlendiğinde kuyruklanma çoğunlukla p99'da görünür.
Aşağıdaki kod, oturum haritasını korumak için `synchronized` kullanırken kilit altında uzak çağrı yaparak pinning üretir. Düzeltme, kilit kapsamını yalnızca bellek içi mutation ile sınırlar ve I/O'yu `ReentrantLock` dışında çalıştırır. `ReentrantLock` altında da I/O yapmayın: virtual thread kilit beklerken park edebilse bile kilidi tutan iş parçacığı downstream'i beklediği sürece uygulama seviyesinde seri hale gelme devam eder.
final class SessionResolver {
private final Map<String, String> sessions = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
private final HttpClient client = HttpClient.newHttpClient();
String resolve(String userId) throws Exception {
lock.lock();
try {
String cached = sessions.get(userId);
if (cached != null) return cached;
} finally {
lock.unlock();
}
String token = client.send(
HttpRequest.newBuilder(URI.create("https://auth.internal/token/" + userId)).build(),
HttpResponse.BodyHandlers.ofString()).body();
lock.lock();
try {
return sessions.computeIfAbsent(userId, ignored -> token);
} finally {
lock.unlock();
}
}
}Spring Data JPA ve Hibernate ORM için bağlantı havuzu admission control
Spring Data JPA ve Hibernate ORM çağrıları virtual thread üzerinde çalışsa da her aktif transaction JDBC bağlantısı tüketir. HikariCP'de `maximum-pool-size=32` iken 2.000 virtual request'i serbest bırakmak, faydalı paralellik yerine havuz kuyruğunu büyütebilir. `connection-timeout` değerini kullanıcı isteğinizin toplam timeout'undan küçük seçin. Örneğin API gateway 2.000 ms'de vazgeçiyorsa, 1.800 ms boyunca havuz bağlantısı beklemek yerine 150-300 ms içinde kontrollü olarak reddetmek, boşuna çalışan request ve zincirleme timeout sayısını düşürür.
spring:
datasource:
hikari:
maximum-pool-size: 32
minimum-idle: 8
connection-timeout: 250
validation-timeout: 1000
leak-detection-threshold: 5000
Havuz boyutunu CPU çekirdeğiyle körlemesine eşitlemeyin. PostgreSQL gibi bir veritabanında uygun eşzamanlı sorgu sayısı; sorgu CPU maliyeti, disk beklemesi, lock contention ve instance CPU'suna bağlıdır. Başlangıç deneyi olarak 16, 32 ve 48 havuz bağlantısıyla aynı k6 testini çalıştırın. Her deneyde PostgreSQL `pg_stat_activity` içindeki aktif bağlantıları, `pg_stat_statements` ortalama yürütme süresini ve Hikari pending sayısını kaydedin. Havuzu 32'den 48'e büyüttüğünüzde throughput artmıyor ama `blk_read_time`, lock wait veya p99 büyüyorsa, yeni bağlantılar veritabanındaki contention'ı artırıyordur.
Özellikle deneyimli ekiplerin gözden kaçırdığı durum, transaction kapsamıdır. Bir `@Transactional` metodunda SQL sorgusundan sonra dış HTTP çağrısı yapılırsa Hibernate ORM bağlantısı metodun sonuna kadar elde tutulabilir. Bu, virtual thread sayısı yükseldiğinde havuzun uzak servisin gecikmesine bağlanması demektir. Transaction'ı yalnızca veritabanı mutation'ını kapsayacak şekilde ayırın; dış çağrıyı transaction sonrasına taşıyın ve transaction süresini Micrometer `@Timed` veya OpenTelemetry span'ları ile ölçün.
Spring Boot'ta downstream limiti ve gerçekçi yük testi
Virtual thread'ler için kritik koruma, her bağımlılığa ayrı bulkhead uygulamaktır. Aşağıdaki `Semaphore`, ödeme sağlayıcısına aynı anda en fazla 40 çağrı gönderir ve 75 ms içinde slot bulunamazsa isteği kuyrukta sınırsız bekletmek yerine hata olarak döndürür. Semaphore beklemesi virtual thread'i park eder, carrier thread'i sürekli meşgul etmez; fakat limit yine de gereklidir, çünkü uzak sistemin bağlantı ve rate limit kapasitesi sınırlıdır.
@Component
final class PaymentClient {
private final Semaphore permits = new Semaphore(40);
PaymentResponse charge(PaymentRequest request) throws Exception {
if (!permits.tryAcquire(75, TimeUnit.MILLISECONDS)) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"payment bulkhead is full");
}
try {
return callRemotePaymentApi(request);
} finally {
permits.release();
}
}
}Önce-sonra deneyini rastgele manuel isteklerle yapmayın. k6 ile 2 dakika sabit 400 VU, ardından 2 dakika 800 VU uygulayın; response body doğrulaması ekleyerek sadece HTTP 200 sayısını değil iş sonucunu da kontrol edin. Baz senaryoda virtual thread kapalı, ikinci senaryoda açık, üçüncü senaryoda açık artı bulkhead etkin olsun. Her koşulda aynı JVM heap limiti, aynı HikariCP boyutu ve aynı downstream test stub gecikmesi kullanılmalıdır.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 400 },
{ duration: '2m', target: 400 },
{ duration: '30s', target: 800 },
{ duration: '2m', target: 800 }
],
thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<800'] }
};
export default function () {
const r = http.get('http://service:8080/api/orders/42');
check(r, { 'status is 200 or 503': x => x.status === 200 || x.status === 503 });
sleep(0.05);
}Java fullstack eğitimi projelerinde tarayıcı retry davranışı bu sonucu bozar. Frontend 503 yanıtını anında ve sınırsız tekrar denerse bulkhead, bağımlılığı korusa bile uygulama ingress'inde retry storm oluşur. İstemcide exponential backoff, jitter ve üst retry sınırı belirleyin; sunucuda ise 503 yanıtına `Retry-After` ekleyin. API metric'lerinde 503 oranını, semaphore available permit değerini ve downstream HTTP timeout sayısını aynı zaman ekseninde inceleyin.
Spring AI, Spring MCP ve Model Context Protocol araç çağrılarında edge case
Spring AI ile Spring MCP kullanan bir uygulamada Model Context Protocol araç çağrısı, normal bir REST endpoint'inden daha değişken süreli olabilir: model birden fazla tool çağrısı planlayabilir, her tool farklı servise gidebilir. Her tool için ortak global executor kullanmak yerine tool türüne göre ayrı semaphore veya Resilience4j Bulkhead tanımlayın. Örneğin `database_lookup` için 16, dosya tarama için 4, CRM HTTP çağrısı için 24 eşzamanlılık limiti koymak; yavaş dosya taramasının CRM tool çağrılarını kuyruğa almasını engeller.
Sinsi hata `ConcurrentHashMap.computeIfAbsent` mapping fonksiyonu içinde token yenileme veya tool keşfi için HTTP çağrısı yapmaktır. JDK dokümantasyonu mapping fonksiyonunun kısa ve basit olması gerektiğini belirtir; aynı hash bin'i etkilenebilir ve uzun I/O çağrısı yüksek çekişme üretir. Token yenilemeyi tek uçuşlu, timeout'lu ayrı bir async işlem olarak tasarlayın; Caffeine `AsyncLoadingCache` kullanıyorsanız loader'a kesin timeout uygulayın ve başarısız `CompletableFuture` sonucunun cache'de kalma davranışını test edin. Spring AI araç trafiğinde bu ayrıntı, JFR'de pinning görülmese bile tool gecikmesinin p99'u büyütmesinin yaygın nedenidir.
Bir spring boot eğitimi laboratuvarında doğrulama için MCP tool çağrısına OpenTelemetry span ekleyin: `tool.name`, `tool.request_size`, `bulkhead.wait_ms` ve `downstream.status_code` alanlarını kaydedin. Ardından 100 eşzamanlı tool çağrısında, sınırsız tasarımla bulkhead'li tasarımın tool başarı oranını ve p99'unu karşılaştırın. Amaç virtual thread sayısını büyütmek değil, hangi fiziksel bağımlılığın doygunluğa ulaştığını trace ve metrikle kanıtlamaktır.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Java backend geliştirmede virtual thread pinning nasıl tespit edilir?
Yük testi sırasında `jcmd $PID JFR.start` ile JFR kaydı alın ve `jdk.VirtualThreadPinned` olayına 10 ms threshold koyun. JDK Mission Control'de olayları stack trace'e göre gruplayın. `synchronized` altında JDBC, HTTP veya dosya I/O görürseniz kilit kapsamını I/O'dan ayırın; düzeltme sonrası aynı k6 senaryosunda pinning olay sayısı ve p99 gecikmesini tekrar ölçün.
Spring Data JPA virtual thread ile HikariCP havuzunu büyütmeli miyim?
Otomatik olarak hayır. 16, 32 ve 48 bağlantı boyutuyla aynı yük testini yapın; `hikaricp.connections.pending`, PostgreSQL `pg_stat_statements` süreleri, lock wait ve API p99 değerlerini karşılaştırın. Daha büyük havuz throughput artırmadan veritabanı lock veya disk beklemesini yükseltiyorsa optimum nokta daha düşük havuz boyutudur.
Spring Framework uygulamasında synchronized virtual thread için neden risklidir?
Virtual thread bir `synchronized` monitörünü tutarken blocking I/O yaparsa carrier platform thread'den unmount edilemeyebilir ve JFR `jdk.VirtualThreadPinned` olayı oluşur. Sadece in-memory state erişimini kilit içinde bırakın, uzak çağrıyı kilit dışına taşıyın. Alternatif olarak `ReentrantLock` kullanılsa bile kilit altında I/O çalıştırmak uygulama seviyesinde serializasyon yarattığı için doğru tasarım değildir.
Spring AI ve Spring MCP tool çağrılarında virtual thread tek başına yeterli mi?
Yeterli değildir. Model Context Protocol ile bir kullanıcı isteği birden fazla tool ve downstream çağrısı başlatabilir. Her tool sınıfına ayrı bulkhead limiti, kesin HTTP timeout ve OpenTelemetry span alanları ekleyin. Özellikle `ConcurrentHashMap.computeIfAbsent` içinde ağ çağrısı yapmayın; tool keşfi veya token yenilemeyi timeout'lu async cache loader'a taşıyı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.



