Kaseta Tarantool: sharding backendu Lua w trzech liniach

Kaseta Tarantool: sharding backendu Lua w trzech liniach

W Mail.ru Group mamy Tarantool - jest to serwer aplikacji w Lua, który pełni także funkcję bazy danych (lub odwrotnie?). Jest szybko i fajnie, ale możliwości jednego serwera wciąż nie są nieograniczone. Skalowanie w pionie też nie jest panaceum, dlatego Tarantool posiada narzędzia do skalowania w poziomie - moduł vshard [1]. Pozwala na dzielenie danych na kilka serwerów, ale trzeba się przy tym majstrować, aby je skonfigurować i dołączyć logikę biznesową.

Dobra wiadomość: zebraliśmy kilka dużych ujęć (np [2], [3]) i stworzyłem kolejny framework, który znacznie uprości rozwiązanie tego problemu.

Nabój Tarantool to nowa platforma do tworzenia złożonych systemów rozproszonych. Pozwala skupić się na pisaniu logiki biznesowej zamiast na rozwiązywaniu problemów z infrastrukturą. Poniżej opowiem Wam jak działa ten framework i jak pisać przy jego pomocy usługi rozproszone.

A na czym dokładnie polega problem?

Mamy tarantulę, mamy vsharda – czego chcieć więcej?

Po pierwsze, jest to kwestia wygody. Konfiguracja vshard jest konfigurowana za pomocą tabel Lua. Aby rozproszony system wielu procesów Tarantool działał poprawnie, konfiguracja musi być wszędzie taka sama. Nikt nie chce tego robić ręcznie. Dlatego używane są wszelkiego rodzaju skrypty, Ansible i systemy wdrażania.

Cartridge sam zarządza konfiguracją vshard, robi to w oparciu o swoje własna konfiguracja rozproszona. Zasadniczo jest to prosty plik YAML, którego kopia jest przechowywana w każdej instancji Tarantool. Uproszczenie polega na tym, że framework sam monitoruje swoją konfigurację i pilnuje, aby wszędzie była taka sama.

Po drugie, jest to znowu kwestia wygody. Konfiguracja vshard nie ma nic wspólnego z rozwojem logiki biznesowej a jedynie odwraca uwagę programisty od jego pracy. Kiedy mówimy o architekturze projektu, najczęściej mówimy o poszczególnych komponentach i ich interakcji. Jest zbyt wcześnie, aby myśleć o wdrożeniu klastra do 3 centrów danych.

Wielokrotnie rozwiązywaliśmy te problemy i w pewnym momencie udało nam się wypracować podejście, które upraszczało pracę z aplikacją przez cały jej cykl życia: tworzenie, rozwój, testowanie, CI/CD, utrzymanie.

Cartridge wprowadza koncepcję roli dla każdego procesu Tarantool. Role to koncepcja, która pozwala programiście skupić się na pisaniu kodu. Wszystkie role dostępne w projekcie można uruchomić na jednej instancji Tarantool i to wystarczy do testów.

Najważniejsze cechy wkładu Tarantool:

  • zautomatyzowana orkiestracja klastrów;
  • poszerzanie funkcjonalności aplikacji o nowe role;
  • szablon aplikacji do opracowania i wdrożenia;
  • wbudowane automatyczne sharding;
  • integracja z frameworkiem testowym Luatest;
  • zarządzanie klastrami za pomocą WebUI i API;
  • narzędzia do pakowania i wdrażania.

Witaj świecie!

Nie mogę się doczekać, aż pokażę sam framework, więc zostawmy historię o architekturze na później i zacznijmy od czegoś prostego. Jeśli założymy, że sam Tarantool jest już zainstalowany, pozostaje tylko zrobić

$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH

Te dwa polecenia zainstalują narzędzia wiersza poleceń i umożliwią utworzenie pierwszej aplikacji na podstawie szablonu:

$ cartridge create --name myapp

I oto co otrzymujemy:

myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│   ├── helper
│   │   ├── integration.lua
│   │   └── unit.lua
│   ├── helper.lua
│   ├── integration/api_test.lua
│   └── unit/sample_test.lua
└── tmp/

