NewSQL = NoSQL+ACID

NewSQL = NoSQL+ACID
Do niedawna Odnoklassniki przechowywały w SQL Server około 50 TB danych przetwarzanych w czasie rzeczywistym. W przypadku takiego wolumenu zapewnienie szybkiego i niezawodnego, a nawet odpornego na awarie centrum danych dostępu przy użyciu SQL DBMS jest prawie niemożliwe. Zwykle w takich przypadkach wykorzystywany jest jeden z magazynów NoSQL, jednak nie wszystko da się przenieść do NoSQL: niektóre podmioty wymagają gwarancji transakcji ACID.

To doprowadziło nas do zastosowania magazynu NewSQL, czyli systemu DBMS, który zapewnia odporność na awarie, skalowalność i wydajność systemów NoSQL, zachowując jednocześnie gwarancje ACID znane z klasycznych systemów. Niewiele jest działających systemów przemysłowych tej nowej klasy, dlatego sami wdrożyliśmy taki system i uruchomiliśmy go komercyjnie.

Jak to działa i co się stało - przeczytaj pod nacięciem.

Dziś miesięczna publiczność Odnoklassnik to ponad 70 milionów unikalnych użytkowników. My Jesteśmy w pierwszej piątce największych sieci społecznościowych na świecie i wśród dwudziestu witryn, na których użytkownicy spędzają najwięcej czasu. Infrastruktura OK obsługuje bardzo duże obciążenia: ponad milion żądań HTTP/s na front. Części floty serwerów liczącej ponad 8000 1 sztuk są zlokalizowane blisko siebie – w czterech moskiewskich centrach danych, co pozwala na opóźnienie sieci pomiędzy nimi mniejsze niż XNUMX ms.

Używamy Cassandry od 2010 roku, począwszy od wersji 0.6. Obecnie działa kilkadziesiąt klastrów. Najszybszy klaster przetwarza ponad 4 miliony operacji na sekundę, a największy przechowuje 260 TB.

Są to jednak wszystkie zwykłe klastry NoSQL używane do przechowywania słabo skoordynowane dane. Chcieliśmy zastąpić główny spójny magazyn danych, Microsoft SQL Server, który był używany od założenia Odnoklassniki. Magazyn składał się z ponad 300 maszyn SQL Server Standard Edition, które zawierały 50 TB danych - podmioty gospodarcze. Dane te podlegają modyfikacji w ramach transakcji ACID i wymagają wysoka spójność.

Aby dystrybuować dane pomiędzy węzłami SQL Server, użyliśmy zarówno pionowego, jak i poziomego partycjonowanie (rozdrobnienie). Historycznie rzecz biorąc, stosowaliśmy prosty schemat fragmentowania danych: każdy podmiot był powiązany z tokenem – funkcją identyfikatora podmiotu. Jednostki z tym samym tokenem zostały umieszczone na tym samym serwerze SQL. Zaimplementowano relację master-detail w taki sposób, aby tokeny rekordu głównego i podrzędnego były zawsze zgodne i znajdowały się na tym samym serwerze. W sieci społecznościowej niemal wszystkie rekordy generowane są w imieniu użytkownika – co oznacza, że ​​wszystkie dane użytkownika w ramach jednego podsystemu funkcjonalnego przechowywane są na jednym serwerze. Oznacza to, że w transakcji biznesowej prawie zawsze brały udział tabele z jednego serwera SQL, co pozwoliło zapewnić spójność danych za pomocą lokalnych transakcji ACID, bez konieczności stosowania powolny i zawodny rozproszone transakcje ACID.

Dzięki shardingowi i przyspieszeniu SQL:

  • Nie stosujemy ograniczeń klucza obcego, ponieważ podczas dzielenia identyfikator jednostki może znajdować się na innym serwerze.
  • Nie używamy procedur składowanych i wyzwalaczy ze względu na dodatkowe obciążenie procesora DBMS.
  • Nie używamy JOIN ze względu na powyższe i dużą liczbę losowych odczytów z dysku.
  • Poza transakcją używamy poziomu izolacji Read Uncommitted, aby zmniejszyć zakleszczenia.
  • Realizujemy tylko krótkie transakcje (średnio krótsze niż 100 ms).
  • Nie stosujemy wielowierszowego UPDATE i DELETE ze względu na dużą liczbę zakleszczeń - aktualizujemy tylko jeden rekord na raz.
  • Zapytania zawsze wykonujemy tylko na indeksach - zapytanie z pełnym planem skanowania tabeli oznacza dla nas przeciążenie bazy danych i spowodowanie jej awarii.

