
Artur Denisow ( , )
Badoo to największa na świecie strona randkowa. Aktualnie mamy zarejestrowanych około 330 milionów użytkowników na całym świecie. Ale, co znacznie ważniejsze w kontekście naszej dzisiejszej rozmowy, to fakt, że przechowujemy około 3 petabajtów zdjęć użytkowników. Codziennie nasi użytkownicy przesyłają około 3,5 miliona nowych zdjęć, a obciążenie przy odczycie wynosi około 80 tysięcy zapytań na sekundę. To całkiem sporo dla naszego backendu i czasami pojawiają się z tym trudności.

Opowiem o projektowaniu tego systemu, który przechowuje i udostępnia zdjęcia w ogóle, a także przedstawię na to spojrzenie z perspektywy programisty. O tym, jak to wszystko się rozwijało, będzie krótka retrospektywa, w której zaznaczę główne etapy, ale szczegółowo będę mówił tylko o tych rozwiązaniach, które obecnie wykorzystujemy.
A teraz zacznijmy.

Jak już wspomniałem, będzie to retrospektywa, a żeby ją zacząć, weźmy najprostszy przykład.

Mamy wspólne zadanie, musimy przyjmować, przechowywać i udostępniać zdjęcia użytkowników. W takiej postaci zadanie jest ogólne, możemy używać czegokolwiek:
- nowoczesnego chmurowego magazynu,
- rozwiązania na zasadzie ''box'', których teraz też jest bardzo dużo;
- możemy skonfigurować kilka maszyn w naszym centrum danych, zamontować na nich duże dyski twarde i przechowywać tam zdjęcia.
Badoo historycznie — i teraz, i wtedy (w czasach, gdy to się dopiero rozwijało) — działa na własnych serwerach, w naszych własnych centrach danych. Dlatego ta opcja była dla nas optymalna.

Po prostu wzięliśmy kilka maszyn, nazwaliśmy je 'photos', otrzymaliśmy taki klaster, który przechowuje zdjęcia. Ale wydaje się, że czegoś brakuje. Aby to wszystko działało, musimy w jakiś sposób określić, na której maszynie jakie zdjęcia będziemy przechowywać. I tutaj też nie trzeba odkrywać Ameryki.

Dodajemy do naszego magazynu z informacjami o użytkownikach jakieś pole. Będzie to klucz shardingu. W naszym przypadku nazwaliśmy go place_id, i ten id miejsca wskazuje na miejsce, w którym przechowywane są zdjęcia użytkowników. Tworzymy mapy.
Na pierwszym etapie można to robić nawet ręcznie — mówimy, że zdjęcie tego użytkownika z takim plikem będzie lądować na takim serwerze. Dzięki tej mapie zawsze wiemy, kiedy użytkownik załadowuje zdjęcie, gdzie je zapisać i skąd je oddać.
To całkowicie trywialny schemat, ale ma wystarczająco istotne zalety. Po pierwsze, jest prosty, jak już powiedziałem, a po drugie, dzięki takiemu podejściu możemy łatwo skalować horyzontalnie, po prostu dostarczając nowe maszyny i dodając je do mapy. Nic więcej nie trzeba robić.
I tak to jakiś czas było u nas.

To było gdzieś w 2009 roku. Dostarczaliśmy maszyny, dostarczaliśmy...
I w pewnym momencie zaczęliśmy zauważać, że ten schemat ma pewne wady. Jakie wady?
Przede wszystkim to ograniczona pojemność. Na jeden fizyczny serwer nie możemy zmieścić tak wielu dysków twardych, jak byśmy chcieli. A to z upływem czasu i wzrostem zbioru danych stało się pewnym problemem.
I po drugie. To nietypowa konfiguracja maszyn, ponieważ takie maszyny trudno ponownie wykorzystać w innych klastrach, są dość specyficzne, tzn. muszą być niskowydajne, ale jednocześnie z dużym dyskiem twardym.
To wszystko działo się w 2009 roku, ale zasadniczo te wymagania są aktualne i dzisiaj. Mamy retrospektywę, więc w 2009 roku wszystko było naprawdę źle z tymi kwestiami.
I ostatni punkt — to cena.

Cena wtedy była bardzo wysoka, i musieliśmy szukać jakichś alternatyw. Tzn. musieliśmy jakoś lepiej wykorzystywać zarówno przestrzeń w centrach danych, jak i same fizyczne serwery, na których to wszystko było umieszczone. Nasi inżynierowie systemowi rozpoczęli dużą analizę, w której przeglądali wiele różnych opcji. Zastanawiali się nad systemami plików klastrów, takimi jak PolyCeph i Lustre. Tam były problemy z wydajnością i dość trudna eksploatacja. Odrzucili. Próbowali zamontować cały zbiór danych przez NFS na każdą maszynę, aby w ten sposób jakoś to skalować. Odczyt też był kiepski, próbowali różnych rozwiązań od różnych dostawców.
I w końcu zdecydowaliśmy się na wykorzystanie tzw. Storage Area Network.

To są duże SHD, które są skierowane na przechowywanie dużych ilości danych. Stanowią one półki z dyskami, które są podłączone do końcowych maszyn przez światłowód. W ten sposób mamy jakąś pulę maszyn, stosunkowo niewielką, a te SHD, które są przezroczyste dla naszej logiki wydawania, tzn. dla naszego nginx lub kogoś innego, obsługują zapytania dotyczące tych zdjęć.
To rozwiązanie miało oczywiste zalety. To SHD. Jest skierowane na przechowywanie zdjęć. To wychodzi taniej, niż gdybyśmy po prostu ustawiać maszyny z dyskami twardymi.
Druga zaleta.

