One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Aloha, ludzie! Nazywam się Oleg Anastasiew, pracuję w Odnoklassnikach w zespole Platformy. Oprócz mnie, w Odnoklassnikach pracuje wiele urządzeń. Mamy cztery centra danych, w których znajduje się około 500 stojaków z ponad 8000 serwerów. W pewnym momencie zrozumieliśmy, że wdrożenie nowego systemu zarządzania pozwoli nam efektywniej wykorzystać sprzęt, ułatwić zarządzanie dostępami, zautomatyzować (prze)dystrybucję zasobów obliczeniowych, przyspieszyć uruchamianie nowych usług oraz reakcje na masowe awarie.

Co z tego wyszło?

Oprócz mnie i mnóstwa sprzętu są też ludzie, którzy pracują z tym sprzętem: inżynierowie znajdujący się bezpośrednio w centrach danych; sieciowcy, którzy konfiguruja infrastrukturę sieciową; administratorzy, czyli SRE, którzy zapewniają niezawodność infrastruktury; oraz zespoły deweloperów, z których każdy odpowiada za część funkcji portalu. Oprogramowanie, które tworzą, działa mniej więcej tak:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Zapytania użytkowników trafiają zarówno na frontend głównego portalu www.ok.ru, jak i na inne, na przykład na fronty API muzyki. Aby przetworzyć logikę biznesową, wywołują serwer aplikacji, który przy przetwarzaniu zapytania uruchamia niezbędne specjalizowane mikroserwisy — one-graph (graf społecznych powiązań), user-cache (cache profili użytkowników) itd.

Każdy z tych serwisów jest uruchamiany na wielu maszynach, a każdy z nich ma odpowiedzialnych deweloperów, którzy odpowiadają za działanie modułów, ich eksploatację i rozwój techniczny. Wszystkie te serwisy działają na serwerach fizycznych, a do niedawna uruchamialiśmy dokładnie jedno zadanie na jeden serwer, co oznacza, że był on wyspecjalizowany na konkretną funkcję.

Dlaczego tak? Taki sposób miał kilka zalet:

  • Ułatwia masowe zarządzanie. Powiedzmy, że zadanie wymaga pewnych bibliotek, pewnych ustawień. Wtedy serwer przypisany jest do dokładnie jednej określonej grupy, opisana jest polityka cfengine dla tej grupy (lub już jest opisana), a ta konfiguracja centralnie i automatycznie rozkłada się na wszystkie serwery tej grupy.
  • Ułatwia diagnozowanieZałóżmy, że obserwujesz zwiększone obciążenie procesora i zdajesz sobie sprawę, że to obciążenie mogła wygenerować tylko ta zadanie, które działa na tym fizycznym procesorze. Poszukiwania winnego kończą się bardzo szybko.
  • Ułatwia monitoringJeśli coś jest nie tak z serwerem, monitor o tym informuje i dokładnie wiesz, kto jest winny.

Usługa składająca się z kilku replik ma przypisane kilka serwerów - po jednym na każdą. Wówczas zasoby obliczeniowe dla usługi są przydzielane bardzo prosto: ile serwis ma serwerów, tyle może maksymalnie wykorzystać zasobów. „Prosto” w tym przypadku nie oznacza, że to łatwe do użycia, lecz że przydział zasobów odbywa się ręcznie.

Takie podejście pozwalało nam również tworzyć specjalistyczne konfiguracje sprzętowe pod aplikacje działające na tym serwerze. Jeśli aplikacja przechowuje duże ilości danych, korzystamy z serwera 4U z obudową na 38 dysków. Jeśli zadanie jest wyłącznie obliczeniowe, możemy kupić tańszy serwer 1U. To jest wydajne pod względem zasobów obliczeniowych. Takie podejście pozwala nam także na cztery razy mniejsze wykorzystanie maszyn przy obciążeniu porównywalnym z jedną przyjazną nam siecią społecznościową.

Taka efektywność wykorzystania zasobów obliczeniowych powinna zapewnić również efektywność ekonomiczną, jeśli przyjąć, że najdroższe są serwery. Przez długi czas najdroższe było właśnie sprzęt, a my włożyliśmy dużo wysiłku w obniżenie kosztów sprzętu, tworząc algorytmy zapewniające odporność na awarie w celu zmniejszenia wymagań dotyczących niezawodności sprzętu. Dziś dotarliśmy do etapu, w którym cena serwera przestała być decydująca. Jeśli nie brać pod uwagę najnowszych egzotyk, konkretna konfiguracja serwerów w szafie nie ma znaczenia. Teraz mamy inny problem — cena zajmowanego miejsca przez serwer w centrum danych, t.j. miejsca w szafie.

Zdając sobie z tego sprawę, postanowiliśmy obliczyć, jak efektywnie wykorzystujemy szafy.
Wzięliśmy cenę najsilniejszego serwera w ekonomicznie uzasadniony sposób, policzyliśmy, ile takich serwerów możemy zmieścić w szafach, ile zadań uruchomilibyśmy na nich, opierając się na starej zasadzie „jeden serwer = jedno zadanie” i w jakim stopniu te zadania mogłyby wykorzystać sprzęt. Policzyliśmy — wzruszyliśmy się. Okazało się, że efektywność wykorzystania szaf wynosi około 11%. Wniosek jest oczywisty: trzeba zwiększyć efektywność korzystania z centrów danych. Wydawałoby się, że rozwiązanie jest oczywiste: na jednym serwerze należy uruchamiać jednocześnie kilka zadań. Ale tu zaczynają się trudności.

Masowa konfiguracja staje się znacznie bardziej skomplikowana — teraz niemożliwe jest przypisanie jednego serwera do jakiejś jednej grupy. W końcu na tym samym serwerze mogą być uruchomione jednocześnie różne zadania różnych zespołów. Ponadto, konfiguracja może kolidować z różnymi aplikacjami. Diagnoza również staje się trudniejsza: jeśli widzisz zwiększone zużycie procesorów lub dysków na serwerze, nie wiesz, które z zadań sprawia problemy.

