
Ten artykuł pomoże zrozumieć, jak działa równoważenie obciążenia w Kubernetes, co się dzieje podczas skalowania długożyjących połączeń oraz dlaczego warto rozważyć równoważenie po stronie klienta, gdy korzystasz z HTTP/2, gRPC, RSockets, AMQP lub innych długożyjących protokołów.
Kilka informacji o tym, jak rozdzielany jest ruch w Kubernetes
Kubernetes oferuje dwie wygodne abstrakcje do wydania aplikacji: serwisy (Services) i wdrożenia (Deployments).
Wdrożenia opisują, jak i ile kopii twojej aplikacji powinno być uruchomionych w danym momencie. Każda aplikacja jest wdrażana jako pod (Pod) i otrzymuje przydzielony adres IP.
Serwisy pełnią funkcję podobną do równoważnika obciążenia. Są przeznaczone do rozdzielania ruchu pomiędzy wieloma podami.
Zobaczmy, jak to wygląda.
- Na poniższym schemacie widzisz trzy instancje jednej aplikacji oraz równoważnik obciążenia:

- Równoważnik obciążenia nazywa się serwis (Service) i otrzymuje adres IP. Każde nadchodzące zapytanie jest przekierowywane do jednego z podów:

- Scenariusz wdrożenia określa liczbę instancji aplikacji. W praktyce rzadko wdrażasz pod bezpośrednio:

- Każdemu podowi przypisywany jest jego własny adres IP:

Użyteczne jest postrzeganie serwisów jako zbioru adresów IP. Za każdym razem, gdy zwracasz się do serwisu, jeden z adresów IP jest wybierany z listy i używany jako adres docelowy.
Wygląda to następująco.
- Przychodzi zapytanie curl 10.96.45.152 do serwisu:

- Serwis wybiera jeden z trzech adresów podów jako miejsce docelowe:

- Ruch jest przekierowywany do konkretnego podu:

Jeśli twoja aplikacja składa się z front-endu i back-endu, będziesz mieć zarówno serwis, jak i wdrożenie dla każdego z nich.
Gdy front-end wysyła zapytanie do back-endu, nie musi wiedzieć, ile dokładnie podów obsługuje back-end: może ich być jeden, dziesięć lub sto.
Front-end również nie zna adresów podów obsługujących back-end.
Gdy front-end wysyła zapytanie do back-endu, korzysta z adresu IP serwisu back-endu, który nie ulega zmianie.
Oto jak to wygląda.
- Pod 1 wysyła zapytanie do wewnętrznego komponentu back-endu. Zamiast wybierać konkretny pod back-endu, przesyła zapytanie do serwisu:

- Usługa wybiera jeden z podów backendu jako adres docelowy:

- Ruch przechodzi od podu 1 do podu 5, wybranego przez usługę:

- Pod 1 nie wie, ile dokładnie takich podów, jak pod 5, jest ukrytych za usługą:

Ale jak dokładnie usługa rozdziela zapytania? Czyżby stosowała równoważenie obciążenia typu round-robin? Przyjrzyjmy się temu.
Równoważenie obciążenia w usługach Kubernetes
Usługi Kubernetes nie istnieją. Nie ma procesu, któremu przypisano byłby adres IP i port.
Możesz się o tym przekonać, wchodząc na dowolny węzeł klastra i wykonując polecenie netstat -ntlp.
Nawet nie będziesz w stanie znaleźć adresu IP przypisanego usłudze.
Adres IP usługi jest umieszczony w warstwie kontrolnej, w kontrolerze, i zapisany w bazie danych — etcd. Ten sam adres jest używany przez inny komponent — kube-proxy.
Kube-proxy otrzymuje listę adresów IP dla wszystkich usług i tworzy zestaw reguł iptables na każdym węźle klastra.
Te reguły mówią: „Jeśli widzimy adres IP usługi, należy zmodyfikować docelowy adres zapytania i wysłać je do jednego z podów”.
Adres IP usługi jest używany tylko jako punkt wejścia i nie jest obsługiwany przez żaden proces nasłuchujący na tym adresie IP i porcie.
Przyjrzyjmy się temu.
- Rozważmy klaster z trzema węzłami. Na każdym węźle obecne są pody:

- Powiązane pody, kolorowane na beżowo, stanowią część usługi. Ponieważ usługa nie istnieje jako proces, jest przedstawiona na szaro:

- Pierwszy pod wysyła zapytanie do usługi i powinien trafić do jednego z powiązanych podów:

- Ale usługa nie istnieje, procesu nie ma. Jak to działa?

- Zanim zapytanie opuści węzeł, przechodzi przez reguły iptables:

- Reguły iptables wiedzą, że nie ma usługi, i zastępują jej adres IP jednym z adresów IP podów powiązanych z tą usługą:

- Zapytanie otrzymuje ważny adres IP jako adres docelowy i jest normalnie przetwarzane:

- W zależności od topologii sieci, zapytanie ostatecznie dociera do podu:

Czy iptables potrafią równoważyć obciążenie?
Nie, iptables są używane do filtrowania i nie zostały zaprojektowane do równoważenia.
Jednak istnieje możliwość napisania zestawu reguł, które działają jak .
I właśnie to zostało zrealizowane w Kubernetes.
Jeśli masz trzy pody, kube-proxy napisze następujące reguły:
- Wybierz pierwszy pod z prawdopodobieństwem 33%, w przeciwnym razie przejdź do następnej reguły.
- Wybierz drugi pod z prawdopodobieństwem 50%, w przeciwnym razie przejdź do następnej zasady.
- Wybierz trzeci pod.
Taki system powoduje, że każdy pod jest wybierany z prawdopodobieństwem 33%.

I nie ma żadnej gwarancji, że pod 2 zostanie wybrany następny po podzie 1.
Uwaga: iptables wykorzystuje statystyczny moduł z losowym rozkładem. W ten sposób algorytm równoważenia obciążenia oparty jest na losowym wyborze.
Teraz, gdy rozumiesz, jak działają usługi, spójrzmy na bardziej interesujące scenariusze działania.
Długotrwałe połączenia w Kubernetes domyślnie nie skalują się.
Każde żądanie HTTP z frontendu do backendu obsługiwane jest przez osobne połączenie TCP, które jest otwierane i zamykane.
Jeśli frontend wysyła 100 żądań na sekundę do backendu, to otwierane i zamykane jest 100 różnych połączeń TCP.
Można skrócić czas przetwarzania żądania i zmniejszyć obciążenie, otwierając jedno połączenie TCP i używając go do wszystkich kolejnych żądań HTTP.
W protokole HTTP wbudowana jest funkcja zwana HTTP keep-alive, czyli ponowne używanie połączenia. W takim przypadku jedno połączenie TCP jest używane do przesyłania i odbierania wielu żądań i odpowiedzi HTTP:

Ta funkcja nie jest domyślnie włączona: zarówno serwer, jak i klient muszą być odpowiednio skonfigurowani.
Sama konfiguracja jest prosta i dostępna dla większości języków programowania i środowisk.
Oto kilka linków do przykładów w różnych językach:
Co się stanie, jeśli użyjemy keep-alive w usłudze Kubernetes?
Załóżmy, że zarówno frontend, jak i backend obsługują keep-alive.
Mamy jedną kopię frontendu i trzy instancje backendu. Frontend wysyła pierwsze żądanie i otwiera połączenie TCP do backendu. Żądanie dociera do usługi, jeden z podów backendu jest wybierany jako adres docelowy. Pod backendu wysyła odpowiedź, a frontend ją odbiera.
W przeciwieństwie do zwykłej sytuacji, gdy po odebraniu odpowiedzi połączenie TCP jest zamykane, w tym przypadku pozostaje otwarte dla kolejnych żądań HTTP.
Co się stanie, jeśli frontend wyśle kolejne żądania do backendu?
Do przesyłania tych żądań zostanie wykorzystane otwarte połączenie TCP, wszystkie żądania trafią do tego samego podu backendu, do którego trafiło pierwsze żądanie.
Czy iptables nie powinny przekierować ruch?
Nie w tym przypadku.
Gdy nawiązywane jest połączenie TCP, przechodzi ono przez zasady iptables, które wybierają konkretny pod backendu, do którego trafi ruch.
Ponieważ wszystkie kolejne zapytania są przesyłane przez już otwarte połączenie TCP, zasady iptables nie są już wywoływane.
Zobaczmy, jak to wygląda.
- Pierwszy pod wysyła zapytanie do usługi:

- Już wiesz, co będzie dalej. Usługa nie istnieje, ale są zasady iptables, które obsłużą zapytanie:

- Jeden z podów backendu zostanie wybrany jako adres docelowy:

- Zapytanie dociera do poda. W tym momencie ustalane jest trwałe połączenie TCP między dwoma podami:

- Jakiekolwiek następne zapytanie z pierwszego poda będzie przebiegać przez już nawiązane połączenie:

W rezultacie uzyskałeś szybszy czas reakcji i większą przepustowość, ale straciłeś możliwość skalowania backendu.
Nawet jeśli masz dwa pod'y w backendzie, przy stałym połączeniu ruch zawsze będzie trafiał na jeden z nich.
Czy można to naprawić?
Ponieważ Kubernetes nie wie, jak balansować trwałe połączenia, zadanie to spoczywa na Tobie.
Usługi to zestaw adresów IP i portów, które nazywane są punktami końcowymi.
Twoja aplikacja może uzyskać listę punktów końcowych z usługi i zdecydować, jak rozdzielać zapytania między nimi. Można nawiązać stałe połączenie z każdym pod'em i balansować zapytania między tymi połączeniami za pomocą round-robin.
Lub zastosować bardziej .
Kod po stronie klienta, który odpowiada za balansowanie, powinien postępować według takiej logiki:
- Uzyskać listę punktów końcowych z usługi.
- Dla każdego punktu końcowego nawiązać stałe połączenie.
- Gdy konieczne jest zrobienie zapytania, użyć jednego z otwartych połączeń.
- Regularnie aktualizować listę punktów końcowych, tworzyć nowe lub zamykać stare stałe połączenia w przypadku zmiany listy.
Tak to będzie wyglądać.
- Zamiast tego, aby pierwszy pod wysłał zapytanie do usługi, możesz balansować zapytania po stronie klienta:

- Musisz napisać kod, który zapyta, które pod'y są częścią usługi:

- Gdy otrzymasz listę, zachowaj ją po stronie klienta i użyj do nawiązania połączenia z podami:

- Sami odpowiadasz za algorytm równoważenia obciążenia:

Teraz pojawia się pytanie: czy ten problem dotyczy tylko HTTP keep-alive?
Równoważenie obciążenia po stronie klienta
HTTP nie jest jedynym protokołem, który może korzystać z trwałych połączeń TCP.
Jeśli Twoja aplikacja używa bazy danych, połączenie TCP nie jest otwierane za każdym razem, gdy potrzebujesz wykonać zapytanie lub uzyskać dokument z bazy danych.
Zamiast tego otwierane i wykorzystywane jest trwałe połączenie TCP z bazą danych.
Jeśli Twoja baza danych jest wdrożona w Kubernetes i dostęp jest udostępniany jako usługa, napotkasz te same problemy, które opisano w poprzedniej sekcji.
Jedna replika bazy danych będzie bardziej obciążona niż pozostałe. Kube-proxy i Kubernetes nie pomogą w równoważeniu połączeń. Musisz zająć się równoważeniem zapytań do swojej bazy danych.
W zależności od tego, jakiej biblioteki używasz do połączenia z bazą danych, mogą istnieć różne sposoby rozwiązania tego problemu.
Poniżej znajduje się przykład dostępu do klastra bazy danych MySQL z Node.js:
var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();
var endpoints = /* pobierz punkty końcowe z usługi */
for (var [index, endpoint] of endpoints) {
poolCluster.add(`mysql-replica-${index}`, endpoint);
}
// Wykonaj zapytania do sklonowanej bazy danych MySQLIstnieje wiele innych protokołów, które używają trwałych połączeń TCP:
- WebSockets i zabezpieczone WebSockets
- HTTP/2
- gRPC
- RSockets
- AMQP
Powinieneś być już zaznajomiony z większością tych protokołów.
Ale jeśli te protokoły są tak popularne, dlaczego nie ma ustandaryzowanego rozwiązania do równoważenia? Dlaczego zmiana logiki klienta jest konieczna? Czy istnieje natywne rozwiązanie Kubernetes?
Kube-proxy i iptables zostały stworzone, aby rozwiązać większość standardowych scenariuszy użycia w przypadku wdrażania w Kubernetes. Zrobiono to dla wygody.
Jeśli używasz usługi internetowej, która udostępnia REST API, masz szczęście — w takim przypadku trwałe połączenia TCP nie są używane, możesz korzystać z każdej usługi Kubernetes.
Ale jak tylko zaczniesz używać trwałych połączeń TCP, musisz się zastanowić, jak równomiernie rozłożyć obciążenie na backendy. Kubernetes nie zawiera gotowych rozwiązań w tym przypadku.
Jednak oczywiście istnieją opcje, które mogą pomóc.
Równoważenie długoterminowych połączeń w Kubernetes
W Kubernetes istnieją cztery typy usług:
- ClusterIP
- NodePort
- LoadBalancer
- Headless
Pierwsze trzy usługi działają na podstawie wirtualnego adresu IP, który jest używany przez kube-proxy do tworzenia reguł iptables. Jednak fundamentem wszystkich usług jest usługa typu headless.
Usługa headless nie jest powiązana z żadnym adresem IP i jedynie udostępnia mechanizm uzyskiwania listy adresów IP oraz portów powiązanych z odpowiednimi podami (punktami końcowymi).
Wszystkie usługi bazują na usłudze headless.
Usługa ClusterIP to usługa headless z pewnymi dodatkami:
- Warstwa zarządzania przypisuje jej adres IP.
- Kube-proxy tworzy niezbędne reguły iptables.
Dzięki temu możesz zignorować kube-proxy i bezpośrednio używać listy punktów końcowych uzyskanych z usługi headless do równoważenia obciążenia w swojej aplikacji.
Ale jak dodać taką logikę do wszystkich aplikacji wdrożonych w klastrze?
Jeśli twoja aplikacja jest już wdrożona, może się wydawać, że to zadanie jest niewykonalne. Istnieje jednak alternatywne rozwiązanie.
Service Mesh może Ci pomóc.
Pewnie już zauważyłeś, że strategia równoważenia obciążenia po stronie klienta jest całkiem standardowa.
Kiedy aplikacja się uruchamia, to:
- Uzyskuje listę adresów IP z usługi.
- Otwiera i utrzymuje pulę połączeń.
- Okresowo aktualizuje pulę, dodając lub usuwając punkty końcowe.
Gdy aplikacja chce wykonać żądanie, to:
- Wybiera dostępne połączenie, korzystając z jakiejś logiki (na przykład round-robin).
- Wykonuje zapytanie.
Te kroki działają zarówno dla połączeń WebSockets, gRPC, jak i AMQP.
Możesz wyodrębnić tę logikę do osobnej biblioteki i używać jej w swoich aplikacjach.
Jednak zamiast tego można skorzystać z siatek usług, takich jak Istio lub Linkerd.
Service Mesh uzupełnia Twoją aplikację procesem, który:
- Automatycznie wyszukuje adresy IP usług.
- Sprawdza połączenia, takie jak WebSockets i gRPC.
- Równoważy żądania, korzystając z odpowiedniego protokołu.
Service Mesh pomaga zarządzać ruchem wewnątrz klastra, ale jest dość zasobożerny. Inne opcje to korzystanie z zewnętrznych bibliotek, na przykład Netflix Ribbon, lub zaprogramowanych proxy, na przykład Envoy.
Co się stanie, jeśli zignorujesz kwestie równoważenia?
Możesz nie używać równoważenia obciążenia i mimo to nie zauważyć żadnych zmian. Spójrzmy na kilka scenariuszy pracy.
Jeśli masz więcej klientów niż serwerów, to nie jest taki duży problem.
Załóżmy, że jest pięciu klientów, którzy łączą się z dwoma serwerami. Nawet jeśli nie ma równoważenia obciążenia, oba serwery będą używane:

Połączenia mogą być rozdzielane nierównomiernie: możliwe, że czterech klientów połączyło się z tym samym serwerem, ale istnieje duża szansa, że oba serwery będą wykorzystywane.
Znacznie bardziej problematyczny jest odwrotny scenariusz.
Jeśli masz mniej klientów i więcej serwerów, twoje zasoby mogą być niewystarczająco wykorzystywane, co może prowadzić do potencjalnych wąskich gardeł.
Załóżmy, że są dwaj klienci i pięć serwerów. W najlepszym przypadku będą dwa stałe połączenia z dwoma serwerami spośród pięciu.
Pozostałe serwery będą stały bezczynnie:

Jeśli te dwa serwery nie mogą poradzić sobie z obsługą żądań klientów, skalowanie poziome nie pomoże.
Podsumowanie
Usługi Kubernetes są stworzone do pracy w większości standardowych scenariuszy aplikacji webowych.
Jednakże, gdy zaczynasz pracować z protokołami aplikacji, które wykorzystują stałe połączenia TCP, takimi jak bazy danych, gRPC lub WebSockets, usługi te przestają być odpowiednie. Kubernetes nie zapewnia wewnętrznych mechanizmów do równoważenia stałych połączeń TCP.
To oznacza, że musisz pisać aplikacje z myślą o możliwości równoważenia po stronie klienta.
Tłumaczenie przygotowane przez zespół .
Co jeszcze przeczytać na ten temat:
- .
- .
- .
Źródło: habr.com




