Te kroki pozwoliły nam wycisnąć niemal maksymalną wydajność z serwerów SQL. Jednak problemów było coraz więcej. Przyjrzyjmy się im.

Problemy z SQLem

  • Ponieważ korzystaliśmy z samodzielnie napisanego fragmentowania, dodawanie nowych fragmentów było wykonywane ręcznie przez administratorów. Przez cały ten czas skalowalne repliki danych nie obsługiwały żądań.
  • Wraz ze wzrostem liczby rekordów w tabeli prędkość wstawiania i modyfikowania maleje; podczas dodawania indeksów do istniejącej tabeli prędkość spada wielokrotnie; tworzenie i ponowne tworzenie indeksów odbywa się z przestojami.
  • Posiadanie niewielkiej ilości systemu Windows dla SQL Server w środowisku produkcyjnym utrudnia zarządzanie infrastrukturą

Ale głównym problemem jest

tolerancja błędów

Klasyczny serwer SQL ma słabą odporność na awarie. Załóżmy, że masz tylko jeden serwer bazy danych, który ulega awarii raz na trzy lata. W tym czasie strona nie działa przez 20 minut, co jest akceptowalne. Jeśli masz 64 serwery, witryna nie działa raz na trzy tygodnie. A jeśli masz 200 serwerów, witryna nie działa co tydzień. To jest problem.

Co można zrobić, aby poprawić odporność serwera SQL na awarie? Wikipedia zaprasza nas do budowania klaster o wysokiej dostępności: gdzie na wypadek awarii któregoś z podzespołów istnieje element zapasowy.

Wymaga to floty drogiego sprzętu: licznych duplikatów, światłowodu, współdzielonej pamięci masowej, a włączenie rezerwy nie działa niezawodnie: około 10% przełączeń kończy się awarią węzła zapasowego, jak pociąg za węzłem głównym.

Jednak główną wadą takiego klastra o wysokiej dostępności jest zerowa dostępność w przypadku awarii centrum danych, w którym jest on zlokalizowany. Odnoklassniki mają cztery centra danych i musimy zapewnić działanie w przypadku całkowitej awarii jednego z nich.

Do tego moglibyśmy użyć Multimaster replikacja wbudowana w SQL Server. Rozwiązanie to jest znacznie droższe ze względu na koszt oprogramowania i ma dobrze znane problemy z replikacją - nieprzewidywalne opóźnienia transakcji przy replikacji synchronicznej oraz opóźnienia w stosowaniu replikacji (i w efekcie utracone modyfikacje) przy replikacji asynchronicznej. Domniemane ręczne rozwiązywanie konfliktów sprawia, że ​​opcja ta jest dla nas całkowicie niemożliwa do zastosowania.

Wszystkie te problemy wymagały radykalnego rozwiązania i zaczęliśmy je szczegółowo analizować. Tutaj musimy zapoznać się z tym, czym głównie zajmuje się SQL Server - transakcjami.

Prosta transakcja

Rozważmy najprostszą transakcję z punktu widzenia programisty SQL stosowanego: dodanie zdjęcia do albumu. Albumy i fotografie przechowywane są na różnych płytach. Album posiada publiczny licznik zdjęć. Następnie taka transakcja podzielona jest na następujące kroki:

  1. Zamykamy album na klucz.
  2. Utwórz wpis w tabeli zdjęć.
  3. Jeżeli zdjęcie ma status publiczny to dodaj do albumu publiczny licznik zdjęć, zaktualizuj zapis i zatwierdź transakcję.

Lub w pseudokodzie:

TX.start("Albums", id);
Album album = albums.lock(id);
Photo photo = photos.create(…);

