1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast

Bu yazımızda nasıl ve neden geliştirdiğimizden bahsedeceğiz. Etkileşim Sistemi – istemci uygulamaları ile 1C:Enterprise sunucuları arasında, bir görevin belirlenmesinden mimari ve uygulama ayrıntılarının düşünülmesine kadar bilgi aktaran bir mekanizma.

Etkileşim Sistemi (bundan sonra SV olarak anılacaktır), garantili teslimata sahip, dağıtılmış, hataya dayanıklı bir mesajlaşma sistemidir. SV, hem çevrimiçi hizmet (1C tarafından sağlanır) hem de kendi sunucu tesislerinizde dağıtılabilen seri üretilen bir ürün olarak sunulan, yüksek ölçeklenebilirliğe sahip, yüksek yüklü bir hizmet olarak tasarlanmıştır.

SV dağıtılmış depolamayı kullanır fındık ve arama motoru Elasticsearch. Ayrıca Java'dan ve PostgreSQL'i yatay olarak nasıl ölçeklendirdiğimizden de bahsedeceğiz.
1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast

Sorunun formüle edilmesi

Etkileşim Sistemini neden oluşturduğumuzu açıklığa kavuşturmak için size 1C'de iş uygulamalarının geliştirilmesinin nasıl çalıştığını biraz anlatacağım.

Başlangıç ​​olarak, henüz ne yaptığımızı bilmeyenler için biraz bizden bahsedelim :) 1C:Enterprise teknoloji platformunu yapıyoruz. Platform, bir iş uygulaması geliştirme aracının yanı sıra iş uygulamalarının platformlar arası bir ortamda çalıştırılmasına olanak tanıyan bir çalışma zamanı içerir.

İstemci-sunucu geliştirme paradigması

1C:Enterprise'da oluşturulan iş uygulamaları üç düzeyde çalışır müşteri sunucusu mimari “DBMS – uygulama sunucusu – istemci”. Uygulama kodu yazıldı yerleşik 1C dili, uygulama sunucusunda veya istemcide yürütülebilir. Uygulama nesneleriyle (dizinler, belgeler vb.) Yapılan tüm çalışmalar ve veritabanının okunması ve yazılması yalnızca sunucuda gerçekleştirilir. Formların ve komut arayüzünün işlevselliği de sunucuda uygulanır. Müşteri, formları alır, açar ve görüntüler, kullanıcıyla “iletişim kurar” (uyarılar, sorular…), formlarda hızlı yanıt gerektiren küçük hesaplamalar yapar (örneğin fiyatı miktarla çarpmak), yerel dosyalarla çalışır, ekipmanlarla çalışmak.

Uygulama kodunda, prosedür ve işlevlerin başlıkları, &AtClient / &AtServer direktiflerini kullanarak (dilin İngilizce versiyonunda &AtClient / &AtServer) kodun nerede yürütüleceğini açıkça belirtmelidir. 1C geliştiricileri artık direktiflerin aslında daha fazlaama bizim için bu artık önemli değil.

Sunucu kodunu istemci kodundan çağırabilirsiniz ancak istemci kodunu sunucu kodundan çağıramazsınız. Bu, çeşitli nedenlerden dolayı yaptığımız temel bir sınırlamadır. Özellikle, sunucu kodunun, istemciden veya sunucudan, nerede çağrılırsa çağrılsın aynı şekilde yürütülecek şekilde yazılması gerektiğinden. Ve sunucu kodunun başka bir sunucu kodundan çağrılması durumunda, böyle bir müşteri yoktur. Ve sunucu kodunun yürütülmesi sırasında, onu çağıran istemci kapanabilir, uygulamadan çıkabilir ve sunucunun artık arayacak kimsesi kalmaz.

1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast
Bir düğme tıklamasını işleyen kod: istemciden bir sunucu prosedürünü çağırmak işe yarar, sunucudan bir istemci prosedürünü çağırmak işe yaramaz