To repozytorium git z gotowym „Hello, World!” aplikacja. Spróbujmy go od razu uruchomić, po uprzednim zainstalowaniu zależności (w tym samego frameworka):

$ tarantoolctl rocks make
$ ./init.lua --http-port 8080

Mamy więc jeden węzeł działający dla przyszłej aplikacji podzielonej na fragmenty. Dociekliwy laik może od razu otworzyć interfejs WWW, skonfigurować klaster jednego węzła za pomocą myszki i cieszyć się efektem, ale na radość jest jeszcze za wcześnie. Na razie aplikacja nie potrafi zrobić nic pożytecznego, więc o wdrożeniu opowiem później, ale teraz czas na pisanie kodu.

Rozwój aplikacji

Wyobraź sobie, że projektujemy projekt, który raz dziennie musi odbierać dane, zapisywać je i generować raport.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Zaczynamy rysować diagram i umieszczamy na nim trzy komponenty: bramę, pamięć masową i harmonogram. Pracujemy dalej nad architekturą. Ponieważ używamy vshard jako pamięci masowej, dodajemy do schematu vshard-router i vshard-storage. Ani brama, ani program planujący nie będą miały bezpośredniego dostępu do pamięci; po to jest router i po to został stworzony.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Ten diagram nadal nie przedstawia dokładnie tego, co będziemy budować w projekcie, ponieważ komponenty wyglądają abstrakcyjnie. Nadal musimy zobaczyć, jak zostanie to rzutowane na prawdziwy Tarantool – pogrupujmy nasze komponenty według procesów.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Nie ma sensu trzymać routera vshard i bramy w oddzielnych instancjach. Dlaczego musimy jeszcze raz surfować po sieci, skoro leży to już w gestii routera? Muszą być uruchamiane w ramach tego samego procesu. Oznacza to, że zarówno brama, jak i plik vshard.router.cfg są inicjowane w jednym procesie i umożliwiają im lokalną interakcję.

Na etapie projektowania wygodnie było pracować z trzema komponentami, ale ja jako programista pisząc kod nie chcę myśleć o uruchomieniu trzech instancji Tarnatool. Muszę przeprowadzić testy i sprawdzić, czy poprawnie napisałem bramkę. A może chcę zademonstrować jakąś funkcję moim współpracownikom. Dlaczego miałbym męczyć się z wdrażaniem trzech kopii? Tak narodziła się koncepcja ról. Rolą jest zwykły moduł Luash, którego cyklem życia zarządza Cartridge. W tym przykładzie są ich cztery - brama, router, pamięć masowa i program planujący. Być może w innym projekcie będzie ich więcej. Wszystkie role można uruchomić w jednym procesie i to wystarczy.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

A jeśli chodzi o wdrożenie do testowania lub produkcji, każdemu procesowi Tarantool przypiszemy własny zestaw ról w zależności od możliwości sprzętowych:

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Zarządzanie topologią

Informacje o tym, gdzie działają role, muszą być gdzieś przechowywane. A tym „gdzieś” jest konfiguracja rozproszona, o której wspomniałem już powyżej. Najważniejszą rzeczą jest topologia klastra. Oto 3 grupy replikacji 5 procesów Tarantool:

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Nie chcemy stracić danych, dlatego z ostrożnością podchodzimy do informacji o przebiegających procesach. Cartridge śledzi konfigurację za pomocą zatwierdzania dwufazowego. Gdy chcemy zaktualizować konfigurację, najpierw sprawdza, czy wszystkie instancje są dostępne i gotowe do zaakceptowania nowej konfiguracji. Następnie druga faza stosuje konfigurację. Tym samym, nawet jeśli jeden egzemplarz okaże się chwilowo niedostępny, nic złego się nie stanie. Konfiguracja po prostu nie zostanie zastosowana i z góry pojawi się błąd.

Również w sekcji topologii wskazany jest tak ważny parametr, jak lider każdej grupy replikacji. Zwykle jest to kopia, która jest nagrywana. Pozostałe są najczęściej tylko do odczytu, chociaż mogą istnieć wyjątki. Czasami odważni programiści nie boją się konfliktów i mogą zapisywać dane do kilku replik równolegle, jednak są pewne operacje, których bez względu na wszystko nie należy wykonywać dwa razy. Do tego jest znak przywódcy.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Życie ról