To, że pojemność stała się znacznie większa, tzn. możemy w znacznie mniejszej objętości pomieścić znacznie więcej storage’u.
Ale były też minusy, które ujawniły się dość szybko. Wraz ze wzrostem liczby użytkowników i obciążenia tej systemu zaczęły pojawiać się problemy z wydajnością. A problem jest dość oczywisty — każde SHD, które ma na celu przechowywanie wielu zdjęć w małym objętości, zazwyczaj cierpi na intensywne odczyty. To w rzeczywistości dotyczy każdego chmurowego storage’u i czegokolwiek innego. Obecnie nie istnieje idealny storage, który byłby nieskończony skalowalny, do którego można by wsadzić wszystko, co tylko się chce, i który dobrze by znosił odczyty. Szczególnie przypadkowe odczyty.

Jak w przypadku naszych zdjęć, ponieważ zdjęcia są żądane w sposób nie-sekwencyjny, co znacząco wpływa na ich wydajność.
Nawet według dzisiejszych danych, jeśli mamy więcej niż 500 RPS na zdjęcia na maszynę, do której podłączony jest storage, zaczynają się już problemy. I to było dla nas dość złe, ponieważ liczba użytkowników rośnie, a wszystko powinno tylko pogarszać się. Musimy to jakoś zoptymalizować.
Aby to zoptymalizować, postanowiliśmy wówczas oczywiście przyjrzeć się profilowi obciążenia — co w ogóle się dzieje, co trzeba zoptymalizować.

I w tym momencie wszystko działa na naszą korzyść.
Mówiłem już na pierwszym slajdzie: mamy 80 tysięcy zapytań na sekundę na odczyt przy zaledwie 3,5 miliona uploadów dziennie. To oznacza różnicę na trzy rzędy wielkości. Oczywiście konieczne jest zoptymalizowanie odczytu i właściwie wiadomo jak.
Jest jeszcze jeden mały szczegół. Specyfika usługi polega na tym, że użytkownik rejestruje się, wgrywa zdjęcie, a następnie aktywnie przegląda innych ludzi, lajkuje ich, a jego profil jest intensywnie pokazywany innym. Potem znajduje parę lub nie, jak to wyjdzie, i na jakiś czas przestaje korzystać z usługi. W tym momencie, kiedy jest aktywny, jego zdjęcia są bardzo popularne — wiele osób je przegląda. Gdy przestaje to robić, dość szybko wypada z intensywnego pokazywania innym, jak to miało miejsce wcześniej, a jego zdjęcia są praktycznie niewidoczne.

To znaczy, mamy bardzo mały, ale gorący zbiór danych. Jednak za nim stoi naprawdę wiele zapytań. Oczywistym rozwiązaniem jest dodanie pamięci podręcznej.
Pamięć podręczna z LRU rozwiąże wszystkie nasze problemy. Co robimy?

Dodajemy przed naszym dużym klastrem z magazynem jeszcze jeden stosunkowo niewielki, który nazywamy fotokaszą (photoscache). To w zasadzie po prostu proxy do pamięci podręcznej.
Jak to działa od wewnątrz? Oto nasz użytkownik, oto magazyn. Wszystko jak wcześniej. Co dodajemy między nimi?

To po prostu maszyna z fizycznym szybkim dyskiem lokalnym. Na przykład z SSD. Na tym dysku przechowywana jest jakaś lokalna pamięć podręczna.
Jak to wygląda? Użytkownik wysyła zapytanie o zdjęcie. NGINX najpierw szuka go w lokalnej pamięci podręcznej. Jeśli go nie ma, wykonuje po prostu proxy_pass do naszego magazynu, pobiera zdjęcie stamtąd i przekazuje je użytkownikowi.
Ale to jest bardzo proste i niejasne, co dzieje się wewnątrz. Działa to mniej więcej tak.

Pamięć podręczna jest logicznie podzielona na trzy warstwy. Kiedy mówię „trzy warstwy”, nie mam na myśli jakiegoś skomplikowanego systemu. Nie, to po prostu trzy katalogi w systemie plików:
- To bufor, w którym znajdują się właśnie wgrane zdjęcia z proxy.
- To gorąca pamięć podręczna, w której przechowywane są aktywnie żądane zdjęcia.
- I zimna pamięć podręczna, do której stopniowo przesuwane są zdjęcia z gorącej, gdy przychodzi do nich mniej zapytań.
Aby to działało, musimy jakoś zarządzać tą pamięcią podręczną, musimy przestawiać zdjęcia w niej itd. To również bardzo prosty proces.

Nginx po prostu zapisuje w RAMDisk access.log dla każdego żądania, w którym wskazuje ścieżkę do zdjęcia, które aktualnie obsłużył (oczywiście ścieżka względna) oraz to, jakim działem zostało obsłużone. Tzn. może tam być napisane „zdjęcie 1”, a następnie bufor, gorący cache, zimny cache lub proxy.
W zależności od tego musimy jakoś podjąć decyzję, co zrobić ze zdjęciem.
Na każdej maszynie działa mały demon, który ciągle odczytuje ten log i w swojej pamięci przechowuje statystyki dotyczące użycia poszczególnych zdjęć.

