Więcej programistów powinno wiedzieć to o bazach danych

Notatka. przeł.: Jaana Dogan to doświadczona inżynierka w Google, która obecnie pracuje nad obserwowalnością usług produkcyjnych firmy napisanych w Go. W tym artykule, który zyskał dużą popularność wśród anglojęzycznej publiczności, zebrała w 17 punktach ważne szczegóły techniczne dotyczące systemów DBMS (i czasami ogólnie systemów rozproszonych), które warto wziąć pod uwagę dla twórców dużych/wymagających aplikacji.

Więcej programistów powinno wiedzieć to o bazach danych

Zdecydowana większość systemów komputerowych śledzi ich stan i dlatego wymaga pewnego rodzaju systemu przechowywania danych. Wiedzę o bazach danych gromadziłem przez długi czas, popełniając po drodze błędy projektowe, które prowadziły do ​​utraty danych i przestojów. W systemach przetwarzających duże ilości informacji bazy danych stanowią serce architektury systemu i pełnią rolę kluczowego elementu przy wyborze optymalnego rozwiązania. Pomimo tego, że dużą wagę przywiązuje się do pracy bazy danych, problemy, które starają się przewidzieć twórcy aplikacji, to często tylko wierzchołek góry lodowej. W tej serii artykułów dzielę się kilkoma pomysłami, które przydadzą się programistom, którzy nie specjalizują się w tej dziedzinie.

  1. Masz szczęście, jeśli w 99,999% przypadków sieć nie powoduje problemów.
  2. ACID oznacza wiele różnych rzeczy.
  3. Każda baza danych ma własne mechanizmy zapewniające spójność i izolację.
  4. Blokowanie optymistyczne przychodzi na ratunek, gdy trudno jest utrzymać zwykłe blokowanie.
  5. Oprócz nieprawidłowych odczytów i utraty danych występują inne anomalie.
  6. Baza danych i użytkownik nie zawsze zgadzają się co do sposobu działania.
  7. Fragmentowanie na poziomie aplikacji można przenieść poza aplikację.
  8. Autoinkrementacja może być niebezpieczna.
  9. Nieaktualne dane mogą być przydatne i nie trzeba ich blokować.
  10. Zniekształcenia są typowe dla wszelkich źródeł czasu.
  11. Opóźnienie ma wiele znaczeń.
  12. Wymagania dotyczące wydajności należy oceniać dla konkretnej transakcji.
  13. Zagnieżdżone transakcje mogą być niebezpieczne.
  14. Transakcje nie powinny być powiązane ze stanem aplikacji.
  15. Planiści zapytań mogą wiele powiedzieć o bazach danych.
  16. Migracja online jest trudna, ale możliwa.
  17. Znaczący wzrost bazy danych pociąga za sobą wzrost nieprzewidywalności.

Chciałbym podziękować Emmanuelowi Odeke, Reinowi Henrichsowi i innym osobom za opinie na temat wcześniejszej wersji tego artykułu.

Masz szczęście, jeśli w 99,999% przypadków sieć nie powoduje problemów.

Pozostaje pytanie, jak niezawodne są nowoczesne technologie sieciowe i jak często systemy ulegają awariom z powodu awarii sieci. Informacje na ten temat są skąpe, a badania są często zdominowane przez duże organizacje posiadające wyspecjalizowane sieci, sprzęt i personel.

Przy wskaźniku dostępności wynoszącym 99,999% dla Spanner (globalnie dystrybuowanej bazy danych Google) Google twierdzi, że tylko 7,6% problemy są związane z siecią. Jednocześnie firma nazywa swoją wyspecjalizowaną sieć „głównym filarem” wysokiej dostępności. Badanie Bailisa i Kingsburyprzeprowadzona w 2014 r., kwestionuje jedno z „błędne przekonania na temat przetwarzania rozproszonego”, którą Peter Deutsch sformułował w 1994 r. Czy sieć jest naprawdę niezawodna?

Kompleksowe badania poza gigantami, prowadzone dla szerszego Internetu, po prostu nie istnieją. Nie ma również wystarczających danych od największych graczy na temat tego, jaki procent problemów ich klientów ma charakter sieciowy. Doskonale zdajemy sobie sprawę z przerw w stosie sieciowym dużych dostawców usług w chmurze, które mogą uniemożliwić cały fragment Internetu na kilka godzin po prostu dlatego, że są to głośne wydarzenia, które wpływają na dużą liczbę osób i firm. Awarie sieci mogą powodować problemy w znacznie większej liczbie przypadków, nawet jeśli nie wszystkie te przypadki są w centrum uwagi. Klienci usług chmurowych również nie wiedzą nic o przyczynach problemów. Jeśli wystąpi awaria, prawie niemożliwe jest przypisanie jej błędowi sieci po stronie usługodawcy. Dla nich usługi stron trzecich to czarne skrzynki. Nie da się ocenić wpływu, nie będąc dużym usługodawcą.

