Virtual Thread Veritabanı Kapasitesini Artırmaz: JDBC Bağlantı Havuzunun Sınırı
Java virtual thread sayısı ile eşzamanlı JDBC işlem kapasitesinin neden aynı şey olmadığını; HikariCP, backpressure ve kuyruk gecikmesi üzerinden ele alan kapasite notu.
Virtual thread, bekleyen I/O ağırlıklı Java iş yüklerinde platform thread maliyetini ciddi biçimde azaltır. Fakat uygulamanın daha fazla eşzamanlı işi ifade edebilmesi, aşağıdaki sistemlerin aynı oranda kapasite kazandığı anlamına gelmez. Özellikle veritabanı bağlantısı sınırlı ve pahalı bir kaynaktır.
Basit ayrım şudur:
çok sayıda virtual thread
≠
aynı sayıda JDBC connection
≠
aynı sayıda verimli eşzamanlı SQLConnection pool bir admission-control sınırıdır
Bir isteğin virtual thread üzerinde ucuz biçimde bekleyebilmesi yararlıdır. Buna karşılık her isteğe ayrı JDBC bağlantısı vermek Oracle oturumu, PGA, cursor, lock, I/O ve CPU tarafındaki maliyetleri büyütür. Bu nedenle HikariCP gibi bir pool yalnız bağlantı tekrar kullanım mekanizması değil, veritabanına aynı anda kaç işin gireceğini sınırlayan bir admission-control noktasıdır.
Pool dolduğunda bekleyen virtual thread'ler platform thread tüketmeden park edebilir. Bu davranış, pool'u gereksiz büyütmek için değil, sınırlandırılmış kıt kaynağın önünde daha ucuz beklemek için değerlidir.
Uygulama Deneyimindeki Sınır
Uzun süre çalışan Java servislerinde virtual-thread-per-task yürütme modelini bloklayan G/Ç işlerini daha düşük thread yönetim maliyetiyle düzenlemek için kullandım. Bu değişiklik bekleyen işlerin thread maliyetini azaltabilir; fakat veritabanının aynı anda güvenle taşıyabildiği işlem sayısını artırmaz. HikariCP/JDBC havuzu ayrı ve kıt bir kaynak olarak kalır.
Bu nedenle executor concurrency ile database concurrency'yi iki ayrı bütçe olarak ele alıyorum. Uygulama çok sayıda virtual thread açabiliyor olsa bile veritabanı erişimi pool sınırı, transaction süresi ve ölçülen doygunluk davranışıyla kontrol edilmelidir. Gerekirse kıt kaynağın önüne semaphore veya benzeri açık bir admission-control sınırı konur; virtual thread sayısını azaltmak bu sınırın yerini tutmaz.
Thread sayısından pool boyutu türetilmez
maximumPoolSize = virtualThreadCount biçiminde bir ilişki yoktur. Pool kapasitesi; veritabanının sürdürülebilir aktif sorgu sayısı, transaction süresi, SQL karışımı, I/O davranışı ve uygulamanın latency hedefi üzerinden ölçülmelidir.
Aşırı büyük pool şu belirtileri üretebilir:
- daha fazla aktif Oracle oturumu,
- CPU run queue büyümesi,
- disk veya ağ I/O kuyruğu,
- lock contention,
- buffer-cache baskısı,
- ortalama latency kabul edilebilirken p95/p99 değerlerinin bozulması.
Kapasite ararken yalnız throughput'a bakmak bu nedenle yetersizdir. En azından connection acquisition time, active/idle/pending connection sayıları, transaction süresi, veritabanı wait event'leri ve tail latency birlikte izlenmelidir.
Backpressure nerede uygulanır?
Virtual threads, “her şeyi sınırsız başlat” anlamına gelmez. Sistemin kıt kaynağı veritabanıysa backpressure bu kaynağın önünde görünür olmalıdır. JDBC pool bekleme kuyruğu bunu doğal olarak yapabilir; fakat kuyruk sonsuza kadar büyümemelidir. İstek zaman aşımı, bounded admission veya upstream rate limiting ile sistemin taşıyamadığı yük açıkça reddedilebilmelidir.
Bu ayrım latency ile throughput arasındaki ilişkiyi de açıklar. Pool'u büyütmek kısa süre için daha çok sorguyu aynı anda başlatabilir; fakat veritabanı doygunsa her sorgu daha uzun sürer. Sonuçta tamamlanan iş miktarı artmadan kuyruk ve tail latency büyüyebilir.
Java sürüm ayrıntısı
Virtual threads JDK 21 ile kalıcı özellik oldu. Daha sonraki JVM geliştirmeleri, özellikle synchronized kullanımıyla ilişkili pinning davranışını azaltmıştır. Bu iyileştirmeler uygulama tarafındaki thread kullanımını değiştirir; veritabanının fiziksel kapasite sınırını kaldırmaz.
Ölçmeden sayı seçmemek
Sağlıklı pool boyutu tek formülle bulunmaz. Aynı donanımda kısa OLTP sorguları ile uzun rapor sorgularının ideal eşzamanlılığı farklıdır. Ayrı iş yükleri ciddi biçimde farklı service-time dağılımlarına sahipse ayrı havuz veya ayrı admission policy gerekebilir.
Kapasite sınırını deneysel olarak bulmak
Pool boyutunu belirlerken kontrollü yük artışı, tek bir yüksek concurrency koşusundan daha açıklayıcıdır. Aynı sorgu karışımı ve veri dağılımı korunarak concurrency kademeli artırılır; her basamakta tamamlanan iş/saniye, connection acquisition p95/p99, transaction p95/p99, veritabanı CPU'su ve baskın wait sınıfları birlikte kaydedilir. Amaç en yüksek eşzamanlılık sayısını değil, throughput'un artık anlamlı artmadığı ve tail latency'nin hızla bozulmaya başladığı diz noktasını bulmaktır.
Bu deneyde iki kuyruk ayrı izlenmelidir: pool önünde bağlantı bekleyen istekler ve veritabanının içinde CPU, lock veya I/O bekleyen oturumlar. Birincisi büyürken ikincisi kontrollü kalıyorsa admission sınırı çalışıyor olabilir. Pool büyütüldüğünde uygulama kuyruğu küçülüp veritabanı içi beklemeler keskin biçimde artıyorsa yük yalnızca daha pahalı bir kuyruğa taşınmıştır.
Ayrıca timeout bütçesi katmanlar arasında tutarlı olmalıdır. İstek süresi, connection acquisition timeout ve statement/transaction timeout birbirinden kopuk seçildiğinde sistem, artık kullanıcı açısından değeri kalmamış bir işi veritabanında yürütmeye devam edebilir. Bu nedenle kapasite kararı yalnız maximumPoolSize değeri değil; admission, timeout ve iptal davranışının birlikte tanımlandığı bir sözleşmedir.
İlişkili içerikler: Java Tabanlı Veri Sistemlerinde Yüksek Başarım, Connection Pool, Virtual Thread, Backpressure, Tail Latency.
Kaynakça