if (photo.status == PUBLIC ) {
    album.incPublicPhotosCount();
}
album.update();

TX.commit();

Widzimy, że najczęstszym scenariuszem transakcji biznesowej jest wczytanie danych z bazy danych do pamięci serwera aplikacji, zmiana czegoś i zapisanie nowych wartości z powrotem do bazy danych. Zwykle w takiej transakcji aktualizujemy kilka podmiotów, kilka tabel.

Podczas realizacji transakcji może nastąpić jednoczesna modyfikacja tych samych danych z innego systemu. Przykładowo Antyspam może uznać, że użytkownik jest w jakiś sposób podejrzany i w związku z tym wszystkie zdjęcia użytkownika nie powinny już być publiczne, należy je przesłać do moderacji, co oznacza zmianę photo.status na inną wartość i wyłączenie odpowiednich liczników. Oczywiście, jeśli operacja ta odbędzie się bez gwarancji atomowości zastosowania i izolacji konkurencyjnych modyfikacji, jak w ACID, wtedy wynik nie będzie taki, jak oczekiwano - albo licznik zdjęć wskaże błędną wartość, albo nie wszystkie zdjęcia zostaną przesłane do moderacji.

Przez całe istnienie Odnoklassników napisano mnóstwo podobnego kodu manipulującego różnymi podmiotami biznesowymi w ramach jednej transakcji. Bazując na doświadczeniach z migracji do NoSQL z Ostateczna spójność Wiemy, że największym wyzwaniem (i inwestycją czasu) jest opracowanie kodu w celu utrzymania spójności danych. Dlatego też uznaliśmy, że głównym wymaganiem dla nowej pamięci masowej jest zapewnienie rzeczywistych transakcji ACID dla logiki aplikacji.

Inne, nie mniej ważne, wymagania były następujące:

  • Jeśli centrum danych ulegnie awarii, musi być dostępny zarówno odczyt, jak i zapis w nowej pamięci.
  • Utrzymanie obecnego tempa rozwoju. Oznacza to, że podczas pracy z nowym repozytorium ilość kodu powinna być w przybliżeniu taka sama, nie powinno być potrzeby dodawania czegokolwiek do repozytorium, opracowywania algorytmów rozwiązywania konfliktów, utrzymywania indeksów wtórnych itp.
  • Szybkość nowego magazynu musiała być dość wysoka, zarówno przy odczycie danych, jak i przy przetwarzaniu transakcji, co w praktyce oznaczało, że nie miały zastosowania rygorystyczne akademickie, uniwersalne, ale powolne rozwiązania, takie jak np. zatwierdzenia dwufazowe.
  • Automatyczne skalowanie w locie.
  • Korzystanie ze zwykłych, tanich serwerów, bez konieczności zakupu egzotycznego sprzętu.
  • Możliwość rozbudowy magazynu przez deweloperów firmy. Innymi słowy, priorytetem były rozwiązania autorskie lub open source, najlepiej w Javie.

Decyzje, decyzje

Analizując możliwe rozwiązania, doszliśmy do dwóch możliwych wyborów architektury:

Pierwszy polega na wzięciu dowolnego serwera SQL i zaimplementowaniu wymaganej odporności na awarie, mechanizmu skalowania, klastra przełączania awaryjnego, rozwiązywania konfliktów oraz rozproszonych, niezawodnych i szybkich transakcji ACID. Oceniliśmy tę opcję jako bardzo nietrywialną i pracochłonną.

Drugą opcją jest wzięcie gotowego magazynu NoSQL z zaimplementowanym skalowaniem, klastrem pracy awaryjnej, rozwiązywaniem konfliktów i samodzielnym wdrażaniem transakcji i SQL. Na pierwszy rzut oka nawet zadanie wdrożenia SQL, nie mówiąc już o transakcjach ACID, wygląda na zadanie, które zajmie lata. Ale potem zdaliśmy sobie sprawę, że zestaw funkcji SQL, którego używamy w praktyce, jest tak samo daleki od ANSI SQL jak Cassandra CQL daleko od ANSI SQL. Przyglądając się jeszcze bliżej CQL, zdaliśmy sobie sprawę, że jest on całkiem bliski temu, czego potrzebowaliśmy.