Bu şu anlama gelir; eğer sunucudan istemci uygulamasına örneğin “uzun süredir devam eden” bir raporun oluşturulmasının bittiği ve raporun görüntülenebildiğine dair bir mesaj göndermek istiyorsak böyle bir yöntemimiz yok. Örneğin, sunucuyu istemci kodundan periyodik olarak sorgulamak gibi hileler kullanmanız gerekir. Ancak bu yaklaşım sistemi gereksiz çağrılarla dolduruyor ve genel olarak pek şık görünmüyor.

Ayrıca örneğin bir telefon görüşmesi geldiğinde de bir ihtiyaç vardır. SIP- Bir arama yaparken, istemci uygulamasına bu konuda bilgi verin; böylece arayan kişinin numarasını karşı taraf veritabanında bulabilir ve kullanıcıya arayan karşı taraf hakkındaki bilgileri gösterebilir. Veya örneğin depoya bir sipariş geldiğinde müşterinin müşteri uygulamasına bunu bildirin. Genel olarak böyle bir mekanizmanın faydalı olabileceği pek çok durum vardır.

Üretimin kendisi

Bir mesajlaşma mekanizması oluşturun. Hızlı, güvenilir, garantili teslimat ve mesajları esnek bir şekilde arama olanağı. Mekanizma temelinde, 1C uygulamalarının içinde çalışan bir mesajlaşma programı (mesajlar, görüntülü aramalar) uygulayın.

Sistemi yatay olarak ölçeklenebilir olacak şekilde tasarlayın. Artan yükün düğüm sayısı artırılarak karşılanması gerekir.

uygulama

SV'nin sunucu kısmını doğrudan 1C:Enterprise platformuna entegre etmemeye, API'si 1C uygulama çözümleri kodundan çağrılabilen ayrı bir ürün olarak uygulamaya karar verdik. Bu, çeşitli nedenlerden dolayı yapıldı; bunlardan en önemlisi, farklı 1C uygulamaları arasında (örneğin, Ticaret Yönetimi ve Muhasebe arasında) mesaj alışverişini mümkün kılmak istememdi. Farklı 1C uygulamaları, 1C:Enterprise platformunun farklı sürümlerinde çalışabilir, farklı sunucularda vb. bulunabilir. Bu gibi durumlarda, SV'nin 1C kurulumlarının "yan tarafında" bulunan ayrı bir ürün olarak uygulanması en uygun çözümdür.

Bu yüzden SV'yi ayrı bir ürün olarak yapmaya karar verdik. Küçük şirketlerin, sunucunun yerel kurulumu ve yapılandırmasıyla ilgili ek yük maliyetlerinden kaçınmak için bulutumuza (wss://1cdialog.com) kurduğumuz CB sunucusunu kullanmalarını öneririz. Büyük müşteriler tesislerine kendi CB sunucularını kurmayı uygun bulabilirler. Bulut SaaS ürünümüzde de benzer bir yaklaşım kullandık 1cTaze – müşterilerin tesislerinde kurulum için seri üretilen bir ürün olarak üretilir ve aynı zamanda bulutumuzda da devreye alınır https://1cfresh.com/.

uygulama

Yükü ve hata toleransını dağıtmak için, bir değil, birkaç tane Java uygulamasını, önlerinde bir yük dengeleyici ile konuşlandıracağız. Bir mesajı düğümden düğüme aktarmanız gerekiyorsa Hazelcast'te yayınlama/abone olma özelliğini kullanın.

İstemci ile sunucu arasındaki iletişim websocket aracılığıyla sağlanır. Gerçek zamanlı sistemler için çok uygundur.

Dağıtılmış önbellek

