W tym artykule opowiemy o tym, jak i dlaczego opracowaliśmy – mechanizm, który przekazuje informacje między aplikacjami klienckimi a serwerami 1C:Enterprise – od sformułowania zadania do przemyślenia architektury i szczegółów realizacji.
System Interakcji (dalej – SI) – to rozproszony, odporny na awarie system wymiany wiadomości z gwarantowaną dostawą. SI został zaprojektowany jako wysokoobciążony serwis o dużej skalowalności, dostępny zarówno jako usługa online (oferowana przez firmę 1C), jak i jako produkt licencyjny, który można wdrożyć na własnych zasobach serwerowych.
SI wykorzystuje rozproszoną pamięć i system wyszukiwania . Ponadto porozmawiamy o Javie i o tym, jak poziomo skalujemy PostgreSQL.
Sformułowanie zadania
Aby zrozumieć, dlaczego stworzyliśmy System Interakcji, opowiem trochę o tym, jak przebiega rozwój aplikacji biznesowych w 1C.
Na początek – kilka słów o nas dla tych, którzy jeszcze nie wiedzą, czym się zajmujemy:) Tworzymy technologiczną platformę „1C:Enterprise”. Platforma obejmuje narzędzie do tworzenia aplikacji biznesowych oraz środowisko uruchomieniowe, umożliwiające działanie aplikacji biznesowych w wieloplatformowym środowisku.
Paradigma klient-serwer w projektowaniu
Aplikacje biznesowe stworzone na platformie „1C:Enterprise” działają w trójwarstwowej wbudowanym języku 1C W kodzie aplikacyjnym w nagłówkach procedur i funkcji należy wyraźnie wskazać, gdzie będzie wykonywany kod — za pomocą dyrektyw &NaKliencie / &NaSerwerze (&AtClient / &AtServer w angielskojęzycznej wersji języka). Programiści w 1C teraz poprawią mnie, mówiąc, że dyrektywy właściwie
W kodzie aplikacyjnym w nagłówkach procedur i funkcji należy wyraźnie wskazać, gdzie będzie wykonywany kod — za pomocą dyrektyw &НаКлиенте / &НаСервере (&AtClient / &AtServer w anglojęzycznej wersji języka). Programiści 1C teraz poprawią mnie, mówiąc, że dyrektywy w rzeczywistości , ale obecnie nie jest to dla nas istotne.
Z kodu klienta można wywołać kod serwera, ale nie można wywołać kodu klienta z kodu serwera. To jest fundamentalne ograniczenie, które wprowadziliśmy z różnych powodów. Między innymi dlatego, że kod serwera musi być napisany w taki sposób, aby działał tak samo, niezależnie od tego, czy jest wywoływany z klienta, czy z serwera. W przypadku wywołania kodu serwera z innego kodu serwera klient nie istnieje jako taki. A także dlatego, że w trakcie wykonywania kodu serwera, klient, który go wywołał, mógł się zamknąć lub wyjść z aplikacji, i serwer nie będzie miał już kogo wywołać.
Kod obsługujący naciśnięcie przycisku: wywołanie procedury serwerowej z klienta zadziała, wywołanie procedury klienta z serwera – nie.
To oznacza, że jeśli z serwera będziemy chcieli przekazać jakieś wiadomości do aplikacji klienckiej, na przykład, że zakończyło się formowanie "długoterminowego" raportu i można go zobaczyć – nie mamy takiego sposobu. Musimy uciekać się do sztuczek, na przykład, z kodu klienta okresowo pytać serwer. Jednak takie podejście obciąża system zbędnymi wywołaniami i ogólnie nie wygląda zbyt elegancko.
Ponadto istnieje potrzeba, na przykład, przy nadchodzących telefonach - powiadomić o tym aplikację kliencką, aby mogła znaleźć w bazie kontrahentów osobę dzwoniącą i pokazać użytkownikowi informacje o dzwoniącym kontrahencie. Lub, na przykład, przy przyjęciu zamówienia na magazyn powiadomić o tym aplikację kliencką zamawiającego. Generalnie jest wiele przypadków, w których taki mechanizm byłby przydatny.
Właściwie postawienie sprawy
Stworzenie mechanizmu wymiany wiadomości. Szybkiego, niezawodnego, z gwarantowaną dostawą, z możliwością elastycznego wyszukiwania wiadomości. Na podstawie tego mechanizmu zrealizować komunikator (wiadomości, połączenia wideo), działający wewnątrz aplikacji 1C.
Zaprojektowanie systemu horyzontalnie skalowalnego. Wzrastające obciążenie powinno być pokrywane przez zwiększenie liczby węzłów.
Realizacja
Postać serwerowa SV zdecydowaliśmy się nie integrować bezpośrednio z platformą 1C:Enterprise, lecz realizować jako osobny produkt, którego API można wywoływać z kodu aplikacji 1C. Zrobiliśmy to z kilku powodów, a głównym z nich było umożliwienie wymiany wiadomości między różnymi aplikacjami 1C (na przykład między Zarządzaniem Handlem a Księgowością). Różne aplikacje 1C mogą działać na różnych wersjach platformy 1C:Enterprise, znajdować się na różnych serwerach itp. W takich warunkach wdrożenie SV jako osobnego produktu, znajdującego się 'z boku' instalacji 1C, to optymalne rozwiązanie.
Zdecydowaliśmy więc na tworzenie SV jako osobnego produktu. Małym firmom zalecamy korzystanie z serwera SV, który zainstalowaliśmy w naszej chmurze (wss://1cdialog.com), aby uniknąć kosztów związanych z lokalną instalacją i konfiguracją serwera. Duzi klienci mogą uznać za zasadne zainstalowanie własnego serwera SV na swoich zasobach. Podobne podejście zastosowaliśmy w naszym produktach SaaS w chmurze. – jest wydawany jako produkt komercyjny do instalacji u klientów, a także uruchamiany w naszej chmurze. .
Aplikacja
Aby rozłożyć obciążenie i zapewnić odporność na awarie, uruchomimy nie jedną aplikację Java, a kilka, a przed nimi umieścimy równoważnik obciążenia. Jeśli konieczne jest przekazanie wiadomości z węzła do węzła – użyjemy publish/subscribe w Hazelcast.
Komunikacja klienta z serwerem – przez websocket. To dobrze nadaje się do systemów czasu rzeczywistego.
Rozproszona pamięć podręczna
Wybieraliśmy między Redis, Hazelcast a Ehcache. Jest rok 2015. Redis właśnie wprowadził nowy klaster (zbyt nowy, strach się bać), istnieje Sentinel z wieloma ograniczeniami. Ehcache nie potrafi grupować się w klaster (ta funkcjonalność pojawiła się później). Postanowiliśmy spróbować z Hazelcast 3.4.
Hazelcast łączy się w klaster 'prosto z pudełka'. W trybie jednego węzła nie jest zbyt użyteczny i można go wykorzystać jedynie jako pamięć podręczną – nie potrafi zapisywać danych na dysku, straciliśmy jedyny węzeł – utraciliśmy dane. Uruchamiamy kilka instancji Hazelcast, między którymi tworzymy kopie zapasowe krytycznych danych. Pamięci podręcznej nie kopiujemy – nie żal jej.
Dla nas Hazelcast to:
- Magazyn sesji użytkowników. Każdorazowe zasięganie sesji w bazie danych jest czasochłonne, dlatego wszystkie sesje umieszczamy w Hazelcast.
- Cache. Szukasz profilu użytkownika – sprawdź w pamięci podręcznej. Napisałeś nową wiadomość – umieść w pamięci podręcznej.
- Tematy do komunikacji instancji aplikacji. Węzeł generuje zdarzenie i umieszcza je w temacie Hazelcast. Inne węzły aplikacji, subskrybowane do tego tematu, otrzymują i przetwarzają zdarzenie.
- Blokady klastrowe. Na przykład, tworzymy dyskusję na unikalnym kluczu (dyskusja singleton w ramach bazy 1C):
conversationKeyChecker.check("BENCZOKOLONKA");
doInClusterLock("BENCZOKOLONKA", () -> {
conversationKeyChecker.check("BENCZOKOLONKA");
createChannel("BENCZOKOLONKA");
});Sprawdziliśmy, że kanału nie ma. Wzięliśmy blokadę, znowu sprawdziliśmy, stworzyliśmy. Jeśli po wzięciu blokady nie sprawdzisz, istnieje ryzyko, że inny wątek w tym momencie również sprawdził i teraz spróbuje stworzyć taką samą dyskusję – a ona już istnieje. Nie można blokować za pomocą synchronized ani zwykłego java Lock. Poprzez bazę – wolno, a bazą szkoda, przez Hazelcast – to, co potrzebne.
Wybieramy DBMS
Mamy duże i udane doświadczenie w pracy z PostgreSQL oraz współpracy z programistami tego DBMS.
Z klastrem PostgreSQL nie jest łatwo – są , , , ale, ogólnie rzecz biorąc, nie jest to noSQL, które skalują się od ręki. NoSQL jako główną bazę danych nie rozważaliśmy, wystarczyło, że bierzemy Hazelcast, z którym wcześniej nie pracowaliśmy.
Skoro musimy skalować relacyjną bazę danych – znaczy, Jak wiesz, przy sharding dzielimy bazę danych na oddzielne części, tak aby każdą z nich można było przenieść na inny serwer.
Pierwsza wersja naszego sharding przewidywała możliwość rozprzestrzenienia każdej z tabel naszej aplikacji na różne serwery w różnych proporcjach. Dużo wiadomości na serwerze A – proszę, przenieśmy część tej tabeli na serwer B. To rozwiązanie krzyczało o przedwczesnej optymalizacji, więc postanowiliśmy ograniczyć się do podejścia multi-tenant.
Można poczytać o multi-tenant na przykład na stronie .
W SV są pojęcia aplikacji i abonenta. Aplikacja to konkretna instalacja aplikacji biznesowej, na przykład ERP lub księgowości, z własnymi użytkownikami i danymi biznesowymi. Abonent to organizacja lub osoba fizyczna, w imieniu której rejestruje się aplikację na serwerze SV. Abonent może zarejestrować wiele aplikacji, a te aplikacje mogą wymieniać się między sobą wiadomościami. Abonent stał się najemcą (tenant) w naszym systemie. Wiadomości różnych abonentów mogą znajdować się w jednej fizycznej bazie; jeśli zauważymy, że jakiś abonent generuje dużo ruchu – przenosimy go do oddzielnej fizycznej bazy (lub nawet oddzielnego serwera bazy danych).
Mamy główną bazę danych, w której znajduje się tabela routingu z informacjami o lokalizacji wszystkich baz danych abonentów.
Aby główna baza danych nie była wąskim gardłem, trzymamy tabelę routingu (i inne często używane dane) w pamięci podręcznej.
Jeśli baza danych abonenta zaczyna zwalniać, wewnętrznie podzielimy ją na partycje. W innych projektach do partycjonowania dużych tabel używamy .
Ponieważ utrata wiadomości użytkowników jest niedopuszczalna, utrzymujemy nasze bazy danych w replikach. Kombinacja replik synchronicznych i asynchronicznych pozwala zabezpieczyć się na wypadek utraty głównej bazy danych. Utrata wiadomości zdarzy się tylko w przypadku jednoczesnej awarii głównej bazy danych i jej synchronicznej repliki.
Jeśli traci się synchroniczną replikę – asynchroniczna replika staje się synchroniczną.
Jeśli traci się główną bazę danych – synchroniczna replika staje się główną bazą danych, a asynchroniczna replika – synchroniczną repliką.
Elasticsearch do wyszukiwania
Ponieważ, oprócz wszystkiego, SV jest także komunikatorem, potrzebne jest szybkie, wygodne i elastyczne wyszukiwanie, z uwzględnieniem morfologii, dla niedokładnych dopasowań. Postanowiliśmy nie wynajdywać koła od nowa i wykorzystać wolny system wyszukiwania Elasticsearch, stworzony na podstawie biblioteki . Elasticsearch uruchamiamy również w klastrze (master – data – data), aby wykluczyć problemy w przypadku awarii węzłów aplikacji.
Na githubie znaleźliśmy dla Elasticsearch i korzystamy z tego. W indeksie Elasticsearch przechowujemy rdzenie słów (określone przez wtyczkę) oraz N-gramy. W miarę wpisywania tekstu przez użytkownika, przeszukujemy wprowadzone dane wśród N-gramów. Przy zapisaniu w indeksie słowo „teksty” zostanie podzielone na następujące N-gramy:
[te, tek, teks, tekst, teksty, ek, eks, ekst, eksty, ks, kst, ksty, st, sty, ty],
A także zachowany zostanie rdzeń słowa „tekst”. Takie podejście pozwala na wyszukiwanie zarówno na początku, jak i w środku oraz na końcu słowa.
Ogólny obraz
Powtórzenie zdjęcia z początku artykułu, ale już z wyjaśnieniami:
- Load balancer wystawiony w internecie; u nas – nginx, może być jakikolwiek.
- Instancje aplikacji Java komunikują się między sobą przez Hazelcast.
- Do pracy z WebSocketem używamy .
- Aplikacja Java jest napisana w Java 8, składa się z bundle'ów. . W planach jest migracja na Java 10 i przejście na moduły.
Rozwój i testowanie
Podczas rozwoju i testowania SV napotkaliśmy szereg ciekawych cech produktów, które wykorzystujemy.
Testy obciążeniowe i wycieki pamięci
Wydanie każdej wersji SV to test obciążeniowy. Uznajemy go za udany, gdy:
- Test działał przez kilka dni bez usterek w obsłudze
- Czas reakcji dla kluczowych operacji nie przekroczył komfortowego progu
- Spadek wydajności w porównaniu do poprzedniej wersji nie przekracza 10%
Baza testowa jest zapełniana danymi – w tym celu pozyskujemy z serwera produkcyjnego informacje o najbardziej aktywnym abonencie, mnożymy jego dane przez 5 (liczba wiadomości, dyskusji, użytkowników) i w ten sposób testujemy.
Testowanie obciążeniowe systemu interakcji przeprowadzamy w trzech konfiguracjach:
- Test stresowy
- Tylko połączenia
- Rejestracja abonentów
Podczas testu stresowego uruchamiamy kilka setek wątków, które nieprzerwanie obciążają system: piszą wiadomości, tworzą dyskusje, pobierają listę wiadomości. Imitujemy działania zwykłych użytkowników (pobierz listę moich nieprzeczytanych wiadomości, napisz do kogoś) oraz rozwiązań programowych (przekaż pakiet innej konfiguracji, przetwórz powiadomienie).
Przykład, jak wygląda część testu stresowego:
- Do systemu wchodzi użytkownik
- Prosi o swoje nieprzeczytane dyskusje
- Z 50% prawdopodobieństwem czyta wiadomości
- Z 50% prawdopodobieństwem pisze wiadomości
- Następnie użytkownik:
- Z 20% prawdopodobieństwem tworzy nową dyskusję
- Losowo wybiera dowolną ze swoich dyskusji
- Wchodzi do środka
- Żąda wiadomości, profili użytkowników
- Tworzy pięć wiadomości skierowanych do losowych użytkowników z tej dyskusji
- Wychodzi z dyskusji
- Powtarza 20 razy
- Wylogowuje się, wraca na początek scenariusza
- Do systemu wchodzi chatbot (emuluje wymianę wiadomości z kodu aplikacji)
- Z 50% prawdopodobieństwem tworzy nowy kanał do wymiany danych (specjalna dyskusja)
- Z 50% prawdopodobieństwem pisze wiadomość w dowolnym z istniejących kanałów
Scenariusz „Tylko połączenia” nie pojawił się bez powodu. Czasami zdarza się sytuacja, że użytkownicy podłączyli system, ale jeszcze się nie zaangażowali. Każdy użytkownik rano o 09:00 włącza komputer, nawiązuje połączenie z serwerem i milczy. Ci ludzie są niebezpieczni, jest ich dużo – w pakietach mają tylko PING/PONG, ale utrzymują połączenie z serwerem (nie mogą go przerwać – a nuż przyjdzie nowa wiadomość). Test odtwarza sytuację, gdy w ciągu pół godziny w systemie stara się zalogować duża liczba takich użytkowników. Jest podobny do testu obciążeniowego, ale skupia się na tym pierwszym wejściu – aby nie było odmów (człowiek nie korzysta z systemu, a on już się wyłącza – trudno wymyślić coś gorszego).
Scenariusz rejestracji abonentów zaczyna się od pierwszego uruchomienia. Przeprowadziliśmy test obciążeniowy i byliśmy pewni, że w komunikacji system nie spowalnia. Ale użytkownicy przyszli i rozpoczęła się sprzedaż rejestracji z powodu przekroczenia limitu czasu. Podczas rejestracji korzystaliśmy , który jest związany z entropią systemu. Serwer nie zdążył zebrać wystarczającej ilości entropii i przy żądaniu nowego SecureRandom zawieszał się na kilka sekund. Jest wiele wyjść z tej sytuacji, na przykład: przejść na mniej bezpieczny /dev/urandom, zainstalować specjalną kartę, która generuje entropię, generować losowe liczby wcześniej i przechowywać je w puli. Tymczasowo zamknęliśmy problem pulą, ale od tego czasu przeprowadzamy osobny test rejestracji nowych abonentów.
Jako generator obciążenia używamy . Nie potrafi pracować z WebSocketem, potrzebny jest plugin. Pierwsze w wynikach wyszukiwania po zapytaniu „jmeter websocket” pojawiają się , w których polecają .
Od tego zaczęliśmy.
Prawie od razu po rozpoczęciu poważnych testów odkryliśmy, że w JMeterze pojawiły się wycieki pamięci.
Plugin to zupełnie odrębna historia, przy 176 gwiazdkach ma 132 forki na Githubie. Sam autor nie aktualizuje go od 2015 roku (wzięliśmy go w 2015, wtedy to nie budziło podejrzeń), kilka problemów na Githubie dotyczących wycieków pamięci, 7 niezamkniętych pull requestów.
Jeśli zdecydujecie się przeprowadzać testy obciążeniowe za pomocą tego pluginu, zwróćcie uwagę na następujące dyskusje:
- W wielowątkowym środowisku użyto zwykłego LinkedList, w rezultacie otrzymaliśmy w czasie wykonywania. Problemy można rozwiązać przechodząc na ConcurrentLinkedDeque lub stosując bloki synchronized. Wybraliśmy pierwszy wariant ().
- Wycieki pamięci, przy rozłączeniu nie jest usuwana informacja o połączeniu ().
- W trybie streaming (gdy websocket nie zostaje zamknięty na końcu próbki, lecz jest używany dalej w planie) nie działają wzorce odpowiedzi ().
To jest jeden z tych, które są na Githubie. Co zrobiliśmy:
- Wzięliśmy (@elyrank) – w nim rozwiązano problemy 1 i 3
- Rozwiązaliśmy problem 2
- Zaktualizowaliśmy jetty z 9.2.14 do 9.3.12
- Owinęliśmy SimpleDateFormat w ThreadLocal; SimpleDateFormat nie jest bezpieczny dla wątków, co prowadziło do NPE w czasie wykonywania
- Usunęliśmy jeszcze jeden wyciek pamięci (niepoprawne zamykanie połączenia przy rozłączeniu)
A mimo to nadal przecieka!
Pamięć zaczynała się kończyć nie po dniu, a po dwóch. Nie mieliśmy czasu, postanowiliśmy uruchomić mniej wątków, ale na czterech agentach. Tego powinno wystarczyć, przynajmniej na tydzień.
Minęły dwa dni…
Teraz pamięć zaczynała się kończyć w Hazelcast. W logach można było zobaczyć, że po kilku dniach testowania Hazelcast zaczyna narzekać na brak pamięci, a po chwili klaster się rozpada, a węzły dalej giną po kolei. Podłączyliśmy JVisualVM do hazelcast i zobaczyliśmy "rośnie piła" – regularnie wywoływał GC, ale nie mógł wyczyścić pamięci.
Okazało się, że w hazelcast 3.4 przy usuwaniu mapy / multiMap (map.destroy()) pamięć nie jest w pełni zwalniana:
Obecnie błąd jest naprawiony w 3.5, ale wtedy był to problem. Tworzyliśmy nowe multiMap z dynamicznymi nazwami i usuwaliśmy je według naszej logiki. Kod wyglądał mniej więcej tak:
public void join(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.put(auth.getUserId(), auth);
}
public void leave(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.remove(auth.getUserId(), auth);
if (sessions.size() == 0) {
sessions.destroy();
}
}Wywołanie:
service.join(auth1, "NOWE_WIADOMOŚCI_W_WĄTKU_UUID1");
service.join(auth2, "NOWE_WIADOMOŚCI_W_WĄTKU_UUID1");multiMap był tworzony dla każdej subskrypcji i usuwany, gdy nie był już potrzebny. Postanowiliśmy, że założymy Map, gdzie kluczem będzie nazwa subskrypcji, a wartościami identyfikatory sesji (na podstawie których później można uzyskać identyfikatory użytkowników, jeśli zajdzie taka potrzeba).
public void join(Authentication auth, String sub) {
addValueToMap(sub, auth.getSessionId());
}
public void leave(Authentication auth, String sub) {
removeValueFromMap(sub, auth.getSessionId());
}Wykresy poprawiły się.
Co jeszcze dowiedzieliśmy się o testowaniu obciążeniowym
- JSR223 należy pisać w groovy i włączyć compilation cache – to znacznie przyspiesza. .
- Wykresy Jmeter-Plugins są łatwiejsze do zrozumienia niż standardowe. .
O naszym doświadczeniu z Hazelcast
Hazelcast był dla nas nowym produktem, zaczęliśmy z nim pracować od wersji 3.4.1, obecnie na naszym serwerze produkcyjnym zainstalowana jest wersja 3.9.2 (w momencie pisania artykułu ostatnia wersja Hazelcast to 3.10).
Generacja ID
Zaczynaliśmy od identyfikatorów całkowitych. Załóżmy, że potrzebujemy kolejnego Long dla nowej encji. Sekwencja w bazie danych się nie sprawdza, tabele są uczestnikami shardingu – może się zdarzyć, że jest wiadomość ID=1 w DB1 i wiadomość ID=1 w DB2, w Elasticsearch nie można użyć takiego ID, w Hazelcast też, ale najgorsze jest to, że jeśli zechcesz połączyć dane z dwóch baz w jedną (na przykład, decydując, że jedna baza jest wystarczająca dla tych abonentów). Można założyć w Hazelcast kilka AtomicLong i przechowywać licznik tam, wtedy wydajność uzyskiwania nowego ID – incrementAndGet plus czas na zapytanie do Hazelcast. Ale w Hazelcast jest coś bardziej optymalnego – FlakeIdGenerator. Każdemu klientowi przy każdym zapytaniu przydzielany jest zakres ID, na przykład pierwszemu – od 1 do 10 000, drugiemu – od 10 001 do 20 000 i tak dalej. Teraz klient może samodzielnie wydawać nowe identyfikatory, dopóki nie wyczerpie przydzielonego mu zakresu. Działa szybko, ale po ponownym uruchomieniu aplikacji (i klienta Hazelcast) zaczyna się nowa sekwencja – stąd luki itp. Ponadto programiści nie do końca rozumieją, dlaczego ID są całkowite, a mimo to są w tak dużym nieładzie. Wszystko dokładnie przeanalizowaliśmy i przeszliśmy na UUID-y.
Zresztą, dla tych, którzy chcą być jak Twitter, istnieje biblioteka Snowcast – to implementacja Snowflake na Hazelcast. Można to zobaczyć tutaj:
Ale do tej pory jeszcze do niej nie doszliśmy.
TransactionalMap.replace
Kolejna niespodzianka: TransactionalMap.replace nie działa. Oto taki 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();
}
});
}
Oczekiwano: newValue
Zaktualizowano: oldValueMusiałem napisać swój własny replace, używając getForUpdate:
protected boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
if (context != null) {
log.trace("[CACHE] Zastępowanie wartości w mapie transakcyjnej");
TransactionalMap map = context.getMap(mapName);
V value = map.getForUpdate(key);
if (oldValue.equals(value)) {
map.put(key, newValue);
return true;
}
return false;
}
log.trace("[CACHE] Zastępowanie wartości w mapie nietransakcyjnej");
IMap map = hazelcastInstance.getMap(mapName);
return map.replace(key, oldValue, newValue);
}Testuj nie tylko zwykłe struktury danych, ale także ich wersje transakcyjne. Czasami IMap działa, a TransactionalMap już nie.
Umieścić nowy JAR bez przestoju
Na początku postanowiliśmy zapisywać obiekty naszych klas w Hazelcast. Na przykład, mamy klasę Application, chcemy ją zapisać i odczytać. Zapisujemy:
IMap map = hazelcastInstance.getMap("application");
map.set(id, application);Odczytujemy:
IMap map = hazelcastInstance.getMap("application");
return map.get(id);Wszystko działa. Potem postanowiliśmy zbudować indeks w Hazelcast, aby móc po nim wyszukiwać:
map.addIndex("subscriberId", false);I przy zapisie nowej encji zaczęliśmy otrzymywać ClassNotFoundException. Hazelcast próbował uzupełnić indeks, ale nic nie wiedział o naszej klasie i chciał, żeby mu dostarczyć JAR z tą klasą. Tak zrobiliśmy, wszystko działało, ale pojawił się nowy problem: jak zaktualizować JAR bez całkowitego zatrzymania klastra? Hazelcast nie przechwytuje nowego JAR przy aktualizacji w trybie dogodnym. W tym momencie postanowiliśmy, że całkiem możemy żyć bez wyszukiwania po indeksie. W końcu, jeżeli używa się Hazelcast jako magazyn w stylu klucz-wartość, to wszystko powinno działać, prawda? Nie do końca. Tu znowu zachowanie IMap i TransactionalMap jest różne. Gdzie IMap nie ma z tym problemu, TransactionalMap rzuca błąd.
IMap. Zapisujemy 5000 obiektów, odczytujemy. Wszystko zgodnie z oczekiwaniami.
@Test
void get5000() {
IMap 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());
}
}A w transakcji nie działa, otrzymujemy ClassNotFoundException:
@Test
void get_transaction() {
IMap 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 transactionalMap = context.getMap("application_t");
Application retrievedInside = transactionalMap.get(id);
assertEquals(id, retrievedInside.getId());
return null;
} finally {
HazelcastTransactionContextHolder.clearContext();
}
});
}W wersji 3.8 pojawił się mechanizm User Class Deployment. Możesz wyznaczyć jeden główny węzeł i aktualizować plik JAR na nim.
Obecnie całkowicie zmieniliśmy podejście: sami serializujemy do JSON i zapisujemy w Hazelcast. Hazelcast nie musi znać struktury naszych klas, a my możemy się aktualizować bez przestojów. Wersjonowaniem obiektów domenowych zarządza aplikacja. Równocześnie mogą działać różne wersje aplikacji, a możliwa jest sytuacja, w której nowa aplikacja zapisuje obiekty z nowymi polami, a stara nie zna jeszcze tych pól. A jednocześnie nowa aplikacja odczytuje obiekty zapisane przez starą aplikację, w których nie ma nowych pól. Takie sytuacje obsługujemy w aplikacji, ale dla uproszczenia nie zmieniamy ani nie usuwamy pól, jedynie rozszerzamy klasy poprzez dodawanie nowych pól.
Jak zapewniamy wysoką wydajność
Cztery podejścia do Hazelcast – dobrze, dwa do bazy danych – źle
Zbieranie danych z pamięci podręcznej jest zawsze lepsze niż z bazy danych, ale nie chcemy trzymać niepotrzebnych rekordów. Decyzję o tym, co cachować, odkładamy na ostatni etap rozwoju. Kiedy nowa funkcjonalność jest gotowa, włączamy w PostgreSQL logowanie wszystkich zapytań (log_min_duration_statement ustawione na 0) i uruchamiamy testy obciążeniowe na około 20 minut. Na podstawie zebranych logów narzędzia takie jak pgFouine i pgBadger potrafią tworzyć analityczne raporty. W raportach przede wszystkim szukamy wolnych i częstych zapytań. Dla wolnych zapytań budujemy plan wykonania (EXPLAIN) i oceniamy, czy można takie zapytanie przyspieszyć. Częste zapytania dotyczące tych samych danych wejściowych dobrze sprawdzają się w pamięci podręcznej. Staramy się, aby zapytania były "płaskie", dotyczące jednej tabeli na zapytanie.
Eksploatacja
SV jako usługa online została uruchomiona wiosną 2017 roku, a jako osobny produkt SV zadebiutował w listopadzie 2017 (wówczas w statusie wersji beta).
Przez ponad rok eksploatacji poważne problemy w działaniu usługi online SV się nie zdarzyły. Usługę online monitorujemy przez , zbieramy i wdrażamy z .
Dystrybucja serwera SV dostarczana jest w postaci natywnych pakietów: RPM, DEB, MSI. Dodatkowo dla Windows udostępniamy jeden instalator w formie pliku EXE, który instaluje serwer, Hazelcast oraz Elasticsearch na jednej maszynie. Na początku nazywaliśmy tę wersję instalacji "demonstracyjną", ale teraz stało się jasne, że to najpopularniejsza opcja wdrożenia.
Źródło: habr.com
