HikariCP ile Dinamik Veri Kaynağı Yönlendirme

HikariCP ile Dinamik Veri Kaynağı Yönlendirme

Transaction kapsamındaki veri kaynağı seçimini HikariCP, ThreadLocal bağlamı ve Spring yönlendirme bileşenleriyle güvenli biçimde kuran yaklaşımı inceler. Havuz kapasitesi, temizleme, gözlemlenebilirlik ve hata davranışı birlikte ele alınır.

"AbstractRoutingDataSource", bağlantı havuzu oluşturmaz. "getConnection()" çağrısını o anda geçerli olan yönlendirme anahtarına göre başka bir "DataSource" nesnesine aktarır. Hedefler HikariCP kullanıyorsa her hedef bağımsız bir "HikariDataSource" ve bağımsız bir fiziksel bağlantı havuzudur. Yönlendirme katmanı bu havuzları tek bir "DataSource" arayüzü altında toplar.

Bu ayrım, çok sayıda Oracle şeması bulunan sistemlerde belirleyicidir. Örneğin 84 şema için 84 HikariCP havuzu tanımlanmışsa "maximumPoolSize=10" toplam sınırı 10 yapmaz. Teorik üst sınır 840 fiziksel bağlantıdır. Bazı havuzlar hiç kullanılmasa bile "minimumIdle", başlangıç davranışı ve bağlantı yaşam döngüsü toplam oturum sayısını etkiler. HikariCP, "maximumPoolSize" değerini havuz başına gerçek bağlantı üst sınırı olarak uygular.

Yönlendirme anı

"AbstractRoutingDataSource", hedef veri kaynağını sorgu metnine, repository sınıfına veya transaction adına bakarak kendiliğinden seçmez. Alt sınıfın uyguladığı "determineCurrentLookupKey()" metodu bir anahtar döndürür. Spring bu anahtarı önceden çözümlenmiş hedef veri kaynakları haritasında arar ve seçilen "DataSource" üzerinden bağlantı alır. Anahtarın türü serbesttir, ancak dönen değer haritadaki anahtar türüyle eşleşmelidir.

En sade uygulama, geçerli anahtarı bir "ThreadLocal" içinde tutar:

public final class DataSourceContext {

private static final ThreadLocal<String> CURRENT = new ThreadLocal<>();

private DataSourceContext() {
    }

public static void set(final String key) {
        CURRENT.remove();
        CURRENT.set(java.util.Objects.requireNonNull(key));
    }

public static String get() {
        return CURRENT.get();
    }

public static void clear() {
        CURRENT.remove();
    }
}

Yönlendirici sınıf yalnızca bu değeri döndürür:

public final class RoutingDataSource extends org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource {

@Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.get();
    }
}

"ThreadLocal.set(null)" yerine "remove()" kullanılmalıdır. Özellikle scheduler, platform thread havuzu veya uzun ömürlü executor kullanan yapılarda thread sonraki görevlerde tekrar kullanılabilir. Önceki görevden kalan anahtar temizlenmezse yeni görev yanlış Oracle şemasına yönlenebilir.

Güvenli yaşam döngüsü şu sırayı izler:

public void dispatch(final String key) {
    DataSourceContext.clear();
    DataSourceContext.set(key);

try {
        transactionalService.process();
    } finally {
        DataSourceContext.clear();
    }
}

Buradaki "transactionalService", ayrı bir Spring bean'i olmalıdır. Yönlendirme anahtarı transaction başlamadan ve ilk JDBC erişiminden önce atanır. "finally" bloğu normal dönüş, checked exception ve runtime exception yollarında temizliği garanti eder.

"InheritableThreadLocal" bu problem için güvenli bir çözüm değildir. Alt thread'e bağlam kopyalamak, transaction kaynağını veya JDBC bağlantısını aktarmak anlamına gelmez. Spring'in zorunlu transaction yönetimi kaynakları mevcut thread'e bağlar. İş başka bir thread'e taşındığında aynı transaction bağlamının devam ettiği varsayılamaz.

Transaction sınırı