Po prostu zbiera tam dane, prowadzi liczniki i okresowo wykonuje następujące czynności. Aktywnie żądane zdjęcia, które zbierają wiele żądań, przenosi do gorącego cache, gdziekolwiek się znajdują.

Zdjęcia, które są rzadko żądane i zaczęły być żądane rzadziej, stopniowo wypycha z gorącego cache do zimnego.

A kiedy w naszym cache brakuje miejsca, po prostu zaczynamy usuwać wszystko z zimnego cache bez rozróżnienia. I to, nawiasem mówiąc, działa bardzo dobrze.
Aby zdjęcie było natychmiast zapisywane podczas proxy w buforze, używamy dyrektywy proxy_store, a bufor to również RAMDisk, tzn. dla użytkownika działa to bardzo szybko. To jeśli chodzi o wnętrze samego serwera cache.
Pozostało pytanie, jak rozdzielać żądania pomiędzy te serwery.
Powiedzmy, że mamy klaster dwudziestu maszyn storage i trzy serwery cache (tak się złożyło).

Musimy w jakiś sposób określić, jakie żądania dotyczą jakich zdjęć i gdzie je umieścić.
Najprostsza opcja to Round Robin. A może działać losowo?
To oczywiście ma szereg wad, ponieważ będziemy bardzo nieefektywnie wykorzystywać cache w takiej sytuacji. Żądania będą trafiały na jakieś losowe maszyny: tutaj jest cached, na sąsiedniej już jej nie ma. I wszystko to, jeśli w ogóle zadziała, to bardzo źle. Nawet przy niewielkiej liczbie maszyn w klastrze.
Musimy w jakiś sposób jednoznacznie określić, na który serwer trafi które żądanie.
Jest prosty sposób. Bierzemy hash z URL’a lub hash z naszego klucza shardującego, który znajduje się w URL’u, i dzielimy go całkowicie przez liczbę serwerów. Czy to zadziała? Tak.

To znaczy, mamy stuprocentowe żądanie, na przykład dla jakiegoś «example_url» zawsze będzie trafiało na serwer z indeksem „2”, a pamięć podręczna będzie stale optymalizowana.
Ale pojawia się problem z reshadingiem w takim schemacie. Resharding — mam na myśli zmianę liczby serwerów.
Załóżmy, że nasz klaster pamięci podręcznej przestał sobie radzić, i postanowiliśmy dodać jeszcze jedną maszynę.
Dodajemy.

Teraz wszystko dzieli się na cztery, a nie na trzy. W ten sposób prawie wszystkie klucze, które wcześniej mieliśmy, prawie wszystkie URL-e teraz znajdują się na innych serwerach. Cała pamięć podręczna została unieważniona w mgnieniu oka. Wszystkie żądania spłynęły do naszego klastra storage, sytuacja stała się zła, usługa padła, a użytkownicy byli niezadowoleni. Tak nie chcemy robić.
Ten wariant również nam nie odpowiada.
Co więc musimy zrobić? Musimy w jakiś sposób efektywnie wykorzystać pamięć podręczną, stale kierować jedno żądanie na ten sam serwer, ale jednocześnie być odporni na reshading. I takie rozwiązanie istnieje, nie jest jakoś bardzo skomplikowane. Nazywa się to consistent hashing.

Jak to wygląda?

Bierzemy jakąś funkcję od klucza shardingowego i rozkładamy wszystkie jej wartości na okręgu. To znaczy, w punkcie 0 zbiegają się jej minimalne i maksymalne wartości. Następnie na tym samym okręgu umieszczamy wszystkie nasze serwery mniej więcej w ten sposób:

Każdy serwer określany jest jednym punktem, a sektor, który idzie do niego w kierunku wskazówek zegara, odpowiednio, jest obsługiwany przez ten host. Kiedy przychodzą do nas żądania, od razu widzimy, że na przykład żądanie A — ma taki hash — i jest obsługiwane przez serwer 2. Żądanie B — przez serwer 3. I tak dalej.

Co w tej sytuacji się dzieje podczas reshadingu?

Nie unieważniamy całej pamięci podręcznej, jak wcześniej, i nie przesuwamy wszystkich kluczy, a przesuwamy każdy sektor na małą odległość w taki sposób, aby w zwolnione miejsce, mówiąc kolokwialnie, zmieścił się nasz szósty serwer, który chcemy dodać, i dodajemy go tam.

Oczywiście, w takiej sytuacji klucze również mogą się przemieszczać. Ale przesuwają się znacznie mniej niż wcześniej. Widzimy, że nasze dwa pierwsze klucze pozostały na swoich serwerach, a zmienił się tylko serwer cache dla ostatniego klucza. Działa to wystarczająco efektywnie, a jeśli dodajesz nowe hosty w sposób inkrementalny, to nie ma tutaj dużego problemu. Po prostu dodajesz po trochu, czekasz, aż cache znowu się napełni, i wszystko działa dobrze.
Pozostaje tylko jedno pytanie przy awariach. Załóżmy, że mamy jakiś serwer, który przestał działać.

