Przejście z Tindera na Kubernetes

Notatka. przeł.: Pracownicy znanego na całym świecie serwisu Tinder podzielili się ostatnio kilkoma szczegółami technicznymi dotyczącymi migracji swojej infrastruktury do Kubernetes. Proces trwał prawie dwa lata i zaowocował uruchomieniem na K8s bardzo dużej platformy, składającej się z 200 usług hostowanych na 48 tys. kontenerów. Jakie ciekawe trudności napotkali inżynierowie Tindera i do jakich wyników doszli? Przeczytaj to tłumaczenie.

Przejście z Tindera na Kubernetes

Dlaczego?

Prawie dwa lata temu Tinder zdecydował się przenieść swoją platformę na Kubernetes. Kubernetes umożliwi zespołowi Tindera konteneryzację i przejście do środowiska produkcyjnego przy minimalnym wysiłku dzięki niezmiennemu wdrożeniu (wdrożenie niezmienne). W tym przypadku zestaw aplikacji, ich wdrożenie i sama infrastruktura byłyby jednoznacznie zdefiniowane przez kod.

Szukaliśmy także rozwiązania problemu skalowalności i stabilności. Kiedy skalowanie stało się krytyczne, często musieliśmy czekać kilka minut na uruchomienie nowych instancji EC2. Pomysł uruchomienia kontenerów i uruchomienia obsługi ruchu w ciągu sekund, a nie minut, stał się dla nas bardzo atrakcyjny.

Proces okazał się trudny. Podczas naszej migracji na początku 2019 r. klaster Kubernetes osiągnął masę krytyczną i zaczęliśmy napotykać różne problemy związane z natężeniem ruchu, rozmiarem klastra i systemem DNS. Po drodze rozwiązaliśmy wiele ciekawych problemów związanych z migracją 200 usług i utrzymaniem klastra Kubernetes składającego się z 1000 węzłów, 15000 48000 podów i XNUMX XNUMX działających kontenerów.

W jaki sposób?

Od stycznia 2018 roku przeszliśmy przez różne etapy migracji. Zaczęliśmy od konteneryzacji wszystkich naszych usług i wdrożenia ich w testowych środowiskach chmurowych Kubernetes. Od października rozpoczęliśmy metodyczną migrację wszystkich istniejących usług do Kubernetes. W marcu kolejnego roku zakończyliśmy migrację i obecnie platforma Tinder działa wyłącznie na platformie Kubernetes.

Tworzenie obrazów dla Kubernetes

Posiadamy ponad 30 repozytoriów kodu źródłowego dla mikroserwisów działających w klastrze Kubernetes. Kod w tych repozytoriach jest napisany w różnych językach (na przykład Node.js, Java, Scala, Go) z wieloma środowiskami wykonawczymi dla tego samego języka.

System kompilacji został zaprojektowany tak, aby zapewnić w pełni konfigurowalny „kontekst kompilacji” dla każdej mikrousługi. Zwykle składa się z pliku Dockerfile i listy poleceń powłoki. Ich treść jest w pełni konfigurowalna, a jednocześnie wszystkie konteksty kompilacji są napisane według ustandaryzowanego formatu. Standaryzacja kontekstów kompilacji umożliwia jednemu systemowi kompilacji obsługę wszystkich mikrousług.

Przejście z Tindera na Kubernetes
Rysunek 1-1. Standaryzowany proces kompilacji za pośrednictwem kontenera Builder

Aby osiągnąć maksymalną spójność pomiędzy środowiskami wykonawczymi (środowiska wykonawcze) ten sam proces kompilacji jest używany podczas programowania i testowania. Stanęliśmy przed bardzo ciekawym wyzwaniem: musieliśmy opracować sposób na zapewnienie spójności środowiska kompilacji na całej platformie. Aby to osiągnąć, wszystkie procesy montażowe przeprowadzane są wewnątrz specjalnego pojemnika. Budowniczy.

