Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast

W tym artykule porozmawiamy o tym, jak i dlaczego się rozwinęliśmy System interakcji – mechanizm przekazujący informacje pomiędzy aplikacjami klienckimi a serwerami 1C:Enterprise – od ustawienia zadania po przemyślenie architektury i szczegółów wdrożenia.

System Interakcji (zwany dalej SV) jest rozproszonym, odpornym na awarie systemem przesyłania wiadomości z gwarantowaną dostawą. SV został zaprojektowany jako usługa o dużym obciążeniu i wysokiej skalowalności, dostępna zarówno jako usługa online (świadczona przez 1C), jak i jako produkt produkowany masowo, który można wdrożyć na własnych serwerach.

SV korzysta z pamięci rozproszonej Leszczyna i wyszukiwarka Elasticsearch. Porozmawiamy także o Javie i o tym, jak poziomo skalujemy PostgreSQL.
Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast

Stwierdzenie problemu

Aby wyjaśnić, dlaczego stworzyliśmy System interakcji, opowiem trochę o tym, jak działa tworzenie aplikacji biznesowych w 1C.

Na początek trochę o nas dla tych, którzy jeszcze nie wiedzą czym się zajmujemy :) Tworzymy platformę technologiczną 1C:Enterprise. Platforma zawiera narzędzie do tworzenia aplikacji biznesowych, a także środowisko wykonawcze, które umożliwia działanie aplikacji biznesowych w środowisku wieloplatformowym.

Paradygmat rozwoju klient-serwer

Aplikacje biznesowe tworzone na 1C:Enterprise działają na trzech poziomach klient-serwer architektura „DBMS – serwer aplikacji – klient”. Wpisany kod aplikacji wbudowany język 1C, można wykonać na serwerze aplikacji lub na kliencie. Cała praca z obiektami aplikacji (katalogi, dokumenty itp.), a także odczyt i zapis bazy danych odbywa się wyłącznie na serwerze. Na serwerze zaimplementowana jest także funkcjonalność formularzy i interfejsu poleceń. Klient wykonuje przyjmowanie, otwieranie i wyświetlanie formularzy, „komunikuje się” z użytkownikiem (ostrzeżenia, pytania...), drobne obliczenia w formularzach wymagających szybkiej reakcji (np. pomnożenie ceny przez ilość), pracę z plikami lokalnymi, praca ze sprzętem.

W kodzie aplikacji nagłówki procedur i funkcji muszą jawnie wskazywać, gdzie kod zostanie wykonany - wykorzystując dyrektywy &AtClient / &AtServer (&AtClient / &AtServer w angielskiej wersji języka). Programiści 1C poprawią mnie teraz, mówiąc, że dyrektywy są w rzeczywistości więcej niż, ale dla nas to nie jest teraz ważne.

Możesz wywołać kod serwera z kodu klienta, ale nie możesz wywołać kodu klienta z kodu serwera. Jest to podstawowe ograniczenie, które wprowadziliśmy z wielu powodów. W szczególności dlatego, że kod serwera musi być napisany w taki sposób, aby wykonywał się w ten sam sposób niezależnie od tego, gdzie zostanie wywołany – od klienta czy z serwera. A w przypadku wywoływania kodu serwera z innego kodu serwera nie ma klienta jako takiego. A ponieważ podczas wykonywania kodu serwera klient, który go wywołał, mógłby się zamknąć, wyjść z aplikacji, a serwer nie miałby już do kogo zadzwonić.

Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast
Kod obsługujący kliknięcie przycisku: wywołanie procedury serwera z klienta będzie działać, wywołanie procedury klienta z serwera nie

Oznacza to, że jeśli chcemy wysłać jakiś komunikat z serwera do aplikacji klienckiej, np. że zakończyło się generowanie „długiego” raportu i można go obejrzeć, to nie mamy takiej metody. Trzeba stosować triki, np. okresowo odpytywać serwer z kodu klienta. Ale takie podejście obciąża system niepotrzebnymi wywołaniami i ogólnie nie wygląda zbyt elegancko.

