Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage
Korytarz przechowywania od St-Petersburga

Cześć wszystkim! Jestem Mons Anderson, architekt platformy Mail.ru Cloud Solutions, opowiem, jak zbudowaliśmy nasze przechowywanie S3, jak działa, jakie rozwiązania okazały się skuteczne, a które warto byłoby zmienić, gdybyśmy zaczęli ten sam projekt od nowa teraz.

Artykuł przygotowany na podstawie prezentacji na @Databases Meetup od Mail.ru Cloud Solutions & Tarantool. W artykule omówimy:

  • jak było zorganizowane przechowywanie Mail.ru, na którym budowaliśmy przechowywanie S3;
  • co dodaliśmy, aby stworzyć Mail.ru Cloud Storage;
  • jak działa obiektowy model przechowywania i jakie kroki podjęto, aby przejść do produkcji;
  • o udoskonaleniach systemu produkcyjnego: failover i skalowanie;
  • jak wdrożyliśmy sharding i resharding;
  • a także o pracy z certyfikatami SSL.

Jeśli nie chcesz czytać, możesz zobaczyć.

Jak było zorganizowane przechowywanie Mail.ru, na którym budowaliśmy przechowywanie S3

Rozwój naszego S3 rozpoczął się na bazie przechowywania w Chmurze Mail.ru, dlatego warto najpierw opowiedzieć, jak ono działa i co potrafi.

Przechowywanie chmury Mail.ru składa się z serwerów z dyskami. Średnio, współczesny serwer storage to 36 dysków po 12-14 terabajtów. Kiedyś dyski były mniejsze, ale przez trzy lata pojemności dysków wzrosły i dzisiaj to prawie pół petabajta surowych danych.

Dyski z różnych serwerów przechowywania są łączone w tzw. „pary” (pair). Para to jednostkowy unit przechowywania plików. W istocie jest to dysk zamontowany w określonym dziale pod określoną ścieżką, gdzie mogą leżeć pliki identyfikowane przez hashe.

Para to historyczna nazwa, zachowała się do dziś, chociaż w parze niekoniecznie muszą być tylko dwa dyski. Może być ich trzy, a także mogą istnieć różne hybrydowe przechowywania, na przykład 3/2.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Pary (pair) — jednostki przechowywania obiektów

Wszystkie pary są przechowywane w PairDB — to aplikacja w oparciu o Tarantool. Wszystkie bazy w naszym przechowywaniu, począwszy od najwcześniejszych, to Tarantool, innych baz nie używamy.

PairDB przechowuje wszystkie pary, ich stany, wolne miejsce, możliwości awaryjne, ostatnie błędy. Może również sama sprawdzać pary, aktualizować ich stan, weryfikować, czy działają, czy nie. Czyli PairDB to ogólny obraz stanu wszystkich dysków naszego systemu.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Pair DB: baza danych z stanem par

Na parach przechowywane są pliki, a aby wiedzieć, na której parach jaki plik się znajduje, potrzebna jest jeszcze jedna baza — FileDB. Przechowuje ona mapowanie, określenie zgodności: plik taki a taki znajduje się na parze takiej a takiej, a także niewielką liczbę potrzebnych atrybutów.

Architektura S3: 3 lata ewolucji Mail.ru Cloud StorageFile DB: miejsce, w którym przechowywany jest plik

Kolejnym istotnym ogniwem jest serwis Nylon, router do pracy z bazami danych. Jest to jednolity punkt dostępu, pozwala na pracę przez jeden interfejs zarówno z PairDB, jak i z FileDB. Jest to usługa stateless, wykonuje balansowanie zapytań, rozumie, na który shard FileDB należy iść, wie, które pary są aktywne, a które nie.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Nylon: router do pracy z bazami danych