Jego wdrożenie kontenerowe wymagało zaawansowanych technik Dockera. Builder dziedziczy identyfikator lokalnego użytkownika i sekrety (takie jak klucz SSH, dane uwierzytelniające AWS itp.) wymagane do uzyskania dostępu do prywatnych repozytoriów Tinder. Montuje lokalne katalogi zawierające źródła w celu naturalnego przechowywania artefaktów kompilacji. Takie podejście poprawia wydajność, ponieważ eliminuje potrzebę kopiowania artefaktów kompilacji między kontenerem Builder a hostem. Przechowywane artefakty kompilacji można ponownie wykorzystać bez dodatkowej konfiguracji.

W przypadku niektórych usług musieliśmy utworzyć kolejny kontener, aby zmapować środowisko kompilacji na środowisko wykonawcze (na przykład biblioteka bcrypt Node.js generuje podczas instalacji artefakty binarne specyficzne dla platformy). Podczas procesu kompilacji wymagania mogą się różnić w zależności od usługi, a ostateczny plik Dockerfile jest kompilowany na bieżąco.

Architektura i migracja klastra Kubernetes

Zarządzanie wielkością klastra

Postanowiliśmy skorzystać kube-och do automatycznego wdrażania klastrów na instancjach Amazon EC2. Na samym początku wszystko działało w jednej wspólnej puli węzłów. Szybko zdaliśmy sobie sprawę z konieczności rozdzielenia obciążeń według rozmiaru i typu instancji, aby efektywniej wykorzystywać zasoby. Logika była taka, że ​​uruchomienie kilku załadowanych wielowątkowych podów okazało się bardziej przewidywalne pod względem wydajności niż ich współistnienie z dużą liczbą jednowątkowych podów.

Ostatecznie ustaliliśmy:

  • m5.4xlarge — do monitorowania (Prometeusz);
  • c5.4xduże - dla obciążenia Node.js (obciążenie jednowątkowe);
  • c5.2xduże - dla Java i Go (obciążenie wielowątkowe);
  • c5.4xduże — dla centrali (3 węzły).

Migracja