I jest też potrzeba, na przykład, gdy przychodzi telefon SIP- wykonując połączenie, powiadom o tym aplikację kliencką, aby na podstawie numeru dzwoniącego odszukała go w bazie kontrahentów i pokazała użytkownikowi informację o kontrahentu dzwoniącym. Lub np. gdy zamówienie dotrze do magazynu, powiadom o tym aplikację kliencką klienta. Ogólnie rzecz biorąc, istnieje wiele przypadków, w których taki mechanizm byłby przydatny.

Sama produkcja

Stwórz mechanizm przesyłania wiadomości. Szybko, niezawodnie, z gwarancją dostawy, z możliwością elastycznego wyszukiwania wiadomości. W oparciu o mechanizm zaimplementuj komunikator (wiadomości, rozmowy wideo) działający w aplikacjach 1C.

Zaprojektuj system tak, aby był skalowalny w poziomie. Rosnące obciążenie należy pokryć zwiększając liczbę węzłów.

realizacja

Postanowiliśmy nie integrować części serwerowej SV bezpośrednio z platformą 1C:Enterprise, ale wdrożyć ją jako osobny produkt, którego API można wywołać z kodu rozwiązań aplikacyjnych 1C. Zrobiono to z wielu powodów, z których głównym było to, że chciałem umożliwić wymianę 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 umieszczonego „z boku” instalacji 1C jest optymalnym rozwiązaniem.