W magazynie należy jakoś umieszczać treści. W tym celu istnieje serwis — Streamer. Udostępnia on dwie metody HTTP: metodę PUT, aby wprowadzić treść do magazynu, oraz metodę GET, aby ją stamtąd pobrać. HTTP to dość popularny i wygodny protokół do przesyłania danych.

Gdy zwracamy się do Streamera, on przez Nylon zwraca się do PairDB, ustala, na którą parę można wprowadzić plik, po czym przekazuje dane przez WebDAV na tę parę.

W zasadzie każdy serwer storage to nginx plus dyski, które są zamontowane na określonych ścieżkach. Możemy z Streamera wprowadzić plik do magazynu, usunąć go, zmienić nazwę lub sprawdzić jego integralność. To wygodny interfejs do niskopoziomowej interakcji z magazynem.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Streamer: punkt dostępu do magazynu

Co dodaliśmy, aby zrealizować magazyn S3

Przeanalizowaliśmy podstawową strukturę magazynu w momencie, gdy planowaliśmy uruchomić magazyn S3. Dzięki metodzie PUT mogliśmy umieścić tam dowolną treść i otrzymać jako identyfikator tych danych hasz. Z tym identyfikatorem można było później przyjść i odebrać oryginalny plik. Jednak to za mało, aby zrealizować S3. W protokole S3, oprócz samego przechowywania obiektów, znajdują się:

  • przechowywanie metadanych — dodatkowych właściwości obiektów;
  • organizowanie dostępu do obiektów za pośrednictwem HTTP;
  • grupowanie obiektów w kolekcjach — bucketach;
  • Endpoint HTTP-S3. S3 organizuje dane w określone struktury — buckety, z których każdy zapewnia punkt dostępu do przechowywania plików.

A separate service was needed to implement this logic. We also wanted to immediately provide an architecture for further growth of the service with linear scalability.

First components

A daemon implementing the S3 API. This is the standard S3 API from Amazon, which supports XML operations for metadata and allows content to be transferred directly. We didn't have to invent anything; it's all described and documented.

We also placed Nginx in front of the service. We used it for SSL termination, load balancing, and some logic on Lua (metrics, logging, and tracing).

For storing S3 metadata, we also chose Tarantool. In the first version, the S3 daemon accessed this database for metadata, while the content was stored in a large repository via Streamer.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage
Nginx + S3 API + metadata

Object storage model

Let's look at how S3 works. A user can create a bucket—a collection of objects. The bucket is addressed by the host name and serves as a subdomain of the service. Within the bucket, a user can create objects. The object's identifier will be the URL. The content of the object is a blob, an array of binary data that we will store in the repository. The object also has attributes: name—that same URL, ACL (access control list), and other additional or arbitrary attributes—all of this is saved in metadata.

The normalized schema of this data might look like this: there are projects that own buckets, which in turn own objects, and objects can be composite. Since one way to upload an object is in parts, there are two auxiliary tables for uploads: uploads and chunks. Also, projects have credentials for access and billing.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage
Data schema

Since we were creating a B2B service with paid access, billing was needed in this scheme.
We also implemented the billing service on Tarantool.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Improvements to the S3 storage: steps to production

We have already developed a working model that can be used: objects and metadata were stored, but several aspects were missing for a production rollout.

Po pierwsze, system limitów przepustowości. Jeśli uruchomimy usługę bez niego, w przypadku maksymalnego obciążenia możemy nieprzewidywalnie przeciążyć jakąś część systemu. Limit przepustowości powinien działać następująco: każde żądanie S3 trafia na konkretny host, ten host jest identyfikatorem koszyka, a koszyk należy do konkretnego klienta. Musimy określić pewną funkcję koszyka, która pozwoliłaby obliczyć limit przepustowości.

Ponadto system limitów przepustowości powinien być wystarczająco wydajny, aby wytrzymać obciążenie, które przychodzi na S3.