I w tym momencie nie chcielibyśmy regenerować tej mapy, unieważniać części cache i tak dalej, jeśli na przykład maszyna zrestartowała się, a my musimy obsługiwać zapytania. Po prostu trzymamy na każdej lokalizacji jedną zapasową kopię cache, która pełni rolę zamiennika dla każdej maszyny, która obecnie jest niedostępna. I jeśli nagle jakiś serwer stał się niedostępny, ruch idzie tam. Zasadniczo nie mamy tam żadnego cache, to znaczy jest on zimny, ale przynajmniej zapytania użytkowników są obsługiwane. Jeśli to krótki interwał, to spokojnie to przetrwamy. Po prostu większe obciążenie trafia na pamięć masową. Jeśli interwał jest długi, możemy już podjąć decyzję — usunąć ten serwer z mapy czy nie, albo może zastąpić go innym.
Mówiąc o systemie cache, przyjrzyjmy się wynikom.
Zdawałoby się, że nie ma tu nic skomplikowanego. Ale ten sposób zarządzania cachem dał nam współczynnik hitów na poziomie około 98%. To znaczy, że z tych 80 tysięcy zapytań na sekundę tylko 1600 dociera do pamięci masowej, co jest całkowicie normalnym obciążeniem, które spokojnie to znosi.
Umieściliśmy te serwery w trzech naszych centrach danych i uzyskaliśmy trzy punkty obecności — Praga, Miami i Hongkong.

Zatem są one mniej więcej lokalnie usytuowane względem każdego z naszych docelowych rynków.
I jako miły bonus otrzymaliśmy ten cache'ujący proxy, na którym procesor tak naprawdę stoi bezczynnie, ponieważ do dostarczania treści nie jest tak bardzo potrzebny. A tam, przy użyciu NGINX + Lua, zrealizowaliśmy wiele przydatnych funkcjonalności.

Na przykład możemy eksperymentować z webp lub progresywnym jpeg (to nowoczesne i efektywne formaty), obserwować, jak to wpływa na ruch, podejmować decyzje, włączać dla określonych krajów itp.; robić dynamiczne zmiany rozmiaru lub przycinać zdjęcia w locie.
To dobry przypadek użycia, gdy na przykład mamy aplikację mobilną, która pokazuje zdjęcia, a aplikacja mobilna nie chce obciążać CPU klienta, aby zażądać dużego zdjęcia i zmieniać jego rozmiar do jakiegoś rozmiaru, aby zmieścić je w widoku. Możemy po prostu dynamicznie określić w URL-u jakieś parametry w UPort i pamięć podręczna zdjęć sama przeskaluje zdjęcie. Zazwyczaj dobiera rozmiar, który fizycznie mamy na dysku, maksymalnie bliski żądanemu, i dostosowuje go w konkretnych już współrzędnych.
Przy okazji, udostępniliśmy nagrania wideo z ostatnich pięciu lat konferencji programistów systemów o dużym obciążeniu. . Oglądaj, poznawaj, dziel się i subskrybuj nasz .
Możemy także dodawać do tego wiele logiki produktowej. Na przykład, możemy dodawać różne znaki wodne na podstawie parametrów URL, możemy zamazywać zdjęcia, zacierać lub pikselizować. Kiedy chcemy pokazać zdjęcie osoby, ale nie chcemy pokazywać jej twarzy, to działa bardzo dobrze, wszystko jest tu zrealizowane.
Co osiągnęliśmy? Osiągnęliśmy trzy punkty obecności, dobry wskaźnik trafień, a jednocześnie nasz CPU nie stoi bezczynnie na tych maszynach. Teraz stał się, oczywiście, ważniejszy niż wcześniej. Musimy stawiać mocniejsze maszyny, ale to się opłaca.
Jeśli chodzi o przesyłanie zdjęć, wszystko jest dość jasne i oczywiste. Myślę, że nie odkryłem Ameryki, tak działa praktycznie każdy CDN.
I prawdopodobnie doświadczony słuchacz mógłby zadać pytanie: dlaczego po prostu nie wziąć i nie zmienić wszystkiego na CDN? Byłoby mniej więcej to samo, wszystkie nowoczesne CDN to potrafią. I tu jest kilka powodów.
Pierwszy to zdjęcia.

To jeden z kluczowych elementów naszej infrastruktury, i potrzebujemy nad nimi jak najwięcej kontroli. Jeśli to jakieś rozwiązanie u zewnętrznego dostawcy, i nie masz nad nim żadnej władzy, będzie ci dość trudno z tym żyć, gdy masz dużą bazę danych i kiedy masz bardzo duży przepływ zapytań użytkowników.
Podam przykład. Teraz, korzystając z naszej infrastruktury, możemy, na przykład, w przypadku jakichkolwiek problemów lub podziemnych uderzeń, wejść na maszynę, aby przeprowadzić debugging. Możemy dodać zbieranie niektórych metryk, które tylko nas interesują, możemy eksperymentować, patrzeć, jak to wpłynie na wykresy i tak dalej. Obecnie zbieramy bardzo dużo statystyk z tego klastra pamięci podręcznej. I okresowo na nie patrzymy i długo badamy pewne anomalia. Gdyby to było po stronie CDN, kontrolowanie tego byłoby znacznie trudniejsze. Albo, na przykład, gdy dochodzi do jakiejkolwiek awarii, wiemy, co się stało, wiemy, jak z tym żyć i jak to pokonać. To pierwszy wniosek.
Drugi wniosek jest również raczej historyczny, ponieważ system rozwija się już długo, a wiele różnych wymagań biznesowych w różnych etapach istniało i nie zawsze pasują one do koncepcji CDN.
I punkt, który wynika z poprzedniego –