Biorąc pod uwagę raporty największych graczy na temat swoich systemów, można śmiało powiedzieć, że masz szczęście, jeśli problemy z siecią stanowią jedynie niewielki procent potencjalnych problemów z przestojami. Komunikacja sieciowa nadal boryka się z tak przyziemnymi problemami, jak awarie sprzętu, zmiany topologii, zmiany konfiguracji administracyjnej i przerwy w dostawie prądu. Niedawno ze zdziwieniem dowiedziałem się, że dodano listę możliwych problemów ukąszenia rekinów (tak, dobrze słyszałeś).

ACID oznacza wiele różnych rzeczy

Akronim ACID oznacza atomowość, spójność, izolację, niezawodność. Te właściwości transakcji mają na celu zapewnienie ich ważności w przypadku awarii, błędów, awarii sprzętu itp. Bez ACID lub podobnych schematów twórcom aplikacji byłoby trudno rozróżnić, za co są odpowiedzialni, a za co odpowiedzialna jest baza danych. Większość relacyjnych transakcyjnych baz danych stara się być zgodna z ACID, ale nowe podejścia, takie jak NoSQL, doprowadziły do ​​powstania wielu baz danych bez transakcji ACID, ponieważ są one drogie we wdrażaniu.

Kiedy po raz pierwszy wszedłem do branży, nasz kierownik techniczny mówił o tym, jak istotna jest koncepcja ACID. Aby być uczciwym, ACID jest uważany za przybliżony opis, a nie za ścisły standard wdrożenia. Dziś uważam, że jest on przede wszystkim przydatny, ponieważ porusza konkretną kategorię problemów (i sugeruje szereg możliwych rozwiązań).

Nie każdy system DBMS jest zgodny z ACID; Jednocześnie implementacje baz danych obsługujące ACID inaczej rozumieją zestaw wymagań. Jednym z powodów, dla których implementacje ACID są niejednolite, jest wiele kompromisów, które należy poczynić, aby wdrożyć wymagania ACID. Twórcy mogą przedstawiać swoje bazy danych jako zgodne z ACID, ale interpretacja przypadków brzegowych może się znacznie różnić, podobnie jak mechanizm obsługi „mało prawdopodobnych” zdarzeń. Programiści mogą przynajmniej uzyskać wysoki poziom zrozumienia zawiłości podstawowych implementacji, aby właściwie zrozumieć ich specjalne zachowanie i kompromisy projektowe.

Debata na temat tego, czy MongoDB spełnia wymagania ACID, trwa nawet po wydaniu wersji 4. MongoDB nie jest wspierany przez długi czas Logowanie, choć domyślnie dane były zapisywane na dysku nie częściej niż raz na 60 sekund. Wyobraź sobie następujący scenariusz: aplikacja wysyła dwa zapisy (w1 i w2). MongoDB pomyślnie przechowuje w1, ale w2 zostaje utracone z powodu awarii sprzętu.

Więcej programistów powinno wiedzieć to o bazach danych
Schemat ilustrujący scenariusz. MongoDB ulega awarii, zanim będzie mogła zapisać dane na dysku

Zapisywanie na dysku jest kosztownym procesem. Unikając częstych zatwierdzeń, programiści poprawiają wydajność nagrywania kosztem niezawodności. MongoDB obecnie obsługuje rejestrowanie, ale nieprawidłowe zapisy mogą nadal mieć wpływ na integralność danych, ponieważ dzienniki są domyślnie przechwytywane co 100 ms. Oznacza to, że podobny scenariusz jest nadal możliwy w przypadku logów i prezentowanych w nich zmian, choć ryzyko jest znacznie mniejsze.

Każda baza danych ma własne mechanizmy spójności i izolacji

Spośród wymagań ACID, spójność i izolacja mogą pochwalić się największą liczbą różnych implementacji, ponieważ zakres kompromisów jest szerszy. Trzeba powiedzieć, że spójność i izolacja to dość drogie funkcje. Wymagają koordynacji i zwiększają konkurencję o spójność danych. Złożoność problemu znacznie wzrasta, gdy konieczne jest poziome skalowanie bazy danych w wielu centrach danych (szczególnie jeśli są one zlokalizowane w różnych regionach geograficznych). Osiągnięcie wysokiego poziomu spójności jest bardzo trudne, ponieważ zmniejsza także dostępność i zwiększa segmentację sieci. Aby uzyskać bardziej ogólne wyjaśnienie tego zjawiska, radzę zapoznać się z Twierdzenie CAP-a. Warto również zauważyć, że aplikacje radzą sobie z niewielkimi ilościami niespójności, a programiści potrafią zrozumieć niuanse problemu na tyle dobrze, że mogą zaimplementować w aplikacji dodatkową logikę w celu obsługi niespójności bez nadmiernego polegania na bazie danych, która sobie z tym poradzi.