Tutaj ponownie użyliśmy Tarantoola. Limity przepustowości to klaster złożony z 21 instancji, instancje są podzielone na grupy, rozproszone na trzech fizycznych węzłach i połączone w dużym klastrze topologicznym. Automatycznie rozprzestrzeniają się po nim zmiany konfiguracyjne: ustalane są limity przepustowości, wartości domyślne oraz konfiguracja. Każdy koszyk obsługiwany jest wyłącznie przez jedną instancję. Gdy konkretne żądanie trafia do danego koszyka, obliczana jest instancja odpowiedzialna za ten koszyk. W ramach tego węzła liczony jest bieżący wskaźnik żądań za pomocą algorytmu podobnego do Token Bucket. Następnie system limitów przepustowości, na podstawie bieżących wskaźników obciążenia i właściwości ustalonych dla konkretnego koszyka, decyduje, czy można zrealizować żądanie, czy nie. Sprawdzenie limitów wykonywane jest na najwcześniejszym etapie realizacji żądania S3, chroniąc wszystkie pozostałe elementy systemu przed nadmiernym obciążeniem.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Pod dużym obciążeniem dość trudno obyć się bez cache. W S3 zakłada się wielokrotne odwołania do tych samych obiektów, czyli jest to gorące przechowywanie. W normalnym przypadku odwołanie do pojedynczego pliku obsługiwane jest przez pełny łańcuch: Streamer, FileDB, PairDB, Storage. Jednak przy wielokrotnym odwołaniu do pliku optymalizujemy dostęp do tej treści za pomocą lokalnej pamięci podręcznej.

Cache jest wielowarstwowy i realizowany za pomocą nginx, lokalnych dysków SSD i dysków RAM. Nie użyliśmy tutaj Tarantoola, ponieważ wygodniej jest dostarczać obiekty z systemu plików, co pozwala na tworzenie struktury cache. Ponadto mamy duże obiekty o maksymalnym rozmiarze 32 gigabajty, a w Tarantool można cache'ować tylko małe obiekty.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Ten pierwszy system, z którym wystartowaliśmy, miał pewną zaplanowaną pojemność, której wystarczyło, aby przeprowadzić badania, zrozumieć produkt oraz sprawdzić, że będzie działał.

Rozwój systemu operacyjnego: failover i skalowanie

System był już w użyciu, a na początku coś przeoczyliśmy — należało dodać failover i skalowanie.

Nasz demon S3 pobierał metadane za pomocą protokołu Tarantool. Zamiast pierwotnej bazy umieściliśmy Tarantool, który pełnił rolę proxy-routera zapytań o metadane. Z perspektywy aplikacji realizującej API, nic się nie zmieniło — wciąż komunikowała się z bazą za pomocą protokołu Tarantool, ale router mógł zapewnić aktywny failover. To znaczy, że mogliśmy sprawdzać dostępność węzła, przetrzymywać pauzy podczas przełączeń i awarii, i tak dalej. Przy tym nie modyfikowaliśmy samej aplikacji.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Więcej informacji o tym, jak zrealizowaliśmy sharding

Następne zagadnienie, które nas zmartwiło — to sharding. System rósł, liczba obiektów zwiększała się i trzeba było zapewnić możliwości dalszego wzrostu.

Wracając do schematu danych: są projekty, są kubełki, kredyty i billing. To obiekty, które z dużym prawdopodobieństwem nie przekroczą granic jednego instancji ani pod względem objętości, ani pod względem zapytań w przewidywalnej przyszłości. Oznacza to, że nie ma sensu ich shardować, dlatego przenieśliśmy je do osobnej instancji, która pozostanie nieshardowana. Umożliwia to bardziej konsekwentne zarządzanie projektami i kubełkami, ponieważ istnieje jedna, nieshardowana jednostka.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