Aby w takiej architekturze istniała abstrakcyjna rola, framework musi w jakiś sposób nią zarządzać. Naturalnie kontrola odbywa się bez ponownego uruchamiania procesu Tarantool. Istnieją 4 wywołania zwrotne do zarządzania rolami. Cartridge sam będzie je wywoływał w zależności od tego, co jest zapisane w jego rozproszonej konfiguracji, stosując w ten sposób konfigurację do określonych ról.

function init()
function validate_config()
function apply_config()
function stop()

Każda rola ma swoją funkcję init. Jest wywoływana raz, gdy rola jest włączona lub gdy Tarantool jest ponownie uruchamiany. Wygodnie jest tam na przykład zainicjować box.space.create, lub planista może uruchomić jakieś włókno w tle, które będzie wykonywać pracę w określonych odstępach czasu.

Jedna funkcja init może nie wystarczyć. Kaseta umożliwia rolom wykorzystanie rozproszonej konfiguracji używanej do przechowywania topologii. Możemy zadeklarować nową sekcję w tej samej konfiguracji i przechowywać w niej fragment konfiguracji biznesowej. W moim przykładzie może to być schemat danych lub ustawienia harmonogramu dla roli osoby planującej.

Połączenia klastrowe validate_config и apply_config za każdym razem, gdy zmienia się konfiguracja rozproszona. Gdy konfiguracja jest stosowana poprzez zatwierdzenie dwufazowe, klaster sprawdza, czy każda rola jest gotowa do zaakceptowania nowej konfiguracji i, jeśli to konieczne, zgłasza użytkownikowi błąd. Kiedy wszyscy zgodzą się, że konfiguracja jest normalna, wówczas plik apply_config.

Role również mają swoją metodę stop, który jest potrzebny do oczyszczenia danych wyjściowych roli. Jeśli powiemy, że program planujący nie jest już potrzebny na tym serwerze, może zatrzymać te włókna, od których zaczął init.

Role mogą ze sobą współdziałać. Jesteśmy przyzwyczajeni do pisania wywołań funkcji w Lua, jednak może się zdarzyć, że dany proces nie będzie pełnił takiej roli, jakiej potrzebujemy. Aby ułatwić prowadzenie rozmów przez sieć, wykorzystujemy moduł pomocniczy rpc (remote procedur call), który zbudowany jest w oparciu o standardowy netbox wbudowany w Tarantool. Może to być przydatne, jeśli na przykład brama chce bezpośrednio poprosić osobę planującą o wykonanie zadania już teraz, zamiast czekać jeden dzień.

Kolejną ważną kwestią jest zapewnienie odporności na awarie. Kaseta wykorzystuje protokół SWIM do monitorowania stanu zdrowia [4]. Krótko mówiąc, procesy wymieniają między sobą „plotki” za pośrednictwem protokołu UDP — każdy proces przekazuje swoim sąsiadom najnowsze wiadomości, a oni odpowiadają. Jeśli nagle odpowiedź nie nadejdzie, Tarantool zaczyna podejrzewać, że coś jest nie tak, a po chwili wyrecytuje śmierć i zaczyna rozpowiadać tę wiadomość wszystkim wokół.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

W oparciu o ten protokół Cartridge organizuje automatyczne przetwarzanie awarii. Każdy proces monitoruje swoje środowisko i jeśli lider nagle przestanie odpowiadać, replika może przejąć jego rolę, a Cartridge odpowiednio konfiguruje działające role.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Trzeba tu zachować ostrożność, ponieważ częste przełączanie w tę i z powrotem może prowadzić do konfliktów danych podczas replikacji. Oczywiście nie należy losowo włączać automatycznego przełączania awaryjnego. Musimy jasno zrozumieć, co się dzieje i mieć pewność, że replikacja nie zostanie zerwana po przywróceniu przywódcy i zwróceniu mu korony.

Z tego wszystkiego można odnieść wrażenie, że role są podobne do mikrousług. W pewnym sensie są po prostu modułami wewnątrz procesów Tarantool. Ale jest też kilka zasadniczych różnic. Po pierwsze, wszystkie role w projekcie muszą znajdować się w tej samej bazie kodu. A wszystkie procesy Tarantoola powinny być uruchamiane z tej samej bazy kodu, aby nie było takich niespodzianek, gdy próbujemy zainicjować harmonogram, ale on po prostu nie istnieje. Nie należy także dopuszczać różnic w wersjach kodu, gdyż zachowanie systemu w takiej sytuacji jest bardzo trudne do przewidzenia i zdebugowania.

W przeciwieństwie do Dockera nie możemy po prostu przyjąć „obrazu” roli, przenieść go na inną maszynę i tam uruchomić. Nasze role nie są tak odizolowane jak kontenery Docker. Ponadto nie możemy uruchomić dwóch identycznych ról w jednej instancji. Rola albo istnieje, albo jej nie ma; w pewnym sensie jest to singleton. I po trzecie, role muszą być takie same w ramach całej grupy replikacyjnej, bo inaczej byłby to absurd – dane są te same, ale konfiguracja inna.

Narzędzia do wdrażania

Obiecałem pokazać, jak Cartridge pomaga wdrażać aplikacje. Aby ułatwić życie innym, framework pakietuje pakiety RPM:

$ cartridge pack rpm myapp -- упакует для нас ./myapp-0.1.0-1.rpm
$ sudo yum install ./myapp-0.1.0-1.rpm

Zainstalowany pakiet zawiera prawie wszystko, czego potrzebujesz: zarówno aplikację, jak i zainstalowane zależności. Tarantool pojawi się również na serwerze jako zależność pakietu RPM i nasza usługa będzie gotowa do uruchomienia. Odbywa się to poprzez systemd, ale najpierw musisz napisać małą konfigurację. Podaj przynajmniej identyfikator URI każdego procesu. Trzy wystarczą na przykład.

$ sudo tee /etc/tarantool/conf.d/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIG

Jest tu ciekawy niuans. Zamiast podawać tylko binarny port protokołu, podajemy cały adres publiczny procesu, łącznie z nazwą hosta. Jest to konieczne, aby węzły klastra wiedziały, jak się ze sobą połączyć. Używanie 0.0.0.0 jako adresu reklamowego_uri jest złym pomysłem; powinien to być zewnętrzny adres IP, a nie powiązanie z gniazdem. Bez tego nic nie będzie działać, więc Cartridge po prostu nie pozwoli ci uruchomić węzła z niewłaściwym reklamą_uri.

Teraz, gdy konfiguracja jest już gotowa, możesz rozpocząć procesy. Ponieważ zwykła jednostka systemowa nie pozwala na uruchomienie więcej niż jednego procesu, aplikacje na Cartridge instalowane są przez tzw. utworzone jednostki, które działają w ten sposób:

$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_B

W konfiguracji określiliśmy port HTTP, na którym Cartridge obsługuje interfejs WWW - 8080. Przejdźmy do tego i spójrzmy:

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Widzimy, że chociaż procesy są uruchomione, nie są jeszcze skonfigurowane. Kaseta nie wie jeszcze, kto z kim ma replikować i nie może samodzielnie podjąć decyzji, więc czeka na nasze działania. Ale wielkiego wyboru nie mamy: życie nowego klastra zaczyna się od skonfigurowania pierwszego węzła. Następnie dodamy pozostałych do klastra, przypiszemy im role i na tym etapie wdrożenie można uznać za zakończone pomyślnie.

Nalejmy do szklanki Twojego ulubionego napoju i zrelaksujmy się po długim tygodniu pracy. Można korzystać z aplikacji.

Kaseta Tarantool: sharding backendu Lua w trzech liniach

Wyniki

Jakie są wyniki? Wypróbuj, użyj, zostaw opinię, utwórz bilety na Githubie.

referencje

[1] Tarantool » 2.2 » Informacje » Informacje o skałach » Moduł vshard

[2] Jak wdrożyliśmy rdzeń działalności inwestycyjnej Alfa-Banku w oparciu o Tarantool

[3] Architektura rozliczeniowa nowej generacji: transformacja wraz z przejściem na Tarantool

[4] SWIM - protokół budowy klastra

[5] GitHub — tarantool/kartridge-cli

[6] GitHub — tarantool/kartridż

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

Dodaj komentarz