Yönlendirme anahtarı her SQL komutundan önce yeniden değerlendirilmez. Spring transaction yöneticisi, routing "DataSource" üzerinden aldığı JDBC bağlantısını mevcut thread'e bağlar. Aynı transaction içindeki "JdbcTemplate" ve diğer Spring JDBC işlemleri bu bağlı bağlantıyı yeniden kullanır.

Bu nedenle aşağıdaki akış beklenen sonucu üretmez:

@Transactional
public void process() {
    DataSourceContext.set("SCHEMA_A");
    repository.insertA();

DataSourceContext.set("SCHEMA_B");
    repository.insertB();
}

İlk JDBC erişimi "SCHEMA_A" havuzundan bağlantı aldıysa transaction süresince aynı fiziksel bağlantı kullanılır. Bağlam daha sonra "SCHEMA_B" olarak değiştirilse bile ikinci işlem otomatik olarak başka havuza geçmez. Yönlendirme anahtarı ile bağlı bağlantı birbirinden kopmuş olur.

Tek transaction içinde birden fazla veri kaynağına yazmak gerekiyorsa "AbstractRoutingDataSource" tek başına yeterli değildir. İşlem iki bağımsız yerel transaction olarak tasarlanabilir, dağıtık transaction yöneticisi kullanılabilir veya iş akışı idempotent adımlara ayrılabilir. Birden fazla Oracle şeması aynı veritabanı oturumu üzerinden erişilebiliyorsa ayrı havuz yerine yetkilendirilmiş şema adlarıyla sorgu yürütmek de farklı bir mimari seçenektir. Bu karar güvenlik sınırlarına ve şema sahipliği modeline bağlıdır.

"REQUIRES_NEW" yeni fiziksel transaction ve yeni bağlantı gerektirebilir. Dış transaction bağlantıyı tutarken iç transaction ikinci bir bağlantı bekler. Çok sayıda eş zamanlı iş akışında her thread dış bağlantıyı tutup iç bağlantı beklerse havuz tükenebilir. Spring belgeleri, bu propagation türünün havuz kapasitesini aşması halinde kilitlenmeye kadar gidebilen kaynak beklemeleri oluşturabileceğini belirtir.

Routing anahtarının AOP ile atanması halinde aspect sırası da transaction sınırının parçasıdır. Routing aspect, transaction interceptor çalışmadan önce bağlamı hazırlamalıdır. Aksi halde transaction yöneticisi varsayılan veya önceki veri kaynağından bağlantı alabilir.

Spring'in varsayılan proxy tabanlı transaction yönetiminde aynı sınıf içindeki self-invocation, "@Transactional" metodunu proxy üzerinden geçirmez. Bu nedenle routing ayarlandıktan sonra aynı nesne üzerindeki başka bir metoda doğrudan çağrı yapmak transaction sınırını beklenen yerde başlatmayabilir. Spring belgeleri proxy modunda yalnız dışarıdan proxy üzerinden gelen çağrıların yakalandığını açıkça belirtir.

Hedef havuzların kurulması

Routing veri kaynağı oluşturulurken hedefler anahtar ve "DataSource" eşleşmesi olarak verilir:

@Bean
public DataSource dataSource(final Map<String, HikariDataSource> pools) {
    final RoutingDataSource routing = new RoutingDataSource();
    final Map<Object, Object> targets = new java.util.HashMap<>();

for (final Map.Entry<String, HikariDataSource> entry : pools.entrySet()) {
        targets.put(entry.getKey(), entry.getValue());
    }

routing.setTargetDataSources(targets);
    routing.setLenientFallback(false);
    routing.afterPropertiesSet();
    return routing;
}

"afterPropertiesSet()" iç durumu hazırlar ve tanımlanan veri kaynaklarını çözümler. Spring tarafından bean yaşam döngüsü içinde oluşturulan bir "AbstractRoutingDataSource" için bu çağrı otomatik yapılır. Nesne elle oluşturulup Spring dışında kullanılıyorsa başlatma sorumluluğu uygulamaya aittir. Çözümlenmiş veri kaynakları haritası dışarıya değiştirilemez görünümle sunulur.