To, że na fotokashach mamy wiele specyficznej logiki, której nie zawsze można dodać na żądanie. Mało prawdopodobne, że jakiś CDN doda jakieś niestandardowe rzeczy na twoje żądanie. Na przykład, szyfrowanie URL, jeśli nie chcesz, aby klient mógł coś zmieniać. Chcesz zmienić URL na serwerze i go zaszyfrować, a potem przekazać tu jakieś dynamiczne parametry.
Jaki wniosek się nasuwa? W naszym przypadku CDN to niezbyt dobra alternatywa.

A w twoim przypadku, jeśli masz jakieś specyficzne wymagania biznesowe, możesz samodzielnie wdrożyć to, co ci pokazałem. I to przy podobnym profilu obciążenia będzie działać doskonale.
Ale jeśli masz jakieś ogólne rozwiązanie, a zadanie nie jest zbyt specyficzne, możesz spokojnie korzystać z CDN. Albo jeśli dla ciebie znacznie ważniejsze jest czas i zasoby niż kontrola.

Nowoczesne CDN mają praktycznie wszystko, o czym ci teraz opowiedziałem. Z wyjątkiem mniej więcej kilku funkcji.
To jest w odniesieniu do dostarczania zdjęć.
Przenieśmy się teraz nieco naprzód w naszej retrospektywie i porozmawiajmy o przechowywaniu.
Rok 2013 trwał.

Serwery cache'ujące dołączyły, problemy z wydajnością zniknęły. Wszystko jest w porządku. Dataset rośnie. W 2013 roku mieliśmy około 80 serwerów podłączonych do storage, oraz około 40 serwerów cache'ujących w każdym centrum danych. To daje 560 terabajtów danych w każdym centrum danych, czyli około petabajta w sumie.

Razem z rosnącym dataset'em zaczęły znacznie wzrastać koszty eksploatacyjne. Na czym to polegało?

W tym schemacie, który jest narysowany — z SAN, z podłączonymi do niego maszynami i cache'ami — jest bardzo wiele punktów awaryjnych. Jeśli z awarią serwerów cache'ujących poradziliśmy sobie wcześniej, tam wszystko jest bardziej przewidywalne i jasne, to po stronie storage było znacznie gorzej.
Po pierwsze, sam Storage Area Network (SAN), który może ulec awarii.
Po drugie, jest podłączony przez światłowód do końcowych maszyn. Mogą wystąpić problemy z kartami optycznymi, switchami.

Jest ich wprawdzie nieco mniej niż samego SAN, ale mimo to, są to też punkty awaryjne.
Następnie sama maszyna, która jest podłączona do storage. Ona również może ulec awarii.

Tak więc mamy trzy punkty awaryjne.
Ponadto, oprócz punktów awaryjnych, to trudny maintenance samych storage’ów.
To złożony system wieloskładnikowy, z którym inżynierowie systemowi mają czasem trudności.
I ostatni, najważniejszy punkt. Jeśli w którymkolwiek z tych trzech punktów wystąpi awaria, istnieje niezerowe ryzyko utraty danych użytkownika, ponieważ może uszkodzić się system plików.

Załóżmy, że uszkodził się system plików. Jego przywracanie trwa, po pierwsze, długo — może to zająć tydzień przy dużej ilości danych. Po drugie, w efekcie najprawdopodobniej otrzymamy mnóstwo niezrozumiałych plików, które trzeba będzie w jakiś sposób powiązać z zdjęciami użytkowników. I ryzykujemy utratę danych. Ryzyko jest dość wysokie. Im częściej występują takie sytuacje, i im więcej problemów pojawia się w całym łańcuchu, tym to ryzyko rośnie.
Trzeba było coś z tym zrobić. I zdecydowaliśmy, że po prostu musimy rezerwować dane. To w zasadzie oczywiste i dobre rozwiązanie. Co zrobiliśmy?

Tak wyglądał nasz serwer, który był wcześniej podłączony do storage. To jedna główna partycja, to po prostu urządzenie blokowe, które w rzeczywistości reprezentuje zamontowany zdalny storage przez światłowód.
Po prostu dodaliśmy drugą partycję.

Postawiliśmy obok drugi storage (na szczęście nie kosztuje to tak dużo) i nazwaliśmy go wydzieleniem backupowym. Również jest podłączony za pomocą światłowodu, znajduje się na tej samej maszynie. Musimy jednak jakoś zsynchronizować dane między nimi.
Tutaj po prostu robimy obok asynchroniczną kolejkę.

Nie jest to zbyt obciążone. Wiemy, że mamy mało rekordów. Kolejka to po prostu tabelka w MySQL, do której wpisywane są linijki typu „trzeba zrobić backup tej fotografii”. Przy każdej zmianie lub przesyłaniu kopiujemy z głównego wydziału na backup asynchronicznie lub po prostu jakimś background workerem.
W ten sposób zawsze mamy dwa spójne wydziały. Nawet jeśli jedna część tego systemu ulegnie awarii, zawsze możemy zamienić główny wydział z backupem, i wszystko będzie działać dalej.
Ale przez to znacznie wzrasta obciążenie odczytu, ponieważ oprócz klientów, którzy czytają z głównego wydziału, gdyż najpierw oglądają zdjęcie tam (jest ono tam bardziej aktualne), a dopiero potem szukają na backupie, jeśli nie znaleźli (ale to już NGINX po prostu robi), jest jeszcze nasz system backupu, który teraz wyciąga z głównego wydziału. To nie jest wąskie gardło, ale nie chciałem zwiększać obciążenia, w zasadzie prawie bez powodu.
Dodaliśmy trzeci dysk, który jest małym SSD, i nazwaliśmy go buforem.