DBMS często zapewniają różne poziomy izolacji. Twórcy aplikacji mogą wybrać najbardziej efektywną na podstawie swoich preferencji. Niska izolacja pozwala na zwiększenie prędkości, ale także zwiększa ryzyko wyścigu danych. Wysoka izolacja zmniejsza to prawdopodobieństwo, ale spowalnia pracę i może prowadzić do konkurencji, która doprowadzi do takich hamulców w podstawie, że zaczną się awarie.

Więcej programistów powinno wiedzieć to o bazach danych
Przegląd istniejących modeli współbieżności i relacji między nimi

Standard SQL definiuje tylko cztery poziomy izolacji, chociaż w teorii i praktyce jest ich znacznie więcej. Jepson.io oferuje doskonały przegląd istniejących modeli współbieżności. Na przykład Google Spanner gwarantuje możliwość serializacji zewnętrznej z synchronizacją zegara i chociaż jest to bardziej rygorystyczna warstwa izolacji, nie jest ona zdefiniowana w standardowych warstwach izolacji.

Standard SQL wymienia następujące poziomy izolacji:

  • Serializable (najbardziej rygorystyczne i kosztowne): Wykonanie z możliwością serializacji ma taki sam efekt, jak wykonanie niektórych transakcji sekwencyjnych. Realizacja sekwencyjna oznacza, że ​​każda kolejna transakcja rozpoczyna się dopiero po zakończeniu poprzedniej. Warto zaznaczyć, że poziom Serializable często wdrażane jako tak zwana izolacja migawkowa (na przykład w Oracle) ze względu na różnice w interpretacji, chociaż sama izolacja migawkowa nie jest reprezentowana w standardzie SQL.
  • Powtarzalne odczyty: Niezatwierdzone rekordy w bieżącej transakcji są dostępne dla bieżącej transakcji, ale zmiany wprowadzone przez inne transakcje (takie jak nowe wiersze) niewidoczny.
  • Przeczytaj zaangażowany: Niezatwierdzone dane nie są dostępne dla transakcji. W takim przypadku transakcje mogą widzieć tylko zatwierdzone dane i mogą wystąpić odczyty fantomowe. Jeśli transakcja wstawi i zatwierdzi nowe wiersze, bieżąca transakcja będzie mogła je zobaczyć po zapytaniu.
  • Przeczytaj niezatwierdzone (najmniej rygorystyczny i kosztowny poziom): Dozwolone są brudne odczyty, transakcje mogą zobaczyć niezatwierdzone zmiany wprowadzone przez inne transakcje. W praktyce poziom ten może być przydatny w przypadku przybliżonych szacunków, takich jak zapytania COUNT(*) na stole.

Poziom Serializable minimalizuje ryzyko wyścigów danych, a jednocześnie jest najdroższy we wdrożeniu i powoduje największe obciążenie konkurencyjne systemu. Inne poziomy izolacji są łatwiejsze do wdrożenia, ale zwiększają prawdopodobieństwo wyścigów danych. Niektóre systemy DBMS pozwalają ustawić niestandardowy poziom izolacji, inne mają silne preferencje i nie wszystkie poziomy są obsługiwane.

Obsługa poziomów izolacji jest często reklamowana w danym systemie DBMS, ale tylko dokładne przestudiowanie jego zachowania może ujawnić, co się faktycznie dzieje.

Więcej programistów powinno wiedzieć to o bazach danych
Przegląd anomalii współbieżności na różnych poziomach izolacji dla różnych systemów DBMS

Martin Kleppmann w swoim projekcie pustelnia Porównuje różne poziomy izolacji, mówi o anomaliach współbieżności i czy baza danych jest w stanie zastosować się do określonego poziomu izolacji. Badania Kleppmanna pokazują, jak różnie twórcy baz danych myślą o poziomach izolacji.

Blokowanie optymistyczne przychodzi na ratunek, gdy trudno jest utrzymać zwykłe blokowanie.

Blokowanie może być bardzo kosztowne, nie tylko dlatego, że zwiększa konkurencję w bazie danych, ale także dlatego, że wymaga ciągłego łączenia się serwerów aplikacji z bazą danych. Segmentacja sieci może zaostrzyć sytuacje blokowania na wyłączność i prowadzić do zakleszczeń trudnych do zidentyfikowania i rozwiązania. W przypadkach, gdy blokowanie wyłączne nie jest odpowiednie, pomaga blokowanie optymistyczne.

Optymistyczny zamek to metoda, w której podczas odczytu ciągu bierze pod uwagę jego wersję, sumę kontrolną lub czas ostatniej modyfikacji. Dzięki temu możesz mieć pewność, że przed zmianą wpisu nie nastąpi żadna niepodzielna zmiana wersji:

UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1