Cassandra i CQL

Co więc jest interesującego w Cassandrze, jakie ma możliwości?

Po pierwsze, możesz tutaj tworzyć tabele obsługujące różne typy danych; możesz wykonać SELECT lub UPDATE na kluczu podstawowym.

CREATE TABLE photos (id bigint KEY, owner bigint,…);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET … WHERE id=?;

Aby zapewnić spójność danych repliki, Cassandra używa podejście oparte na kworum. W najprostszym przypadku oznacza to, że gdy trzy repliki tego samego wiersza zostaną umieszczone w różnych węzłach klastra, zapis uznaje się za udany, jeśli większość węzłów (tj. dwa z trzech) potwierdziła powodzenie tej operacji zapisu . Dane wierszowe uznaje się za spójne, jeśli podczas odczytu większość węzłów została odpytana i je potwierdziła. Dzięki trzem replikom gwarantowana jest pełna i natychmiastowa spójność danych w przypadku awarii jednego węzła. Takie podejście pozwoliło nam wdrożyć jeszcze bardziej niezawodny schemat: zawsze wysyłaj żądania do wszystkich trzech replik, czekając na odpowiedź z dwóch najszybszych. W tym przypadku spóźniona odpowiedź trzeciej repliki jest odrzucana. Węzeł, który spóźnia się z odpowiedzią, może mieć poważne problemy - hamulce, wyrzucanie śmieci w JVM, bezpośrednie odzyskiwanie pamięci w jądrze Linuksa, awaria sprzętu, odłączenie od sieci. Nie ma to jednak żadnego wpływu na działalność lub dane Klienta.

Podejście, w którym kontaktujemy się z trzema węzłami i otrzymujemy odpowiedź z dwóch, nazywa się spekulacja: prośba o dodatkowe repliki jest wysyłana jeszcze zanim „odpadną”.

Kolejną zaletą Cassandry jest Batchlog, mechanizm zapewniający, że partia wprowadzonych zmian zostanie albo w pełni zastosowana, albo nie zostanie zastosowana wcale. To pozwala nam rozwiązać A w ACID - atomowość od razu po wyjęciu z pudełka.

Najbliższe transakcjom w Cassandrze są tzw. „lekkie transakcje„. Ale daleko im do „prawdziwych” transakcji ACID: w rzeczywistości jest to okazja do zrobienia CAS na danych tylko z jednego rekordu, stosując konsensus przy użyciu ciężkiego protokołu Paxos. Dlatego prędkość takich transakcji jest niska.

Czego nam brakowało w Cassandrze

Musieliśmy więc wdrożyć w Cassandrze prawdziwe transakcje ACID. Dzięki niemu moglibyśmy łatwo zaimplementować dwie inne wygodne funkcje klasycznego DBMS: spójne szybkie indeksy, które pozwoliłyby nam dokonywać selekcji danych nie tylko za pomocą klucza podstawowego, oraz zwykły generator monotonicznych, automatycznie zwiększających się identyfikatorów.

Stożek

W ten sposób narodził się nowy system DBMS Stożek, składający się z trzech typów węzłów serwerowych:

  • Storage – (prawie) standardowe serwery Cassandra odpowiedzialne za przechowywanie danych na dyskach lokalnych. W miarę wzrostu obciążenia i objętości danych ich ilość można łatwo skalować do dziesiątek i setek.
  • Koordynatorzy transakcji - zapewniają realizację transakcji.
  • Klientami są serwery aplikacji, które realizują operacje biznesowe i inicjują transakcje. Takich klientów może być tysiące.

NewSQL = NoSQL+ACID

Serwery wszystkich typów stanowią część wspólnego klastra i do komunikacji między sobą korzystają z wewnętrznego protokołu komunikatów Cassandra plotka do wymiany informacji o klastrze. Dzięki Heartbeat serwery uczą się o wzajemnych awariach, utrzymują jeden schemat danych – tabele, ich strukturę i replikację; schemat partycjonowania, topologia klastra itp.

Klienci

NewSQL = NoSQL+ACID