W schemacie są również obiekty, które rosną linearne — najpierw było ich setki tysięcy, a obecnie ich liczba liczona jest w miliardach. Takie obiekty wraz z ich częściami musiały być przeniesione do klastra shardowanego.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Podzieliliśmy schemat, ale obiekty muszą współpracować z kubełkami: obiekt zawsze należy do konkretnego kubełka, a na kubełku działa ACL. Dlatego dla każdego shardu z obiektami przechowujemy kopię cieniową każdego kubełka. Dodatkowo, podczas modyfikacji obiektów i realizacji zapytań, trzeba liczyć objętość dla realizacji billingu, dlatego na każdym shardzie są liczniki billingowe.

Dodaliśmy również kilka dodatkowych tabel i komponentów:

  • kosz, do usuwania starych projektów, które są usuwane lub zamrażane;
  • kolejka dla zadań w tle, co oznacza, że główne magazynowanie może obsługiwać zadania w tle, które należy wykonać w klastrze;
  • wsparcie lifecycle — mechanizmu, który pozwala na pracę z obiektami i zarządzanie ich cyklem życia.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Ponieważ część danych przenieśliśmy na shardy, potrzebny był proxy shardujący. Można by było wykorzystać router do tej roli, ale oddzielny proxy shardujący, odpowiedzialny tylko za shardowanie danych, pozwala na zbieranie danych w całości z routera, nie myśląc o shardowaniu.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Osobno opowiem, dlaczego nie skorzystaliśmy z gotowego rozwiązania, a chcieliśmy stworzyć niestandardową funkcję shardowania.

Zobaczmy, jak to jest zorganizowane. Mamy 256 dostępnych shardów. Dla każdego bucketu przypisujemy zakres za pomocą pewnej funkcji konsystentnej. To jest proste — tak samo, jak za pomocą funkcji konsystentnej określasz przynależność do jednego sharda, określasz startowy shard i przydzielasz zakres:

f(bucket, shards) = subset

To znaczy, że jeśli weźmiesz bucket, możesz powiedzieć, że on i jego dane zawsze będą leżały na konkretnym podzbiorze wszystkich shardów. To pozwala zredukować wpływ niektórych bucketów na inne i uprościć pracę zapytań map-reduce, gdy trzeba na przykład stworzyć listing obiektów bucketu. W tym celu trzeba zapytać wszystkie shardy, na których te obiekty są przechowywane. Gdyby obiekty leżały na wszystkich shardach, każdy listing wpływałby na cały system, a tutaj wpływa tylko na konkretny podzbiór.

Dalej — każdy obiekt należy do konkretnego bucketu, więc gdy zwracamy się o obiekt, to pytamy o obiekt po nazwie w konkretnym bucketcie. To znaczy, że możemy określić funkcję dla obiektu nie z całego dostępnego zakresu shardów, a tylko z podzbioru jego bucketu:

f(object, subset) = shard

Bierzemy konkretny obiekt, jako argumenty funkcji przekazujemy tam nie wszystkie shardy, a podzbiór jego bucketu — i otrzymujemy konkretny shard.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Zatem shardowanie zostało zrealizowane, istnieje proxy shardujące. Następnie zostaje z routera i bazy danych metadanych korzystać z proxy shardującego. Na przykład do tworzenia obiektów kopii zapasowych — gdy tworzymy bucket, główne magazynowanie musi stworzyć przedstawiciela tego bucketu na wszystkich shardach, na których powinien być obecny.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Jak zrealizowaliśmy resharding

Największym problemem shardingu jest resharding. Ważne było dla nas, aby zrobić to bez przestojów, ponieważ system już działał w produkcji. Pokażę, jak rozwiązaliśmy ten problem na przykładzie podobnego zadania z żywą migracją danych z jednego projektu do drugiego.