"setLenientFallback(false)" kritik sistemlerde daha güvenli davranır. Varsayılan değer "true" olduğundan haritada bulunmayan bir anahtar sessizce varsayılan veri kaynağına düşebilir. Yanlış yazılmış şema anahtarı bu durumda hata vermek yerine başka şemada işlem yapabilir. "false" kullanıldığında null olmayan fakat eşleşmeyen anahtar "IllegalStateException" üretir.

Varsayılan veri kaynağı yalnız gerçekten tanımlı bir iş kuralı varsa kullanılmalıdır. Anahtarın unutulması programlama hatasıysa null değerin de hata üretmesi daha doğru olabilir. Bunun için "determineCurrentLookupKey()" veya "determineTargetDataSource()" çevresinde ek doğrulama yapılabilir.

Hedef haritayı çalışma sırasında değiştirip aynı routing nesnesini kullanmak dikkat gerektirir. "AbstractRoutingDataSource", yapılandırılmış hedefleri başlatma sırasında çözümler. Yeni bir havuz eklemek için yalnız kaynak haritaya eleman eklemek yeterli değildir. Yeniden başlatma işlemi, eş zamanlı "getConnection()" çağrıları ve eski havuzun kapanışı birlikte yönetilmelidir. Resmi sınıf genel amaçlı, kilitsiz bir dinamik havuz kayıt sistemi sunmaz.

Sabit sayıda Oracle şeması için hedefleri uygulama başlangıcında oluşturmak daha öngörülebilir davranır. Yeni veri kaynağının çalışma sırasında eklenmesi zorunluysa routing katmanının değişmez bir kayıt görüntüsü üzerinde çalışması, yeni görüntünün atomik olarak yayımlanması ve çıkarılan Hikari havuzlarının aktif bağlantıları tamamlandıktan sonra kapatılması gerekir.

HikariCP kapasite hesabı

Her hedef ayrı havuz olduğu için kapasite hesabı iki düzeyde yapılmalıdır. İlk düzey tek şemanın eş zamanlı işlem ihtiyacıdır. İkinci düzey bütün havuzların Oracle üzerinde oluşturabileceği toplam oturum sayısıdır.

Toplam teorik sınır şöyledir:

C_toplam = Σ maximumPoolSize_i

84 havuzun her biri için "maximumPoolSize=10" tanımlamak, her şemada aynı anda 10 bağlantıya ihtiyaç bulunduğu anlamına gelmez. Trafik birkaç yoğun şemada toplanıyorsa havuzlar farklı boyutlandırılabilir. Eşit konfigürasyon yönetimi kolaylaştırır, fakat veritabanı kapasitesini doğru dağıtmayabilir.

"minimumIdle" belirtilmezse HikariCP sabit boyutlu havuza yakın davranmayı önerir. Bu davranış ani yükte bağlantı oluşturma gecikmesini azaltır. Çok sayıda seyrek kullanılan veri kaynağında ise bütün havuzların yüksek sayıda boş bağlantı tutmasına yol açabilir. "minimumIdle < maximumPoolSize" kullanıldığında "idleTimeout" devreye girer ve havuz ihtiyaç azaldığında boş bağlantıları asgari değere kadar azaltabilir.

"connectionTimeout", havuz doluyken çağrının bağlantı bekleyebileceği en uzun süredir. Virtual thread kullanılması bu sınırı kaldırmaz. Virtual thread beklemeyi daha düşük platform thread maliyetiyle taşıyabilir, ancak Oracle tarafındaki fiziksel bağlantı sayısı yine "maximumPoolSize" ile sınırlıdır. Çok sayıda görev havuz önünde bekliyorsa sistemin aktarım kapasitesi artmaz, yalnız kuyruk büyür.

"maxLifetime", ağ veya veritabanı altyapısının bağlantıyı sonlandırdığı süreden kısa seçilmelidir. HikariCP kullanımda olan bağlantıyı zorla kapatmaz. Bağlantı havuza döndüğünde emekliye ayırır. "keepaliveTime" yalnız boş bağlantılarda çalışır ve "maxLifetime" değerinden küçük olmalıdır. JDBC4 destekleyen sürücülerde özel "connectionTestQuery" yerine "Connection.isValid()" kullanılması önerilir.