Jak to teraz działa.
Użytkownik przesyła zdjęcie na bufor, następnie wysyłany jest event do kolejki o tym, że należy je skopiować na dwa wydziały. Jest kopiowane, a zdjęcie przez jakiś czas (powiedzmy 24 godziny) żyje na buforze, a dopiero potem jest usuwane. To znacznie poprawia doświadczenie użytkownika, ponieważ użytkownik zazwyczaj przesyła zdjęcie, po czym od razu zaczynają spływać requesty, lub sam odświeża stronę. Ale to wszystko zależy od aplikacji, która dokonuje uploadu.
Lub na przykład inni ludzie, którym zaczyna się pokazywać, od razu wysyłają requesty w związku z tym zdjęciem. W cache jeszcze go nie ma, pierwszy request następuje bardzo szybko. W zasadzie, tak samo jak z cachem zdjęć. Wolny storage w ogóle nie bierze w tym udziału. A kiedy po 24 godzinach będzie usunięte, będzie już albo zakotwiczone w naszej warstwie cache, albo najprawdopodobniej nikomu nie będzie już potrzebne. Tzn. doświadczenie użytkownika bardzo wzrosło dzięki tym prostym manipulacjom.
No i najważniejsze: przestaliśmy tracić dane.

Powiedzmy, że przestaliśmy potencjalnie możemy stracić dane, ponieważ tak naprawdę ich nie straciliśmy. Jednak niebezpieczeństwo istniało. Widzimy, że takie rozwiązanie jest oczywiście dobre, ale przypomina trochę łagodzenie objawów problemu, zamiast rozwiązać go całkowicie. Niektóre problemy tutaj pozostały.
Po pierwsze, to punkt awarii w postaci samego fizycznego hosta, na którym działa cała ta maszyna; to nadal jest problemem.

Po drugie, pozostały problemy z SAN’ami, ciężka konserwacja itp. To nie był krytyczny czynnik, ale miło byłoby spróbować żyć bez tego.
Stworzyliśmy trzecią wersję (w zasadzie drugą) — wersję rezerwowania. Jak to wyglądało?
To, co mieliśmy –

Główne problemy, z którymi się borykamy, dotyczą faktu, że to fizyczny host.
Po pierwsze, eliminujemy SAN'y, ponieważ chcemy przeprowadzić eksperyment, chcemy spróbować po prostu lokalnych dysków twardych.

To już rok 2014-2015, a sytuacja z dyskami i ich pojemnością w jednym hoście znacznie się poprawiła. Postanowiliśmy, dlaczego by nie spróbować.
Następnie po prostu bierzemy naszą partycję backupową i przenosimy ją fizycznie na oddzielną maszynę.

W ten sposób uzyskujemy następujący schemat. Mamy dwie maszyny, które przechowują te same zestawy danych. Wzajemnie rezerwują się całkowicie i synchronizują dane przez sieć poprzez asynchroniczną kolejkę w tym samym MySQL.

Dlaczego to działa dobrze — ponieważ mamy niewiele zapisów. To znaczy, gdyby zapis był porównywalny z odczytem, moglibyśmy napotkać jakiś narzut sieciowy i problemy. Mało zapisów, dużo odczytów — ten sposób działa dobrze, tzn. dość rzadko kopiujemy zdjęcia między tymi dwoma serwerami.
Jak to działa, jeśli spojrzymy na to nieco dokładniej.

Upload. Balancer po prostu wybiera losowe hosty z pary i umieszcza na nim upload. Oczywiście przeprowadza testy zdrowotne, upewniając się, że maszyna nie wypadła. To znaczy, że przesyła zdjęcia tylko na działający serwer, a następnie przez asynchroniczną kolejkę wszystko to kopiuje do jego sąsiada. Z upload'em jest wszystko bardzo proste.
Z zadaniem jest trochę bardziej skomplikowane.

Tutaj pomógł nam Lua, ponieważ na zwykłym NGINX trudno czasami zrealizować taką logikę. Najpierw wysyłamy żądanie do pierwszego serwera, sprawdzamy, czy tam jest zdjęcie, ponieważ potencjalnie może być ono załadowane na sąsiedni serwer, a do nas jeszcze nie dotarło. Jeśli zdjęcie tam jest, to dobrze. Od razu przekazujemy je klientowi i być może je cache'ujemy.

Jeśli go nie ma, po prostu prosimy sąsiada o zdjęcie i tam otrzymujemy je gwarantowanie.

Tak więc można znów powiedzieć: mogą być problemy z wydajnością, ponieważ ciągłe okrążenia — zdjęcie zostało załadowane, a tutaj go nie ma, wykonujemy dwa zapytania zamiast jednego, to powinno działać wolno.
W naszej sytuacji to nie działa wolno.