Ale najważniejsze jest to, że między zadaniami uruchomionymi na jednej maszynie nie ma izolacji. Oto, na przykład, wykres średniego czasu odpowiedzi zadania serwera przed i po tym, jak na tym samym serwerze uruchomiono jeszcze jedno, całkowicie niezwiązane z pierwszym, aplikację obliczeniową — czas odpowiedzi dla głównego zadania znacząco wzrósł.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Oczywiste jest, że trzeba uruchamiać zadania albo w kontenerach, albo w maszynach wirtualnych. Ponieważ praktycznie wszystkie zadania uruchamiamy pod jedną OS (Linux) lub są do niej dostosowane, nie musimy wspierać wielu różnych systemów operacyjnych. W związku z tym wirtualizacja nie jest potrzebna, a ze względu na dodatkowe koszty będzie mniej efektywna niż konteneryzacja.

Docker jest dobrym kandydatem do uruchamiania kontenerów na serwerach: obrazy systemów plików skutecznie rozwiązują problemy z konfliktującymi konfiguracjami. Możliwość tworzenia obrazów z kilku warstw pozwala nam znacznie zmniejszyć objętość danych potrzebnych do ich wdrożenia w infrastrukturze, wydzielając wspólne części do osobnych warstw podstawowych. Wówczas podstawowe (i najbardziej obszerne) warstwy szybko zostaną zbuforowane w całej infrastrukturze, a do dostarczenia wielu różnych typów aplikacji i wersji wystarczy przesłać jedynie niewielkie warstwy.

Dodatkowo, gotowy rejestr i tagowanie obrazów w Dockerze oferują nam gotowe mechanizmy do wersjonowania i dostarczania kodu do produkcji.

Docker, podobnie jak każda inna technologia tego typu, zapewnia nam pewien poziom izolacji kontenerów od razu. Na przykład, izolacja pamięci - każdy kontener ma przydzielony limit wykorzystania pamięci maszyny, którego nie może przekroczyć. Można również izolować kontenery w zakresie użycia CPU. Dla nas standardowa izolacja okazała się niewystarczająca. Ale o tym poniżej.

Bezpośrednie uruchamianie kontenerów na serwerach to tylko część problemu. Drugą częścią jest umieszczanie kontenerów na serwerach. Należy zrozumieć, który kontener można umieścić na którym serwerze. To nie jest tak prosta sprawa, ponieważ kontenery należy rozmieszczać na serwerach jak najściślej, nie obniżając jednocześnie ich wydajności. Takie rozmieszczenie może być trudne także z punktu widzenia odporności na awarie. Często chcemy umieszczać repliki tego samego serwisu w różnych szafach lub nawet w różnych salach data center, aby w przypadku awarii szafy lub sali nie stracić jednocześnie wszystkich replik serwisu.

Ręczne rozmieszczanie kontenerów nie jest opcją, kiedy masz 8 tysięcy serwerów i 8-16 tysięcy kontenerów.

Ponadto, chcieliśmy dać programistom większą niezależność w przydzielaniu zasobów, aby mogli samodzielnie umieszczać swoje serwisy w produkcji, bez pomocy administratora. Jednocześnie chcieliśmy zachować kontrolę, aby jakiś drugorzędny serwis nie zużył wszystkich zasobów naszych data center.

Oczywiste jest, że potrzebna jest warstwa zarządzająca, która zajmowałaby się tym automatycznie.

Oto prosta i zrozumiała wizualizacja, którą uwielbiają wszyscy architekci: trzy kwadraty.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

one-cloud masters — odporna na błędy klaster, odpowiedzialny za orkiestrację chmury. Programista przesyła do mastera manifest, który zawiera wszystkie niezbędne informacje do umieszczenia usługi. Master na tej podstawie wydaje polecenia wybranym minionom (maszynom przeznaczonym do uruchamiania kontenerów). Na minionach znajduje się nasz agent, który otrzymuje polecenie, wydaje już swoje polecenia Docker, a Docker konfiguruje jądro systemu Linux do uruchomienia odpowiedniego kontenera. Oprócz wykonania poleceń agent nieprzerwanie informuje mastera o zmianach stanu zarówno maszyny-miniona, jak i uruchomionych na niej kontenerów.

Rozdzielanie zasobów

A teraz zajmijmy się trudniejszym problemem dzielenia zasobów dla wielu minionów.

Zasób obliczeniowy w one-cloud to:

  • Moc obliczeniowa procesora wykorzystywana przez konkretne zadanie.
  • Ilość pamięci dostępnej dla zadania.
  • Ruch sieciowy. Każdy z minionów ma konkretny interfejs sieciowy z ograniczoną przepustowością, więc nie można przydzielać zadań bez uwzględnienia ilości danych przesyłanych przez sieć.
  • Dyski. Oprócz, oczywiście, miejsca na dane zadania, przydzielamy także typ dysku: HDD lub SSD. Dyski mogą obsłużyć określoną liczbę zapytań na sekundę — IOPS. Dlatego dla zadań generujących więcej IOPS, niż może obsłużyć jeden dysk, przydzielamy również 'spindle' — czyli urządzenia dyskowe, które należy zarezerwować wyłącznie dla zadania.

Zatem dla jakiejś usługi, na przykład user-cache, możemy zapisać zużywane zasoby w ten sposób: 400 rdzeni procesora, 2,5 TB pamięci, 50 Gbit/s ruchu w obie strony, 6 TB miejsca na HDD, umieszczonego na 100 spindle'ach. Lub w bardziej znanej nam formie tak:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Zasoby usługi user-cache zużywają tylko część wszystkich dostępnych zasobów w infrastrukturze produkcyjnej. Dlatego chcemy, aby nagle, z powodu błędu operatora lub nie, user-cache nie zużył więcej zasobów, niż mu przydzielono. To znaczy, że musimy ograniczyć zasoby. Ale do czego moglibyśmy przypisać kwotę?

Wróćmy do naszego mocno uproszczonego schematu interakcji komponentów i narysujmy go z większą ilością szczegółów — oto tak:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Co rzuca się w oczy:

  • Frontend webowy i muzyka korzystają z izolowanych klastrów tego samego serwera aplikacji.
  • Można wyróżnić logiczne warstwy, do których należą te klastry: frontend, cache, warstwa przechowywania i zarządzania danymi.
  • Frontend jest heterogeniczny, to różne funkcjonalne podsystemy.
  • Cache również można rozmieścić w podsystemach, których dane przechowują.

Jeszcze raz narysujmy obrazek:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

O, widzimy hierarchię! A więc można przydzielać zasoby większym kawałkom: przypisać odpowiedzialnego programistę do węzła tej hierarchii, odpowiadającego funkcjonalnemu podsystemowi (jak "music" na rysunku), a do tego samego poziomu hierarchii przypiąć kwotę. Taka hierarchia również pozwala nam elastyczniej organizować usługi dla wygody zarządzania. Na przykład, wszystkie web, ponieważ to bardzo duża grupa serwerów, dzielimy na kilka mniejszych grup, pokazanych na obrazku jako group1, group2.

Usuwając zbędne linie, możemy zapisać każdy węzeł naszego rysunku w bardziej płaskiej formie: group1.web.front, api.music.front, user-cache.cache.

W ten sposób dochodzimy do pojęcia "hierarchicznej kolejki". Ma ona swoją nazwę, jak "group1.web.front". Przypisywana jest jej kwota na zasoby i prawa użytkowników. Osobie z DevOps nadamy uprawnienia do wysyłania usługi do kolejki, a taki pracownik może uruchamiać coś w kolejce, a osobie z OpsDev — prawa administracyjne, i teraz może zarządzać kolejką, przypisywać tam ludzi, przyznawać tym ludziom prawa itd. Usługi uruchamiane w tej kolejce będą realizowane w ramach kwoty kolejki. Jeśli obliczeniowa kwota kolejki jest niewystarczająca do jednoczesnego wykonania wszystkich usług, będą one wykonywane kolejno, tworząc w ten sposób rzeczywiście kolejkę.

Rozważmy usługi bardziej szczegółowo. Usługa ma pełną nazwę, która zawsze zawiera nazwę kolejki. Wtedy usługa frontu webowego będzie miała nazwę ok-web.group1.web.front. A usługa serwera aplikacji, do którego się odnosi, będzie nosić nazwę ok-app.group1.web.front. Każda usługa ma manifest, który zawiera wszystkie niezbędne informacje do rozmieszczenia na określonych maszynach: ile zasobów wymaga to zadanie, jaką konfigurację potrzebuje, ile replik powinno być, właściwości do obsługi awarii tej usługi. Po rozmieszczeniu usługi na maszynach pojawiają się jej instancje. Są one również jednoznacznie nazwane — jako numer instancji oraz nazwa usługi: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

To bardzo wygodne: patrząc tylko na nazwę uruchomionego kontenera, możemy od razu wiele wywnioskować.

A teraz bliżej zapoznajmy się z tym, co te instancje w rzeczywistości wykonują: z zadaniami.

Klasy izolacji zadań

Wszystkie zadania w OК (a zapewne wszędzie) można podzielić na grupy:

  • Zadania o krótkim czasie opóźnienia — prod. Dla takich zadań i usług bardzo ważne jest opóźnienie odpowiedzi (latency), jak szybko każde z zapytań zostanie przetworzone przez system. Przykłady zadań: fronty webowe, cache'e, serwery aplikacji, OLTP magazyny itp.
  • Zadania obliczeniowe — batch. Tutaj szybkość przetwarzania każdego konkretnego zapytania nie jest istotna. Ważne jest, ile całkowicie obliczeń w danym (dużym) czasie to zadanie wykona (throughput). Będą to wszelkie zadania MapReduce, Hadoop, uczenie maszynowe, statystyka.
  • Zadania w tle — idle. Dla takich zadań ani latency, ani throughput nie są zbyt ważne. Należą do nich różne testy, migracje, przeliczenia, konwersje danych z jednego formatu do drugiego. Z jednej strony są podobne do obliczeniowych, z drugiej — nie ma dla nas aż tak dużego znaczenia, jak szybko się zakończą.

Zobaczmy, jak takie zadania zużywają zasoby, na przykład procesora.

Zadania o krótkim czasie opóźnienia. Takie zadanie będzie miało wzór zużycia CPU podobny do tego:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Na przetwarzanie przychodzi zapytanie od użytkownika, zadanie zaczyna wykorzystywać wszystkie dostępne rdzenie CPU, przetwarza, zwraca odpowiedź, czeka na następne zapytanie i zatrzymuje się. Pojawia się następne zapytanie — znów wykorzystujemy wszystko, co było, obliczamy, czekamy na następne.

Aby zapewnić minimalne opóźnienie dla takiego zadania, musimy wziąć maksymalne zasoby, które zużywa i zarezerwować odpowiednią liczbę rdzeni na minionie (maszynie, która będzie wykonywać zadanie). Wtedy wzór rezerwacji dla naszego zadania będzie wyglądał następująco:

alloc: cpu = 4 (max)

I jeśli mamy maszynę-minion z 16 rdzeniami, to możemy na niej umieścić dokładnie cztery takie zadania. Zauważmy, że średnie zużycie procesora w takich zadaniach jest często bardzo niskie — co jest oczywiste, ponieważ znaczną część czasu zadanie spędza w oczekiwaniu na zapytanie i nic nie robi.

Zadania obliczeniowe. Będą miały nieco inny wzór:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Średnie zużycie zasobów procesora w takich zadaniach jest dość wysokie. Często chcemy, aby zadanie obliczeniowe było wykonane w określonym czasie, dlatego musimy zarezerwować minimalną liczbę procesorów, które są potrzebne, aby całe obliczenie zakończyło się w akceptowalnym czasie. Jego formuła rezerwacji będzie wyglądać tak:

alloc: cpu = [1,*

„Proszę umieścić na minionie, gdzie jest przynajmniej jeden wolny rdzeń, a potem ile się da — wszystko weźmie”.

Tutaj efektywność wykorzystania jest już znacznie lepsza niż w przypadku zadań o krótkim czasie oczekiwania. Ale zyski będą znacznie większe, jeśli połączymy oba typy zadań na jednej maszynie-minion i na bieżąco będziemy rozdzielać jej zasoby. Kiedy zadanie o krótkim czasie oczekiwania potrzebuje procesora — otrzymuje go natychmiast, a gdy zasoby stają się zbędne — przechodzą do zadania obliczeniowego, tzn. mniej więcej tak:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Ale jak to zrobić?

Na początku zajmijmy się prod i jego alloc: cpu = 4. Musimy zarezerwować cztery rdzenie. W Docker run można to zrobić na dwa sposoby:

  • Za pomocą opcji --cpuset=1-4, tzn. przypisać zadaniu cztery określone rdzenie na maszynie.
  • Użyć --cpuquota=400_000 --cpuperiod=100_000, przypisać kwotę czasu procesora, tzn. wskazać, że co 100 ms czasu rzeczywistego zadanie zużywa nie więcej niż 400 ms czasu procesora. Otrzymujemy te same cztery rdzenie.

Ale który z tych sposobów będzie odpowiedni?

Cpuset wygląda dość atrakcyjnie. Zadanie ma cztery dedykowane rdzenie, co oznacza, że pamięci podręczne procesora będą działały maksymalnie efektywnie. Ma to również swoją odwrotną stronę: musielibyśmy zająć się rozkładem obliczeń na mniej obciążone rdzenie maszyny zamiast na system operacyjny, co jest dość nietrywialnym zadaniem, szczególnie jeśli spróbujemy umieścić takie zadania wsadowe na takiej maszynie. Testy pokazały, że lepiej sprawdza się opcja z kwotą: w ten sposób system operacyjny ma więcej swobody w wyborze rdzenia do wykonania zadania w danym momencie, a czas procesora jest rozdzielany bardziej efektywnie.

Zajmiemy się tym, jak w dockerze zarezerwować minimalną liczbę rdzeni. Kwota dla zadań wsadowych już nie ma zastosowania, ponieważ nie ma potrzeby ograniczać maksimum, wystarczy tylko zapewnić minimum. Tutaj dobrze sprawdza się opcja docker run --cpushares.

Uzgodniliśmy, że jeśli zadanie wsadowe wymaga gwarancji minimum na jeden rdzeń, to wskazujemy --cpushares=1024, a jeśli minimum na dwa rdzenie, to wskazujemy --cpushares=2048. Udziały CPU w żaden sposób nie wpływają na podział czasu procesora, dopóki go nie brakuje. W ten sposób, jeśli produkcja w danej chwili nie wykorzystuje wszystkich swoich czterech rdzeni — nic nie ogranicza zadań wsadowych, mogą one korzystać z dodatkowego czasu procesora. W sytuacji niedoboru procesora, gdy produkcja wykorzystała wszystkie swoje cztery rdzenie i napotkała kwotę — pozostały czas procesora będzie podzielony proporcjonalnie do cpushares, tzn. w sytuacji trzech wolnych rdzeni jedno otrzyma zadanie z 1024 cpushares, a pozostałe dwa — zadanie z 2048 cpushares.

Jednak użycie kwoty i udziałów nie jest wystarczające. Musimy zapewnić, że zadanie z krótkim opóźnieniem ma priorytet przed zadaniem wsadowym w rozdzielaniu czasu procesora. Bez takiej priorytetyzacji zadanie wsadowe zajmie cały czas procesora w momencie, gdy będzie on potrzebny produkcji. W Docker run nie ma żadnych opcji priorytetyzacji kontenerów, ale z pomocą przychodzą polityki planowania procesora w Linuksie. Szczegółowo można o tym poczytać tutaj, a w ramach tego artykułu przejrzymy je krótko:

  • SCHED_OTHER
    Domyślnie otrzymują wszystkie zwykłe procesy użytkownika na maszynie Linuks.
  • SCHED_BATCH
    Przeznaczone do zasobożernych procesów. Przy przypisaniu zadania do procesora wprowadza się tzw. karę aktywacji: takie zadanie z mniejszym prawdopodobieństwem otrzyma zasoby procesora, jeśli w danym momencie korzysta z niego zadanie z SCHED_OTHER
  • SCHED_IDLE
    Proces w tle o bardzo niskim priorytecie, nawet niższym niż nice –19. Używamy naszej biblioteki z otwartym kodem one-nio, aby ustawić odpowiednią politykę podczas uruchamiania kontenera poprzez wywołanie

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Ale nawet jeśli nie programujesz w Javie, to samo można zrobić za pomocą polecenia chrt:

chrt -i 0 $pid

Zestawmy wszystkie nasze poziomy izolacji w jedną tabelę dla przejrzystości:

Klasa izolacji
Przykład alloc
Opcje Docker run
sched_setscheduler chrt*

Prod
cpu = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Batch
Cpu = [1, * )
--cpushares=1024
SCHED_BATCH

Idle
Cpu= [2, *)
--cpushares=2048
SCHED_IDLE

*Jeżeli wykonujesz chrt wewnątrz kontenera, może być potrzebna capability sys_nice, ponieważ domyślnie Docker odbiera tę capability podczas uruchamiania kontenera.

Ale zadania zużywają nie tylko procesor, ale również ruch, który wpływa na opóźnienie zadania sieciowego jeszcze bardziej niż niewłaściwe przydzielenie zasobów procesora. Dlatego naturalnie chcemy uzyskać dokładnie taki sam obraz dla ruchu. To znaczy, gdy zadanie prod wysyła jakieś pakiety do sieci, kwotujemy maksymalną prędkość (formuła alloc: lan=[*,500mbps) ), z jaką prod może to robić. A dla batch zapewniamy tylko minimalną przepustowość, ale nie ograniczamy maksymalnej (formuła alloc: lan=[10Mbps,*) ) Przy tym ruch prod powinien mieć priorytet przed zadaniami batch.
Tutaj Docker nie ma żadnych prymitywów, które moglibyśmy wykorzystać. Ale z pomocą przychodzi nam Linux Traffic Control. Udało nam się osiągnąć pożądany rezultat za pomocą dyscypliny Hierarchical Fair Service Curve. Dzięki niej wyodrębniamy dwie klasy ruchu: wysokopriorytetowy prod i niskopriorytetowy batch/idle. W rezultacie konfiguracja dla ruchu wychodzącego jest następująca:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

tutaj 1:0 — „korzeniowy qdisc” dyscypliny hsfc; 1:1 — podrzędna klasa hsfc z ogólnym limitem przepustowości wynoszącym 8 Gbit/s, do której zalicza się podrzędne klasy wszystkich kontenerów; 1:2 — podrzędna klasa hsfc wspólna dla wszystkich zadań batch i idle z „dynamicznym” limitem, o którym mowa poniżej. Pozostałe podrzędne klasy hsfc to klasy wydzielone dla aktualnie działających kontenerów prod z limitami odpowiadającymi ich manifestom — 450 i 400 Mbit/s. Każdej klasie hsfc przypisana jest kolejka qdisc fq lub fq_codel, w zależności od wersji jądra linux, aby uniknąć strat pakietów przy skokach ruchu.

Zwykle dyscypliny tc służą do priorytetyzacji tylko ruchu wychodzącego. Ale chcemy również priorytetyzować ruch przychodzący — przecież jakaś zadanie batch może swobodnie zająć cały przychodzący kanał, otrzymując na przykład dużą paczkę danych wejściowych do map&reduce. W tym celu używamy modułu ifb, który tworzy wirtualny interfejs ifbX dla każdego interfejsu sieciowego i przekierowuje ruch przychodzący z interfejsu do wychodzącego na ifbX. Następnie dla ifbX działają te same dyscypliny kontroli ruchu wychodzącego, dla którego konfiguracja hsfc będzie bardzo podobna:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

W trakcie eksperymentów odkryliśmy, że najlepsze wyniki hsfc uzyskuje się, gdy klasa 1:2 niepriorytetowego ruchu batch/idle jest ograniczona na maszynach-minionach do pewnego wolnego pasma. W przeciwnym razie ruch niepriorytetowy zbyt mocno wpływa na opóźnienie zadań prod. Obecna wartość wolnego pasma jest określana przez miniond co sekundę, mierząc średnie zużycie ruchu przez wszystkie zadania prod danego miniona One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach i odejmując ją od przepustowości interfejsu sieciowego One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach z niewielkim zapasem, t.j.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Pasma są określane niezależnie dla ruchu przychodzącego i wychodzącego. I zgodnie z nowymi wartościami miniond rekonfiguruje limit klasy niepriorytetowej 1:2.

W ten sposób zrealizowaliśmy wszystkie trzy klasy izolacji: prod, batch i idle. Te klasy mają duży wpływ na charakterystyki wykonania zadań. Dlatego zdecydowaliśmy się umieścić ten atrybut na szczycie hierarchii, aby przy spojrzeniu na nazwę hierarchicznej kolejki od razu było jasne, z czym mamy do czynienia:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Wszystkie nasze znane web i music fronty są wtedy umieszczane w hierarchiach pod prod. Na przykład pod batch umieśćmy usługę music catalog, który okresowo tworzy katalog utworów z zestawu załadowanych plików mp3 w „Odnoklassniki”. Przykładem usługi o idle może być transformator muzyki, normalizujący poziom głośności muzyki.

Po usunięciu zbędnych linii możemy zapisać nazwy naszych usług w bardziej płaskiej formie, dodając klasę izolacji zadania na końcu pełnej nazwy usługi: web.front.prod, catalog.music.batch, transformer.music.idle.

I teraz, patrząc na nazwę usługi, rozumiemy nie tylko to, jaką funkcję ona pełni, ale także jej klasę izolacji, a zatem jej krytyczność itd.

Wszystko jest świetnie, ale jest jedna gorzka prawda. Całkowita izolacja zadań działających na jednej maszynie jest niemożliwa.

Co udało nam się osiągnąć: jeśli zadanie batch intensywnie wykorzystuje tylko zasoby procesora, to wbudowany harmonogram CPU Linux bardzo dobrze radzi sobie ze swoim zadaniem, a wpływ na zadanie prod jest praktycznie żaden. Ale jeśli to zadanie batch zaczyna intensywnie pracować z pamięcią, wpływ współzależności już się manifestuje. Dzieje się tak, ponieważ z pamięci „wypłukiwane” są cache procesora zadania prod — w rezultacie w cache rośnie liczba błędów, a procesor przetwarza zadanie prod wolniej. Takie zadanie batch może zwiększyć opóźnienia naszego typowego kontenera prod o 10%.

Izolacja ruchu jest jeszcze trudniejsza, ponieważ nowoczesne karty sieciowe mają wewnętrzną kolejkę pakietów. Jeśli pakiet od zadania batch trafił tam jako pierwszy, to jako pierwszy zostanie przesłany przez kabel, i nic tu nie można zrobić.

Ponadto udało nam się jak dotąd rozwiązać tylko problem priorytetyzacji ruchu TCP: dla UDP podejście z hsfc nie działa. I nawet w przypadku ruchu TCP, jeśli zadanie batch generuje dużo ruchu, daje to również około 10% wzrostu opóźnienia zadania prod.

Odporność na awarie

Jednym z celów przy tworzeniu one-cloud było poprawienie odporności na awarie Odnoklassników. Dlatego chciałbym bardziej szczegółowo omówić możliwe scenariusze awarii i incydentów. Zacznijmy od prostego scenariusza — awarii kontenera.

Kontener sam w sobie może zawieść na kilka sposobów. Może to być jakiś eksperyment, błąd lub problem w manifeście, przez który zadanie produkcyjne zaczyna zużywać więcej zasobów, niż wskazano w manifeście. Mieliśmy przypadek: programista zaimplementował jeden skomplikowany algorytm, wielokrotnie go przerabiał, tak się pogubił, że ostatecznie zadanie zaawansowane znacznie się zapętliło. A ponieważ zadanie produkcyjne ma wyższy priorytet niż wszystkie inne na tych samych minionach, zaczęło konsumować wszystkie dostępne zasoby procesora. W tej sytuacji uratowała izolacja, a konkretnie kwota czasu procesora. Jeśli zadaniu przydzielona jest kwota, nie zużyje więcej. Dlatego inne zadania produkcyjne i wsadowe, które pracowały na tej samej maszynie, niczego nie zauważyły.

Drugim możliwym problemem jest awaria kontenera. I tutaj ratują nas polityki ponownego uruchomienia, wszyscy je znają, Docker radzi sobie doskonale. Praktycznie wszystkie zadania produkcyjne mają politykę ponownego uruchomienia always. Czasami używamy on_failure dla zadań wsadowych lub do debugowania kontenerów produkcyjnych.

Co można zrobić w przypadku niedostępności całego miniona?

Oczywiście, uruchomić kontener na innej maszynie. Najciekawsze jest to, co się dzieje z adresem IP (adresami) przypisanymi do kontenera.

Możemy przypisywać kontenerom te same adresy IP, co maszynom-minionom, na których te kontenery są uruchamiane. Wtedy, gdy kontener jest uruchamiany na innej maszynie, jego adres IP się zmienia, a wszyscy klienci muszą zrozumieć, że kontener się przeniósł, teraz trzeba chodzić pod inny adres, co wymaga odrębnej usługi Service Discovery.

Service Discovery to wygodne rozwiązanie. Na rynku jest wiele rozwiązań o różnych poziomach odporności na awarie do organizacji rejestrów usług. Często w takich rozwiązaniach realizowana jest logika load balancera, przechowywanie dodatkowej konfiguracji w postaci KV-store itp.
Jednakże chcielibyśmy obejść się bez potrzeby wdrażania oddzielnego rejestru, ponieważ oznaczałoby to wprowadzenie systemu krytycznego, który byłby wykorzystywany przez wszystkie usługi w produkcji. A więc byłby to potencjalny punkt awarii i trzeba by wybrać lub opracować bardzo odporne rozwiązanie, co, jak wiadomo, jest bardzo trudne, czasochłonne i kosztowne.

I jeszcze jedna poważna wada: aby nasza stara infrastruktura mogła działać z nową, należałoby przepisać absolutnie wszystkie zadania pod kątem użycia jakiegoś systemu odkrywania usług. Pracy jest BARDZO dużo, a w niektórych miejscach staje się to niemożliwe, gdy mówimy o urządzeniach niskopoziomowych działających na poziomie jądra systemu operacyjnego lub bezpośrednio z hardwarem. Realizacja tej funkcjonalności za pomocą ustalonych wzorców rozwiązań, takich jak na przykład side-car oznaczałaby w niektórych przypadkach dodatkowe obciążenie, a w innych — komplikację eksploatacji i dodatkowe scenariusze awarii. Nie chcieliśmy komplikować, dlatego zdecydowaliśmy się na uczynienie użycia Service Discovery opcjonalnym.

W one-cloud IP podąża za kontenerem, tzn. każdy egzemplarz zadania ma swój własny adres IP. Ten adres jest „statyczny”: przypisywany jest każdemu egzemplarzowi w momencie pierwszego uruchomienia usługi w chmurze. Jeśli w ciągu życia usługi istniało różne liczby egzemplarzy — to na końcu przypisanych będzie tyle adresów IP, ile maksymalnie było egzemplarzy.

W późniejszym czasie te adresy się nie zmieniają: przypisane są raz i istnieją przez cały okres życia usługi w produkcji. Adresy IP podążają za kontenerami w sieci. Jeśli kontener zostanie przeniesiony na inny minion, to adres również przejdzie za nim.

W ten sposób, przyporządkowanie nazwy usługi do listy jej adresów IP zmienia się bardzo rzadko. Jeśli jeszcze raz spojrzymy na nazwy egzemplarzy usługi, które wspomnieliśmy na początku artykułu (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), zauważymy, że przypominają one FQDN używane w DNS. I rzeczywiście, do wyświetlania nazw instancji usług w ich adresach IP używamy protokołu DNS. Przy czym ten DNS zwraca wszystkie zarezerwowane adresy IP wszystkich kontenerów — zarówno działających, jak i zatrzymanych (załóżmy, że używane są trzy repliki, a mamy tam pięć zarezerwowanych adresów — wszystkie pięć będzie zwracanych). Klienci, otrzymując te informacje, spróbują nawiązać połączenie ze wszystkimi pięcioma replikami — i w ten sposób określą, które działają. Taki sposób określania dostępności jest znacznie bardziej niezawodny, nie wymaga ani DNS, ani Service Discovery, co oznacza, że nie ma również trudnych do rozwiązania problemów z aktualnością informacji i odpornością na awarie tych systemów. Co więcej, w krytycznych usługach, od których zależy działanie całego portalu, możemy w ogóle nie używać DNS, a po prostu wpisać adresy IP w konfiguracji.

Realizacja takiego przenoszenia adresów IP między kontenerami może być nietrywialna — zatrzymamy się na tym, jak to działa, na następnym przykładzie:

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Załóżmy, że master one-cloud wydaje polecenie minionowi M1 uruchomienia 1.ok-web.group1.web.front.prod z adresem 1.1.1.1. Na minionie działa BIRD, który ogłasza ten adres w specjalnych serwerach route reflector. Ostatnie mają sesję BGP z urządzeniem sieciowym, do którego transmitowany jest rout adresu 1.1.1.1 na M1. M1 przekierowuje pakiety do wewnątrz kontenera już za pomocą systemów Linux. Serwerów route reflector jest trzy, ponieważ jest to bardzo krytyczna część infrastruktury one-cloud — bez nich sieć w one-cloud nie będzie działać. Umieszczamy je w różnych szafach, w miarę możliwości usytuowanych w różnych salach data center, aby zmniejszyć prawdopodobieństwo jednoczesnej awarii wszystkich trzech.

Załóżmy teraz, że połączenie między masterem one-cloud a minionem M1 zanikło. Master one-cloud będzie teraz działał, zakładając, że M1 całkowicie zawiódł. To znaczy, wyda polecenie minionowi M2 uruchomienia web.group1.web.front.prod z tym samym adresem 1.1.1.1. Teraz mamy dwa konfliktujące trasy w sieci dla 1.1.1.1: na M1 i na M2. Aby rozwiązać takie konflikty, używamy Multi Exit Discriminator, który jest wskazywany w ogłoszeniu BGP. Jest to liczba, która wskazuje wagę ogłaszanego szlaku. Z konfliktujących wybrana zostanie trasa z mniejszym wartością MED. Mistrz one-cloud obsługuje MED jako integralną część adresów IP kontenerów. Po raz pierwszy adres jest wystawiany z dość wysokim MED = 1 000 000. W sytuacji takiego awaryjnego przeniesienia kontenera mistrz zmniejsza MED, a M2 już otrzyma polecenie ogłoszenia adresu 1.1.1.1 z MED = 999 999. Instancja działająca na M1 pozostanie przy tym bez połączenia, a jej dalszy los mało nas interesuje do momentu przywrócenia połączenia z mistrzem, kiedy to zostanie zatrzymana jako stary duplikat.

Awarie

Wszystkie systemy zarządzania centrami danych zawsze akceptowalnie radzą sobie z drobnymi awariami. Wypadnięcie kontenera to normy praktycznie wszędzie.

Przyjrzyjmy się, jak radzimy sobie z awarią, na przykład awarią zasilania w jednym lub więcej salach centrum danych.

Co oznacza awaria dla systemu zarządzania centrum danych? Przede wszystkim to masowe jednoczesne awarię wielu maszyn, a system zarządzania musi jednocześnie migrować bardzo wiele kontenerów. Ale jeśli awaria jest bardzo rozległa, może się zdarzyć, że wszystkie zadania nie będą mogły zostać przeniesione na innych minionów, ponieważ pojemność zasobów centrum danych spada poniżej 100% obciążenia.

Często awariom towarzyszy również awaria warstwy sterującej. Może to się zdarzyć z powodu uszkodzenia sprzętu, ale częściej z powodu tego, że awarie nie są testowane, a warstwa zarządzająca sama upada pod zwiększonym obciążeniem.

Co można z tym wszystkim zrobić?

Masywne migracje oznaczają, że w infrastrukturze pojawia się wiele działań, migracji i lokacji. Każda z migracji może zajmować jakiś czas, potrzebny na dostarczenie i rozpakowanie obrazów kontenerów do minionów, uruchomienie i inicjalizację kontenerów itp. Dlatego wskazane jest, aby ważniejsze zadania uruchamiać przed mniej ważnymi.

Znowu przyjrzyjmy się znanej nam hierarchii usług i spróbujmy ustalić, jakie zadania chcemy uruchomić w pierwszej kolejności.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Oczywiście, to są procesy, które bezpośrednio uczestniczą w obsłudze zapytań użytkowników, tzn. prod. Wskazujemy to za pomocą priorytetu umieszczania — liczby, która może być przypisana do kolejki. Jeśli jakaś kolejka ma wyższy priorytet, jej usługi są umieszczane w pierwszej kolejności.

Na prod przypisujemy wyższe priorytety, 0; na batch — nieco niższe, 100; na idle — jeszcze niższe, 200. Priorytety są stosowane hierarchicznie. Wszystkie zadania niższe w hierarchii będą miały odpowiedni priorytet. Jeśli chcemy, aby w obrębie prod pamięci podręczne były uruchamiane przed frontami, to przypisujemy priorytety na cache = 0 i na front podkolejki = 1. Jeśli jednak, na przykład, chcemy, aby z frontów najpierw uruchamiał się główny portal, a front muzyczny już potem, to temu ostatniemu możemy przypisać niższy priorytet — 10.

Następnym problemem jest brak zasobów. Tak więc, mieliśmy awarię dużej liczby sprzętu, całych sal data center, a uruchomiliśmy tyle usług, że teraz dla wszystkich brakuje zasobów. Musimy zdecydować, które zadania poświęcić, aby działały podstawowe krytyczne usługi.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

W przeciwieństwie do priorytetu umieszczania, nie możemy po prostu poświęcić wszystkich zadań batch, ponieważ niektóre z nich są ważne dla działania portalu. Dlatego wydzieliliśmy osobno priorytet wypierania zadań. Przy umieszczaniu zadanie z wyższym priorytetem może wypchnąć, tzn. zatrzymać zadanie z niższym priorytetem, jeśli nie ma już wolnych minionów. Przy tym zadanie z niskim priorytetem prawdopodobnie pozostanie niewykonane, tzn. dla niej nie będzie już odpowiedniego miniona z wystarczającą ilością wolnych zasobów.

W naszej hierarchii bardzo łatwo jest wskazać taki priorytet wypierania, aby zadania prod i batch wypierały lub zatrzymywały zadania idle, ale nie nawzajem, wskazując dla idle priorytet równy 200. Podobnie jak w przypadku priorytetu umieszczania, możemy wykorzystać naszą hierarchię do opisywania bardziej złożonych reguł. Na przykład, określimy, że poświęcamy funkcję muzyki, jeśli zabraknie nam zasobów dla głównego portalu webowego, ustawiając dla odpowiednich węzłów priorytet niższy: 10.

Awaria DC całkowicie

Dlaczego może zawieść całe data center? Żywioł. Był dobry post, jak huragan wpłynął na działanie data centerMożna uznać, że żywiołem są bezdomni, którzy w pewnym momencie spalili światłowód w kanale, przez co cały data center stracił kontakt z innymi lokalizacjami. Przyczyny awarii mogą leżeć także w czynniku ludzkim: operator wydaje takie polecenie, że całe data center pada. Takie sytuacje mogą wystąpić z powodu dużego błędu. Ogólnie rzecz biorąc, data center padają — to nie jest rzadkość. U nas zdarza się to co kilka miesięcy.

I oto co robimy, aby nikt #okżyj nie publikował w Twitterze.

Pierwsza strategia — izolacja. Każdy instancja one-cloud jest izolowana i może zarządzać maszynami tylko jednego data center. To znaczy, że utrata chmury z powodu błędów lub niewłaściwego polecenia operatora — to utrata tylko jednego data center. Jesteśmy na to gotowi: mamy politykę rezerwacji, w której kopie aplikacji i danych są umieszczane we wszystkich data center. Korzystamy z baz danych odpornych na awarie i okresowo testujemy awarie.
Ponieważ mamy dziś cztery data center, mamy także cztery oddzielne, całkowicie izolowane instancje one-cloud.

Takie podejście nie tylko chroni przed fizyczną awarią, ale także może chronić przed błędami operatora.

A co jeszcze można zrobić w związku z czynnikiem ludzkim? Kiedy operator wydaje chmurze dziwne lub potencjalnie niebezpieczne polecenie, może zostać nagle poproszony o rozwiązanie małego zadania, aby sprawdzić, jak dobrze się zastanowił. Na przykład, jeśli to jakieś masowe zatrzymanie wielu replik lub po prostu dziwne polecenie — zmniejszenie liczby replik lub zmiana nazwy obrazu, a nie tylko numeru wersji w nowym manifeście.

One-cloud — system operacyjny na poziomie centrum danych w Odnoklassnikach

Podsumowanie

Cechy wyróżniające one-cloud:

  • Hierarchiczny i przejrzysty schemat nazewnictwa usług i kontenerów, który pozwala bardzo szybko zrozumieć, co to za zadanie, do czego się odnosi, jak działa i kto za nie odpowiada.
  • Stosujemy naszą technikę łączenia zadań prod- i batch-na minionach, aby poprawić efektywność współdzielenia maszyn. Zamiast cpuset używamy kwot CPU, udziałów, polityk planowania CPU i Linux QoS.
  • Nie udało się całkowicie izolować kontenerów działających na jednej maszynie, ale ich wzajemny wpływ pozostaje na poziomie do 20%.
  • Organizacja usług w hierarchię pomaga przy automatycznej likwidacji awarii za pomocą priorytetów usytuowania i wymuszania.

FAQ

Dlaczego nie wybraliśmy gotowego rozwiązania.

  • Różne klasy izolacji zadań wymagają różnej logiki przy ich umieszczaniu na minionach. Jeśli zadania produkcyjne można umieszczać za pomocą prostego rezerwowania zasobów, to zadania wsadowe i idle muszą być umieszczane, śledząc rzeczywiste wykorzystanie zasobów na maszynach-minionach.
  • Potrzeba uwzględnienia takich zasobów wykorzystywanych przez zadania, jak:
    • przepustowość sieci;
    • typy i 'spindle' dysków.
  • Potrzeba wskazywania priorytetów serwisów przy likwidacji awarii, praw i kwot zespołów na zasoby, co jest realizowane za pomocą hierarchicznych kolejek w one-cloud.
  • Potrzeba posiadania ludzkich nazw kontenerów, aby skrócić czas reakcji na awarie i incydenty.
  • Niemożność jednoczesnej ogólnolokalnej implementacji Service Discovery; potrzeba długiego współistnienia z zadaniami umieszczonymi na fizycznych hostach — co rozwiązuje zastosowanie 'statycznych' adresów IP, które podążają za kontenerami, a w konsekwencji wymóg unikalnej integracji z dużą infrastrukturą sieciową.

Wszystkie te funkcje wymagałyby znacznych przeróbek istniejących rozwiązań pod siebie, a po oszacowaniu ilości pracy zrozumieliśmy, że możemy opracować nasze rozwiązanie z podobnym nakładem pracy. Jednak nasze rozwiązanie będzie znacznie łatwiejsze w eksploatacji i rozwoju — nie ma w nim niepotrzebnych abstrakcji, które wspierałyby nam nieistotne funkcjonalności.

Dziękujemy tym, którzy czytają te ostatnie zdania, za cierpliwość i uwagę!

Ź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