Kubernetes – jak działa cluster IP

Niedawno pisałem o implementacji sieci opartej o Flannel. Zabrakło tam jednak jednej istotnej rzeczy – clusterIP. W tym poście przeczytasz jak dokładnie jest to zaimplementowane.

Jeśli patrzyłeś na organizację interfejsów, adresów itd., to pewnie rzuciło Ci się w oczy, że nigdzie nie było routingu ani żadnego innego śladu po adresach IP przydzielonych do serwisów. Jeśli korzystasz z domyślnej konfiguracji, jaką Kubernetes daje, to jest to pula 10.96.0.0/16. Z niej można wybrać dowolny adres IP który będzie dostępny w całym klastrze Kubernetesa i routowany do konkretnego serwisu:


apiVersion: v1
kind: Service
metadata:
name: dinemic-io
labels:
app: dinemic-io
spec:
ports:
- port: 80
selector:
app: mnabozny-pl
clusterIP: 10.96.0.123

Serwis za pomocą definicji z pola selector wyszukuje odpowiednie aplikacje, do których będzie przekierowany ruch na port 80. Będąc w dowolnym miejscu klastra (node lub K8s master) zawsze będziemy mogli połączyć się z tym adresem, na port 80. Patrząc natomaist w tablicę routingu, nie znajdziemy ani jednego wpisu wskazującego na taką pulę adresów:


maciek> ip route show
default via 192.168.1.1 dev eth0 onlink
10.0.0.0/24 dev eth1 proto kernel scope link src 10.0.0.1
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink
10.244.4.0/24 via 10.244.4.0 dev flannel.1 onlink
...

Pula adresów 10.0.0.0/24 to fizyczna sieć klastra, w którym działa Kubernetes. 10.244.X.0/24 to pula adresów, które Kubernetes przydziela poszczególnym podom na nodach (gdzie X odpowiada nodowi, a konkretny numerek podowi).

Traceroute do adresu powyższego serwisu wychodzi zawsze poza moją sieć, zgodnie z tym, co mówi tablica routingu. Wget na http://10.96.0.123:80 zawsze magicznie dociera do celu. Gdzie więc czai się ta magia?

Iptables dla clusterIP

Popatrzmy w ipsables. Za pomocą sudo iptables-save można wylistować wszystkie regułki, które są aktualnie załadowane do jądra. Jest tego dość sporo, dla tego można wspomóc się grepem:


maciek> sudo iptables-save | grep 10.96.0.123
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.96.0.123/32 -p tcp -m comment --comment "cloudover/dinemic-io: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.96.0.123/32 -p tcp -m comment --comment "cloudover/dinemic-io: cluster IP" -m tcp --dport 80 -j KUBE-SVC-AA7UXNSLITSSEFAY

Do łańcucha KUBE-SERVICES jest przekierowywany cały ruch z łańcucha INPUT. Pierwsza z powyższych reułek oznacza pakiety, które mają być oznaczone do MASQUERADE. Druga natomiast przekazuje je do konkretnego łańcucha – KUBE-SVC-AA7UXNSLITSSEFAY. Obie reguły działają dla docelowego adresu 10.96.0.123, na docelowym porcie 80.

Łańcuch dla konkretnego serwisu

W powyższym outpucie z iptables-save można zobaczyć, że druga regułka przekierowuje druch do konkretnego łańcucha, odpowiadającego serwisowi – KUBE-SVC-AA7UXNSLITSSEFAY. Przyjżyjmy się mu bliżej:


maciek> sudo iptables -nL KUBE-SVC-AA7UXNSLITSSEFAY -t nat
Chain KUBE-SVC-AA7UXNSLITSSEFAY (1 references)
target prot opt source destination
KUBE-SEP-Q3TQA7ZS25JCTF3F all -- 0.0.0.0/0 0.0.0.0/0

Powyższy serwis przekierowuje cały ruch (100%) do łańcucha odpowiadającemu konkretnemu podowi, na port 80, robiąc przy okazji DNAT:


maciek> sudo iptables -nL KUBE-SEP-Q3TQA7ZS25JCTF3F -t nat
Chain KUBE-SEP-Q3TQA7ZS25JCTF3F (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- 10.244.3.115 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp to:10.244.3.115:80

Czyli, podsumowując, dowolny pakiet wchodzący do noda (INPUT) lub wychodzący z niego (OUTPUT) jest procesowany przez łańcuch KUBE-SERVICES. W nim natomiast K8S dodaje pojedyncze regułki, które wyłapują ruch kierowany do poszczególnych clusterIP przydzielonych do serwisów. Każdy serwis i clusterIP posiada swój osobny łańcuch KUBE-SVC-…, w którym jest rozdzielany ruch do poszczególnych podów i ich łańcuchów, w których jest realizowany DNAT.

Serwis dla wielu podów

Powyższy przykład pokazywał wpisy dla serwisu, który przekazuje ruch do jednego poda. Co jeśli przeskalujemy go do dwóch instancji? Szybkie kubectl apply -k . i po chwili mamy kilka zmian! Chwila oznacza dokładnie tyle, ile trzeba aby nowy pod się uruchomił i zaczął działać. Popatrzmy jeszcze raz w całe iptables. KUBE-SYSTEM pozostaje bez zmian, dalej kieruje ruch dla clusterIP 10.96.0.123 do naszego serwisu, w łańcuchu KUBE-SVC-AA7UXNSLITSSEFAY.

Natomiast sam łańcuch KUBE-SVC-AA7UXNSLITSSEFAY ma już nowe wpisy:

> sudo iptables -nL KUBE-SVC-AA7UXNSLITSSEFAY -t nat
Chain KUBE-SVC-AA7UXNSLITSSEFAY (1 references)
target prot opt source destination
KUBE-SEP-Q3TQA7ZS25JCTF3F all -- 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000
KUBE-SEP-OCALVIKCSC2PZKNJ all -- 0.0.0.0/0 0.0.0.0/0

Dodana została na początku nowa regułka kierująca 50% ruchu (popatrz na komentarz – statistic mode, z probability 0.5) do nowego poda, którego ruch jest kształtowany przez łancuch KUBE-SEP-Q3TQA7ZS25JCTF3F. Pozostałe 50% ruchu wpada w ostatnią regułkę, która jest tam od początku. Jeśli przeskalujemy nasz deployment do większej ilości podów, to udział procentowy odpowiednio spadnie do 100/N.

Skalowalność samego cluster IP

Można mieć wrażenie, że każdy pod w klastrze K8S jest w pewnym stopniu single point of failure i jest to prawdą. Natomiast dołożenie nad podami i naszymi aplikacjami abstrakcyjnej warstwy serwisów, które są czymś w rodzaju load balancera, pozwala bardzo łatwo ograniczyć ten słaby punkt. Jeśli nasze pody z aplikacją będą odpowiednio rozproszone po klastrze, w dużej ilości instancji, to awaria dowolnego poda nie wpłynie na dostępność naszego serwisu.

A co z samym clusterIP? Zauważ, że to konkretne IP, na które tu patrzyliśmy nie jest nigdzie “fizycznie” widoczne. Każdy host w klastrze K8S posiada swój własny zestaw regułek, zwykle spójny z innymi, przynajmniej pod kątem clusterIP. Próbując dostać się do konkretnego serwisu, właśnie przez clusterIP tak na prawdę iptables losuje nam IP poda, na który będziemy przekierowani i DNAT’owani.

Również dzięki temu można przekierować/przeroutować ruch np. z VPN lub zewnętrznej sieci do dowolnego noda, który stanie się wtedy gateway’em. Taki node po otrzymaniu pakietu do jednego z clusterIP przekieruje takie zapytanie dalej, do odpowiedniego poda.

Troubleshooting

A wszystko to, bo ostatnio pomieszałem labelki w selektorze aplikacji www i mysql’a 🙂 około połowa połączeń była odrzucana, przez co strona częściej nie działała niż działała. Jeśli doświadczasz losowego connection refused, to prawdopodobnie właśnie przy load balancingu/losowaniu poda K8S przekierowuje Twoje zapytanie do innego poda, który w danej chwili ma uruchomioną inną usługę. Po sprawdzeniu, zmianie labelek i przeładowaniu całego serwisu wszystko zaczyna działać.