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
Dobra wiadomość: zebraliśmy kilka dużych ujęć (np
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.
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.
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.
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.
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:
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:
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.
Ż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
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.
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:
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.
Wyniki
Jakie są wyniki? Wypróbuj, użyj, zostaw opinię, utwórz bilety na Githubie.
referencje
[1]
Źródło: www.habr.com