Zbieramy mnóstwo metryk z tego systemu, a hipotetyczny wskaźnik trafności tego mechanizmu wynosi około 95%. To znaczy, że opóźnienie tego backupu jest małe, i dzięki temu praktycznie zawsze, po tym jak zdjęcie zostało załadowane, pobieramy je za pierwszym razem i nie chodzimy dwa razy.
W ten sposób, co jeszcze zyskaliśmy, co jest bardzo fajne?
Dawniej mieliśmy główny dział backupu i czytaliśmy z nich sekwencyjnie. Zawsze najpierw szukaliśmy na głównym, a potem na backupie. To było jedno podejście.
Teraz utylizujemy odczyt z dwóch maszyn jednocześnie. Rozdzielamy zapytania metodą Round Robin. W małym procencie przypadków wykonujemy dwa zapytania. Ale dzięki temu teraz mamy dwukrotnie większą rezerwę czytania niż wcześniej. I obciążenie znacznie się zmniejszyło zarówno na maszynach odsyłających, jak i na pamięciach, które mieliśmy w tym czasie.
Co do odporności na awarie. Właściwie to o to głównie walczyliśmy. Z odpornością na awarie wszystko poszło świetnie.

Jedna maszyna przestaje działać.

Żadnych problemów! Inżynier systemowy może nawet nie budzić się w nocy, poczeka do rana, nic złego się nie stanie.
Jeśli nawet kolejka zamarza przy awarii tej maszyny, też nie ma żadnych problemów, po prostu log będzie gromadzony najpierw na żywej maszynie, a potem już dojdzie do kolejki, a następnie do tej maszyny, która zostanie przywrócona do działania po pewnym czasie.

To samo dotyczy konserwacji. Po prostu wyłączamy jedną z maszyn, ręcznie usuwamy ją ze wszystkich pul, przestaje ona przetwarzać ruch, wykonujemy jakieś prace konserwacyjne, coś poprawiamy, a potem przywracamy ją do działania i ten backup dogania się wystarczająco szybko. Tzn. przy dobie jednego dnia przestoju jednej maszyny dogania się w granicach kilku minut. To jest naprawdę bardzo mało. Mówiąc o odporności na błędy, jeszcze raz podkreślam, wszystko tu jest świetne.
Jakie wnioski można wyciągnąć z tego schematu rezerwacji?
Uzyskaliśmy odporność na błędy.
Prosta eksploatacja. Ponieważ na maszynach znajdują się lokalne twarde dyski, jest to o wiele wygodniejsze z punktu widzenia inżynierów, którzy z tym pracują.
Uzyskaliśmy podwójny zapas przy odczycie.
To bardzo dobry bonus w dodatku do odporności na błędy.
Ale są i problemy. Teraz mamy znacznie bardziej skomplikowany rozwój różnych funkcji związanych z tym, ponieważ system stał się w 100% ostatecznie spójny.

Musimy, powiedzmy, w jakiejś pracy w tle ciągle myśleć: "Na którym serwerze teraz działamy?", "Czy na pewno tutaj jest aktualne zdjęcie?" itd. To, naturalnie, wszystko jest owinięte w różne obiekty i dla programisty, który pisze logikę biznesową, jest to przezroczyste. Jednakże pojawiła się znaczna złożoność. Ale jesteśmy gotowi to zaakceptować w zamian za korzyści, które z tego wynikają.
I tutaj znów pojawia się pewien konflikt.
Na początku mówiłem, że trzymanie wszystkiego na lokalnych twardych dyskach jest złe. A teraz mówię, że nam się to podoba.
Tak, rzeczywiście, z biegiem czasu sytuacja znacznie się zmieniła i teraz ten sposób ma wiele zalet. Po pierwsze, mamy znacznie prostsza eksploatację.
Po drugie, to jest wydajniejsze, ponieważ nie mamy tych automatycznych kontrolerów do podłączania do macierzy dyskowych.
Tam jest ogromna maszyna, a to po prostu kilka dysków, które konkretne są tutaj na maszynie skonfigurowane w RAID.
Ale są i minusy.

To jest około 1,5 razy droższe niż używanie SAN-ów nawet przy dzisiejszych cenach. Dlatego zdecydowaliśmy się tak odważnie nie konwertować całego naszego dużego klastra na maszyny z lokalnymi twardymi dyskami i postanowiliśmy pozostawić rozwiązanie hybrydowe.
Połowa naszych maszyn pracuje na dyskach twardych (no, nie połowa – pewnie 30 procent). A reszta to stare maszyny, na których wcześniej była pierwsza schemat rezerwacji. Po prostu je przestawiliśmy, ponieważ nie potrzebujemy ani nowych danych, ani nic innego, po prostu przenieśliśmy montaż z jednego fizycznego hosta na dwa.
I zyskaliśmy dużą rezerwę w odczycie, a my się powiększyliśmy. Jeśli wcześniej montowaliśmy jeden storage na jedną maszynę, teraz montujemy cztery na jedną parę, na przykład. I to działa poprawnie.
Podsumujmy krótko to, co osiągnęliśmy, o co walczyliśmy i co nam się udało.
Podsumowanie
Mamy użytkowników – całe 33 mln.
Mamy trzy punkty obecności – Praga, Miami, Hongkong.
Zawierają one warstwę buforującą, która składa się z maszyn z szybkimi lokalnymi dyskami (SSD), na których działa prosta infrastruktura z NGINX, jego access.log oraz demony w Pythonie, które wszystko przetwarzają i zarządzają cachem.
Jeśli chcesz, w swoim projekcie, jeśli zdjęcia nie są dla Ciebie tak krytyczne, jak dla nas, lub jeśli trade-off kontrola w porównaniu z szybkością rozwoju i kosztami zasobów jest dla Ciebie w inny sposób, możesz spokojnie zastąpić to CDN, nowoczesne CDN dobrze to robią.
Następnie znajduje się warstwa pamięci, na której mamy klastry par maszyn, które wzajemnie się rezerwują, asynchronicznie kopiując pliki z jednego na drugi przy każdej zmianie.
Część tych maszyn pracuje z lokalnymi dyskami twardymi.
Część tych maszyn jest podłączona do SAN.

