
W dzisiejszych czasach wysoka dostępność usług jest wymagana wszędzie i zawsze, a nie tylko w dużych, drogich projektach. Tymczasowo niedostępne strony internetowe z komunikatem „Przepraszamy, trwają prace konserwacyjne” wciąż się zdarzają, ale zwykle budzą raczej pobłażliwy uśmiech. Dodajmy do tego życie w chmurach, gdzie uruchomienie dodatkowego serwera wymaga jedynie jednego wywołania API, nie martwiąc się przy tym o „fizyczną” eksploatację. Już nie ma usprawiedliwienia, dlaczego krytyczny system nie został zrealizowany w sposób niezawodny z wykorzystaniem technologii klastrowych i redundancji.
Opowiemy, jakie rozwiązania rozważaliśmy dla zapewnienia niezawodności baz danych w naszych usługach i do jakich wniosków doszliśmy. Dodatkowo zaprezentujemy demonstrację z dalekosiężnymi wnioskami.
Legacy w architekturze zapewnienia wysokiej dostępności
Jeszcze lepiej widać to w kontekście rozwoju różnych systemów opensource. Stare rozwiązania były zmuszone dodawać technologie wysokiej dostępności w miarę rosnącego popytu. Ich jakość była różna. Rozwiązania nowej generacji stawiają wysoką dostępność u podstaw swojej architektury. Na przykład MongoDB pozycjonuje klaster jako podstawowy wariant użycia. Klaster skalowalny jest poziomo, co stanowi silną przewagę konkurencyjną tego systemu baz danych.
Wracając do PostgreSQL. To jeden z najstarszych popularnych projektów opensource, którego pierwsza wersja ukazała się w 1995 roku. Zespół projektu przez długi czas nie uważał wysokiej dostępności za problem, który należy rozwiązać po stronie systemu. Dlatego technologia replikacji do tworzenia kopii danych stała się wbudowana dopiero w wersji 8.2 w 2006 roku, ale była to replikacja plików (log shipping). W 2010 roku w wersji 9.0 pojawiła się replikacja strumieniowa, która jest podstawą do tworzenia różnych klastrów. To, co szczególnie zaskakuje osoby, które zapoznają się z PostgreSQL po Enterprise SQL lub nowoczesnych NoSQL — standardowym rozwiązaniem w społeczności jest po prostu para master-replica z synchronizowaną lub asynchronizowaną replikacją. Przy tym w standardzie przełączenie mastera odbywa się ręcznie, a kwestia przełączania klientów jest również oferowana do samodzielnego rozwiązania.
Jak postanowiliśmy tworzyć niezawodny PostgreSQL i jakie rozwiązania wybraliśmy.
Jednak PostgreSQL nie stałby się tak popularny, gdyby nie istniała ogromna liczba projektów i narzędzi, które pomagają zbudować rozwiązanie odporne na awarie, które nie wymaga stałej uwagi. W chmurze (MCS) od samego początku DBaaS dostępne były pojedyncze serwery PostgreSQL oraz pary master-replica z asynchroniczną replikacją.
Naturalnie, chcieliśmy uprościć życie wszystkim i uczynić dostępną instalację PostgreSQL, która mogłaby służyć jako fundament dla wysoko dostępnych usług, nad którymi nie trzeba by było ciągle czuwać i budzić się w nocy, aby dokonać przełączenia. W tej dziedzinie istnieją zarówno stare, sprawdzone rozwiązania, jak i pokolenie nowych narzędzi, które wykorzystują najnowsze osiągnięcia.
Dziś problem wysokiej dostępności nie dotyczy już rezerwacji (to oczywiste), ale konsensusu — algorytmu wyboru lidera (Leader election). Najczęściej poważne awarie nie wynikają z braku serwerów, ale z problemów z konsensusem: nowy lider nie został wybrany, pojawiło się dwóch liderów w różnych centrach danych itd. Przykład — awaria w klastrze MySQL na Githubie — napisali .
Podstawa matematyczna w tej kwestii jest bardzo poważna. Z jednej strony istnieje , która nakłada teoretyczne ograniczenia na możliwości budowy rozwiązań HA, z drugiej strony — matematycznie udowodnione algorytmy określenia konsensusu, takie jak i . Na tej podstawie istnieją dość popularne DCS (systemy zdecentralizowanego konsensusu) — Zookeeper, etcd, Consul. Dlatego jeśli system podejmowania decyzji działa na jakimś własnym algorytmie, napisanym samodzielnie, należy podchodzić do niego z dużą ostrożnością. Po analizie ogromnej liczby systemów zatrzymaliśmy się na Patroni — systemie open source, głównie rozwijanym przez firmę Zalando.
Jako liryczne wtrącenie powiem, że rozważaliśmy również rozwiązania multi-master, czyli klastry, które można poziomo skalować pod kątem zapisu. Jednak z dwóch głównych powodów zdecydowaliśmy się nie tworzyć takiego klastra. Po pierwsze, takie rozwiązania są bardzo złożone, co zwiększa liczbę potencjalnych punktów awarii. Trudno będzie stworzyć stabilne rozwiązanie na wszystkie przypadki. Po drugie, w takim przypadku PostgreSQL przestaje być czysty (native), niektóre funkcje będą niedostępne, a niektóre aplikacje mogą napotKAć ukryte błędy podczas działania.
Patroni
Jak więc działa Patroni? Programiści nie postanowili wynajdywać na nowo koła i zaproponowali użycie jako podstawy jednego z sprawdzonych rozwiązań DCS. Wszystkie pytania dotyczące synchronizacji konfiguracji, wyboru lidera i kworum są mu powierzone. Wybraliśmy do tego etcd.
Następnie Patroni zajmuje się właściwym zastosowaniem wszystkich ustawień w PostgreSQL oraz konfiguracją replikacji, a także realizacją poleceń przełączenia na switchover i failover (czyli – przełączania głównego bazy przy normalnych i nietypowych warunkach). Konkretnie w chmurze MCS można stworzyć klaster składający się z bazy głównej, synchronizowanej repliki oraz jednej lub kilku asynchronicznych replik. Obecność synchronizowanej repliki zapewnia bezpieczeństwo danych co najmniej na dwóch serwerach, i to ta replika będzie głównym „kandydatem na głównego”.
Ponieważ etcd rozkłada się na tych samych serwerach, zaleca się liczbę serwerów 3 lub 5, dla optymalnej wartości kworum. Taki klaster można poziomo skalować pod kątem odczytu (o skalowaniu pod kątem zapisu pisałem wcześniej). Należy jednak pamiętać, że asynchroniczne replikacje mają skłonności do opóźnień, szczególnie przy wysokich obciążeniach.
Użycie takich replik do odczytu (hot standby) jest uzasadnione w przypadku zadań raportowania lub analizy i odciąża serwer główny.
Jeśli chcesz stworzyć taki klaster samodzielnie, będziesz potrzebował:
- przygotować 3 lub więcej serwerów, skonfigurować adresację IP oraz zasady zapory między nimi;
- zainstalować pakiety dla usług etcd, Patroni, PostgreSQL;
- skonfigurować klaster etcd;
- skonfigurować usługę patroni do pracy z PostgreSQL.
To put it simply, you need to correctly create about a dozen configuration files and not make any mistakes. It's definitely worth using a configuration management tool, such as Ansible, for this. However, there is still no highly available TCP load balancer. Creating one is a separate task.
For those who need a ready-made cluster but don't want to tinker with everything, we tried to simplify life and created a ready-to-use cluster on Patroni in our cloud, which you can test for free. In addition to the cluster itself, we made:
- A TCP load balancer; it always points to the current master, either synchronous or asynchronous replica, respectively, across different ports;
- An API for switching the active master in Patroni.
They can be connected both through the MCS cloud API and through the web console.
Demo
To test the capabilities of the PostgreSQL cluster in the MCS cloud, let's see how a live application behaves during database issues.
Below is the code for an application that will log artificial events and display them on the screen. In case of errors, it will report them and continue running in a loop until we stop it with the Ctrl + C combination.
from __future__ import print_function
from datetime import datetime
from random import randint
from time import sleep
import psycopg2
def main():
try:
connection = psycopg2.connect(user = "admin",
password = "P@ssw0rd",
host = "89.208.87.38",
port = "5432",
database = "myproddb")
cursor = connection.cursor()
cursor.execute("SELECT version();")
record = cursor.fetchone()
print("Connection opened to", record[0])
cursor.execute(
"INSERT INTO log VALUES ({});".format(randint(1, 10000)))
connection.commit()
cursor.execute("SELECT COUNT(event_id) from log;")
record = cursor.fetchone()
print("Logged a value, overall count: {}".format(record[0]))
except Exception as error:
print ("Error while connecting to PostgreSQL", error)
finally:
if connection:
cursor.close()
connection.close()
print("Connection closed")
if __name__ == '__main__':
try:
while True:
try:
print(datetime.now())
main()
sleep(3)
except Exception as e:
print("Caught error:n", e)
sleep(1)
except KeyboardInterrupt:
print("exit")
The application requires PostgreSQL to work. Let's create a cluster in the MCS cloud using the API. In a regular terminal, where the OS_TOKEN variable contains the token for accessing the API (which can be obtained with the command openstack token issue), we will enter the commands:
Creating a cluster:
cat < pgc10.json
{"cluster":{"name":"postgres10","allow_remote_access":true,"datastore":{"type":"postgresql","version":"10"},"databases":[{"name":"myproddb"}],"users":[{"databases":[{"name":"myproddb"}],"name":"admin","password":"P@ssw0rd"}],"instances":[{"key_name":"shared","availability_zone":"DP1","flavorRef":"d659fa16-c7fb-42cf-8a5e-9bcbe80a7538","nics":[{"net-id":"b91eafed-12b1-4a46-b000-3984c7e01599"}],"volume":{"size":50,"type":"DP1"}},{"key_name":"shared","availability_zone":"DP1","flavorRef":"d659fa16-c7fb-42cf-8a5e-9bcbe80a7538","nics":[{"net-id":"b91eafed-12b1-4a46-b000-3984c7e01599"}],"volume":{"size":50,"type":"DP1"}},{"key_name":"shared","availability_zone":"DP1","flavorRef":"d659fa16-c7fb-42cf-8a5e-9bcbe80a7538","nics":[{"net-id":"b91eafed-12b1-4a46-b000-3984c7e01599"}],"volume":{"size":50,"type":"DP1"}}]}}
EOF
curl -s -H "X-Auth-Token: $OS_TOKEN"
-H 'Accept: application/json'
-H 'Content-Type: application/json'
-d @pgc10.json https://infra.mail.ru:8779/v1.0/ce2a41bbd1434013b85bdf0ba07c770f/clusters

Gdy klaster przejdzie w status ACTIVE, wszystkie pola zaktualizują wartości — klaster jest gotowy.
W GUI:

Spróbujmy się połączyć i stworzyć tabelę:
psql -h 89.208.87.38 -U admin -d myproddb
Hasło dla użytkownika admin:
psql (11.1, serwer 10.7)
Wpisz "help", aby uzyskać pomoc.
myproddb=> CREATE TABLE log (event_id integer NOT NULL);
CREATE TABLE
myproddb=> INSERT INTO log VALUES (1),(2),(3);
INSERT 0 3
myproddb=> SELECT * FROM log;
event_id
----------
1
2
3
(3 wiersze)
myproddb=>

W aplikacji podamy aktualne ustawienia do połączenia z PostgreSQL. Podamy adres TCP-load balancera, co sprawi, że nie będzie potrzeby ręcznego przełączania na adres głównego serwera. Uruchamiamy go. Jak widać, zdarzenia są pomyślnie logowane w bazie danych.

Planowane przełączanie głównego serwera
Teraz przetestujemy działanie naszej aplikacji podczas planowanego przełączania głównego serwera:

Obserwujemy aplikację. Widzimy, że praca aplikacji rzeczywiście jest przerywana, ale trwa to tylko kilka sekund, w tym konkretnym przypadku maksymalnie 9.