Zamiast standardowych sterowników używany jest tryb Fat Client. Taki węzeł nie przechowuje danych, ale może pełnić funkcję koordynatora realizacji żądań, czyli sam Klient pełni rolę koordynatora swoich żądań: odpytuje repliki pamięci masowej i rozwiązuje konflikty. Jest to nie tylko bardziej niezawodne i szybsze niż standardowy sterownik, który wymaga komunikacji ze zdalnym koordynatorem, ale także pozwala kontrolować transmisję żądań. Poza transakcją otwartą na kliencie żądania są wysyłane do repozytoriów. Jeżeli klient otworzył transakcję, wówczas wszystkie żądania w ramach transakcji przesyłane są do koordynatora transakcji.
NewSQL = NoSQL+ACID

Koordynator ds. transakcji C*One

Koordynator to coś, co wdrożyliśmy dla C*One od podstaw. Odpowiada za zarządzanie transakcjami, blokadami i kolejnością stosowania transakcji.

Dla każdej obsługiwanej transakcji koordynator generuje znacznik czasu: każda kolejna transakcja jest większa od poprzedniej. Ponieważ system rozwiązywania konfliktów Cassandry opiera się na znacznikach czasu (z dwóch sprzecznych rekordów, za bieżący uważany jest ten z najnowszym znacznikiem czasu), konflikt będzie zawsze rozwiązywany na korzyść kolejnej transakcji. W ten sposób wdrożyliśmy Zegarek Lamporta - tani sposób rozwiązywania konfliktów w systemie rozproszonym.

Zamki

Aby zapewnić izolację, zdecydowaliśmy się zastosować najprostszą metodę - blokady pesymistyczne oparte na kluczu podstawowym rekordu. Innymi słowy, w przypadku transakcji rekord musi zostać najpierw zablokowany, a dopiero potem odczytany, zmodyfikowany i zapisany. Dopiero po pomyślnym zatwierdzeniu rekord może zostać odblokowany, aby mogły z niego korzystać konkurencyjne transakcje.

Implementacja takiego blokowania jest prosta w środowisku nierozproszonym. W systemie rozproszonym istnieją dwie główne opcje: albo wdrożyć rozproszone blokowanie w klastrze, albo rozprowadzić transakcje w taki sposób, aby transakcje dotyczące tego samego rekordu były zawsze obsługiwane przez tego samego koordynatora.

Ponieważ w naszym przypadku dane są już rozproszone pomiędzy grupami transakcji lokalnych w SQL, zdecydowano się przypisać lokalne grupy transakcji do koordynatorów: jeden koordynator realizuje wszystkie transakcje z tokenami od 0 do 9, drugi - z tokenami od 10 do 19, i tak dalej. W rezultacie każda z instancji koordynatora staje się masterem grupy transakcji.

Wtedy można zaimplementować blokady w postaci banalnej HashMapy w pamięci koordynatora.

Błędy koordynatorów

Ponieważ jeden koordynator obsługuje wyłącznie grupę transakcji, bardzo ważne jest szybkie ustalenie faktu jego awarii, aby druga próba realizacji transakcji przekroczyła limit czasu. Aby było to szybkie i niezawodne, zastosowaliśmy w pełni połączony protokół pulsu kworum:

W każdym centrum danych znajdują się co najmniej dwa węzły koordynujące. Okresowo każdy koordynator wysyła komunikat pulsu do pozostałych koordynatorów i informuje ich o jego funkcjonowaniu, a także o tym, jakie komunikaty pulsu otrzymał od których koordynatorów w klastrze ostatni raz.

NewSQL = NoSQL+ACID

Otrzymując podobne informacje od innych w ramach swoich komunikatów pulsu, każdy koordynator sam decyduje, które węzły klastra funkcjonują, a które nie, kierując się zasadą kworum: jeżeli węzeł X otrzymał informację od większości węzłów w klastrze o normalnym odbiór komunikatów z węzła Y, wówczas działa Y. I odwrotnie, gdy tylko większość zgłosi brak wiadomości z węzła Y, wówczas Y odmówił. Ciekawe, że jeśli kworum poinformuje węzeł X, że nie otrzymuje już od niego wiadomości, wówczas sam węzeł X uzna, że ​​uległ awarii.