Jednym z kroków przygotowawczych do migracji ze starej infrastruktury na Kubernetes było przekierowanie dotychczasowej, bezpośredniej komunikacji pomiędzy usługami na nowe moduły równoważenia obciążenia (Elastic Load Balancers (ELB). Zostały utworzone w określonej podsieci wirtualnej chmury prywatnej (VPC). Ta podsieć została połączona z platformą VPC Kubernetes. Pozwoliło nam to na stopniową migrację modułów, bez uwzględnienia określonej kolejności zależności usług.

Te punkty końcowe utworzono przy użyciu ważonych zestawów rekordów DNS, których rekordy CNAME wskazywały na każdy nowy ELB. Aby się przełączyć, dodaliśmy nowy wpis wskazujący na nowy ELB usługi Kubernetes z wagą 0. Następnie ustawiliśmy Time To Live (TTL) wpisu ustawionego na 0. Następnie stare i nowe wagi zostały powoli dostosowywane i ostatecznie 100% obciążenia zostało przesłane na nowy serwer. Po zakończeniu przełączania wartość TTL powróciła do bardziej adekwatnego poziomu.

Moduły Java, które posiadaliśmy, radziły sobie z DNS o niskim TTL, ale aplikacje Node nie. Jeden z inżynierów przepisał część kodu puli połączeń i umieścił ją w menedżerze, który aktualizował pule połączeń co 60 sekund. Wybrane podejście sprawdziło się bardzo dobrze i nie spowodowało zauważalnego pogorszenia wydajności.

Lekcje

Ograniczenia struktury sieciowej

Wczesnym rankiem 8 stycznia 2019 r. platforma Tinder niespodziewanie uległa awarii. W odpowiedzi na niepowiązany wzrost opóźnień platformy wcześniej tego ranka wzrosła liczba podów i węzłów w klastrze. Spowodowało to wyczerpanie pamięci podręcznej ARP na wszystkich naszych węzłach.

Istnieją trzy opcje systemu Linux związane z pamięcią podręczną ARP:

Przejście z Tindera na Kubernetes
(źródło)

gc_thresh3 - to jest twarde ograniczenie. Pojawienie się w dzienniku wpisów „przepełnienia tablicy sąsiada” oznaczało, że nawet po synchronicznym zbieraniu elementów bezużytecznych (GC) w pamięci podręcznej ARP nie było wystarczającej ilości miejsca do przechowywania sąsiedniego wpisu. W tym przypadku jądro po prostu całkowicie odrzuciło pakiet.

Używamy Flanela jako szkielet sieciowy w Kubernetesie. Pakiety są przesyłane przez VXLAN. VXLAN to tunel L2 wzniesiony nad siecią L3. Technologia wykorzystuje enkapsulację MAC-in-UDP (protokół MAC Address-in-User Datagram Protocol) i umożliwia rozbudowę segmentów sieci warstwy 2. Protokół transportowy w fizycznej sieci centrum danych to IP plus UDP.

Przejście z Tindera na Kubernetes
Rysunek 2–1. Schemat flaneli (źródło)

Przejście z Tindera na Kubernetes
Rysunek 2–2. Pakiet VXLAN (źródło)

Każdy węzeł roboczy Kubernetes przydziela wirtualną przestrzeń adresową za pomocą maski/24 z większego bloku/9. Dla każdego węzła tak jest środki jeden wpis w tablicy routingu, jeden wpis w tablicy ARP (w interfejsie flannel.1) i jeden wpis w tablicy przełączania (FDB). Są dodawane przy pierwszym uruchomieniu węzła roboczego lub przy każdym wykryciu nowego węzła.

Ponadto komunikacja węzeł-pod (lub pod-pod) ostatecznie przechodzi przez interfejs eth0 (jak pokazano na powyższym schemacie flaneli). Powoduje to dodatkowy wpis w tablicy ARP dla każdego odpowiedniego hosta źródłowego i docelowego.

W naszym środowisku ten rodzaj komunikacji jest bardzo powszechny. Dla obiektów usług w Kubernetes tworzony jest ELB, a Kubernetes rejestruje każdy węzeł w ELB. ELB nic nie wie o podach i wybrany węzeł może nie być ostatecznym miejscem docelowym pakietu. Chodzi o to, że gdy węzeł odbierze pakiet od ELB, rozpatruje go z uwzględnieniem reguł iptables dla określonej usługi i losowo wybiera zasobnik w innym węźle.

W chwili awarii klaster liczył 605 węzłów. Z powodów podanych powyżej wystarczyło to, aby przezwyciężyć znaczenie gc_thresh3, co jest ustawieniem domyślnym. Kiedy to nastąpi, nie tylko pakiety zaczynają być odrzucane, ale cała wirtualna przestrzeń adresowa Flannel z maską /24 znika z tablicy ARP. Komunikacja między węzłami i zapytania DNS są przerywane (DNS jest hostowany w klastrze; szczegółowe informacje można znaleźć w dalszej części tego artykułu).

Aby rozwiązać ten problem, musisz zwiększyć wartości gc_thresh1, gc_thresh2 и gc_thresh3 i zrestartuj Flannel, aby ponownie zarejestrować brakujące sieci.

Nieoczekiwane skalowanie DNS

Podczas procesu migracji aktywnie wykorzystywaliśmy DNS do zarządzania ruchem i sukcesywnego przenoszenia usług ze starej infrastruktury do Kubernetes. Ustawiamy stosunkowo niskie wartości TTL dla powiązanych zestawów rekordów w Route53. Kiedy stara infrastruktura działała na instancjach EC2, nasza konfiguracja programu rozpoznawania nazw wskazywała na Amazon DNS. Przyjęliśmy to za oczywistość i wpływ niskiego TTL na nasze usługi i usługi Amazon (takie jak DynamoDB) pozostał w dużej mierze niezauważony.

Podczas migracji usług do Kubernetes odkryliśmy, że DNS przetwarzał 250 tysięcy żądań na sekundę. W rezultacie w aplikacjach zaczęły pojawiać się ciągłe i poważne przekroczenia limitu czasu dla zapytań DNS. Stało się to pomimo niesamowitych wysiłków mających na celu optymalizację i zmianę dostawcy DNS na CoreDNS (który przy szczytowym obciążeniu osiągnął 1000 podów działających na 120 rdzeniach).

Badając inne możliwe przyczyny i rozwiązania, odkryliśmy статью, opisujący warunki wyścigu wpływające na strukturę filtrowania pakietów netfiltra w Linuksie. Zaobserwowane przez nas przekroczenia limitu czasu w połączeniu ze wzrostem licznika wstawienie_nie powiodło się w interfejsie Flannel były zgodne z ustaleniami artykułu.

Problem pojawia się na etapie translacji adresu sieci źródłowej i docelowej (SNAT i DNAT) i późniejszego wpisania do tablicy contrack. Jednym z obejść omawianych wewnętrznie i sugerowanych przez społeczność było przeniesienie DNS do samego węzła roboczego. W tym przypadku:

  • SNAT nie jest potrzebny, ponieważ ruch pozostaje wewnątrz węzła. Nie trzeba go kierować przez interfejs eth0.
  • DNAT nie jest potrzebny, ponieważ docelowy adres IP jest lokalny dla węzła, a nie losowo wybranego poda zgodnie z regułami iptables.

Postanowiliśmy pozostać przy tym podejściu. CoreDNS został wdrożony jako DaemonSet w Kubernetes i zaimplementowaliśmy lokalny serwer DNS węzła rozwiąż.konf każdego kapsuły, ustawiając flagę --cluster-dns polecenia kubelet . To rozwiązanie okazało się skuteczne w przypadku przekroczeń limitów czasu DNS.

Jednak nadal obserwowaliśmy utratę pakietów i wzrost licznika wstawienie_nie powiodło się w interfejsie Flanelowym. Problem ten utrzymywał się po wdrożeniu obejścia, ponieważ udało nam się wyeliminować SNAT i/lub DNAT tylko dla ruchu DNS. Warunki wyścigu zostały zachowane dla innych rodzajów ruchu. Na szczęście większość naszych pakietów to TCP i jeśli wystąpi problem, są one po prostu retransmitowane. Wciąż staramy się znaleźć odpowiednie rozwiązanie dla wszystkich rodzajów ruchu.

Korzystanie z Envoy w celu lepszego równoważenia obciążenia

Po migracji usług backendu do Kubernetesa zaczęły pojawiać się problemy z niezrównoważonym obciążeniem między podami. Odkryliśmy, że HTTP Keepalive powodował zawieszanie się połączeń ELB na pierwszych gotowych podach każdego wdrożenia. Zatem większość ruchu przechodziła przez niewielki procent dostępnych podów. Pierwszym testowanym przez nas rozwiązaniem było ustawienie MaxSurge na 100% w przypadku nowych wdrożeń w przypadku najgorszych scenariuszy. Efekt okazał się znikomy i mało obiecujący w przypadku większych wdrożeń.

Innym rozwiązaniem, które zastosowaliśmy, było sztuczne zwiększanie żądań zasobów dla usług krytycznych. W tym przypadku kapsuły umieszczone w pobliżu miałyby więcej miejsca na manewr w porównaniu z innymi ciężkimi kapsułami. Na dłuższą metę to by się nie sprawdziło, bo byłoby marnowaniem zasobów. Ponadto nasze aplikacje Node były jednowątkowe i dlatego mogły korzystać tylko z jednego rdzenia. Jedynym realnym rozwiązaniem było zastosowanie lepszego równoważenia obciążenia.

Od dawna chcieliśmy w pełni docenić Wysłannik. Obecna sytuacja pozwoliła nam wdrożyć go w bardzo ograniczony sposób i uzyskać natychmiastowe rezultaty. Envoy to wysokowydajny serwer proxy warstwy XNUMX typu open source przeznaczony do dużych aplikacji SOA. Może wdrażać zaawansowane techniki równoważenia obciążenia, w tym automatyczne ponowne próby, wyłączniki automatyczne i globalne ograniczanie szybkości. (Notatka. przeł.: Więcej na ten temat możesz przeczytać w ten artykuł o Istio, które jest oparte na Envoy.)

Opracowaliśmy następującą konfigurację: dla każdego podu i jednej trasy należy mieć wózek boczny Envoy i połączyć klaster z kontenerem lokalnie przez port. Aby zminimalizować potencjalne kaskadowanie i zachować mały promień trafienia, wykorzystaliśmy flotę front-proxy Envoy, po jednym na strefę dostępności (AZ) dla każdej usługi. Polegali na prostym silniku wykrywania usług napisanym przez jednego z naszych inżynierów, który po prostu zwracał listę podów w każdym AZ dla danej usługi.

Następnie wysłannicy usług wykorzystali ten mechanizm wykrywania usług z jednym klastrem i trasą nadrzędną. Ustawiliśmy odpowiednie limity czasu, zwiększyliśmy ustawienia wszystkich wyłączników automatycznych i dodaliśmy konfigurację minimalnych ponownych prób, aby pomóc w przypadku pojedynczych awarii i zapewnić płynne wdrożenia. Umieściliśmy TCP ELB przed każdym z tych wysłanników usług. Nawet jeśli funkcja utrzymywania aktywności z naszej głównej warstwy proxy utknęła w niektórych modułach Envoy, nadal były one w stanie znacznie lepiej poradzić sobie z obciążeniem i zostały skonfigurowane tak, aby równoważyć je za pomocą funkcji less_request w backendie.

Do wdrożenia użyliśmy haka preStop zarówno w zasobnikach aplikacji, jak i zasobnikach bocznych. Hook wywołał błąd podczas sprawdzania statusu punktu końcowego administratora znajdującego się w kontenerze przyczepy bocznej i przeszedł na chwilę w stan uśpienia, aby umożliwić zakończenie aktywnych połączeń.

Jednym z powodów, dla których mogliśmy tak szybko działać, są szczegółowe wskaźniki, które udało nam się łatwo zintegrować z typową instalacją Prometheusa. Pozwoliło nam to dokładnie zobaczyć, co się dzieje, podczas dostosowywania parametrów konfiguracyjnych i redystrybucji ruchu.

Wyniki były natychmiastowe i oczywiste. Zaczęliśmy od usług najbardziej niezbilansowanych i w tej chwili plasuje się na czele 12 najważniejszych usług w klastrze. W tym roku planujemy przejście na pełną siatkę usług z bardziej zaawansowanym wykrywaniem usług, wyłączaniem obwodów, wykrywaniem wartości odstających, ograniczaniem szybkości i śledzeniem.

Przejście z Tindera na Kubernetes
Rysunek 3–1. Konwergencja procesora jednej usługi podczas przejścia na Envoy

Przejście z Tindera na Kubernetes

Przejście z Tindera na Kubernetes

Wynik końcowy

Dzięki temu doświadczeniu i dodatkowym badaniom zbudowaliśmy silny zespół ds. infrastruktury posiadający duże umiejętności w zakresie projektowania, wdrażania i obsługi dużych klastrów Kubernetes. Wszyscy inżynierowie Tindera mają teraz wiedzę i doświadczenie w zakresie pakowania kontenerów i wdrażania aplikacji w Kubernetes.

Gdy na starej infrastrukturze pojawiła się potrzeba zwiększenia wydajności, musieliśmy poczekać kilka minut na uruchomienie nowych instancji EC2. Teraz kontenery zaczynają działać i przetwarzać ruch w ciągu kilku sekund, a nie minut. Planowanie wielu kontenerów w jednej instancji EC2 zapewnia również lepszą koncentrację poziomą. W rezultacie prognozujemy znaczną redukcję kosztów EC2019 w 2 roku w porównaniu do roku ubiegłego.

Migracja trwała prawie dwa lata, ale zakończyliśmy ją w marcu 2019 roku. Obecnie platforma Tinder działa wyłącznie na klastrze Kubernetes składającym się z 200 usług, 1000 węzłów, 15 000 podów i 48 000 działających kontenerów. Infrastruktura nie jest już wyłączną domeną zespołów operacyjnych. Wszyscy nasi inżynierowie dzielą się tą odpowiedzialnością i kontrolują proces tworzenia i wdrażania swoich aplikacji, używając wyłącznie kodu.

PS od tłumacza

Przeczytaj także serię artykułów na naszym blogu:

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

Dodaj komentarz