Poniżej znajduje się schemat naszego klastra, który powstał po wdrożeniu shardingu. Mamy nginx, API S3, router, bazę primary z projektami, proxy shardujące oraz same shardy.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Powyżej pominąłem, że na pewnym etapie projektu był produktowy cel: „Uruchomić jeszcze jedno repozytorium, Icebox, jako Hotbox, tylko dla zimnych danych”. W zasadzie to takie samo repozytorium, ale pod innymi adresami URL i bez pamięci podręcznej.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Icebox był używany rzadziej niż Hotbox, dlatego długo obywał się bez jakiegokolwiek shardingu. Ostatecznie zdecydowaliśmy się z niego zrezygnować i połączyć Hotbox i Icebox w jeden serwis, oddzielając jedynie klasy przechowywania.

Koszyki w repozytoriach nie się krzyżowały, można je było łatwo połączyć i przenieść, ale klienci korzystali zarówno z jednego, jak i z drugiego repozytorium, więc musieliśmy rozwiązać problem braku przestojów. Nie można było po prostu wyłączyć i skopiować. Przeprowadziliśmy migrację w kilku etapach.

Na początek zsynchronizowaliśmy primary-repozytoria. Mieliśmy Tarantoola i mogliśmy przy tworzeniu obiektów zrobić to w ten sposób:

  • do bazy przychodzi żądanie utworzenia koszyka, na przykład w Hotbox;
  • Tarantool sprawdza w innej bazie (w tym przypadku w Icebox), że taki koszyk nie istnieje;
  • jeśli koszyk istnieje, baza informuje, że nie można go utworzyć, a został zsynchronizowany jako istniejący.
    Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage
    Synchronizacja koszyków

W tym repozytorium, które miało przyjąć wszystkie dane, wprowadziliśmy oznaczenie dla projektów i koszyków, które mówiło, gdzie przechowywany jest ten obiekt. Mógł on być przechowywany lokalnie, to znaczy w Hotbox, w Icebox — wtedy w nowym repozytorium nie ma żadnych danych, lub mógł być w stanie migracji.

Jeśli dla projektu lub koszyka ustawiono oznaczenie Migrating, to podczas migracji żądanie wykonywano najpierw w nowym repozytorium, w którym dane miały się znajdować, a jeśli ich tam nie było, to żądania były kierowane do alternatywnego repozytorium.

Następnie przełączyliśmy ruch. Ponieważ API mogło obsługiwać zarówno żądania Icebox, jak i żądania Hotbox, mogliśmy bez przestojów przełączyć ruch, po prostu przenosząc hosty i dodając odpowiednie wpisy w Nginx.

Po przekierowaniu ruchu, Nginx i API od Icebox mogły zostać usunięte.
Następnie usunęliśmy Icebox nginx i S3 API — i wszystko zaczęło działać:

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Potem uruchomiliśmy proces migracji w tle, który działa wewnątrz bazy – przechodzi przez wszystkie projekty i ich kosze, ustawia dla nich oznaczenie Migrating, przenosi dane, a po zakończeniu przenoszenia ustawia oznaczenie Local.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Po przeniesieniu danych nie potrzebujemy już starego magazynu, więc usuwamy pozostałe części starego systemu, a także usuwamy wsparcie statusu migracji z kodu.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

W oparciu o te same zasady wykonaliśmy również resharding ze starego magazynu na sharded:

  • Oznaczyliśmy wszystkie kosze jako Non-sharded. Wszystkie zapytania trafiały do oryginalnego, nieshardowanego magazynu.
  • Nowe kosze były tworzone od razu w statusie Sharded.
  • Jedno po drugim braliśmy kosze, ustalaliśmy status Migrating i przenosiliśmy dane.

Zapotrzebowanie było obsługiwane w następujący sposób:

  • Czytamy w nowym, potem w starym.
  • Tworzymy tylko w nowym.
  • Aktualizujemy dwuetapowo: jeśli w nowym nie ma, przenosimy z starego do nowego, potem aktualizujemy.

Praca z certyfikatami SSL

Na froncie używamy Nginx. W naszym przypadku to nie jest zwykły Nginx, lecz OpenResty, Nginx z wsparciem LuaJIT.