W tym przypadku aktualizacja tabeli products nie zostanie przeprowadzona, jeśli inna operacja dokonała wcześniej zmian w tym wierszu. Jeśli na tym wierszu nie wykonano żadnych innych operacji, nastąpi zmiana dla jednego wiersza i można powiedzieć, że aktualizacja przebiegła pomyślnie.

Oprócz nieprawidłowych odczytów i utraty danych występują inne anomalie

Jeśli chodzi o spójność danych, nacisk kładzie się na możliwość wystąpienia warunków wyścigowych, które mogą prowadzić do nieprawidłowych odczytów i utraty danych. Na tym jednak nie kończą się anomalie w danych.

Jednym z przykładów takich anomalii są zniekształcenia zapisu (pisz przekrzywienia). Zniekształcenia są trudne do wykrycia, ponieważ zwykle nie są aktywnie poszukiwane. Nie wynikają one z nieprawidłowych odczytów lub utraty danych, ale z naruszenia ograniczeń logicznych nałożonych na dane.

Rozważmy na przykład aplikację monitorującą, która wymaga, aby jeden operator był cały czas pod telefonem:

BEGIN tx1;                      BEGIN tx2;
SELECT COUNT(*)
FROM operators
WHERE oncall = true;
0                               SELECT COUNT(*)
                                FROM operators
                                WHERE oncall = TRUE;
                                0
UPDATE operators                UPDATE operators
SET oncall = TRUE               SET oncall = TRUE
WHERE userId = 4;               WHERE userId = 2;
COMMIT tx1;                     COMMIT tx2;

W powyższej sytuacji, w przypadku pomyślnego zatwierdzenia obu transakcji, nastąpi uszkodzenie rekordu. Chociaż nie doszło do błędnych odczytów ani utraty danych, integralność danych została naruszona: obecnie dwie osoby są uważane za dyżurujące w tym samym czasie.

Izolacja z możliwością serializacji, projekt schematu lub ograniczenia bazy danych mogą pomóc w wyeliminowaniu uszkodzeń zapisu. Programiści muszą być w stanie zidentyfikować takie anomalie podczas programowania, aby uniknąć ich w środowisku produkcyjnym. Jednocześnie zniekształceń rejestracyjnych jest niezwykle trudno szukać w bazie kodu. Zwłaszcza w dużych systemach, gdzie za realizację funkcji opartych na tych samych tabelach odpowiadają różne zespoły programistyczne i nie zgadzają się co do specyfiki dostępu do danych.

Baza danych i użytkownik nie zawsze są zgodni co do tego, co zrobić

Jedną z kluczowych cech baz danych jest gwarancja kolejności wykonania, jednak sama kolejność może nie być przejrzysta dla twórcy oprogramowania. Bazy danych wykonują transakcje w kolejności ich otrzymania, a nie w kolejności zamierzonej przez programistów. Kolejność transakcji jest trudna do przewidzenia, szczególnie w mocno obciążonych systemach równoległych.

Podczas programowania, szczególnie podczas pracy z bibliotekami nieblokującymi, kiepski styl i niska czytelność mogą powodować, że użytkownicy będą wierzyć, że transakcje są wykonywane sekwencyjnie, podczas gdy w rzeczywistości mogą pojawiać się w bazie danych w dowolnej kolejności.

Na pierwszy rzut oka w poniższym programie T1 i T2 wywoływane są sekwencyjnie, ale jeśli te funkcje nie blokują się i od razu zwracają wynik w postaci obietnica, wówczas o kolejności wywołań decydować będą momenty ich wprowadzenia do bazy:

wynik1 = T1() // rzeczywiste wyniki są obietnicami
wynik2 = T2()

Jeśli wymagana jest atomowość (tzn. wszystkie operacje muszą zostać zakończone lub przerwane) i liczy się kolejność, wówczas operacje T1 i T2 muszą zostać wykonane w ramach pojedynczej transakcji.

Fragmentowanie na poziomie aplikacji można przenieść poza aplikację

Sharding to metoda poziomego partycjonowania bazy danych. Niektóre bazy danych potrafią automatycznie dzielić dane w poziomie, inne nie potrafią lub nie są w tym zbyt dobre. Kiedy architekci/programiści danych będą w stanie dokładnie przewidzieć sposób dostępu do danych, mogą utworzyć poziome partycje w przestrzeni użytkownika, zamiast delegować tę pracę do bazy danych. Proces ten nazywany jest „dzieleniem na poziomie aplikacji” (fragmentowanie na poziomie aplikacji).

Niestety, nazwa ta często powoduje błędne przekonanie, że sharding żyje w usługach aplikacji. W rzeczywistości można go zaimplementować jako oddzielną warstwę przed bazą danych. W zależności od ilości danych i iteracji schematu wymagania dotyczące fragmentowania mogą stać się dość złożone. Niektóre strategie mogą skorzystać na możliwości iteracji bez konieczności ponownego wdrażania serwerów aplikacji.

Więcej programistów powinno wiedzieć to o bazach danych
Przykład architektury, w której serwery aplikacji są oddzielone od usługi shardingu

Przeniesienie fragmentowania do osobnej usługi zwiększa możliwość korzystania z różnych strategii fragmentowania bez konieczności ponownego wdrażania aplikacji. Wites jest przykładem takiego systemu shardingu na poziomie aplikacji. Vitess zapewnia poziome sharding dla MySQL i umożliwia klientom łączenie się z nim za pośrednictwem protokołu MySQL. System dzieli dane na różne węzły MySQL, które nic o sobie nie wiedzą.

Autoinkrementacja może być niebezpieczna

AUTOINCREMENT to powszechny sposób generowania kluczy podstawowych. Często zdarza się, że bazy danych są wykorzystywane jako generatory identyfikatorów, a baza danych zawiera tabele przeznaczone do generowania identyfikatorów. Istnieje kilka powodów, dla których generowanie kluczy podstawowych przy użyciu funkcji automatycznego zwiększania jest dalekie od ideału:

  • W rozproszonej bazie danych autoinkrementacja stanowi poważny problem. Do wygenerowania identyfikatora wymagana jest blokada globalna. Zamiast tego możesz wygenerować identyfikator UUID: nie wymaga to interakcji między różnymi węzłami bazy danych. Automatyczna inkrementacja z blokadami może prowadzić do rywalizacji i znacznie zmniejszyć wydajność wstawek w sytuacjach rozproszonych. Niektóre systemy DBMS (na przykład MySQL) mogą wymagać specjalnej konfiguracji i większej uwagi, aby prawidłowo zorganizować replikację master-master. Łatwo też popełnić błędy podczas konfiguracji, co doprowadzi do niepowodzeń nagrywania.
  • Niektóre bazy danych mają algorytmy partycjonowania oparte na kluczach podstawowych. Kolejne identyfikatory mogą prowadzić do nieprzewidywalnych gorących punktów i zwiększonego obciążenia niektórych partycji, podczas gdy inne pozostają bezczynne.
  • Klucz podstawowy to najszybszy sposób uzyskania dostępu do wiersza w bazie danych. Dzięki lepszym sposobom identyfikacji rekordów identyfikatory sekwencyjne mogą zamienić najważniejszą kolumnę w tabelach w bezużyteczną kolumnę wypełnioną bezsensownymi wartościami. Dlatego też, jeśli to możliwe, proszę wybrać globalnie unikalny i naturalny klucz podstawowy (np. nazwę użytkownika).

Przed podjęciem decyzji o wyborze podejścia należy rozważyć wpływ identyfikatorów automatycznego zwiększania i identyfikatorów UUID na indeksowanie, partycjonowanie i fragmentowanie.

Nieaktualne dane mogą być przydatne i nie wymagają blokowania

Kontrola współbieżności wielu wersji (MVCC) implementuje wiele wymagań dotyczących spójności, które zostały pokrótce omówione powyżej. Niektóre bazy danych (na przykład Postgres, Spanner) używają MVCC do „zasilania” transakcji migawkami – starszymi wersjami bazy danych. Transakcje migawkowe można również serializować, aby zapewnić spójność. Podczas odczytu ze starej migawki odczytywane są nieaktualne dane.

Odczytywanie nieco nieaktualnych danych może być przydatne na przykład podczas generowania analiz na podstawie danych lub obliczania przybliżonych wartości zagregowanych.

Pierwszą zaletą pracy ze starszymi danymi jest małe opóźnienie (szczególnie jeśli baza danych jest rozproszona w różnych lokalizacjach geograficznych). Po drugie, transakcje tylko do odczytu są wolne od blokad. Jest to znacząca zaleta w przypadku aplikacji, które dużo czytają, o ile są w stanie obsłużyć nieaktualne dane.

Więcej programistów powinno wiedzieć to o bazach danych
Serwer aplikacji odczytuje dane z lokalnej repliki, które są nieaktualne o 5 sekund, nawet jeśli najnowsza wersja jest dostępna po drugiej stronie Pacyfiku

Systemy DBMS automatycznie usuwają starsze wersje i, w niektórych przypadkach, umożliwiają to na żądanie. Na przykład Postgres pozwala użytkownikom to zrobić VACUUM na żądanie, a także okresowo wykonuje tę operację automatycznie. Spanner uruchamia moduł zbierający elementy bezużyteczne, aby pozbyć się migawek starszych niż godzinę.

Wszelkie źródła czasu podlegają zniekształceniom

Najlepiej strzeżoną tajemnicą informatyki jest to, że wszystkie interfejsy API pomiaru czasu kłamią. Tak naprawdę nasze maszyny nie znają dokładnej, aktualnej godziny. Komputery zawierają kryształy kwarcu, które generują wibracje, które służą do odmierzania czasu. Jednakże nie są one wystarczająco dokładne i mogą wyprzedzać/opóźniać dokładny czas. Przesunięcie może osiągnąć 20 sekund dziennie. Dlatego czas na naszych komputerach musi być okresowo synchronizowany z czasem sieciowym.