Her "HikariDataSource" uygulama kapanırken kapatılmalıdır. Hedef havuzlar ayrı Spring bean'leri olarak kaydedilmişse container "close()" yaşam döngüsünü yönetebilir. Havuzlar yalnız routing bean'i oluşturulurken yerel nesneler olarak üretilip haritaya konulursa Spring bunları ayrı bean olarak görmez. Bu durumda routing kayıt sınıfı bütün havuzları açık biçimde kapatmalıdır. Spring, kendi yönettiği bean'lerde public "close()" ve "shutdown()" metotlarını varsayılan destroy metodu olarak algılayabilir.

Lazy bağlantı ve gözlemlenebilirlik

"DataSourceTransactionManager", transaction başlarken bağlantıyı erken alabilir. "LazyConnectionDataSourceProxy", gerçek JDBC bağlantısını ilk "Statement" oluşturulana kadar erteleyebilir. Bu davranış, SQL çalıştırmayan transaction'ların havuzdan bağlantı tüketmesini önler. Read-only veya isolation tabanlı routing kullanıldığında transaction özelliklerinin hedef seçimine dahil edilmesini de kolaylaştırır.

Proxy sırası önemlidir. "TransactionAwareDataSourceProxy" kullanılacaksa en dış katmanda bulunmalıdır. "LazyConnectionDataSourceProxy" onun altında, routing veri kaynağı ise fiziksel hedeflerin önünde konumlandırılabilir. Kesin sıra kullanılan transaction manager ve erişim tekniğine göre kurulmalıdır. Spring, "TransactionAwareDataSourceProxy" ile lazy proxy birlikte kullanıldığında transaction-aware proxy'nin dışta olmasını ister.

JPA kullanılan sistemlerde routing anahtarının "EntityManager" fiziksel bağlantı almadan önce belirlenmesi gerekir. Persistence context oluşturulduktan veya ilk sorgu çalıştıktan sonra anahtarı değiştirmek güvenilir bir yönlendirme yöntemi değildir. Aynı "EntityManagerFactory", routing "DataSource" üzerinden çalışabilir, ancak bütün hedef şemaların entity modeli ve Hibernate beklentileriyle uyumlu olması gerekir.

İzleme yalnız routing veri kaynağı üzerinde yapılmamalıdır. Her Hikari havuzu için en az şu değerler ayrı izlenmelidir:

  • Aktif bağlantı
  • Boş bağlantı
  • Bekleyen thread
  • Toplam bağlantı
  • Bağlantı edinme süresi
  • Timeout sayısı
  • Bağlantı kullanım süresi

Spring Boot, veri kaynağı metriklerinde aktif, boş, azami ve asgari bağlantı sayılarını sunabilir ve metrikleri bean adına göre etiketleyebilir. Çok sayıda havuz bulunan sistemlerde "poolName", routing anahtarı ve Oracle servis adı tutarlı olmalıdır. SQL metni veya kullanıcı kimliği gibi yüksek çeşitliliğe sahip değerler metrik etiketi yapılmamalıdır.

Routing hataları için anahtarın kendisi loglanabilir, ancak bağlantı parolası, tam JDBC URL içindeki gizli bilgiler ve kişisel veriler loga yazılmamalıdır. Her işlemde routing anahtarını bilgi seviyesinde loglamak yüksek trafikte gereksiz I/O oluşturabilir. Hata, timeout ve beklenmeyen fallback olayları ayrı sayaçlarla izlenmelidir.

"AbstractRoutingDataSource" ile HikariCP birlikte kullanıldığında ana tasarım ilkesi nettir. Yönlendirme anahtarı transaction başlamadan önce atanır, transaction boyunca değişmez kabul edilir ve iş tamamlandığında mutlaka temizlenir. Her hedef havuz bağımsız kapasite ve yaşam döngüsüne sahiptir. Toplam bağlantı sınırı bütün havuzların toplamından hesaplanır. Sessiz fallback kapatılır ve yanlış şema seçimi olağan bir çalışma durumu değil, sistem hatası olarak ele alınır.

Bu sayfanın QR kodu