Awaria maszyny
Teraz spróbujemy zasymulować awarię maszyny wirtualnej, aktualnego głównego serwera. Można by po prostu wyłączyć maszynę wirtualną z interfejsu Horizon, ale to byłoby standardowe wyłączenie. Takie przełączenie zostanie obsłużone przez wszystkie usługi, w tym Patroni.
Potrzebujemy jednak nieprzewidywalnego wyłączenia. Dlatego poprosiłem naszych administratorów, aby w celach testowych wyłączyli maszynę wirtualną — aktualnego głównego serwera — w sposób awaryjny.

W tym samym czasie nasza aplikacja kontynuowała działanie. Oczywiście, takie nagłe przełączenie głównego serwera nie mogło przejść niezauważone.
2019-03-29 10:45:56.071234
Połączenie otwarte do PostgreSQL 10.7 na x86_64-pc-linux-gnu, skompilowane przez gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36), 64-bit
Zapisano wartość, całkowita liczba: 453
Połączenie zamknięte
2019-03-29 10:45:59.205463
Połączenie otwarte do PostgreSQL 10.7 na x86_64-pc-linux-gnu, skompilowane przez gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36), 64-bit
Zapisano wartość, całkowita liczba: 454
Połączenie zamknięte
2019-03-29 10:46:02.661440
Błąd podczas łączenia z serwerem PostgreSQL, połączenie zostało niespodziewanie zamknięte
Prawdopodobnie oznacza to, że serwer zakończył pracę w sposób nieprawidłowy
przed lub podczas przetwarzania żądania.
Wykryty błąd:
lokalna zmienna 'connection' odniesiona przed przypisaniem
……………………………………………………….. - tutaj jest jakieś ilości błędów
2019-03-29 10:46:30.930445
Błąd podczas łączenia z serwerem PostgreSQL, połączenie zostało niespodziewanie zamknięte
Prawdopodobnie oznacza to, że serwer zakończył pracę w sposób nieprawidłowy
przed lub podczas przetwarzania żądania.
Wykryty błąd:
lokalna zmienna 'connection' odniesiona przed przypisaniem
2019-03-29 10:46:31.954399
Połączenie otwarte do PostgreSQL 10.7 na x86_64-pc-linux-gnu, skompilowane przez gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36), 64-bit
Zapisano wartość, całkowita liczba: 455
Połączenie zamknięte
2019-03-29 10:46:35.409800
Połączenie otwarte do PostgreSQL 10.7 na x86_64-pc-linux-gnu, skompilowane przez gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36), 64-bit
Zapisano wartość, całkowita liczba: 456
Połączenie zamknięte
^Cexit
Jak widać, aplikacja była w stanie kontynuować działanie w mniej niż 30 sekund. Tak, pewna liczba użytkowników usługi może zauważyć problemy. Jednak to poważna awaria serwera, co zdarza się rzadko. Przy tym administrator raczej nie zdążyłby zareagować tak szybko, chyba że czekałby w konsoli z gotowym skryptem przełączania.
Wnioski
Moim zdaniem taki klaster daje ogromne przewagi dla administratorów. W zasadzie poważne awarie i awarie serwerów baz danych nie będą zauważalne dla aplikacji i, w konsekwencji, dla użytkowników. Nie będzie potrzeby naprawiania czegoś w pośpiechu i przełączania się na tymczasowe konfiguracje, serwery itp. A jeśli takie rozwiązanie zastosować w formie gotowej usługi w chmurze, nie będzie potrzeby marnowania czasu na jego przygotowanie. Będzie można zająć się czymś ciekawszym.
Źródło: habr.com