Komunikaty pulsu są wysyłane z dużą częstotliwością, około 20 razy na sekundę, z okresem 50 ms. W Javie trudno jest zagwarantować odpowiedź aplikacji w ciągu 50 ms ze względu na porównywalną długość przerw powodowanych przez moduł zbierający elementy bezużyteczne. Udało nam się osiągnąć ten czas reakcji przy użyciu modułu zbierającego elementy bezużyteczne G1, który pozwala nam określić cel na czas trwania przerw w GC. Czasami jednak, dość rzadko, przerwy kolektora przekraczają 50 ms, co może prowadzić do fałszywego wykrycia uszkodzenia. Aby temu zapobiec, koordynator nie zgłasza awarii węzła zdalnego w momencie zaniku od niego pierwszego komunikatu heartbeat, tylko w przypadku, gdy zniknęło ich kilka z rzędu.W ten sposób udało nam się wykryć awarię węzła koordynatora w 200 SM.

Ale nie wystarczy szybko zrozumieć, który węzeł przestał działać. Musimy coś z tym zrobić.

Rezerwacja

Klasyczny schemat zakłada, w przypadku niepowodzenia głównego, rozpoczęcie nowych wyborów za pomocą jednego z nich modny uniwersalny algorytmy. Algorytmy takie mają jednak dobrze znane problemy ze zbieżnością czasu i długością samego procesu wyborczego. Takich dodatkowych opóźnień udało nam się uniknąć, stosując schemat wymiany koordynatora w w pełni połączonej sieci:

NewSQL = NoSQL+ACID

Załóżmy, że chcemy wykonać transakcję w grupie 50. Ustalmy z góry schemat zastępczy, czyli które węzły wykonają transakcje w grupie 50 w przypadku awarii głównego koordynatora. Naszym celem jest utrzymanie funkcjonalności systemu w przypadku awarii centrum danych. Ustalmy, że pierwszą rezerwą będzie węzeł z innego centrum danych, a drugą rezerwą będzie węzeł z trzeciego. Schemat ten jest wybierany jednorazowo i nie ulega zmianie do czasu zmiany topologii klastra, czyli do czasu wejścia do niego nowych węzłów (co zdarza się bardzo rzadko). Procedura wyboru nowego aktywnego mastera w przypadku awarii starego będzie zawsze następująca: pierwsza rezerwa stanie się aktywnym masterem, a jeśli przestanie działać, druga rezerwa stanie się aktywnym masterem.

Ten schemat jest bardziej niezawodny niż algorytm uniwersalny, ponieważ aby aktywować nowego mistrza, wystarczy określić awarię starego.

Ale jak klienci zrozumieją, który mistrz teraz pracuje? Niemożliwe jest przesłanie informacji do tysięcy klientów w ciągu 50 ms. Możliwa jest sytuacja, gdy klient wysyła żądanie otwarcia transakcji, nie wiedząc jeszcze, że ten master już nie działa, a żądanie upłynie. Aby temu zapobiec, klienci spekulacyjnie wysyłają żądanie otwarcia transakcji do mastera grupy i obu jego rezerw na raz, ale na to żądanie odpowie tylko ten, który w danym momencie jest aktywnym masterem. Klient będzie nawiązywał całą późniejszą komunikację w ramach transakcji wyłącznie z aktywnym masterem.

Mistrzowie kopii zapasowych umieszczają otrzymane żądania dotyczące transakcji, które nie są ich własnymi, w kolejce nienarodzonych transakcji, gdzie są przechowywane przez pewien czas. Jeśli aktywny master umrze, nowy master przetwarza żądania otwarcia transakcji ze swojej kolejki i odpowiada klientowi. Jeśli klient otworzył już transakcję ze starym masterem, to druga odpowiedź jest ignorowana (i oczywiście taka transakcja nie zostanie sfinalizowana i zostanie powtórzona przez klienta).

Jak przebiega transakcja