Zdecydowaliśmy się więc stworzyć SV jako osobny produkt. Małym firmom zalecamy korzystanie z serwera CB, który zainstalowaliśmy w naszej chmurze (wss://1cdialog.com), aby uniknąć kosztów ogólnych związanych z lokalną instalacją i konfiguracją serwera. Duzi klienci mogą uznać za wskazane zainstalowanie własnego serwera CB w swoich obiektach. Podobne podejście zastosowaliśmy w naszym produkcie chmurowym SaaS 1cŚwieże – produkowany jest jako produkt masowy do montażu u klientów, a także wdrażany jest w naszej chmurze https://1cfresh.com/.

Aplikacja

Aby rozłożyć odporność na obciążenie i błędy, wdrożymy nie jedną aplikację Java, ale kilka, z modułem równoważenia obciążenia przed nimi. Jeśli chcesz przesłać wiadomość z węzła do węzła, użyj funkcji publikowania/subskrybowania w Hazelcast.

Komunikacja pomiędzy klientem a serwerem odbywa się poprzez websocket. Świetnie nadaje się do systemów czasu rzeczywistego.

Rozproszona pamięć podręczna

Wybraliśmy pomiędzy Redis, Hazelcast i Ehcache. Jest rok 2015. Redis właśnie wypuścił nowy klaster (zbyt nowy, straszny), jest Sentinel z wieloma ograniczeniami. Ehcache nie wie, jak złożyć się w klaster (ta funkcjonalność pojawiła się później). Postanowiliśmy spróbować z Hazelcast 3.4.
Hazelcast jest składany w klaster po wyjęciu z pudełka. W trybie pojedynczego węzła nie jest to zbyt przydatne i może służyć jedynie jako pamięć podręczna - nie wie, jak zrzucić dane na dysk, jeśli stracisz jedyny węzeł, stracisz dane. Wdrażamy kilka Hazelcastów, pomiędzy którymi tworzymy kopie zapasowe krytycznych danych. Nie tworzymy kopii zapasowych pamięci podręcznej – nie przeszkadza nam to.

Dla nas Hazelcast to:

  • Przechowywanie sesji użytkowników. Za każdym razem przejście do bazy danych w celu uzyskania sesji zajmuje dużo czasu, dlatego umieszczamy wszystkie sesje w Hazelcast.
  • Pamięć podręczna. Jeśli szukasz profilu użytkownika, sprawdź pamięć podręczną. Napisałem nową wiadomość - umieść ją w pamięci podręcznej.
  • Tematy dotyczące komunikacji pomiędzy instancjami aplikacji. Węzeł generuje zdarzenie i umieszcza je w temacie Hazelcast. Inne węzły aplikacji subskrybowane w tym temacie odbierają i przetwarzają zdarzenie.
  • Zamki klastrowe. Na przykład tworzymy dyskusję za pomocą unikalnego klucza (dyskusja singletonowa w bazie danych 1C):

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

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

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

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

Sprawdziliśmy, czy nie ma kanału. Wzięliśmy zamek, sprawdziliśmy go ponownie i stworzyliśmy. Jeśli po zdjęciu blokady nie sprawdzisz blokady, jest szansa, że ​​w tym momencie sprawdził także inny wątek i teraz spróbuje założyć taką samą dyskusję - ale ona już istnieje. Nie można blokować za pomocą zsynchronizowanej lub zwykłej blokady Java. Przez bazę danych - jest powolna i szkoda bazy danych; poprzez Hazelcast - tego potrzebujesz.

Wybór systemu DBMS

Mamy rozległe i udane doświadczenie w pracy z PostgreSQL i współpracy z twórcami tego systemu DBMS.

Z klastrem PostgreSQL nie jest to łatwe – jest XL, XC, Cytus, ale ogólnie nie są to NoSQL-y, które można skalować od razu po wyjęciu z pudełka. Nie uważaliśmy NoSQL za główny magazyn, wystarczyło, że wzięliśmy Hazelcast, z którym wcześniej nie pracowaliśmy.

Oznacza to, że musisz skalować relacyjną bazę danych fragmentowanie. Jak wiadomo, metodą shardingu dzielimy bazę danych na osobne części, tak aby każdą z nich można było umieścić na osobnym serwerze.

Pierwsza wersja naszego shardingu zakładała możliwość dystrybucji każdej z tabel naszej aplikacji na różne serwery w różnych proporcjach. Na serwerze A jest dużo wiadomości - proszę przenieś część tej tabeli na serwer B. Ta decyzja po prostu krzyczała o przedwczesnej optymalizacji, więc postanowiliśmy ograniczyć się do podejścia wielodostępnego.

O multi-tenantach można przeczytać np. na stronie internetowej Dane Citusa.

SV ma koncepcje aplikacji i abonenta. Aplikacja to specyficzna instalacja aplikacji biznesowej, takiej jak ERP lub Księgowość, wraz z jej użytkownikami i danymi biznesowymi. Abonent to organizacja lub osoba fizyczna, w imieniu której aplikacja jest rejestrowana na serwerze SV. Abonent może mieć zarejestrowanych kilka aplikacji, które mogą wymieniać między sobą wiadomości. Abonent stał się najemcą w naszym systemie. Wiadomości od kilku abonentów mogą być umieszczone w jednej fizycznej bazie danych; jeśli widzimy, że abonent zaczął generować duży ruch, przenosimy go do osobnej fizycznej bazy danych (lub nawet osobnego serwera bazy danych).

Posiadamy główną bazę danych, w której przechowywana jest tablica routingu zawierająca informacje o lokalizacji wszystkich baz danych abonentów.

Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast

Aby główna baza danych nie była wąskim gardłem, tablicę routingu (i inne często potrzebne dane) przechowujemy w pamięci podręcznej.

Jeżeli baza danych abonenta zacznie zwalniać, podzielimy ją wewnątrz na partycje. W innych projektach używamy pg_pathman.

Ponieważ utrata wiadomości użytkowników jest zła, utrzymujemy nasze bazy danych za pomocą replik. Połączenie replik synchronicznych i asynchronicznych pozwala zabezpieczyć się na wypadek utraty głównej bazy danych. Utrata wiadomości nastąpi tylko w przypadku jednoczesnej awarii podstawowej bazy danych i jej synchronicznej repliki.

W przypadku utraty repliki synchronicznej replika asynchroniczna staje się synchroniczna.
Jeśli główna baza danych zostanie utracona, replika synchroniczna stanie się główną bazą danych, a replika asynchroniczna stanie się repliką synchroniczną.

Elasticsearch do wyszukiwania

Ponieważ SV jest między innymi komunikatorem, wymaga szybkiego, wygodnego i elastycznego wyszukiwania z uwzględnieniem morfologii, przy użyciu nieprecyzyjnych dopasowań. Postanowiliśmy nie wymyślać koła na nowo i skorzystać z darmowej wyszukiwarki Elasticsearch, stworzonej w oparciu o bibliotekę Lucena. Wdrażamy Elasticsearch także w klastrze (master – dane – dane), aby wyeliminować problemy w przypadku awarii węzłów aplikacji.

Na githubie znaleźliśmy Rosyjska wtyczka morfologiczna dla Elasticsearch i użyj go. W indeksie Elasticsearch przechowujemy korzenie słów (które określa wtyczka) i N-gramy. Gdy użytkownik wprowadza tekst do wyszukania, szukamy wpisanego tekstu wśród N-gramów. Po zapisaniu do indeksu słowo „teksty” zostanie podzielone na następujące N-gramy:

[te, tek, tex, tekst, teksty, ek, ex, ext, teksty, ks, kst, ksty, st, sty, ty],

Zachowany zostanie również rdzeń słowa „tekst”. Takie podejście pozwala wyszukiwać na początku, w środku i na końcu słowa.

Wielkie zdjęcie

Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast
Powtórzenie obrazu z początku artykułu, ale z objaśnieniami:

  • Balancer ujawniony w Internecie; mamy nginx, może to być dowolny.
  • Instancje aplikacji Java komunikują się ze sobą za pośrednictwem Hazelcast.
  • Do pracy z gniazdem internetowym używamy Netty.
  • Aplikacja Java jest napisana w języku Java 8 i składa się z pakietów OSGi. W planach jest migracja do Java 10 i przejście na moduły.

Rozwój i testowanie

W procesie opracowywania i testowania SV natknęliśmy się na szereg interesujących cech produktów, których używamy.

Testy obciążeniowe i wycieki pamięci

Wydanie każdego wydania SV obejmuje testowanie obciążenia. Odnosi sukces, gdy:

  • Test działał kilka dni i nie było żadnych awarii serwisowych
  • Czas reakcji na kluczowe operacje nie przekroczył komfortowego progu
  • Pogorszenie wydajności w porównaniu do poprzedniej wersji wynosi nie więcej niż 10%

Testową bazę danych wypełniamy danymi - w tym celu pobieramy informację o najaktywniejszym abonencie z serwera produkcyjnego, mnożymy jego numery przez 5 (liczba wiadomości, dyskusji, użytkowników) i tak go testujemy.

Testy obciążeniowe układu interakcji przeprowadzamy w trzech konfiguracjach:

  1. test warunków skrajnych
  2. Tylko połączenia
  3. Rejestracja abonenta

Podczas stress testu uruchamiamy kilkaset wątków, które bez przerwy ładują system: pisząc wiadomości, tworząc dyskusje, otrzymując listę wiadomości. Symulujemy działania zwykłych użytkowników (pozyskujemy listę moich nieprzeczytanych wiadomości, piszemy do kogoś) i rozwiązania programowe (przesyłamy pakiet o innej konfiguracji, przetwarzamy alert).

Na przykład tak wygląda część testu warunków skrajnych:

  • Użytkownik loguje się
    • Prosi o nieprzeczytane dyskusje
    • 50% szans na przeczytanie wiadomości
    • Na 50% prawdopodobnie wyśle ​​SMS-a
    • Następny użytkownik:
      • Ma 20% szans na utworzenie nowej dyskusji
      • Losowo wybiera dowolną ze swoich dyskusji
      • Wchodzi do środka
      • Żąda wiadomości, profili użytkowników
      • Tworzy pięć wiadomości adresowanych do losowych użytkowników z tej dyskusji
      • Opuszcza dyskusję
      • Powtarza 20 razy
      • Wylogowuje się, wraca na początek skryptu

    • Do systemu wchodzi chatbot (emuluje przesyłanie wiadomości z kodu aplikacji)
      • Ma 50% szans na utworzenie nowego kanału wymiany danych (dyskusja specjalna)
      • 50% prawdopodobne, że napisze wiadomość na którykolwiek z istniejących kanałów

Scenariusz „Tylko połączenia” pojawił się nie bez powodu. Zachodzi sytuacja: użytkownicy podłączyli się do systemu, ale jeszcze się nie zaangażowali. Każdy użytkownik włącza komputer o godzinie 09:00 rano, nawiązuje połączenie z serwerem i milczy. Ci goście są niebezpieczni, jest ich wielu - jedyne pakiety, jakie mają, to PING/PONG, ale utrzymują połączenie z serwerem (nie mogą go utrzymać - co jeśli pojawi się nowa wiadomość). Test odtwarza sytuację, w której duża liczba takich użytkowników próbuje zalogować się do systemu w ciągu pół godziny. Przypomina stress test, jednak skupia się właśnie na tym pierwszym wejściu – tak, żeby nie było awarii (człowiek nie korzysta z systemu, a on już odpada – trudno o coś gorszego).

Skrypt rejestracji abonenta rozpoczyna się od pierwszego uruchomienia. Przeprowadziliśmy stress test i mieliśmy pewność, że system nie zwalnia podczas korespondencji. Ale użytkownicy przyszli i rejestracja zaczęła się nie powieść z powodu przekroczenia limitu czasu. Podczas rejestracji użyliśmy / Dev / random, co jest związane z entropią układu. Serwer nie miał czasu na zgromadzenie wystarczającej entropii i kiedy zażądano nowego SecureRandom, zamarł na dziesiątki sekund. Wyjść z tej sytuacji jest wiele, np.: przejść na mniej bezpieczne /dev/urandom, zainstalować specjalną płytkę generującą entropię, wygenerować z wyprzedzeniem liczby losowe i przechowywać je w puli. Tymczasowo zamknęliśmy problem z pulą, ale od tego czasu prowadzimy osobny test rejestracji nowych abonentów.

Używamy jako generatora obciążenia JMeter. Nie wie, jak pracować z websocketem; potrzebuje wtyczki. Pierwsze w wynikach wyszukiwania hasła „jmeter websocket” to: artykuły z BlazeMeter, które polecają plugin autorstwa Macieja Zaleskiego.

Od tego postanowiliśmy zacząć.

Niemal natychmiast po rozpoczęciu poważnych testów odkryliśmy, że JMeter zaczął powodować wycieki pamięci.

Wtyczka to osobna, wielka historia; ze 176 gwiazdkami ma 132 forki na githubie. Sam autor nie angażował się w to od 2015 roku (wzięliśmy to w 2015 roku, wtedy nie budziło to żadnych podejrzeń), kilka problemów z githubem dotyczących wycieków pamięci, 7 niezamkniętych pull requestów.
Jeśli zdecydujesz się przeprowadzić testy obciążenia za pomocą tej wtyczki, zwróć uwagę na następujące dyskusje:

  1. W środowisku wielowątkowym użyto zwykłej listy LinkedList i wynik był taki NPE w czasie wykonywania. Można to rozwiązać poprzez przejście na ConcurrentLinkedDeque lub zsynchronizowane bloki. Sami wybraliśmy pierwszą opcję (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Wyciek pamięci; podczas rozłączania informacje o połączeniu nie są usuwane (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. W trybie przesyłania strumieniowego (kiedy websocket nie jest zamykany na końcu próbki, ale jest używany w dalszej części planu) wzorce odpowiedzi nie działają (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

To jeden z tych na githubie. Co zrobiliśmy:

  1. Wziął widelec Elyrana Kogana (@elyrank) – naprawia problemy 1 i 3
  2. Rozwiązany problem 2
  3. Zaktualizowano molo z 9.2.14 do 9.3.12
  4. Zapakowany SimpleDateFormat w ThreadLocal; SimpleDateFormat nie jest bezpieczny dla wątków, co doprowadziło do NPE w czasie wykonywania
  5. Naprawiono kolejny wyciek pamięci (połączenie zostało nieprawidłowo zamknięte po rozłączeniu)

A jednak płynie!

Pamięć zaczęła się kończyć nie w ciągu jednego dnia, ale za dwa. Nie było już absolutnie czasu, więc postanowiliśmy uruchomić mniej wątków, ale na czterech agentach. To powinno wystarczyć na co najmniej tydzień.

Minęły dwa dni...

Teraz Hazelcastowi kończy się pamięć. Logi wykazały, że po kilku dniach testów Hazelcast zaczął narzekać na brak pamięci, a po pewnym czasie klaster się rozpadł, a węzły nadal jeden po drugim obumierały. Połączyliśmy JVisualVM z Hazelcast i zobaczyliśmy „rosnącą piłę” - regularnie wywoływała ona GC, ale nie mogła wyczyścić pamięci.

Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast

Okazało się, że w hazelcast 3.4, podczas usuwania mapy/multiMap (map.destroy()) pamięć nie jest całkowicie zwalniana:

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

Błąd został naprawiony w wersji 3.5, ale wtedy był to problem. Stworzyliśmy nowe multiMapy z dynamicznymi nazwami i usunęliśmy je zgodnie z naszą logiką. Kod wyglądał mniej więcej tak:

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();
    }
}

Wszystko:

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

multiMap był tworzony dla każdej subskrypcji i usuwany, gdy nie był potrzebny. Zdecydowaliśmy, że uruchomimy Mapę , kluczem będzie nazwa subskrypcji, a wartościami będą identyfikatory sesji (z których można następnie 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 uległy poprawie.

Jak i dlaczego napisaliśmy skalowalną usługę o dużym obciążeniu dla 1C: Enterprise: Java, PostgreSQL, Hazelcast

Czego jeszcze dowiedzieliśmy się o testowaniu obciążenia?

  1. JSR223 musi być napisany w groovy i zawierać pamięć podręczną kompilacji - jest znacznie szybszy. Połączenie.
  2. Wykresy Jmeter-Plugins są łatwiejsze do zrozumienia niż standardowe. Połączenie.

O naszych doświadczeniach z Hazelcast

Hazelcast był dla nas nowym produktem, zaczęliśmy z nim pracować od wersji 3.4.1, obecnie na naszym serwerze produkcyjnym działa wersja 3.9.2 (w chwili pisania tego tekstu najnowsza wersja Hazelcast to 3.10).

Generowanie identyfikatora

Zaczęliśmy od identyfikatorów całkowitych. Wyobraźmy sobie, że potrzebujemy kolejnego Longa dla nowej istoty. Sekwencja w bazie danych nie jest odpowiednia, tabele biorą udział w shardingu - okazuje się, że w DB1 jest komunikat ID=1, a w DB1 komunikat ID=2, nie da się tego ID umieścić ani w Elasticsearch, ani w Hazelcast , ale najgorzej jest, jeśli chcesz połączyć dane z dwóch baz w jedną (np. uznając, że dla tych abonentów wystarczy jedna baza). Możesz dodać kilka AtomicLongów do Hazelcast i trzymać tam licznik, wtedy wydajność uzyskania nowego identyfikatora jest zwiększana iGet plus czas na żądanie do Hazelcast. Ale Hazelcast ma coś bardziej optymalnego - FlakeIdGenerator. Kontaktując się z każdym klientem, podawany jest mu zakres identyfikatorów, np. pierwszy – od 1 do 10 000, drugi – od 10 001 do 20 000 i tak dalej. Teraz klient może samodzielnie wystawiać nowe identyfikatory aż do wyczerpania się przyznanego mu zakresu. Działa szybko, ale po ponownym uruchomieniu aplikacji (i klienta Hazelcast) rozpoczyna się nowa sekwencja - stąd pominięcia itp. Ponadto programiści tak naprawdę nie rozumieją, dlaczego identyfikatory są liczbami całkowitymi, ale są tak niespójne. Zważyliśmy wszystko i przeszliśmy na UUID.

Nawiasem mówiąc, dla tych, którzy chcą być jak Twitter, istnieje taka biblioteka Snowcast - jest to implementacja Snowflake na Hazelcast. Możesz zobaczyć to tutaj:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Ale już się tym nie zajmowaliśmy.

Transakcyjna mapa.zastąp

Kolejna niespodzianka: TransactionalMap.replace nie działa. Oto 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

Musiałem napisać własną zamianę za pomocą getForUpdate:

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);
}

Testuj nie tylko zwykłe struktury danych, ale także ich wersje transakcyjne. Zdarza się, że IMap działa, ale TransactionalMap już nie istnieje.

Włóż nowy plik JAR bez przestojów

Najpierw postanowiliśmy nagrać obiekty naszych zajęć w Hazelcast. Na przykład mamy klasę Application, chcemy ją zapisać i przeczytać. Ratować:

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

Czytanie:

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

Wszystko działa. Następnie zdecydowaliśmy się zbudować indeks w Hazelcast, aby wyszukiwać według:

map.addIndex("subscriberId", false);

A pisząc nowy podmiot, zaczęli otrzymywać wyjątek ClassNotFoundException. Hazelcast próbował dodać coś do indeksu, ale nie wiedział nic o naszej klasie i chciał, aby dostarczono do niej plik JAR z tą klasą. Właśnie to zrobiliśmy, wszystko działało, ale pojawił się nowy problem: jak zaktualizować plik JAR bez całkowitego zatrzymywania klastra? Hazelcast nie pobiera nowego pliku JAR podczas aktualizacji węzeł po węźle. W tym momencie zdecydowaliśmy, że możemy żyć bez wyszukiwania indeksów. W końcu, jeśli użyjesz Hazelcast jako magazynu klucz-wartość, to wszystko będzie działać? Nie bardzo. Tutaj znowu zachowanie IMap i TransactionalMap jest inne. Tam, gdzie IMap nie dba o to, TransactionalMap zgłasza błąd.

IMapa. Piszemy 5000 obiektów, czytamy je. Wszystko jest oczekiwane.

@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());
    }
}