Z jednej strony jest to wygodniejsze w eksploatacji i nieco bardziej wydajne, z drugiej strony jest to wygodne z punktu widzenia gęstości rozmieszczenia i ceny za gigabajt.
To taki krótki przegląd architektury tego, co osiągnęliśmy i jak to wszystko się rozwijało.
Jeszcze kilka prostych porad od kefa.
Po pierwsze, jeśli nagle zdecydujesz, że musisz pilnie poprawić swoją infrastrukturę zdjęć, najpierw zmierz to, bo możliwe, że nic nie trzeba poprawiać.

Podam przykład. Mamy klaster maszyn, które dostarczają zdjęcia z attachmentów w czatach, i tam do tej pory działa schemat z 2009 roku, i nikt na tym nie cierpi. Wszystkim jest dobrze, wszystkim się podoba.
Aby przeprowadzić pomiary, najpierw zbierz wiele metryk, spojrz na nie, a następnie zdecyduj, z czego jesteś niezadowolony i co należy poprawić. Do mierzenia mamy świetne narzędzie zwane Pinba.
Umożliwia on zbieranie bardzo szczegółowych statystyk z NGINX dla każdego żądania, kodów odpowiedzi oraz rozkładów czasów — cokolwiek zechcesz. Posiada wtyczki do różnych systemów analitycznych, a potem możesz wszystko to ładnie przeglądać.
Najpierw zmierzono - potem poprawiono.
Dalej. Optymalizujemy odczyt za pomocą pamięci podręcznej, a zapis - poprzez sharding, ale to jest oczywisty punkt.

Dalej. Jeśli dopiero zaczynasz budować swój system, znacznie lepiej jest traktować zdjęcia jako pliki immutable. Ponieważ w ten sposób tracisz od razu cały typ problemów związanych z unieważnieniem pamięci podręcznej, jak logika ma znaleźć odpowiednią wersję zdjęcia, i tak dalej.

Załóżmy, że wrzuciłeś stówkę, a następnie ją obróciłeś, upewnij się, że jest to fizycznie inny plik. To znaczy, nie myśl: 'teraz zaoszczędzę trochę miejsca, zapisując w tym samym pliku, zmieniając wersję'. Zawsze to źle działa, potem jest z tym wiele bólu głowy.
Następny punkt. O resize w locie.
Kiedyś, gdy użytkownicy przesyłali zdjęcia, cięliśmy od razu całą masę rozmiarów na wszelki wypadek dla różnych klientów, które wszystkie leżały na dysku. Teraz z tego zrezygnowaliśmy.
Zostawiliśmy tylko trzy podstawowe rozmiary: mały, średni i duży. Wszystko inne po prostu skalujemy w dół z rozmiaru, który jest podstawą zapytania w Uport, po prostu robiąc downsizing i oddajemy użytkownikowi.
Koszt CPU w warstwie pamięci podręcznej jest znacznie niższy niż gdybyśmy stale regenerowali te rozmiary na każdym storage'u. Załóżmy, że chcemy dodać nowy, to zajmie miesiąc - uruchomić wszędzie skrypt, który to wszystko starannie zrobi, a przy tym nie uszkodzi klastra. To znaczy, jeśli jest możliwość, lepiej jest mieć jak najmniej fizycznych rozmiarów, ale aby istniało jakieś rozkładanie, powiedzmy, trzy. A wszystko inne po prostu resize'ować w locie za pomocą gotowych modułów. To jest teraz bardzo łatwe i dostępne.
A inkrementalna asynchroniczna kopia zapasowa to dobra rzecz.
Jak pokazuje nasza praktyka, tego typu schemat świetnie działa z opóźnionym kopiowaniem zmodyfikowanych plików.

Ostatni punkt jest również oczywisty. Jeśli w twojej infrastrukturze nie ma teraz takich problemów, ale istnieje coś, co może się zepsuć, z pewnością zepsuje się, gdy tego będzie nieco więcej. Dlatego lepiej pomyśleć o tym wcześniej i unikać problemów. To wszystko ode mnie.
Kontakt
»
»
Ten raport to transkrypcja jednego z najlepszych wystąpień na konferencji dla twórców wysokowydajnych systemów. . Do konferencji HighLoad++ 2017 pozostało mniej niż miesiąc.
Mamy już gotową , obecnie intensywnie formowane jest harmonogram.
W tym roku kontynuujemy badanie tematu architektur i skalowania:
- / Игорь Васильев
- / Дмитрий Егоров
- / Анатолий Пласковский
- / Роман Шеховцов, Алексей Громатчиков
- / Филипп Дельгядо
Niektóre z tych materiałów są również wykorzystywane przez nas w kursie online o tworzeniu wysokowydajnych systemów. — to seria starannie dobranych listów, artykułów, materiałów i filmów. Już teraz w naszym podręczniku jest ponad 30 unikalnych materiałów. Dołącz do nas!
Źródło: habr.com