Załóżmy, że klient wysłał do koordynatora prośbę o otwarcie transakcji dla takiego a takiego podmiotu z takim a takim kluczem podstawowym. Koordynator blokuje ten obiekt i umieszcza go w tabeli blokad w pamięci. W razie potrzeby koordynator odczytuje tę encję z pamięci i przechowuje powstałe dane w stanie transakcyjnym w pamięci koordynatora.

NewSQL = NoSQL+ACID

Gdy klient chce zmienić dane w transakcji, wysyła do koordynatora prośbę o modyfikację jednostki, a koordynator umieszcza nowe dane w tabeli statusów transakcji w pamięci. To kończy nagrywanie – w pamięci nie jest zapisywane żadne nagranie.

NewSQL = NoSQL+ACID

Gdy Klient w ramach aktywnej transakcji zażąda własnych zmienionych danych, koordynator postępuje w następujący sposób:

  • jeżeli identyfikator znajduje się już w transakcji, to dane są pobierane z pamięci;
  • jeśli w pamięci nie ma identyfikatora, to z węzłów magazynujących odczytywane są brakujące dane, łączone z tymi, które już znajdują się w pamięci, a wynik przekazywany jest klientowi.

Zatem klient może odczytać własne zmiany, ale inni klienci nie widzą tych zmian, ponieważ są one przechowywane tylko w pamięci koordynatora; nie ma ich jeszcze w węzłach Cassandry.

NewSQL = NoSQL+ACID

Kiedy klient wysyła zatwierdzenie, stan, który znajdował się w pamięci usługi, jest zapisywany przez koordynatora w zarejestrowanej partii i wysyłany jako zarejestrowana partia do pamięci Cassandry. Sklepy robią wszystko, co konieczne, aby pakiet ten został atomowo (całkowicie) zastosowany i zwracają odpowiedź koordynatorowi, który zwalnia blokady i potwierdza klientowi powodzenie transakcji.

NewSQL = NoSQL+ACID

Aby cofnąć, koordynator musi jedynie zwolnić pamięć zajmowaną przez stan transakcji.

W wyniku powyższych usprawnień wdrożyliśmy zasady ACID:

  • Atomowość. To gwarancja, że ​​żadna transakcja nie zostanie częściowo zarejestrowana w systemie, albo wszystkie jej podoperacje zostaną zrealizowane, albo żadna nie zostanie zrealizowana. Trzymamy się tej zasady poprzez rejestrację partii w Cassandrze.
  • Spójność. Każda udana transakcja z definicji rejestruje tylko prawidłowe wyniki. Jeżeli po otwarciu transakcji i wykonaniu części operacji okaże się, że wynik jest nieprawidłowy, następuje wycofanie transakcji.
  • Izolacja. Kiedy transakcja jest wykonywana, współbieżne transakcje nie powinny mieć wpływu na jej wynik. Konkurencyjne transakcje są izolowane za pomocą pesymistycznych blokad na koordynatorze. W przypadku odczytów poza transakcją zasada izolacji jest przestrzegana na poziomie Read Committed.
  • Stabilność. Niezależnie od problemów na niższych poziomach – awarii systemu, awarii sprzętu – zmiany wprowadzone w wyniku pomyślnie zakończonej transakcji powinny pozostać zachowane po wznowieniu operacji.

Czytanie według indeksów

Weźmy prostą tabelę:

CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,
…)

Posiada identyfikator (klucz podstawowy), właściciela i datę modyfikacji. Musisz złożyć bardzo prostą prośbę - wybrać dane właściciela z datą zmiany „na ostatni dzień”.

SELECT *
WHERE owner=?
AND modified>?

Aby takie zapytanie zostało szybko przetworzone, w klasycznym DBMS SQL należy zbudować indeks po kolumnach (właściciel, zmodyfikowany). Możemy to zrobić całkiem łatwo, ponieważ mamy teraz gwarancje ACID!

Indeksy w C*One

Istnieje tabela źródłowa ze zdjęciami, w której kluczem podstawowym jest identyfikator rekordu.

NewSQL = NoSQL+ACID

Dla indeksu C*One tworzy nową tabelę będącą kopią oryginału. Klucz jest taki sam jak wyrażenie indeksowe i zawiera także klucz podstawowy rekordu z tabeli źródłowej:

NewSQL = NoSQL+ACID

Teraz zapytanie o „właściciel na ostatni dzień” można przepisać jako wybór z innej tabeli:

SELECT * FROM i1_test
WHERE owner=?
AND modified>?

Spójność danych na zdjęciach tabeli źródłowej i tabeli indeksowej i1 jest utrzymywana automatycznie przez koordynatora. Na podstawie samego schematu danych po otrzymaniu zmiany koordynator generuje i przechowuje zmianę nie tylko w tabeli głównej, ale także w kopiach. Na tabeli indeksów nie są wykonywane żadne dodatkowe akcje, nie są odczytywane logi i nie są stosowane żadne blokady. Oznacza to, że dodawanie indeksów prawie nie zużywa zasobów i praktycznie nie ma wpływu na szybkość stosowania modyfikacji.

Używając ACID, byliśmy w stanie zaimplementować indeksy podobne do SQL. Są spójne, skalowalne, szybkie, łatwe do komponowania i wbudowane w język zapytań CQL. Do obsługi indeksów nie są wymagane żadne zmiany w kodzie aplikacji. Wszystko jest tak proste jak w SQL. A co najważniejsze, indeksy nie wpływają na szybkość wykonywania modyfikacji oryginalnej tabeli transakcji.

Co się stało

Opracowaliśmy C*One trzy lata temu i wprowadziliśmy go do użytku komercyjnego.

Co ostatecznie otrzymaliśmy? Oceńmy to na przykładzie podsystemu przetwarzania i przechowywania zdjęć, jednego z najważniejszych rodzajów danych w sieci społecznościowej. Nie mówimy tu o samych bryłach fotografii, ale o wszelkiego rodzaju metainformacjach. Teraz Odnoklassniki mają około 20 miliardów takich rekordów, system przetwarza 80 tysięcy żądań odczytu na sekundę, do 8 tysięcy transakcji ACID na sekundę związanych z modyfikacją danych.

Kiedy korzystaliśmy z SQL ze współczynnikiem replikacji = 1 (ale w RAID 10), metainformacje o zdjęciach były przechowywane w wysoce dostępnym klastrze składającym się z 32 maszyn z systemem Microsoft SQL Server (plus 11 kopii zapasowych). Do przechowywania kopii zapasowych przeznaczono także 10 serwerów. W sumie 50 drogich samochodów. Jednocześnie układ pracował przy obciążeniu znamionowym, bez rezerwy.

Po migracji na nowy system otrzymaliśmy współczynnik replikacji = 3 – po jednej kopii w każdym data center. System składa się z 63 węzłów magazynowania Cassandra i 6 maszyn koordynujących, co daje łącznie 69 serwerów. Ale te maszyny są znacznie tańsze, ich całkowity koszt to około 30% kosztu systemu SQL. Jednocześnie obciążenie utrzymuje się na poziomie 30%.

Wraz z wprowadzeniem C*One, opóźnienie również się zmniejszyło: w SQL operacja zapisu trwała około 4,5 ms. W C*One – około 1,6 ms. Czas trwania transakcji wynosi średnio mniej niż 40 ms, zatwierdzenie zostaje zakończone w ciągu 2 ms, czas odczytu i zapisu wynosi średnio 2 ms. 99. percentyl - tylko 3-3,1 ms, liczba przekroczeń limitu czasu spadła 100-krotnie - wszystko za sprawą powszechnego stosowania spekulacji.

Do tej pory większość węzłów SQL Server została wycofana z użytku, a nowe produkty są opracowywane wyłącznie przy użyciu C*One. Przystosowaliśmy C*One do pracy w naszej chmurze jedna chmura, co pozwoliło przyspieszyć wdrażanie nowych klastrów, uprościć konfigurację i zautomatyzować działanie. Bez kodu źródłowego wykonanie tego byłoby znacznie trudniejsze i bardziej kłopotliwe.

Teraz pracujemy nad przeniesieniem pozostałych naszych magazynów do chmury – ale to już zupełnie inna historia.

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

Dodaj komentarz