Do synchronizacji używane są serwery NTP, ale sam proces synchronizacji jest narażony na opóźnienia w sieci. Nawet synchronizacja z serwerem NTP w tym samym centrum danych zajmuje trochę czasu. Oczywiste jest, że praca z publicznym serwerem NTP może prowadzić do jeszcze większych zakłóceń.

Zegary atomowe i ich odpowiedniki GPS lepiej nadają się do określania aktualnej godziny, ale są drogie i wymagają skomplikowanej konfiguracji, dlatego nie można ich zainstalować w każdym samochodzie. Z tego powodu centra danych stosują podejście warstwowe. Zegary atomowe i/lub GPS pokazują dokładny czas, po którym jest on transmitowany do innych maszyn za pośrednictwem serwerów pomocniczych. Oznacza to, że każda maszyna doświadczy pewnego przesunięcia dokładnego czasu.

Sytuację pogarsza fakt, że aplikacje i bazy danych często znajdują się na różnych maszynach (jeśli nie w różnych centrach danych). Zatem czas będzie się różnić nie tylko w węzłach DB rozproszonych na różnych maszynach. Inaczej będzie także na serwerze aplikacji.

Google TrueTime przyjmuje zupełnie inne podejście. Większość ludzi uważa, że ​​postęp Google'a w tym kierunku tłumaczy się banalnym przejściem na zegary atomowe i GPS, ale to tylko część szerszego obrazu. Oto jak działa TrueTime:

  • TrueTime korzysta z dwóch różnych źródeł: GPS i zegarów atomowych. Zegary te mają nieskorelowane tryby awarii. [szczegóły na stronie 5 tutaj - około. tłumacz.), więc ich wspólne zastosowanie zwiększa niezawodność.
  • TrueTime ma niezwykłe API. Zwraca czas jako przedział z wbudowanym błędem pomiaru i niepewnością. Rzeczywisty moment w czasie znajduje się gdzieś pomiędzy górną i dolną granicą przedziału. Spanner, rozproszona baza danych Google, po prostu czeka, aż będzie można bezpiecznie stwierdzić, że bieżący czas jest poza zakresem. Ta metoda wprowadza do systemu pewne opóźnienia, szczególnie jeśli niepewność na urządzeniach głównych jest wysoka, ale zapewnia poprawność nawet w sytuacji rozproszonej globalnie.

Więcej programistów powinno wiedzieć to o bazach danych
Komponenty Spanner używają TrueTime, gdzie TT.now() zwraca interwał, więc Spanner po prostu śpi do momentu, w którym będzie miał pewność, że bieżący czas minął pewien punkt

Zmniejszona dokładność określania aktualnego czasu oznacza wydłużenie czasu trwania operacji Spanner i spadek wydajności. Dlatego ważne jest zachowanie jak największej dokładności, nawet jeśli uzyskanie zegarka w pełni dokładnego nie jest możliwe.

Opóźnienie ma wiele znaczeń

Jeśli zapytasz kilkunastu ekspertów, czym jest opóźnienie, prawdopodobnie uzyskasz różne odpowiedzi. W systemie DBMS opóźnienie jest często nazywane „opóźnieniem bazy danych” i różni się od tego, co postrzega klient. Faktem jest, że klient obserwuje sumę opóźnienia sieci i opóźnienia bazy danych. Możliwość wyodrębnienia rodzaju opóźnienia ma kluczowe znaczenie podczas debugowania narastających problemów. Zbierając i wyświetlając metryki, zawsze staraj się mieć oko na oba typy.

Wymagania dotyczące wydajności należy oceniać dla konkretnej transakcji

Czasami charakterystyka wydajności systemu DBMS i jego ograniczenia są określone w kategoriach przepustowości zapisu/odczytu i opóźnienia. Zapewnia to ogólny przegląd kluczowych parametrów systemu, ale przy ocenie wydajności nowego systemu DBMS znacznie bardziej kompleksowym podejściem jest osobna ocena krytycznych operacji (dla każdego zapytania i/lub transakcji). Przykłady:

  • Zapisz przepustowość i opóźnienie podczas wstawiania nowego wiersza do tabeli X (z 50 milionami wierszy) z określonymi ograniczeniami i uzupełnieniem wierszy w powiązanych tabelach.
  • Opóźnienie w wyświetlaniu znajomych znajomych danego użytkownika, gdy średnia liczba znajomych wynosi 500.
  • Opóźnienie w pobieraniu 100 najważniejszych wpisów z historii użytkownika, gdy użytkownik obserwuje 500 innych użytkowników z X wpisami na godzinę.

Ocena i eksperymenty mogą obejmować takie krytyczne przypadki, dopóki nie będziesz mieć pewności, że baza danych spełnia wymagania dotyczące wydajności. Podobna praktyczna zasada uwzględnia również ten podział podczas zbierania metryk opóźnień i określania SLO.

Podczas zbierania metryk dla każdej operacji należy pamiętać o dużej liczności. Użyj dzienników, kolekcji zdarzeń lub rozproszonego śledzenia, aby uzyskać dane debugowania o dużej mocy. W artykule "Chcesz debugować opóźnienia?» możesz zapoznać się z metodologiami debugowania opóźnień.

Zagnieżdżone transakcje mogą być niebezpieczne

Nie każdy system DBMS obsługuje transakcje zagnieżdżone, ale gdy tak się dzieje, takie transakcje mogą skutkować nieoczekiwanymi błędami, które nie zawsze są łatwe do wykrycia (to znaczy powinno być oczywiste, że istnieje jakiś rodzaj anomalii).

Można uniknąć stosowania transakcji zagnieżdżonych, korzystając z bibliotek klienckich, które mogą je wykryć i ominąć. Jeśli nie można porzucić transakcji zagnieżdżonych, należy zachować szczególną ostrożność przy ich realizacji, aby uniknąć nieoczekiwanych sytuacji, w których zakończone transakcje zostaną przypadkowo przerwane ze względu na zagnieżdżenia.

Hermetyzowanie transakcji w różnych warstwach może prowadzić do nieoczekiwanych transakcji zagnieżdżonych, a z punktu widzenia czytelności kodu może utrudniać zrozumienie intencji autora. Spójrz na następujący program:

with newTransaction():
   Accounts.create("609-543-222")
   with newTransaction():
       Accounts.create("775-988-322")
       throw Rollback();

Jaki będzie wynik powyższego kodu? Czy wycofa obie transakcje, czy tylko tę wewnętrzną? Co się stanie, jeśli będziemy polegać na wielu warstwach bibliotek, które enkapsulują tworzenie transakcji za nas? Czy będziemy w stanie zidentyfikować i poprawić takie przypadki?

Wyobraź sobie warstwę danych z wieloma operacjami (np. newAccount) jest już zaimplementowany we własnych transakcjach. Co się stanie, jeśli uruchomisz je jako część logiki biznesowej wyższego poziomu, która działa w ramach własnej transakcji? Jaka byłaby izolacja i spójność w tym przypadku?

function newAccount(id string) {
  with newTransaction():
      Accounts.create(id)
}

Zamiast szukać odpowiedzi na takie niekończące się pytania, lepiej unikać transakcji zagnieżdżonych. W końcu Twoja warstwa danych może z łatwością wykonywać operacje wysokiego poziomu bez tworzenia własnych transakcji. Ponadto sama logika biznesowa jest w stanie zainicjować transakcję, wykonać na niej operacje, zatwierdzić lub przerwać transakcję.

function newAccount(id string) {
   Accounts.create(id)
}
// In main application:
with newTransaction():
   // Read some data from database for configuration.
   // Generate an ID from the ID service.
   Accounts.create(id)
   Uploads.create(id) // create upload queue for the user.

Transakcje nie powinny być powiązane ze stanem aplikacji

Czasami kuszące jest wykorzystanie stanu aplikacji w transakcjach w celu zmiany pewnych wartości lub ulepszenia parametrów zapytania. Najważniejszym niuansem, który należy wziąć pod uwagę, jest właściwy zakres zastosowania. Klienci często wznawiają transakcje, gdy występują problemy z siecią. Jeśli transakcja zależy od stanu zmienianego przez inny proces, może wybrać niewłaściwą wartość w zależności od możliwości wyścigu danych. Transakcje muszą uwzględniać ryzyko wystąpienia wyścigu danych w aplikacji.

var seq int64
with newTransaction():
    newSeq := atomic.Increment(&seq)
    Entries.query(newSeq)
    // Other operations...

Powyższa transakcja będzie zwiększać numer sekwencyjny przy każdym wykonaniu, niezależnie od wyniku końcowego. Jeśli zatwierdzenie nie powiedzie się z powodu problemów z siecią, przy ponownej próbie żądanie zostanie wykonane z innym numerem sekwencyjnym.

Planiści zapytań mogą wiele powiedzieć o bazie danych

Planiści zapytań określają sposób wykonania zapytania w bazie danych. Analizują także żądania i optymalizują je przed wysłaniem. Planiści mogą podać jedynie pewne możliwe szacunki w oparciu o sygnały, którymi dysponują. Na przykład, jaka jest najlepsza metoda wyszukiwania następującego zapytania?

SELECT * FROM articles where author = "rakyll" order by title;

Wyniki można uzyskać na dwa sposoby:

  • Pełny skan tabeli: Możesz przejrzeć każdy wpis w tabeli i zwrócić artykuły o pasującym nazwisku autora, a następnie je uporządkować.
  • Skan indeksu: Możesz użyć indeksu, aby znaleźć pasujące identyfikatory, pobrać te wiersze, a następnie je uporządkować.

Zadaniem osoby planującej zapytania jest określenie, która strategia jest najlepsza. Warto wziąć pod uwagę, że planiści zapytań mają jedynie ograniczone możliwości przewidywania. Może to prowadzić do złych decyzji. Administratorzy baz danych i programiści mogą ich używać do diagnozowania i dostrajania zapytań o słabszej wydajności. Nowe wersje DBMS mogą konfigurować planery zapytań, a autodiagnostyka może pomóc podczas aktualizacji bazy danych, jeśli nowa wersja prowadzi do problemów z wydajnością. Dzienniki powolnych zapytań, raporty o problemach z opóźnieniami lub statystyki czasu wykonania mogą pomóc w identyfikacji zapytań wymagających optymalizacji.

Niektóre metryki prezentowane przez narzędzie do planowania zapytań mogą podlegać zakłóceniom (szczególnie podczas szacowania opóźnień lub czasu procesora). Dobrym dodatkiem do planistów są narzędzia do śledzenia i śledzenia ścieżki wykonania. Pozwalają zdiagnozować tego typu problemy (niestety, nie wszystkie SZBD zapewniają takie narzędzia).

Migracja online jest trudna, ale możliwa

Migracja online, migracja na żywo lub migracja w czasie rzeczywistym oznacza przenoszenie z jednej bazy danych do drugiej bez przestojów i uszkodzenia danych. Migracja na żywo jest łatwiejsza do przeprowadzenia, jeśli przejście odbywa się w tym samym systemie DBMS/silniku. Sytuacja staje się bardziej skomplikowana, gdy konieczne jest przejście do nowego systemu DBMS o innych wymaganiach dotyczących wydajności i schematu.

Istnieją różne modele migracji online. Oto jeden z nich:

  • Włącz podwójny wpis w obu bazach danych. Nowa baza danych na tym etapie nie zawiera wszystkich danych, a jedynie przyjmuje najnowsze dane. Kiedy już będziesz tego pewien, możesz przejść do następnego kroku.
  • Włącz odczyt z obu baz danych.
  • Skonfiguruj system tak, aby odczyt i zapis odbywały się przede wszystkim na nowej bazie danych.
  • Przestań zapisywać do starej bazy danych, kontynuując odczytywanie z niej danych. Na tym etapie nowa baza danych jest jeszcze pozbawiona części danych. Należy je skopiować ze starej bazy danych.
  • Stara baza danych jest tylko do odczytu. Skopiuj brakujące dane ze starej bazy danych do nowej. Po zakończeniu migracji zmień ścieżki do nowej bazy danych, a zatrzymaj starą i usuń ją z systemu.

W celu uzyskania dodatkowych informacji polecam kontakt Artykuł, który szczegółowo opisuje strategię migracji Stripe opartą na tym modelu.

Znaczący wzrost bazy danych pociąga za sobą wzrost nieprzewidywalności

Rozrost bazy danych prowadzi do nieprzewidywalnych problemów związanych z jej skalą. Im więcej wiemy o wewnętrznej strukturze bazy danych, tym lepiej możemy przewidzieć, w jaki sposób będzie ona skalowana. Jednak niektórych momentów nadal nie da się przewidzieć.
W miarę powiększania się bazy dotychczasowe założenia i oczekiwania dotyczące wymagań dotyczących ilości danych i przepustowości sieci mogą stać się nieaktualne. Wtedy pojawia się pytanie o poważne zmiany projektu, ulepszenia operacyjne na dużą skalę, ponowne przemyślenie wdrożeń lub migrację do innych systemów DBMS, aby uniknąć potencjalnych problemów.

Nie myśl jednak, że wystarczy doskonała znajomość wewnętrznej struktury istniejącej bazy danych. Nowe skale przyniosą ze sobą nowe niewiadome. Nieprzewidywalne problemy, nierówna dystrybucja danych, nieoczekiwane problemy z przepustowością i sprzętem, stale rosnący ruch i nowe segmenty sieci zmuszą Cię do ponownego przemyślenia podejścia do bazy danych, modelu danych, modelu wdrożenia i rozmiaru bazy danych.

...

Kiedy zaczynałem myśleć o opublikowaniu tego artykułu, na mojej pierwotnej liście znajdowało się już pięć kolejnych pozycji. Potem przyszła ogromna liczba nowe pomysły o tym, co jeszcze można uwzględnić. Dlatego w artykule poruszono najmniej oczywiste problemy, które wymagają maksymalnej uwagi. Nie oznacza to jednak, że temat został wyczerpany i nie będę już do niego wracać w moich przyszłych materiałach i nie będę wprowadzać zmian w obecnym.

PS

Przeczytaj także na naszym blogu:

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

Dodaj komentarz