Redis, Hazelcast ve Ehcache arasında seçim yaptık. 2015 yılı. Redis yeni bir küme yayınladı (çok yeni, korkutucu), birçok kısıtlamaya sahip Sentinel var. Ehcache bir kümeye nasıl birleştirileceğini bilmiyor (bu işlevsellik daha sonra ortaya çıktı). Hazelcast 3.4 ile denemeye karar verdik.
Hazelcast, kutudan çıktığı gibi bir küme halinde bir araya getirilmiştir. Tek düğüm modunda pek kullanışlı değildir ve yalnızca önbellek olarak kullanılabilir - verilerin diske nasıl döküleceğini bilmiyor, tek düğümü kaybederseniz verileri kaybedersiniz. Aralarında kritik verileri yedeklediğimiz birkaç Hazelcast dağıtıyoruz. Önbelleği yedeklemeyiz – bunu umursamayız.

Bizim için Hazelcast:

  • Kullanıcı oturumlarının depolanması. Her seferinde bir oturum için veritabanına gitmek uzun zaman alıyor, bu nedenle tüm oturumları Hazelcast'e koyuyoruz.
  • Önbellek. Bir kullanıcı profili arıyorsanız önbelleği kontrol edin. Yeni bir mesaj yazdım - önbelleğe koy.
  • Uygulama örnekleri arasındaki iletişime ilişkin konular. Düğüm bir etkinlik oluşturur ve bunu Hazelcast konusuna yerleştirir. Bu konuya abone olan diğer uygulama düğümleri olayı alır ve işler.
  • Küme kilitleri. Örneğin, benzersiz bir anahtar kullanarak bir tartışma oluşturuyoruz (1C veritabanındaki tek tartışma):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Kanal olmadığını kontrol ettik. Kilidi aldık, tekrar kontrol ettik ve oluşturduk. Kilidi aldıktan sonra kilidi kontrol etmezseniz, o anda başka bir başlığın da kontrol edilmesi ve şimdi aynı tartışmayı oluşturmaya çalışması ihtimali vardır - ancak bu zaten mevcuttur. Senkronize veya normal Java Kilidini kullanarak kilitleyemezsiniz. Veritabanı aracılığıyla - yavaş ve veritabanına yazık; Hazelcast aracılığıyla - ihtiyacınız olan şey bu.

Bir DBMS seçme

PostgreSQL ile çalışma ve bu DBMS'nin geliştiricileriyle işbirliği yapma konusunda kapsamlı ve başarılı bir deneyime sahibiz.

PostgreSQL kümesiyle bu kolay değil; XL, XC, narenciye, ancak genel olarak bunlar kutudan çıkan NoSQL'ler değildir. NoSQL’i ana depolama alanı olarak görmedik, daha önce çalışmadığımız Hazelcast’i almamız yeterliydi.

İlişkisel bir veritabanını ölçeklendirmeniz gerekiyorsa bunun anlamı parçalama. Bildiğiniz gibi sharding ile veritabanını ayrı parçalara ayırıyoruz, böylece her biri ayrı bir sunucuya yerleştirilebiliyor.

Parçalamamızın ilk sürümü, uygulamamızın tablolarının her birini farklı sunuculara farklı oranlarda dağıtma yeteneğini varsayıyordu. A sunucusunda çok sayıda mesaj var - lütfen bu tablonun bir kısmını B sunucusuna taşıyalım. Bu karar sadece erken optimizasyon hakkında çığlık attı, bu yüzden kendimizi çok kiracılı bir yaklaşımla sınırlamaya karar verdik.

Örneğin, web sitesinde çok kiracılı hakkında bilgi edinebilirsiniz. Citus Verileri.

SV'de uygulama ve abone kavramları bulunmaktadır. Uygulama, ERP veya Muhasebe gibi bir iş uygulamasının kullanıcıları ve iş verileriyle birlikte özel bir kurulumudur. Abone, uygulamanın SV sunucusunda adına kayıtlı olduğu kuruluş veya kişidir. Bir abonenin birden fazla uygulaması kayıtlı olabilir ve bu uygulamalar birbirleriyle mesaj alışverişinde bulunabilir. Abone sistemimizde kiracı oldu. Birkaç aboneden gelen mesajlar tek bir fiziksel veritabanında bulunabilir; bir abonenin çok fazla trafik oluşturmaya başladığını görürsek, onu ayrı bir fiziksel veritabanına (veya hatta ayrı bir veritabanı sunucusuna) taşıyoruz.