Kolejny element systemu — praca z certyfikatami SSL. W magazynie S3 możesz ustawić własną domenę do dostępu do konkretnego kosza, po prostu korzystając z CNAME. Jednak, bez HTTPS w dzisiejszych czasach się nie da: własna domena oznacza własny certyfikat SSL.

Jak już mówiłem, za balansowanie i terminację SSL odpowiada Nginx. W naszym przypadku to nie jest zwykły Nginx, lecz OpenResty, Nginx z wsparciem LuaJIT.

To pozwoliło nam dość łatwo nauczyć nasz Nginx udostępniać dowolne certyfikaty. Co więcej, musieliśmy udostępniać certyfikaty dynamicznie (bez potrzeby wpisywania ich w pliku konfiguracyjnym). Skorzystaliśmy z rozszerzenia ssl_certificate_by_lua, które umożliwia odczytanie certyfikatu z dowolnego źródła bezpośrednio podczas TLS handshake. Jako magazyn certyfikatów również wykorzystaliśmy Tarantool: pozwala to na zarządzanie certyfikatami z zewnątrz i zapewnia niezwykle szybką obsługę.

Zrealizowano również osobny demon, którego zadaniem jest regularne aktualizowanie certyfikatów wydanych przez Let’s Encrypt.

Architektura S3: 3 lata ewolucji Mail.ru Cloud Storage

Co bym zachował, a co zrobił inaczej, gdybym projektował magazyn od nowa

Co powinno być używane od samego początku

Shardowanie od razu. Wiele problemów sprawił resharding. Łatwo to zrobić, jednak w przypadku projektów, które potrzebują skalowania, lepiej od razu wziąć shardowany klaster, nawet na minimalnej liczbie węzłów. Wdrożenie shardingu na starcie jest prawie darmowe w porównaniu do wprowadzenia shardingu w działającym systemie.

Praca z Tarantool przez load balancery. Obecnie wszystkie nowe bazy od razu podłączamy do pracy przez load balancery. Umożliwia to rozszerzenie funkcjonalności i osiągnięcie wyższej odporności na awarie.

Auto failover. Zainstalowałbym wszystkie narzędzia potrzebne do auto failover, ponieważ pierwsze niepowodzenia po uruchomieniu były związane z jego brakiem. Po doświadczeniach z S3 wszystkie następne produkty były uruchamiane z uwzględnieniem tego.

Funkcja S3 „Wersjonowanie”. Początkowo wydawało się, że to nie jest bardzo potrzebna funkcjonalność. Wbudowanie tej możliwości w architekturę działającego systemu jest niezwykle trudne.

Osobne billing. To, jak zintegrowaliśmy billing z naszym systemem, dobrze sprawdziło się na początku, ale później zaczęło przeszkadzać, lepiej byłoby go zainstalować jako całkowicie oddzielną usługę.

Co było udanym rozwiązaniem

Model danych. Historia pokazuje, że w miarę rozwoju usługi dobrze trafiamy w model danych Amazona, dlatego możemy wdrażać te funkcje, które tam się znajdują.

Schemat shardowania. Poparłbym podobne shardowanie w oparciu o kubełki, ponieważ pozwala to dobrze rozdzielać zapytania z różnych kubełków po dużym klastrze.

Użycie Tarantool. Tarantool znacznie pomógł w rozwijaniu usługi i jej modyfikacji, łatwo pracowaliśmy z danymi, transformowaliśmy i shardowaliśmy magazyn, nie musieliśmy wchodzić na poziom aplikacji.

Ta prezentacja pierwszy raz została zaprezentowana na @Databases Meetup by Mail.ru Cloud Solutions&Tarantool. Zobacz wideo inne prezentacje i subskrybuj zapowiedzi wydarzeń w Telegramie Wokół Kubernetes w Mail.ru Group.

Możesz również obejrzeć mój stary wykład o S3 lub przeczytać artykuł mojego kolegi na temat magazynu blokowego.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster