Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Aloha, ludzie! Nazywam się Oleg Anastasyev, pracuję w Odnoklassnikach w zespole Platformy. A poza mną w Odnoklassnikach pracuje dużo sprzętu. Posiadamy cztery centra danych z około 500 szafami i ponad 8 tysiącami serwerów. W pewnym momencie zdaliśmy sobie sprawę, że wprowadzenie nowego systemu zarządzania pozwoli nam efektywniej załadować sprzęt, ułatwi zarządzanie dostępami, zautomatyzuje (re)dystrybucję zasobów obliczeniowych, przyspieszy uruchamianie nowych usług i przyspieszy reakcje do wypadków na dużą skalę.

Co z tego wynikło?

Oprócz mnie i całej masy sprzętu są też ludzie, którzy pracują z tym sprzętem: inżynierowie pracujący bezpośrednio w centrach danych; networkerzy, którzy konfigurują oprogramowanie sieciowe; administratorzy lub SRE, którzy zapewniają odporność infrastruktury; i zespoły deweloperskie, każdy z nich odpowiada za część funkcjonalności portalu. Tworzone przez nich oprogramowanie działa mniej więcej tak:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Żądania użytkowników są odbierane zarówno na frontach głównego portalu www.ok.rui na innych, na przykład na frontach Music API. Aby przetworzyć logikę biznesową, wywołują serwer aplikacji, który podczas przetwarzania żądania wywołuje niezbędne wyspecjalizowane mikrousługi - jeden wykres (wykres połączeń społecznościowych), pamięć podręczną użytkownika (pamięć podręczną profili użytkowników) itp.

Każda z tych usług jest wdrożona na wielu maszynach, a każda z nich ma odpowiedzialnych programistów odpowiedzialnych za funkcjonowanie modułów, ich działanie i rozwój technologiczny. Wszystkie te usługi działają na serwerach sprzętowych, a do niedawna uruchamialiśmy dokładnie jedno zadanie na serwer, czyli specjalizowało się ono pod konkretne zadanie.

Dlaczego? To podejście miało kilka zalet:

  • Odciążony zarządzanie masami. Powiedzmy, że zadanie wymaga pewnych bibliotek i pewnych ustawień. I wtedy serwer jest przypisany do dokładnie jednej konkretnej grupy, zostaje opisana polityka cfengine dla tej grupy (lub została już opisana), a ta konfiguracja jest centralnie i automatycznie wdrażana na wszystkich serwerach w tej grupie.
  • Uproszczony diagnostyka. Załóżmy, że patrzysz na zwiększone obciążenie procesora centralnego i zdajesz sobie sprawę, że obciążenie to może być wygenerowane jedynie przez zadanie uruchamiane na tym procesorze sprzętowym. Poszukiwanie winnego kończy się bardzo szybko.
  • Uproszczony monitoring. Jeśli coś jest nie tak z serwerem, monitor zgłasza to i wiesz dokładnie, kto jest winny.

Do usługi składającej się z kilku replik przydzielanych jest kilka serwerów - po jednym dla każdego. Następnie zasoby obliczeniowe dla usługi są przydzielane w bardzo prosty sposób: liczba serwerów, jakie posiada usługa, maksymalna ilość zasobów, jakie może zużyć. „Łatwy” nie oznacza tutaj, że jest łatwy w użyciu, ale w tym sensie, że alokacja zasobów odbywa się ręcznie.

Takie podejście również nam pozwoliło wyspecjalizowane konfiguracje żelaza dla zadania uruchomionego na tym serwerze. Jeżeli zadanie przechowuje duże ilości danych wówczas stosujemy serwer 4U z obudową mieszczącą 38 dysków. Jeśli zadanie ma charakter czysto obliczeniowy, wówczas możemy kupić tańszy serwer 1U. Jest to wydajne obliczeniowo. Między innymi takie podejście pozwala nam na wykorzystanie czterokrotnie mniejszej liczby maszyn przy obciążeniu porównywalnym z jednym przyjaznym portalem społecznościowym.

Taka efektywność w wykorzystaniu zasobów obliczeniowych powinna zapewniać także efektywność ekonomiczną, jeśli wyjdziemy z założenia, że ​​najdroższą rzeczą są serwery. Przez długi czas sprzęt był najdroższy, dlatego włożyliśmy wiele wysiłku w obniżenie ceny sprzętu, opracowując algorytmy odporności na awarie, aby zmniejszyć wymagania dotyczące niezawodności sprzętu. I dzisiaj doszliśmy do etapu, w którym cena serwera przestała być decydująca. Jeśli nie weźmiesz pod uwagę najnowszych egzotyków, konkretna konfiguracja serwerów w szafie nie ma znaczenia. Teraz mamy kolejny problem – cena miejsca zajmowanego przez serwer w centrum danych, czyli miejsca w szafie.

Zdając sobie sprawę, że tak właśnie jest, postanowiliśmy obliczyć, jak efektywnie wykorzystujemy stojaki.
Wzięliśmy cenę najpotężniejszego serwera z ekonomicznie uzasadnionych, obliczyliśmy, ile takich serwerów moglibyśmy umieścić w szafach, ile zadań na nich wykonalibyśmy w oparciu o stary model „jeden serwer = jedno zadanie” i ile takich zadania mogą korzystać ze sprzętu. Liczyli i wylewali łzy. Okazało się, że nasza efektywność w wykorzystaniu regałów wynosi około 11%. Wniosek jest oczywisty: musimy zwiększyć efektywność wykorzystania centrów danych. Wydawać by się mogło, że rozwiązanie jest oczywiste: trzeba uruchomić kilka zadań na jednym serwerze jednocześnie. Ale tu zaczynają się trudności.

Konfiguracja masowa staje się znacznie bardziej skomplikowana - nie można teraz przypisać żadnej pojedynczej grupy do serwera. Przecież teraz na jednym serwerze można uruchomić kilka zadań różnych poleceń. Ponadto konfiguracja może powodować konflikty w przypadku różnych aplikacji. Diagnoza staje się również bardziej skomplikowana: jeśli zauważysz zwiększone zużycie procesora lub dysku na serwerze, nie wiesz, które zadanie powoduje problemy.

Ale najważniejsze jest to, że nie ma izolacji między zadaniami uruchomionymi na tym samym komputerze. Oto przykładowy wykres średniego czasu odpowiedzi zadania serwera przed i po uruchomieniu na tym samym serwerze innej aplikacji obliczeniowej, w żaden sposób nie powiązanej z pierwszą - czas odpowiedzi zadania głównego znacząco wzrósł.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Oczywiście zadania należy uruchamiać albo w kontenerach, albo na maszynach wirtualnych. Ponieważ prawie wszystkie nasze zadania działają pod jednym systemem operacyjnym (Linux) lub są do niego przystosowane, nie musimy obsługiwać wielu różnych systemów operacyjnych. W związku z tym wirtualizacja nie jest potrzebna; ze względu na dodatkowe obciążenie będzie mniej wydajna niż konteneryzacja.

Jako implementacja kontenerów do uruchamiania zadań bezpośrednio na serwerach, Docker jest dobrym kandydatem: obrazy systemów plików dobrze rozwiązują problemy ze sprzecznymi konfiguracjami. Fakt, że obrazy mogą składać się z kilku warstw, pozwala znacząco ograniczyć ilość danych wymaganych do ich wdrożenia w infrastrukturze, rozdzielając części wspólne na osobne warstwy bazowe. Następnie podstawowe (i najbardziej obszerne) warstwy zostaną dość szybko buforowane w całej infrastrukturze, a aby zapewnić wiele różnych typów aplikacji i wersji, konieczne będzie przeniesienie tylko małych warstw.

Dodatkowo gotowy rejestr i tagowanie obrazów w Dockerze daje nam gotowe prymitywy do wersjonowania i dostarczania kodu na produkcję.

Docker, jak każda inna podobna technologia, zapewnia nam pewien poziom izolacji kontenerów od razu po wyjęciu z pudełka. Przykładowo izolacja pamięci – każdy kontener otrzymuje limit wykorzystania pamięci maszynowej, powyżej którego nie będzie zużywał. Można także izolować kontenery na podstawie użycia procesora. Nam jednak standardowa izolacja nie wystarczyła. Ale o tym poniżej.

Bezpośrednie uruchamianie kontenerów na serwerach to tylko część problemu. Druga część związana jest z hostingiem kontenerów na serwerach. Musisz zrozumieć, który kontener można umieścić na którym serwerze. Nie jest to takie proste zadanie, gdyż kontenery trzeba rozmieszczać na serwerach możliwie najgęściej, nie zmniejszając przy tym ich szybkości. Takie umiejscowienie może być również trudne z punktu widzenia odporności na uszkodzenia. Często chcemy umieścić repliki tej samej usługi w różnych szafach lub nawet w różnych pomieszczeniach centrum danych, aby w przypadku awarii szafy lub pomieszczenia nie utracić od razu wszystkich replik usług.

Ręczna dystrybucja kontenerów nie wchodzi w grę przy 8 tysiącach serwerów i 8-16 tysiącach kontenerów.

Dodatkowo chcieliśmy dać programistom większą niezależność w alokacji zasobów, aby mogli sami hostować swoje usługi na produkcji, bez pomocy administratora. Jednocześnie chcieliśmy zachować kontrolę, aby jakaś drobna usługa nie pochłonęła wszystkich zasobów naszych data center.

Oczywiście potrzebujemy warstwy kontrolnej, która zrobiłaby to automatycznie.

W ten sposób doszliśmy do prostego i zrozumiałego obrazu, który uwielbiają wszyscy architekci: trzy kwadraty.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

one-cloud masters to klaster pracy awaryjnej odpowiedzialny za orkiestrację chmury. Deweloper wysyła do mastera manifest, który zawiera wszystkie informacje niezbędne do hostowania usługi. Na jej podstawie mistrz wydaje polecenia wybranym sługusom (maszynom przeznaczonym do obsługi kontenerów). Sługusy mają naszego agenta, który odbiera polecenie, wydaje swoje polecenia Dockerowi, a Docker konfiguruje jądro Linuksa, aby uruchomić odpowiedni kontener. Oprócz wykonywania poleceń agent na bieżąco raportuje masterowi zmiany stanu zarówno maszyny minionowej, jak i działających na niej kontenerów.

Alokacja zasobów

Przyjrzyjmy się teraz problemowi bardziej złożonej alokacji zasobów dla wielu stronników.

Zasób obliczeniowy w jednej chmurze to:

  • Ilość mocy procesora zużywanej przez określone zadanie.
  • Ilość pamięci dostępnej dla zadania.
  • Ruch sieciowy. Każdy ze sługusów posiada specyficzny interfejs sieciowy o ograniczonej przepustowości, dlatego nie da się rozdzielać zadań bez uwzględnienia ilości danych przesyłanych przez sieć.
  • Dyski. Oprócz oczywiście miejsca na te zadania przydzielamy także rodzaj dysku: HDD lub SSD. Dyski mogą obsłużyć skończoną liczbę żądań na sekundę – IOPS. Dlatego do zadań generujących więcej IOPS, niż jest w stanie obsłużyć pojedynczy dysk, przydzielamy także „wrzeciona”, czyli urządzenia dyskowe, które muszą być zarezerwowane wyłącznie dla tego zadania.

Wtedy dla jakiejś usługi, np. dla user-cache, możemy rejestrować zużywane zasoby w ten sposób: 400 rdzeni procesora, 2,5 TB pamięci, ruch 50 Gbit/s w obie strony, 6 TB miejsca na dysku twardym umiejscowionym na 100 wrzecionach. Lub w bardziej znanej formie, takiej jak ta:

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

Zasoby usług pamięci podręcznej użytkowników zużywają tylko część wszystkich dostępnych zasobów w infrastrukturze produkcyjnej. Dlatego chcę mieć pewność, że nagle, z powodu błędu operatora lub nie, pamięć podręczna użytkownika nie zużyje więcej zasobów, niż jest jej przydzielone. Oznacza to, że musimy ograniczać zasoby. Ale z czym moglibyśmy powiązać tę kwotę?

Wróćmy do naszego znacznie uproszczonego schematu interakcji komponentów i narysujmy go na nowo, dodając więcej szczegółów - na przykład tak:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Co rzuca się w oczy:

  • Interfejs WWW i muzyka korzystają z izolowanych klastrów tego samego serwera aplikacji.
  • Można wyróżnić warstwy logiczne, do których należą te klastry: fronty, pamięci podręczne, przechowywanie danych oraz warstwę zarządzania.
  • Frontend jest heterogeniczny, składa się z różnych podsystemów funkcjonalnych.
  • Pamięci podręczne mogą być również rozproszone po całym podsystemie, którego dane buforują.

Narysujmy jeszcze raz obrazek:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Ba! Tak, widzimy hierarchię! Oznacza to, że możesz dystrybuować zasoby w większych fragmentach: przypisz odpowiedzialnego programistę do węzła tej hierarchii odpowiadającego podsystemowi funkcjonalnemu (jak „muzyka” na obrazku) i przypisz limit do tego samego poziomu hierarchii. Hierarchia ta pozwala nam również na bardziej elastyczną organizację usług, co ułatwia zarządzanie. Na przykład dzielimy całą sieć, ponieważ jest to bardzo duża grupa serwerów, na kilka mniejszych grup, pokazanych na obrazku jako grupa 1, grupa 2.

Usuwając dodatkowe linie, możemy zapisać każdy węzeł naszego obrazu w bardziej płaskiej formie: group1.web.front, api.music.front, pamięć podręczna użytkownika.cache.

W ten sposób dochodzimy do pojęcia „kolejki hierarchicznej”. Ma nazwę taką jak „group1.web.front”. Przypisany jest do niego limit zasobów i praw użytkownika. Osobie z DevOps nadamy uprawnienia do wysłania usługi do kolejki i taki pracownik będzie mógł uruchomić coś w kolejce, a osoba z OpsDev będzie miała uprawnienia administratora i teraz będzie mogła zarządzać kolejką, przydzielać tam ludzi, nadaj tym osobom uprawnienia itp. Usługi uruchomione w tej kolejce będą działać w ramach limitu kolejki. Jeżeli limit obliczeniowy kolejki nie wystarczy do wykonania wszystkich usług na raz, wówczas będą one wykonywane sekwencyjnie, tworząc w ten sposób samą kolejkę.

Przyjrzyjmy się bliżej usługom. Usługa ma w pełni kwalifikowaną nazwę, która zawsze zawiera nazwę kolejki. Wtedy frontowa usługa sieciowa będzie miała nazwę ok-web.group1.web.front. Zostanie wywołana usługa serwera aplikacji, do której uzyskuje dostęp ok-app.group1.web.front. Każda usługa posiada manifest, który określa wszystkie niezbędne informacje do umieszczenia na konkretnych maszynach: ile zasobów zużywa to zadanie, jaka jest do tego potrzebna konfiguracja, ile powinno być replik, właściwości do obsługi awarii tej usługi. A po umieszczeniu usługi bezpośrednio na maszynach pojawiają się jej instancje. Są one również nazywane jednoznacznie – jako numer instancji i nazwa usługi: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

Jest to bardzo wygodne: patrząc tylko na nazwę działającego kontenera, od razu możemy się wiele dowiedzieć.

Przyjrzyjmy się teraz bliżej, co faktycznie wykonują te instancje: zadania.

Zajęcia z izolowaniem zadań

Wszystkie zadania w OK (i prawdopodobnie wszędzie) można podzielić na grupy:

  • Zadania o krótkim opóźnieniu - prod. W przypadku takich zadań i usług bardzo ważne jest opóźnienie odpowiedzi (latencja), czyli to, jak szybko każde z żądań zostanie przetworzone przez system. Przykłady zadań: fronty internetowe, pamięci podręczne, serwery aplikacji, pamięć masowa OLTP itp.
  • Zadania obliczeniowe – wsadowe. Tutaj prędkość przetwarzania każdego konkretnego żądania nie jest istotna. Dla nich ważne jest, ile obliczeń wykona to zadanie w określonym (długim) okresie czasu (przepustowości). Będą to dowolne zadania MapReduce, Hadoop, machine learning, statystyki.
  • Zadania w tle — bezczynne. W przypadku takich zadań ani opóźnienia, ani przepustowość nie są bardzo ważne. Obejmuje to różne testy, migracje, ponowne obliczenia i konwersję danych z jednego formatu na inny. Z jednej strony są one podobne do wyliczonych, z drugiej strony nie ma dla nas większego znaczenia, jak szybko zostaną zrealizowane.

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

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

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Otrzymuje się żądanie od użytkownika do przetworzenia, zadanie zaczyna wykorzystywać wszystkie dostępne rdzenie procesora, przetwarza je, zwraca odpowiedź, czeka na kolejne żądanie i zatrzymuje się. Przyszła kolejna prośba - znowu wybraliśmy wszystko, co było, przeliczyliśmy i czekamy na następną.

Aby zagwarantować minimalne opóźnienie dla takiego zadania, musimy wziąć maksymalne zużywane przez nie zasoby i zarezerwować wymaganą liczbę rdzeni na minionie (maszynie, która wykona zadanie). Wtedy formuła rezerwacji dla naszego problemu będzie wyglądać następująco:

alloc: cpu = 4 (max)

a jeśli mamy maszynę stworzoną z 16 rdzeniami, to można na niej postawić dokładnie cztery takie zadania. Szczególnie zauważamy, że średnie zużycie procesora przez takie zadania jest często bardzo niskie - co jest oczywiste, ponieważ przez znaczną część czasu zadanie czeka na żądanie i nic nie robi.

Zadania obliczeniowe. Ich wzór będzie nieco inny:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Średnie zużycie zasobów procesora dla takich zadań jest dość wysokie. Często zależy nam na tym, aby zadanie obliczeniowe wykonało się w określonym czasie, dlatego musimy zarezerwować minimalną wymaganą liczbę procesorów, aby całe obliczenia zostały wykonane w akceptowalnym czasie. Jego formuła rezerwacji będzie wyglądać następująco:

alloc: cpu = [1,*)

„Proszę, połóż go na stronniku, na którym znajduje się co najmniej jeden wolny rdzeń, a wtedy, ile ich będzie, pożre wszystko”.

Tutaj efektywność użycia jest już znacznie lepsza niż przy zadaniach z krótkim opóźnieniem. Ale zysk będzie znacznie większy, jeśli połączysz oba rodzaje zadań na jednej maszynie stworów i będziesz dystrybuował jej zasoby w drodze. Gdy zadanie z krótkim opóźnieniem wymaga procesora, otrzymuje go natychmiast, a gdy zasoby nie są już potrzebne, przekazywane są do zadania obliczeniowego, czyli mniej więcej tak:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Ale jak to zrobić?

Najpierw spójrzmy na prod i jego przydział: cpu = 4. Musimy zarezerwować cztery rdzenie. W uruchomieniu Dockera można to zrobić na dwa sposoby:

  • Korzystanie z opcji --cpuset=1-4, czyli przydziel do zadania cztery konkretne rdzenie na maszynie.
  • Использовать --cpuquota=400_000 --cpuperiod=100_000, przydziel limit czasu procesora, czyli wskaż, że każde 100 ms czasu rzeczywistego zadanie zajmuje nie więcej niż 400 ms czasu procesora. Otrzymuje się te same cztery rdzenie.

Ale która z tych metod jest odpowiednia?

cpuset wygląda całkiem atrakcyjnie. Zadanie ma cztery dedykowane rdzenie, co oznacza, że ​​pamięci podręczne procesora będą działać możliwie najefektywniej. Ma to również swoją wadę: musielibyśmy wziąć na siebie zadanie rozłożenia obliczeń na nieobciążone rdzenie maszyny zamiast na system operacyjny, a jest to zadanie raczej nietrywialne, szczególnie jeśli spróbujemy umieścić zadania wsadowe na takim maszyna. Testy wykazały, że lepiej sprawdza się tutaj opcja z limitem: dzięki temu system operacyjny ma większą swobodę w wyborze rdzenia do wykonania zadania w danym momencie, a czas procesora jest rozkładany bardziej efektywnie.

Zastanówmy się, jak dokonać rezerwacji w Dockerze na podstawie minimalnej liczby rdzeni. Limit na zadania wsadowe już nie obowiązuje, bo nie trzeba ograniczać maksimum, wystarczy zagwarantować minimum. I tutaj opcja pasuje dobrze docker run --cpushares.

Uzgodniliśmy, że jeśli partia wymaga gwarancji na chociaż jeden rdzeń, to to wskazujemy --cpushares=1024, a jeśli są co najmniej dwa rdzenie, wskazujemy --cpushares=2048. Udziały procesora nie ingerują w żaden sposób w rozkład czasu procesora, o ile jest go wystarczająco dużo. Zatem jeśli prod nie wykorzystuje aktualnie wszystkich swoich czterech rdzeni, nic nie ogranicza zadań wsadowych i mogą one zająć dodatkowy czas procesora. Natomiast w sytuacji, gdy procesorów brakuje, jeżeli prod zużył wszystkie cztery rdzenie i osiągnął swój limit, pozostały czas procesora zostanie podzielony proporcjonalnie na cpushares, czyli w sytuacji trzech wolnych rdzeni, jeden będzie przydzielony zadaniu z 1024 procesorami, a pozostałe dwa zostaną przydzielone zadaniu z 2048 procesorami.

Jednak użycie kwot i udziałów nie wystarczy. Musimy się upewnić, że podczas alokacji czasu procesora zadanie z krótkim opóźnieniem ma pierwszeństwo przed zadaniem wsadowym. Bez takiego priorytetyzacji zadanie wsadowe zajmie cały czas procesora w momencie, gdy będzie potrzebne prod. W uruchomieniu Dockera nie ma opcji ustalania priorytetów kontenerów, ale przydatne są zasady harmonogramu procesora w systemie Linux. Możesz o nich szczegółowo przeczytać tutaj, a w ramach tego artykułu omówimy je pokrótce:

  • SCHED_OTHER
    Domyślnie wszystkie normalne procesy użytkownika na komputerze z systemem Linux odbierają.
  • SCHED_BATCH
    Zaprojektowany do procesów wymagających dużych zasobów. Przy umieszczaniu zadania na procesorze wprowadzana jest tzw. kara aktywacyjna: jest mniejsze prawdopodobieństwo, że takie zadanie otrzyma zasoby procesora, jeśli jest aktualnie używane przez zadanie z SCHED_OTHER
  • SCHED_IDLE
    Proces w tle o bardzo niskim priorytecie, nawet niższym niż nice -19. Korzystamy z naszej biblioteki open source jeden-nio, aby ustawić niezbędną 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

Dla przejrzystości podsumujmy wszystkie nasze poziomy izolacji w jednej tabeli:

Klasa izolacji
Przykład przydzielenia
Opcje uruchamiania Dockera
sched_setscheduler chrt*

Szturchać
procesor = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Partia
Procesor = [1, *)
--cpushares=1024
SCHED_BATCH

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

*Jeśli robisz chrt z wnętrza kontenera, możesz potrzebować funkcji sys_nice, ponieważ domyślnie Docker usuwa tę możliwość podczas uruchamiania kontenera.

Ale zadania zużywają nie tylko procesor, ale także ruch, który wpływa na opóźnienie zadania sieciowego jeszcze bardziej niż nieprawidłowa alokacja zasobów procesora. Dlatego naturalnie chcemy uzyskać dokładnie taki sam obraz ruchu. Oznacza to, że gdy zadanie prod wysyła pewne pakiety do sieci, ograniczamy maksymalną prędkość (wzór przydział: lan=[*,500Mbps) ), za pomocą którego prod może to zrobić. A w przypadku partii gwarantujemy tylko minimalną przepustowość, ale nie ograniczamy maksymalnej (wzór przydział: lan=[10Mbps,*) ) W takim przypadku ruch prod powinien mieć pierwszeństwo przed zadaniami wsadowymi.
Tutaj Docker nie ma żadnych prymitywów, których moglibyśmy użyć. Ale przychodzi nam z pomocą Kontrola ruchu w systemie Linux. Dzięki dyscyplinie udało nam się osiągnąć pożądany rezultat Hierarchiczna krzywa uczciwej obsługi. Za jego pomocą wyróżniamy dwie klasy ruchu: prod o wysokim priorytecie oraz wsadowy/idle o niskim priorytecie. W rezultacie konfiguracja ruchu wychodzącego wygląda następująco:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

tutaj 1:0 jest „główną kolejką qdisc” dyscypliny hsfc; 1:1 - klasa potomna hsfc z łącznym limitem przepustowości 8 Gbit/s, pod którą umieszczone są klasy potomne wszystkich kontenerów; 1:2 — klasa podrzędna hsfc jest wspólna dla wszystkich zadań wsadowych i bezczynnych z „dynamicznym” limitem, co omówiono poniżej. Pozostałe klasy podrzędne hsfc to klasy dedykowane 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 Linuksa, aby uniknąć utraty pakietów podczas gwałtownych wzrostów ruchu.

Zwykle dyscypliny tc służą do priorytetyzacji tylko ruchu wychodzącego. Ale chcemy również nadać priorytet ruchowi przychodzącemu - w końcu niektóre zadania wsadowe mogą z łatwością wybrać cały kanał przychodzący, otrzymując na przykład dużą partię danych wejściowych do map&reduce. W tym celu używamy modułu jeśli b, który tworzy wirtualny interfejs ifbX dla każdego interfejsu sieciowego i przekierowuje ruch przychodzący z interfejsu do ruchu wychodzącego na ifbX. Co więcej, w przypadku ifbX wszystkie te same dyscypliny pracują nad kontrolą ruchu wychodzącego, dla którego konfiguracja hsfc będzie bardzo podobna:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Podczas eksperymentów odkryliśmy, że hsfc wykazuje najlepsze wyniki, gdy klasa 1:2 niepriorytetowego ruchu wsadowego/bezczynnego jest ograniczona na maszynach minionów do nie więcej niż pewnego wolnego pasa. W przeciwnym razie ruch niepriorytetowy ma zbyt duży wpływ na opóźnienia zadań prod. miniond co sekundę określa aktualną ilość wolnego pasma, mierząc średnie zużycie ruchu przez wszystkie zadania prod danego stwora Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach i odejmując ją od przepustowości interfejsu sieciowego Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach z niewielkim marginesem, tj.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Pasma są definiowane niezależnie dla ruchu przychodzącego i wychodzącego. Zgodnie z nowymi wartościami miniond ponownie konfiguruje limit klas niepriorytetowych 1:2.

W ten sposób zaimplementowaliśmy wszystkie trzy klasy izolacji: prod, wsadową i idle. Klasy te w dużym stopniu wpływają na charakterystykę wykonania zadań. Dlatego postanowiliśmy umieścić ten atrybut na górze hierarchii, tak aby patrząc na nazwę kolejki hierarchicznej od razu było jasne z czym mamy do czynienia:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Wszyscy nasi przyjaciele sieć и muzyka fronty są następnie umieszczane w hierarchii pod prod. Na przykład w partii umieśćmy usługę katalog muzyczny, który okresowo zestawia katalog utworów z zestawu plików mp3 przesłanych do Odnoklassników. Przykładem usługi w stanie bezczynności może być transformator muzyczny, który normalizuje poziom głośności muzyki.

Po ponownym usunięciu dodatkowych linii możemy napisać nasze nazwy usług bardziej płasko, dodając klasę izolacji zadań na końcu pełnej nazwy usługi: web.front.prod, katalog.muzyka.partia, transformator.muzyka.bezczynność.

A teraz patrząc na nazwę usługi rozumiemy nie tylko jaką funkcję pełni, ale także jej klasę izolacji, co oznacza jej krytyczność itp.

Wszystko super, ale jest jedna gorzka prawda. Niemożliwe jest całkowite odizolowanie zadań działających na jednej maszynie.

Co udało nam się osiągnąć: jeśli partia intensywnie zużywa tylko zasobów procesora, wówczas wbudowany harmonogram procesora systemu Linux wykonuje swoje zadanie bardzo dobrze i praktycznie nie ma wpływu na zadanie prod. Ale jeśli to zadanie wsadowe zacznie aktywnie współpracować z pamięcią, wówczas już pojawia się wzajemny wpływ. Dzieje się tak, ponieważ zadanie prod jest „wypłukiwane” z pamięci podręcznej procesora - w rezultacie pamięć podręczna nie zwiększa się, a procesor przetwarza zadanie prod wolniej. Takie zadanie wsadowe może zwiększyć opóźnienie naszego typowego kontenera prod o 10%.

Izolowanie ruchu jest jeszcze trudniejsze ze względu na fakt, że nowoczesne karty sieciowe posiadają wewnętrzną kolejkę pakietów. Jeśli pakiet z zadania wsadowego dotrze tam jako pierwszy, to jako pierwszy zostanie przesłany kablem i nic nie będzie można z tym zrobić.

Ponadto jak dotąd udało nam się rozwiązać jedynie problem ustalania priorytetów ruchu TCP: podejście hsfc nie działa w przypadku UDP. Nawet w przypadku ruchu TCP, jeśli zadanie wsadowe generuje duży ruch, daje to również około 10% wzrost opóźnienia zadania prod.

tolerancja błędów

Jednym z celów przy opracowywaniu jednej chmury była poprawa odporności na błędy Odnoklassniki. Dlatego w dalszej kolejności chciałbym bardziej szczegółowo rozważyć możliwe scenariusze awarii i wypadków. Zacznijmy od prostego scenariusza – awarii kontenera.

Sam kontener może zawieść na kilka sposobów. Może to być jakiś eksperyment, błąd lub błąd w manifeście, przez który zadanie prod zaczyna zużywać więcej zasobów, niż wskazano w manifeście. Mieliśmy przypadek: programista zaimplementował jeden złożony algorytm, przerobił go wiele razy, przemyślał i popadł w taki zamęt, że ostatecznie problem został zapętlony w bardzo nietrywialny sposób. A ponieważ zadanie prod ma wyższy priorytet niż wszystkie inne na tych samych stworach, zaczęło zużywać wszystkie dostępne zasoby procesora. W tej sytuacji izolacja, a raczej limit czasu procesora, uratował sytuację. Jeśli zadaniu przydzielono przydział, zadanie nie zużyje więcej. Dlatego zadania wsadowe i inne zadania prod, które działały na tym samym komputerze, niczego nie zauważyły.

Drugim możliwym problemem jest spadający kontener. I tu ratują nas zasady restartu, wszyscy je znają, sam Docker robi świetną robotę. Prawie wszystkie zadania prod mają zasadę „zawsze uruchamiaj ponownie”. Czasami używamy on_failure do zadań wsadowych lub do debugowania kontenerów prod.

Co możesz zrobić, jeśli cały stronnik jest niedostępny?

Oczywiście uruchom kontener na innej maszynie. Interesującą częścią jest to, co dzieje się z adresami IP przypisanymi do kontenera.

Kontenerom możemy przypisać te same adresy IP, co maszynom pomocniczym, na których działają te kontenery. Następnie, gdy kontener zostanie uruchomiony na innej maszynie, zmienia się jego adres IP i wszyscy klienci muszą zrozumieć, że kontener się przeniósł i teraz muszą udać się pod inny adres, co wymaga osobnej usługi Service Discovery.

Wykrywanie usług jest wygodne. Na rynku dostępnych jest wiele rozwiązań o różnym stopniu odporności na błędy w zakresie organizacji rejestru serwisowego. Często takie rozwiązania implementują logikę równoważenia obciążenia, przechowują dodatkową konfigurację w postaci pamięci KV itp.
Chcielibyśmy jednak uniknąć konieczności wdrażania osobnego rejestru, gdyż oznaczałoby to wprowadzenie systemu krytycznego, z którego korzystają wszystkie usługi na produkcji. Oznacza to, że jest to potencjalny punkt awarii i należy wybrać lub opracować rozwiązanie bardzo odporne na awarie, co jest oczywiście bardzo trudne, czasochłonne i kosztowne.

I jeszcze jedna duża wada: aby nasza stara infrastruktura współpracowała z nową, musielibyśmy przepisać absolutnie wszystkie zadania, aby skorzystać z jakiegoś systemu Service Discovery. Pracy jest dużo, a w niektórych miejscach jest to prawie niemożliwe, jeśli chodzi o urządzenia niskiego poziomu, które działają na poziomie jądra systemu operacyjnego lub bezpośrednio ze sprzętem. Implementacja tej funkcjonalności z wykorzystaniem ustalonych wzorców rozwiązań, takich jak wózek boczny w niektórych miejscach oznaczałoby dodatkowe obciążenie, w innych - komplikację w działaniu i dodatkowe scenariusze awarii. Nie chcieliśmy komplikować rzeczy, dlatego zdecydowaliśmy się, że korzystanie z Service Discovery będzie opcjonalne.

W one-cloud adres IP podąża za kontenerem, czyli każda instancja zadania ma swój własny adres IP. Adres ten jest „statyczny”: jest przypisywany do każdej instancji w momencie pierwszego wysłania usługi do chmury. Jeśli usługa miała w swoim życiu różną liczbę instancji, to ostatecznie zostanie jej przydzielonych tyle adresów IP, ile było maksymalnej liczby instancji.

Następnie adresy te nie ulegają zmianie: są przydzielane jednorazowo i istnieją przez cały czas trwania usługi w produkcji. Adresy IP podążają za kontenerami w sieci. Jeśli kontener zostanie przekazany innemu stronnikowi, adres będzie podążał za nim.

Dlatego mapowanie nazwy usługi na listę adresów IP zmienia się bardzo rzadko. Jeśli jeszcze raz spojrzysz na nazwy instancji usług, o których wspominaliś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 nazwy FQDN używane w DNS. Zgadza się, do mapowania nazw instancji usług na ich adresy IP używamy protokołu DNS. Co więcej, ten DNS zwraca wszystkie zarezerwowane adresy IP wszystkich kontenerów – zarówno uruchomionych, jak i zatrzymanych (powiedzmy, że używane są trzy repliki, a mamy tam zarezerwowanych pięć adresów – wszystkie pięć zostanie zwróconych). Klienci po otrzymaniu tej informacji będą starali się nawiązać połączenie ze wszystkimi pięcioma replikami i w ten sposób ustalić, które z nich działają. Ta opcja określania dostępności jest znacznie bardziej niezawodna, nie obejmuje ani DNS, ani Service Discovery, co oznacza, że ​​nie ma trudnych problemów do rozwiązania w celu zapewnienia aktualności informacji i odporności tych systemów na awarie. Co więcej, w usługach krytycznych, od których zależy działanie całego portalu, nie możemy w ogóle skorzystać z DNS, a jedynie wpisać do konfiguracji adresy IP.

Wdrożenie takiego transferu IP za kontenerami może nie być trywialne — przyjrzymy się, jak to działa na następującym przykładzie:

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Powiedzmy, że mistrz jednej chmury wydaje polecenie minionowi M1, aby uciekał 1.ok-web.group1.web.front.prod z adresem 1.1.1.1. Działa na stwora BIRD, który reklamuje ten adres na specjalnych serwerach reflektor trasy. Te ostatnie posiadają sesję BGP ze sprzętem sieciowym, na który tłumaczona jest trasa o adresie 1.1.1.1 na M1. M1 kieruje pakiety wewnątrz kontenera przy użyciu systemu Linux. Istnieją trzy serwery odzwierciedlające trasy, ponieważ jest to bardzo krytyczna część infrastruktury jednej chmury - bez nich sieć w jednej chmurze nie będzie działać. Umieszczamy je w różnych szafach, jeśli to możliwe, w różnych pomieszczeniach centrum danych, aby zmniejszyć prawdopodobieństwo awarii wszystkich trzech jednocześnie.

Załóżmy teraz, że połączenie między mistrzem jednej chmury a sługą M1 zostało utracone. Mistrz jednej chmury będzie teraz działać przy założeniu, że M1 całkowicie zawiódł. Oznacza to, że wyda polecenie stworowi M2, aby wystartował web.group1.web.front.prod z tym samym adresem 1.1.1.1. Teraz mamy dwie sprzeczne trasy w sieci dla wersji 1.1.1.1: na M1 i na M2. Aby rozwiązać takie konflikty, używamy Multi Exit Discriminator, który jest określony w ogłoszeniu BGP. Jest to liczba pokazująca wagę reklamowanej trasy. Spośród tras kolidujących ze sobą zostanie wybrana trasa o niższej wartości MED. Moduł główny jednej chmury obsługuje MED jako integralną część adresów IP kontenerów. Po raz pierwszy adres jest zapisywany z odpowiednio dużym MED = 1 000 000. W sytuacji takiego awaryjnego transferu kontenera kapitan zmniejsza MED, a M2 otrzyma już polecenie rozgłaszania adresu 1.1.1.1 z MED = 999 999. Instancja działająca na M1 pozostanie w tym przypadku nie ma połączenia, a jej dalsze losy mało nas interesują, dopóki nie zostanie przywrócone połączenie z masterem, kiedy zostanie zatrzymany jak stare ujęcie.

Wypadki

Wszystkie systemy zarządzania centrami danych zawsze radzą sobie z drobnymi awariami w akceptowalny sposób. Przepełnienie kontenerów to niemal wszędzie norma.

Przyjrzyjmy się, jak postępujemy w sytuacjach awaryjnych, takich jak awaria zasilania w jednym lub większej liczbie pomieszczeń centrum danych.

Co wypadek oznacza dla systemu zarządzania centrum danych? Przede wszystkim jest to masowa, jednorazowa awaria wielu maszyn, a system sterowania musi migrować wiele kontenerów jednocześnie. Ale jeśli katastrofa jest na bardzo dużą skalę, może się zdarzyć, że wszystkich zadań nie uda się ponownie przydzielić innym sługom, ponieważ pojemność zasobów centrum danych spadnie poniżej 100% obciążenia.

Często wypadkom towarzyszy awaria warstwy kontrolnej. Może się to zdarzyć z powodu awarii jego sprzętu, ale częściej z powodu nietestowania wypadków, a sama warstwa kontrolna spada z powodu zwiększonego obciążenia.

Co możesz z tym wszystkim zrobić?

Masowe migracje oznaczają, że w infrastrukturze zachodzi duża liczba działań, migracji i wdrożeń. Każda z migracji może zająć trochę czasu, aby dostarczyć i rozpakować obrazy kontenerów sługom, uruchomić i zainicjować kontenery itp. Dlatego pożądane jest, aby ważniejsze zadania były uruchamiane przed mniej ważnymi.

Przyjrzyjmy się jeszcze raz znanej nam hierarchii usług i spróbujmy zdecydować, które zadania chcemy uruchomić w pierwszej kolejności.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Są to oczywiście procesy bezpośrednio zaangażowane w obsługę żądań użytkowników, czyli prod. Wskazujemy to za pomocą priorytet miejsca docelowego — numer, który można przypisać do kolejki. Jeśli kolejka ma wyższy priorytet, jej usługi są umieszczane na pierwszym miejscu.

Na prod nadajemy wyższe priorytety, 0; w partii - nieco niżej, 100; na biegu jałowym - jeszcze niżej, 200. Priorytety są stosowane hierarchicznie. Wszystkie zadania znajdujące się niżej w hierarchii będą miały odpowiedni priorytet. Jeśli chcemy, aby pamięci podręczne wewnątrz prod były uruchamiane przed frontendami, to przypisujemy priorytety cache = 0 i front subqueues = 1. Jeśli np. chcemy, aby najpierw z frontów uruchamiany był portal główny, a dopiero front muzyczny wtedy temu drugiemu możemy nadać niższy priorytet - 10.

Następnym problemem jest brak środków. Zatem duża ilość sprzętu, całe hale centrum danych uległy awarii i ponownie uruchomiliśmy tak wiele usług, że obecnie nie ma wystarczających zasobów dla wszystkich. Należy zdecydować, które zadania należy poświęcić, aby zapewnić działanie głównych usług krytycznych.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

W przeciwieństwie do priorytetu rozmieszczenia, nie możemy bezkrytycznie poświęcać wszystkich zadań wsadowych, niektóre z nich są ważne dla działania portalu. Dlatego podkreśliliśmy osobno priorytet wywłaszczenia zadania. Po umieszczeniu zadanie o wyższym priorytecie może wyprzedzić, tj. zatrzymać, zadanie o niższym priorytecie, jeśli nie ma już wolnych stronników. W takim przypadku zadanie o niskim priorytecie prawdopodobnie pozostanie nieumieszczone, czyli nie będzie już dla niego odpowiedniego stronnika z wystarczającą ilością wolnych zasobów.

W naszej hierarchii bardzo łatwo jest określić priorytet wywłaszczania, tak aby zadania prod i wsadowe wywłaszczały lub zatrzymywały bezczynne zadania, ale nie siebie nawzajem, poprzez określenie priorytetu bezczynności równego 200. Podobnie jak w przypadku priorytetu umieszczenia, możemy może wykorzystać naszą hierarchię do opisania bardziej złożonych reguł. Przykładowo, wskażmy, że rezygnujemy z funkcji muzycznej, jeśli nie mamy wystarczających zasobów dla głównego portalu internetowego, ustawiając priorytet dla odpowiednich węzłów niższy: 10.

Całe wypadki w DC

Dlaczego całe centrum danych może ulec awarii? Element. To był dobry post huragan wpłynął na pracę centrum danych. Za elementy można uznać osoby bezdomne, które kiedyś spaliły optykę w kolektorze, a centrum danych całkowicie straciło kontakt z innymi obiektami. Przyczyną awarii może być także czynnik ludzki: operator wyda taką komendę, że całe centrum danych upadnie. Może się to zdarzyć z powodu dużego błędu. Ogólnie rzecz biorąc, upadek centrów danych nie jest rzadkością. U nas zdarza się to raz na kilka miesięcy.

I właśnie to robimy, aby uniemożliwić komukolwiek tweetowanie #alive.

Pierwszą strategią jest izolacja. Każda instancja w jednej chmurze jest izolowana i może zarządzać maszynami tylko w jednym centrum danych. Oznacza to, że utrata chmury z powodu błędów lub nieprawidłowych poleceń operatora oznacza utratę tylko jednego centrum danych. Jesteśmy na to gotowi: mamy politykę redundancji, w której repliki aplikacji i danych znajdują się we wszystkich centrach danych. Korzystamy z odpornych na awarie baz danych i okresowo testujemy pod kątem awarii.
Od dziś mamy cztery centra danych, czyli cztery oddzielne, całkowicie izolowane instancje jednej chmury.

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

Co jeszcze można zrobić z czynnikiem ludzkim? Kiedy operator wydaje chmurze jakieś dziwne lub potencjalnie niebezpieczne polecenie, może nagle zostać poproszony o rozwiązanie małego problemu i sprawdzenie, jak dobrze myślał. Np. jeśli jest to jakieś masowe zatrzymanie wielu replik lub po prostu dziwne polecenie - zmniejszenie ilości replik lub zmiana nazwy obrazu, a nie tylko numeru wersji w nowym manifeście.

Jedna chmura - system operacyjny na poziomie centrum danych w Odnoklassnikach

Wyniki

Charakterystyczne cechy jednej chmury:

  • Hierarchiczny i wizualny schemat nazewnictwa usług i kontenerów, co pozwala bardzo szybko dowiedzieć się, czym jest zadanie, czego dotyczy, jak działa i kto jest za nie odpowiedzialny.
  • Stosujemy nasze technika łączenia produktów i partiizadania na sługusach, aby poprawić efektywność udostępniania maszyn. Zamiast cpuset używamy przydziałów procesora, udziałów, zasad harmonogramu procesora i Linux QoS.
  • Całkowite odizolowanie kontenerów działających na tej samej maszynie nie było możliwe, ale ich wzajemny wpływ utrzymuje się w granicach 20%.
  • Organizowanie usług w hierarchię pomaga w automatycznym odtwarzaniu po awarii priorytety rozmieszczenia i wywłaszczenia.

FAQ

Dlaczego nie skorzystaliśmy z gotowego rozwiązania?

  • Różne klasy izolacji zadań wymagają innej logiki, gdy są umieszczane na stworach. Jeśli zadania prod mogą być umieszczane poprzez zwykłą rezerwację zasobów, wówczas należy umieszczać zadania wsadowe i bezczynne, śledząc rzeczywiste wykorzystanie zasobów na maszynach pomocniczych.
  • Konieczność uwzględnienia zasobów zużywanych przez zadania, takich jak:
    • przepustowość sieci;
    • rodzaje i „wrzeciona” dysków.
  • Konieczność wskazania priorytetów służb podczas reagowania kryzysowego, uprawnień i przydziałów poleceń dotyczących zasobów, rozwiązywana jest za pomocą hierarchicznych kolejek w jednej chmurze.
  • Konieczność nadawania kontenerom nazw przez ludzi, aby skrócić czas reakcji na wypadki i incydenty
  • Niemożność jednorazowego, powszechnego wdrożenia Service Discovery; konieczność długotrwałej współistnienia z zadaniami hostowanymi na hostach sprzętowych - coś, co rozwiązują „statyczne” adresy IP podążające za kontenerami, a w konsekwencji konieczność unikalnej integracji z dużą infrastrukturą sieciową.

Wszystkie te funkcje wymagałyby znacznych modyfikacji istniejących rozwiązań pod kątem naszych potrzeb i po oszacowaniu nakładu pracy zdaliśmy sobie sprawę, że przy mniej więcej takich samych kosztach pracy możemy opracować własne rozwiązanie. Ale Twoje rozwiązanie będzie znacznie łatwiejsze w obsłudze i rozwoju – nie zawiera zbędnych abstrakcji wspierających funkcjonalności, których nie potrzebujemy.

Tym, którzy przeczytali ostatnie linijki, dziękujemy za cierpliwość i uwagę!

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

Dodaj komentarz