Tüm abone veritabanlarının konumu hakkındaki bilgileri içeren bir yönlendirme tablosunun saklandığı bir ana veritabanımız var.

1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast

Ana veritabanının darboğaz oluşturmasını önlemek için yönlendirme tablosunu (ve diğer sık ​​ihtiyaç duyulan verileri) önbellekte tutuyoruz.

Abonenin veritabanı yavaşlamaya başlarsa onu içeride bölümlere ayıracağız. Kullandığımız diğer projelerde pg_pathman.

Kullanıcı mesajlarını kaybetmek kötü bir şey olduğundan veritabanlarımızı replikalarla koruyoruz. Senkronize ve asenkron kopyaların birleşimi, ana veritabanının kaybı durumunda kendinizi sigortalamanıza olanak tanır. İleti kaybı yalnızca birincil veritabanı ve onun eşzamanlı kopyası aynı anda başarısız olursa ortaya çıkar.

Eşzamanlı bir çoğaltma kaybolursa, eşzamansız çoğaltma zaman uyumlu hale gelir.
Ana veritabanı kaybolursa, senkronize kopya ana veritabanı haline gelir ve senkronize olmayan kopya, senkronize kopya haline gelir.

Arama için Elasticsearch

Diğer şeylerin yanı sıra, SV aynı zamanda bir haberci olduğundan, morfolojiyi hesaba katan ve kesin olmayan eşleşmeler kullanan hızlı, kullanışlı ve esnek bir arama gerektirir. Tekerleği yeniden icat etmemeye ve kütüphaneyi temel alarak oluşturulan ücretsiz arama motoru Elasticsearch'ü kullanmaya karar verdik. Lusen. Ayrıca uygulama düğümlerinin arızalanması durumunda sorunları ortadan kaldırmak için Elasticsearch'ü bir kümede (ana – veri – veri) konuşlandırıyoruz.

Github'da bulduk Rusça morfoloji eklentisi Elasticsearch için kullanın ve kullanın. Elasticsearch indeksinde kelime köklerini (eklentinin belirlediği) ve N gramlarını saklıyoruz. Kullanıcı aranacak metni girdiğinde, yazılan metni N gramlar arasında ararız. Dizine kaydedildiğinde “metinler” kelimesi aşağıdaki N gramlara bölünecektir:

[bunlar, tek, tex, metin, metinler, ek, eski, dahili, metinler, ks, kst, ksty, st, sty, sen],

Ve “metin” kelimesinin kökü de korunacaktır. Bu yaklaşım, kelimenin başında, ortasında ve sonunda arama yapmanızı sağlar.

Genel resim

1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast
Makalenin başlangıcındaki resmin tekrarı, ancak açıklamalarla birlikte:

  • Dengeleyici internette ifşa edildi; nginx'imiz var, herhangi biri olabilir.
  • Java uygulama örnekleri birbirleriyle Hazelcast aracılığıyla iletişim kurar.
  • Kullandığımız bir web soketiyle çalışmak için Netty.
  • Java uygulaması Java 8'de yazılmıştır ve paketlerden oluşur OSGI. Planlar arasında Java 10'a geçiş ve modüllere geçiş yer alıyor.

Geliştirme ve test

SV'yi geliştirme ve test etme sürecinde kullandığımız ürünlerin bir dizi ilginç özelliğiyle karşılaştık.

Yük testi ve bellek sızıntıları

Her SV sürümünün sürümü, yük testini içerir. Aşağıdaki durumlarda başarılıdır:

  • Test birkaç gün çalıştı ve herhangi bir hizmet hatası yaşanmadı
  • Anahtar işlemler için yanıt süresi rahat bir eşiği aşmadı
  • Önceki versiyona göre performans bozulması %10'dan fazla değil