Ale to nie działa w transakcji, otrzymujemy wyjątek ClassNotFoundException:

@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();
        }
    });
}

W wersji 3.8 pojawił się mechanizm wdrażania klasy użytkownika. Możesz wyznaczyć jeden węzeł główny i zaktualizować na nim plik JAR.

Teraz całkowicie zmieniliśmy nasze podejście: sami serializujemy go do formatu JSON i zapisujemy w Hazelcast. Hazelcast nie musi znać struktury naszych zajęć i możemy aktualizować bez przestojów. Wersjonowanie obiektów domeny jest kontrolowane przez aplikację. Jednocześnie mogą działać różne wersje aplikacji, możliwa jest sytuacja, gdy nowa aplikacja zapisuje obiekty z nowymi polami, a stara jeszcze o tych polach nie wie. Jednocześnie nowa aplikacja odczytuje obiekty zapisane przez starą aplikację, które nie posiadają nowych pól. Radzimy sobie z takimi sytuacjami wewnątrz aplikacji, jednak dla uproszczenia nie zmieniamy i nie usuwamy pól, a jedynie rozszerzamy klasy dodając nowe pola.

Jak zapewniamy wysoką wydajność

Cztery wyjazdy do Hazelcast – dobrze, dwa do bazy – źle

Przejście do pamięci podręcznej w poszukiwaniu danych jest zawsze lepsze niż udanie się do bazy danych, ale nie chcesz też przechowywać nieużywanych rekordów. Decyzję o tym, co buforować, pozostawiamy do ostatniego etapu rozwoju. Po zakodowaniu nowej funkcjonalności włączamy logowanie wszystkich zapytań w PostgreSQL (log_min_duration_statement na 0) i uruchamiamy testy obciążeniowe na 20 minut.Wykorzystując zebrane logi, narzędzia takie jak pgFouine i pgBadger mogą budować raporty analityczne. W raportach szukamy przede wszystkim zapytań wolnych i częstych. Dla powolnych zapytań budujemy plan wykonania (EXPLAIN) i oceniamy, czy takie zapytanie można przyspieszyć. Częste żądania tych samych danych wejściowych dobrze mieszczą się w pamięci podręcznej. Staramy się, aby zapytania były „płaskie”, jedna tabela na zapytanie.

Eksploatacja

SV jako usługa internetowa została uruchomiona wiosną 2017 roku, natomiast jako odrębny produkt SV został wydany w listopadzie 2017 roku (wówczas w wersji beta).

Przez ponad rok działalności nie było żadnych poważnych problemów w działaniu serwisu CB online. Monitorujemy usługę online poprzez Zabbix, zbieraj i wdrażaj z Bambus.

Dystrybucja serwera SV dostarczana jest w postaci pakietów natywnych: RPM, DEB, MSI. Dodatkowo dla systemu Windows zapewniamy pojedynczy instalator w postaci pojedynczego pliku EXE, który instaluje serwer, Hazelcast i Elasticsearch na jednym komputerze. Początkowo nazywaliśmy tę wersję instalacji wersją „demo”, ale teraz stało się jasne, że jest to najpopularniejsza opcja wdrożenia.

Źródło: www.habr.com

Dodaj komentarz