Test veritabanını verilerle dolduruyoruz - bunu yapmak için üretim sunucusundan en aktif abone hakkında bilgi alıyoruz, sayılarını 5 ile çarpıyoruz (mesaj, tartışma, kullanıcı sayısı) ve bu şekilde test ediyoruz.

Etkileşim sisteminin yük testini üç konfigürasyonda gerçekleştiriyoruz:

  1. stres testi
  2. Yalnızca bağlantılar
  3. Abone kaydı

Stres testi sırasında birkaç yüz iş parçacığı başlatıyoruz ve onlar sistemi durmadan yüklüyorlar: mesaj yazma, tartışma oluşturma, mesaj listesi alma. Sıradan kullanıcıların eylemlerini (okunmamış mesajlarımın bir listesini almak, birine yazmak) ve yazılım çözümlerini (farklı bir konfigürasyona sahip bir paket iletmek, bir uyarıyı işlemek) eylemlerini simüle ediyoruz.

Örneğin, stres testinin bir kısmı şöyle görünür:

  • Kullanıcı oturum açar
    • Okunmamış tartışmalarınızı talep eder
    • %50'sinin mesajları okuma olasılığı
    • %50 ihtimalle mesaj atacak
    • Sonraki kullanıcı:
      • Yeni bir tartışma yaratma şansı %20'dir
      • Tartışmalarından herhangi birini rastgele seçer
      • İçeri girer
      • İstek mesajları, kullanıcı profilleri
      • Bu tartışmadaki rastgele kullanıcılara gönderilen beş mesaj oluşturur
      • Tartışmadan ayrılır
      • 20 kez tekrarlanır
      • Oturumu kapatır, betiğin başına döner

    • Sisteme bir chatbot girer (uygulama kodundaki mesajlaşmayı taklit eder)
      • Veri alışverişi için yeni bir kanal oluşturma şansı %50'dir (özel tartışma)
      • Mevcut kanallardan herhangi birine mesaj yazma olasılığı %50

“Yalnızca Bağlantılar” senaryosu bir nedenden dolayı ortaya çıktı. Şöyle bir durum var: Kullanıcılar sisteme bağlandı ancak henüz dahil olmadılar. Her kullanıcı sabah saat 09:00'da bilgisayarı açar, sunucuya bağlantı kurar ve sessiz kalır. Bu adamlar tehlikelidir, çok sayıda vardır; sahip oldukları tek paket PING/PONG'dur, ancak sunucuyla bağlantıyı korurlar (bunu sürdüremezler - ya yeni bir mesaj gelirse). Test, bu tür çok sayıda kullanıcının yarım saat içinde sisteme giriş yapmaya çalıştığı bir durumu yeniden üretiyor. Stres testine benzer, ancak odak noktası tam olarak bu ilk girdiye odaklanır - böylece herhangi bir başarısızlık olmaz (kişi sistemi kullanmaz ve zaten düşer - daha kötü bir şey düşünmek zordur).

Abone kayıt komut dosyası ilk başlatmadan itibaren başlar. Stres testi yaptık ve yazışma sırasında sistemin yavaşlamadığından emin olduk. Ancak kullanıcılar geldi ve zaman aşımı nedeniyle kayıtlar başarısız olmaya başladı. Kayıt olurken kullandık / Dev / randomsistemin entropisi ile ilgilidir. Sunucunun yeterli entropi biriktirecek zamanı yoktu ve yeni bir SecureRandom istendiğinde onlarca saniye dondu. Bu durumdan kurtulmanın birçok yolu vardır, örneğin: daha az güvenli olan /dev/urandom'a geçin, entropi üreten özel bir kart kurun, önceden rastgele sayılar üretin ve bunları bir havuzda saklayın. Havuzdaki sorunu geçici olarak kapattık ancak o zamandan beri yeni abonelerin kaydedilmesi için ayrı bir test yürütüyoruz.

Yük oluşturucu olarak kullanıyoruz JMeter. Websocket ile nasıl çalışılacağını bilmiyor; bir eklentiye ihtiyacı var. “jmeter websocket” sorgusu için arama sonuçlarında ilk sıralar şöyle: BlazeMeter'dan makaleler, tavsiye eden Maciej Zaleski'nin eklentisi.

İşte başlamaya karar verdik.

Ciddi testlere başladıktan hemen sonra JMeter'ın bellek sızdırmaya başladığını keşfettik.

Eklenti ayrı bir büyük hikaye; 176 yıldızla github'da 132 çatala sahip. Yazarın kendisi 2015'ten beri bunu taahhüt etmedi (2015'te aldık, sonra şüphe uyandırmadı), bellek sızıntılarıyla ilgili birkaç github sorunu, 7 kapatılmamış çekme isteği.
Bu eklentiyi kullanarak yük testi yapmaya karar verirseniz lütfen aşağıdaki tartışmalara dikkat edin:

  1. Çok iş parçacıklı bir ortamda normal bir LinkedList kullanıldı ve sonuç şuydu: NPE çalışma zamanında. Bu, ConcurrentLinkedDeque'e geçilerek veya senkronize bloklarla çözülebilir. Kendimiz için ilk seçeneği seçtik (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Bellek sızıntısı; bağlantı kesildiğinde bağlantı bilgileri silinmez (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Akış modunda (örnekliğin sonunda websocket kapatılmadığında ancak planda daha sonra kullanıldığında), Yanıt kalıpları çalışmaz (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Bu github'dakilerden biri. Yaptığımız:

  1. Almış çatal Elyran Kogan (@elyrank) – 1. ve 3. sorunları düzeltir
  2. Çözülmüş sorun 2
  3. İskele 9.2.14'ten 9.3.12'ye güncellendi
  4. SimpleDateFormat'ı ThreadLocal'a sardı; SimpleDateFormat iş parçacığı açısından güvenli değil, bu da çalışma zamanında NPE'ye yol açtı
  5. Başka bir bellek sızıntısı düzeltildi (bağlantı kesildiğinde bağlantı hatalı şekilde kapatıldı)

Ve yine de akıyor!

Hafıza bir günde değil iki günde tükenmeye başladı. Kesinlikle hiç zaman kalmamıştı, bu yüzden daha az konu açmaya karar verdik, ancak dört temsilciyle. Bu en az bir hafta için yeterli olmalıydı.

İki gün geçti...

Artık Hazelcast'in belleği tükeniyor. Günlükler, birkaç günlük testin ardından Hazelcast'in hafıza eksikliğinden şikayet etmeye başladığını ve bir süre sonra kümenin parçalandığını ve düğümlerin birer birer ölmeye devam ettiğini gösterdi. JVisualVM'yi hazelcast'e bağladık ve bir "yükselen testere" gördük - düzenli olarak GC'yi çağırıyordu ancak hafızayı temizleyemiyordu.

1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast

Hazelcast 3.4'te bir haritayı / multiMap'i (map.destroy()) silerken belleğin tamamen serbest bırakılmadığı ortaya çıktı:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Hata şimdi 3.5'te düzeltildi, ancak o zamanlar bir sorundu. Mantığımıza göre dinamik isimlerle yeni multiMap'ler oluşturduk ve sildik. Kod şuna benziyordu:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Örneğin:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap her abonelik için oluşturuldu ve ihtiyaç duyulmadığında silindi. Haritaya başlamaya karar verdik anahtar aboneliğin adı olacak ve değerler oturum tanımlayıcıları olacaktır (gerekirse bundan sonra kullanıcı tanımlayıcılarını alabilirsiniz).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Grafikler iyileşti.

1C için yüksek yüklü ölçeklenebilir bir hizmeti nasıl ve neden yazdık: Enterprise: Java, PostgreSQL, Hazelcast

Yük testi hakkında başka neler öğrendik?

  1. JSR223'ün mükemmel bir şekilde yazılması ve derleme önbelleği içermesi gerekir - çok daha hızlıdır. Bağlantı.
  2. Jmeter-Plugins grafiklerinin anlaşılması standart grafiklerden daha kolaydır. Bağlantı.

Hazelcast ile olan deneyimimiz hakkında

Hazelcast bizim için yeni bir üründü, 3.4.1 sürümünden itibaren onunla çalışmaya başladık, şu anda üretim sunucumuz 3.9.2 sürümünü çalıştırıyor (bu yazının yazıldığı sırada Hazelcast'in en son sürümü 3.10'du).

Kimlik oluşturma

Tamsayı tanımlayıcılarla başladık. Yeni bir varlık için başka bir Long'a ihtiyacımız olduğunu düşünelim. Veritabanındaki sıralama uygun değil, tablolar parçalamayla ilgili - DB1'de bir mesaj ID=1 ve DB1'de bir mesaj ID=2 olduğu ortaya çıktı, bu kimliği Elasticsearch'e veya Hazelcast'e koyamazsınız , ancak en kötüsü, iki veritabanındaki verileri bir veritabanında birleştirmek istiyorsanız (örneğin, bu aboneler için bir veritabanının yeterli olduğuna karar vermek). Hazelcast'e birkaç AtomicLong ekleyebilir ve sayacı orada tutabilirsiniz, ardından yeni bir kimlik edinme performansı, artımAndGet artı Hazelcast'e yapılan istek süresidir. Ancak Hazelcast'in daha optimal bir özelliği var: FlakeIdGenerator. Her müşteriyle iletişim kurarken onlara bir kimlik aralığı verilir; örneğin ilki 1'den 10'e, ikincisi 000'den 10'e kadar vb. Artık müşteri, kendisine verilen aralık bitene kadar yeni tanımlayıcıları kendi başına yayınlayabilir. Hızlı çalışır, ancak uygulamayı (ve Hazelcast istemcisini) yeniden başlattığınızda yeni bir sıra başlar - dolayısıyla atlamalar vb. Ayrıca geliştiriciler kimliklerin neden tamsayı olduğunu, ancak bu kadar tutarsız olduğunu gerçekten anlamıyorlar. Her şeyi tarttık ve UUID'lere geçtik.

Bu arada, Twitter gibi olmak isteyenler için böyle bir Snowcast kütüphanesi var - bu, Snowflake'in Hazelcast'in üstüne bir uygulamasıdır. Burada görebilirsiniz:

github.com/noctarius/snowcast
github.com/twitter/kar tanesi

Ama artık buna ulaşamadık.

TransactionalMap.replace

Başka bir sürpriz: TransactionalMap.replace çalışmıyor. İşte bir test:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

GetForUpdate'i kullanarak kendi değişikliğimi yazmak zorunda kaldım:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Yalnızca normal veri yapılarını değil aynı zamanda bunların işlemsel versiyonlarını da test edin. IMap çalışıyor ancak TransactionalMap artık mevcut değil.

Kesinti olmadan yeni bir JAR ekleyin

Öncelikle sınıflarımıza ait nesneleri Hazelcast'e kaydetmeye karar verdik. Mesela bir Application sınıfımız var, onu kaydedip okumak istiyoruz. Kaydetmek:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Biz okuyun:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Her şey çalışıyor. Daha sonra Hazelcast'te aşağıdakilere göre arama yapmak için bir dizin oluşturmaya karar verdik:

map.addIndex("subscriberId", false);

Ve yeni bir varlık yazarken ClassNotFoundException almaya başladılar. Hazelcast indekse eklemeye çalıştı ancak sınıfımız hakkında hiçbir şey bilmiyordu ve kendisine bu sınıfla bir JAR sağlanmasını istiyordu. Tam da bunu yaptık, her şey işe yaradı, ancak yeni bir sorun ortaya çıktı: Kümeyi tamamen durdurmadan JAR nasıl güncellenir? Hazelcast, düğüm düğüm güncelleme sırasında yeni JAR'ı almıyor. Bu noktada indeks aramadan yaşayabileceğimize karar verdik. Sonuçta, Hazelcast'i anahtar/değer deposu olarak kullanırsanız her şey işe yarayacak mı? Tam olarak değil. Burada yine IMap ve TransactionalMap'in davranışı farklıdır. IMap'in umursamadığı durumlarda TransactionalMap bir hata verir.

IMap. 5000 nesne yazıyoruz, okuyoruz. Her şey bekleniyor.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Ancak bir işlemde çalışmıyor, bir ClassNotFoundException alıyoruz:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

3.8'de Kullanıcı Sınıfı Dağıtım mekanizması ortaya çıktı. Bir ana düğüm belirleyebilir ve üzerindeki JAR dosyasını güncelleyebilirsiniz.

Artık yaklaşımımızı tamamen değiştirdik: bunu kendimiz JSON'a serileştiriyoruz ve Hazelcast'e kaydediyoruz. Hazelcast'in sınıflarımızın yapısını bilmesine gerek yok ve kesinti yaşamadan güncelleme yapabiliyoruz. Etki alanı nesnelerinin sürümlendirilmesi uygulama tarafından kontrol edilir. Uygulamanın farklı sürümleri aynı anda çalışıyor olabilir ve yeni uygulamanın yeni alanlara sahip nesneler yazdığı ancak eski uygulamanın bu alanları henüz bilmediği bir durum mümkündür. Ve aynı zamanda yeni uygulama, eski uygulama tarafından yazılan ve yeni alanları olmayan nesneleri okur. Bu tür durumları uygulama içerisinde ele alıyoruz ancak kolaylık olması açısından alanları değiştirmiyor veya silmiyoruz, sadece yeni alanlar ekleyerek sınıfları genişletiyoruz.

Yüksek performansı nasıl sağlıyoruz?

Hazelcast'e dört gezi - iyi, iki tanesi veri tabanına - kötü

Veriler için önbelleğe gitmek her zaman veritabanına gitmekten daha iyidir ancak kullanılmayan kayıtları da depolamak istemezsiniz. Neyin önbelleğe alınacağına ilişkin kararı geliştirmenin son aşamasına bırakıyoruz. Yeni işlevsellik kodlandığında, PostgreSQL'deki tüm sorguların günlüğe kaydedilmesini açarız (log_min_duration_statement - 0) ve 20 dakika boyunca yük testi gerçekleştiririz. Toplanan günlükleri kullanarak pgFouine ve pgBadger gibi yardımcı programlar analitik raporlar oluşturabilir. Raporlarda öncelikle yavaş ve sık sorguları ararız. Yavaş sorgular için bir yürütme planı (EXPLAIN) oluşturuyoruz ve böyle bir sorgunun hızlandırılıp hızlandırılamayacağını değerlendiriyoruz. Aynı giriş verileri için sık sık yapılan istekler önbelleğe iyi uyum sağlar. Sorgu başına bir tablo olacak şekilde sorguları "düz" tutmaya çalışıyoruz.

Sömürü

Çevrimiçi bir hizmet olarak SV, 2017 baharında devreye alındı ​​ve ayrı bir ürün olarak SV, Kasım 2017'de (o sırada beta sürüm statüsünde) piyasaya sürüldü.

Bir yılı aşkın süredir CB çevrimiçi hizmetinin işleyişinde ciddi bir sorun yaşanmadı. Çevrimiçi hizmeti şu adresten izliyoruz: Zabbix, buradan toplayın ve dağıtın Bambu.

SV sunucusu dağıtımı yerel paketler biçiminde sağlanır: RPM, DEB, MSI. Ayrıca Windows için sunucuyu, Hazelcast'i ve Elasticsearch'ü tek bir makineye yükleyen tek bir EXE biçiminde tek bir yükleyici sağlıyoruz. Başlangıçta kurulumun bu sürümüne "demo" sürümü adını verdik, ancak artık bunun en popüler dağıtım seçeneği olduğu açıkça ortaya çıktı.

Kaynak: habr.com

Yorum ekle