Case study
Kubernetes aplikacija koristi javno DNS ime za interni API: kako zadrzati saobracaj unutar klastera
Klijent nas je kontaktirao zato sto bi njihov sajt povremeno postajao primetno spor.
Vecinu vremena radio je normalno. Iskoriscenost CPU-a i memorije delovala je razumno, application Pods su bili zdravi, i nije bilo ocigledne korelacije sa aktivnostima deploy-a ili opterecenjem baze podataka.
Zatim bi, bez ocigledne promene unutar klastera, vreme generisanja stranice poraslo.
Nekoliko minuta kasnije mogao je da se vrati u normalu.
Taj obrazac je problem cinio teskim za reprodukciju i lakim za pogresno pripisivanje samoj aplikaciji.
Prva korisna korelacija: external network RTT
Tokom jednog od sporijih perioda, uporedili smo vreme odziva aplikacije sa merenjima mreze sa Kubernetes nodova i Podova.
Zanimljiv deo nije bila latencija izmedu Kubernetes servisa.
Bila je latencija ka spoljnim network resursima.
Periodi lose performanse sajta korelirali su sa povecanim RTT-om van klastera.
Tacni okidac nije uvek bio isti. Uocili smo nekoliko uslova koji su to mogli da izazovu:
- sporiji external DNS resolution;
- privremeno zasicenje spoljnog mreznog kanala;
- povecan packet loss ili RTT upstream;
- routing promene;
- odrzavanje ili degradacija kod provajdera.
Nijedan od tih uslova nije bio dovoljno ozbiljan da sajt ucini potpuno nedostupnim.
Samo su dodavali latenciju.
Pitanje je bilo zasto je spoljašnja latencija uticala na zahteve izmedu komponenti koje su radile u istom Kubernetes klasteru.
Pronalazenje neocekivane spoljne zavisnosti
Aplikacija je koristila API URL slican:
| |
To je bilo potpuno razumno za browser-e koji pristupaju aplikaciji sa Interneta.
Problem je bio sto je server-side aplikacija koristila isti URL.
Zahtev koji je poticao iz Pod-a zato je otprilike pratio ovu putanju:
flowchart LR
APP["Application Pod"]
DNS["CoreDNS"]
PDNS["Public DNS"]
EDGE["Public network / CDN / Load Balancer"]
GW["Kubernetes Gateway"]
API["API Service"]
APP --> DNS
DNS --> PDNS
PDNS --> EDGE
EDGE --> GW
GW --> API
Aplikacija i API su mogli da rade na susednim Kubernetes nodovima, ali je komunikacija izmedu njih i dalje zavisila od spoljne infrastrukture.
U praksi, interni zahtev je izlazio iz klastera i ponovo ulazio u njega.
To je cinilo latenciju aplikacije zavisnom od sistema koji uopste nisu trebalo da budu ukljuceni u taj zahtev.
Zasto je problem bio povremen
To je takodje objasnilo zasto je sajt bio tezak za troubleshooting.
Kada je external RTT bio normalan, dodatna mrežna putanja dodavala je dovoljno malo latencije da to niko nije primecivao.
Kada su external DNS, uplink, upstream routing ili provajder postali sporiji, svaki server-side API zahtev nasledio je to kasnjenje.
Na primer, render stranice moze zahtevati vise API poziva:
Ako svaki zahtev dobije dodatnu mrežnu latenciju, efekat moze da se akumulira.
U zavisnosti od toga da li aplikacija te zahteve izvrsava sekvencijalno ili paralelno, relativno umereno povecanje RTT-a moze postati vidljivo povecanje vremena generisanja stranice.
Sama infrastruktura zato moze izgledati zdravo:
dok korisnici i dalje imaju utisak sporog sajta.
Skrivena zavisnost je:
Ocigledno resenje nije uvek moguce
Ako konfiguracija aplikacije moze da razlikuje browser-side i server-side API URL-ove, najcistije resenje je jednostavno.
Koristite javni hostname za browser-e:
| |
i Kubernetes service discovery za server-side komunikaciju:
| |
Na primer:
Tada interni zahtev postaje:
flowchart LR
APP["Application Pod"]
API["API Service"]
APP -->|"cluster-local traffic"| API
Nema javnog DNS resolution-a, spoljnog ruta, CDN-a ili javnog load balancer-a.
Ali promena aplikacije nije nužno bila najbolja opcija u ovom slucaju.
Isti API hostname je vec bio deo konfiguracije aplikacije, a postojala je i druga komplikacija: hostname nije morao da mapira na jedan jedini Kubernetes Service.
Jedan hostname moze da predstavlja vise Kubernetes Servisa
Razmotrite Gateway API konfiguraciju gde:
| |
ide ka jednom Service-u,
| |
ide ka drugom,
a sve ostalo ide ka glavnom API-ju.
Na primer:
| |
U ovoj konfiguraciji, promena:
| |
u:
| |
bila bi pogresna.
Zahtev:
| |
i dalje mora da prodje kroz routing sloj tako da /auth stigne do auth-api.
Isto vazi i za routing zasnovan na header-ima, weighted backends, URL rewrites, redirects ili druge Gateway politike.
Zato je zahtev postao precizniji:
Zadrzati
api.example.comnepromenjen iz perspektive aplikacije, ali ga rezolovati na cluster-internal ulaznu tacku kada zahtev potice iz Kubernetes-a.
Split-horizon DNS
Ovo moze da se implementira internim DNS resolution-om.
Spolja:
Unutar Kubernetes-a:
Aplikacija i dalje zahteva potpuno isti URL:
| |
ali CoreDNS usmerava konekciju ka internom Kubernetes odredistu.
flowchart LR
APP["Application Pod"]
DNS["CoreDNS"]
GW["Internal Gateway"]
ROUTE["HTTPRoute"]
AUTH["auth-api"]
FILES["files-api"]
CORE["core-api"]
APP -->|"api.example.com"| DNS
DNS -->|"internal address"| GW
GW --> ROUTE
ROUTE -->|"/auth/*"| AUTH
ROUTE -->|"/files/*"| FILES
ROUTE -->|"/*"| CORE
Ovo ima vaznu osobinu: menja se samo DNS resolution.
URL i dalje sadrzi:
| |
pa HTTP zahtev i dalje nosi:
| |
a HTTPS konekcija i dalje koristi api.example.com kao TLS server name.
Gateway routing zasnovan na originalnom hostname-u zato i dalje radi.
CoreDNS rewrite
Jedan nacin da se ovo implementira unutar Kubernetes-a jeste pomocu CoreDNS rewrite plugina.
Pretpostavimo da je interni Gateway dostupan preko:
| |
CoreDNS pravilo moze da mapira javno ime na njega:
Lookup tada postaje:
Aplikacija ne mora da zna da se ovo desilo.
Pojednostavljena CoreDNS konfiguracija mogla bi da izgleda ovako:
| |
Sada se aplikacija i dalje konektuje na:
| |
ali paketi ostaju unutar klastera.
Zadrzite Gateway kada on obezbedjuje application routing
Za jedan backend, CoreDNS bi teoretski mogao da ukazuje direktno na Service.
Za slozenije aplikacije, bezbednije je zadrzati interni Gateway.
Rezultujuca arhitektura izgleda ovako:
flowchart TD
INTERNET["Internet clients"]
PODS["Application Pods"]
PDNS["Public DNS"]
CDNS["CoreDNS"]
PUBLIC["Public Gateway"]
INTERNAL["Internal Gateway"]
ROUTE["HTTPRoute<br/>api.example.com"]
AUTH["auth-api"]
FILES["files-api"]
CORE["core-api"]
INTERNET --> PDNS
PDNS --> PUBLIC
PODS --> CDNS
CDNS --> INTERNAL
PUBLIC --> ROUTE
INTERNAL --> ROUTE
ROUTE --> AUTH
ROUTE --> FILES
ROUTE --> CORE
I eksterni i interni klijenti koriste isti logicki hostname i ista routing pravila.
Razlika je samo u nacinu na koji dolaze do Gateway-a.
HTTPS zahteva posebnu paznju
DNS rewriting ne menja portove niti zavrsava TLS.
Ako aplikacija zahteva:
| |
interior endpoint mora da podrzava HTTPS na ocekovanom portu.
Ovo ne bi radilo:
DNS ne moze da pretvori HTTPS konekciju u HTTP.
Interni Gateway je ovde koristan zato sto moze da terminira TLS koristeci sertifikat validan za:
| |
a zatim da prosledi zahteve na interne HTTP servise.
Na primer:
| |
Klijent i dalje proverava sertifikat prema api.example.com, pa sertifikat koji prezentuje interni Gateway mora biti validan za taj hostname.
Testirajte rutu pre nego sto promenite DNS
DNS ne mora da se menja da bi se arhitektura testirala.
Pretpostavimo da interni Gateway Service ima ClusterIP:
| |
Iz application Pod-a:
curl --resolve forsira konekciju ka specificiranoj IP adresi uz zadrzavanje hostname-a.
To znaci da test i dalje koristi:
| |
a TLS i dalje vidi:
| |
To ga cini korisnim testom bas za ovaj scenario.
Testirajte sve vazne rute:
Tek nakon sto ti zahtevi stignu do ocekivanih backend-ova treba uvesti DNS override.
Proverite iz stvarnog application Pod-a
Nakon primene CoreDNS promene:
Vracena adresa sada bi trebalo da pripada internom putu, a ne javnom load balancer-u ili CDN-u.
Zatim testirajte pravi URL:
Ne bi trebalo vise da bude potrebe za --resolve.
Gateway ruta bi takodje trebalo da se proveri:
| |
i:
Za troubleshooting je vredno proveriti kompletan put, a ne stati nakon uspesnog DNS resolution-a:
Uspešan lookup sam po sebi ne dokazuje da zahtev stize do zeljene pozadine.
hostAliases mogu biti korisni za dokazivanje hipoteze
Pre nego sto promenite CoreDNS na nivou celog klastera, privremeni Pod-level override moze biti koristan:
Ovo je razuman dijagnosticki korak.
Ako latencija aplikacije prestane da prati external network RTT nakon ove promene, hipoteza je znacajno potvrdena.
Ali hostAliases obicno nije dobro trajno resenje.
IP adresa je hardkodovana. Ako se Gateway Service ponovo kreira sa drugacijim ClusterIP-jem, konfiguracija zastareva.
CoreDNS umesto toga moze da mapira javni hostname na Kubernetes Service DNS ime:
To cuva Kubernetes service discovery.
ExternalName resava suprotan problem
ExternalName Service moze da natera Kubernetes Service ime da pokazuje na eksterni hostname:
To daje:
Nas zahtev je obrnut:
zato ExternalName ne resava ovaj slucaj.
Nemojte slucajno ukloniti funkcionalnost koju pruza javni edge
Jos jedna provera je potrebna pre nego sto zaobidete javnu putanju.
Spoljni endpoint mozda obezbedjuje funkcionalnost kao sto su:
Ako interni zahtevi zavise od bilo cega od toga, zaobilaženje edge-a moze promeniti ponasanje aplikacije.
Pravi cilj nije:
zaobici sto je vise moguce infrastrukture.
Vec:
ukloniti infrastrukturu koja ne bi smela da bude u internoj putanji zahteva, uz zadrzavanje infrastrukture koja je deo ponasanja aplikacije.
U ovom slucaju, zadrzavanje Gateway routing sloja uz uklanjanje javnog DNS-a i spoljne mrežne putanje dalo nam je tu separaciju.
Pre i posle
Originalna putanja zahteva bila je:
flowchart LR
APP["Application Pod"]
CDNS["CoreDNS"]
PDNS["Public DNS"]
NET["External network"]
EDGE["CDN / Public LB"]
GW["Gateway"]
ROUTE["HTTPRoute"]
API["API"]
APP --> CDNS
CDNS --> PDNS
PDNS --> NET
NET --> EDGE
EDGE --> GW
GW --> ROUTE
ROUTE --> API
Performanse sajta su zato delom zavisile od spoljašnjih mrežnih uslova.
Nakon promene:
flowchart LR
APP["Application Pod"]
CDNS["CoreDNS"]
GW["Internal Gateway"]
ROUTE["HTTPRoute"]
API["API"]
APP -->|"api.example.com"| CDNS
CDNS -->|"cluster-local"| GW
GW --> ROUTE
ROUTE --> API
URL aplikacije je ostao nepromenjen:
| |
Ponasanje routing-a je ostalo nepromenjeno.
Ali server-side komunikacija vise nije zavisila od external DNS resolution-a, upstream network RTT-a, dostupnosti javnog load-balancer-a ili routing uslova provajdera.
Sira lekcija o troubleshooting-u
Pocetni simptom je izgledao kao problem performansi aplikacije:
Sajt je obicno brz, ali povremeno postaje spor bez ociglednog razloga.
CPU, memorija, performanse baze podataka i zdravstveno stanje Kubernetes-a nisu bili dovoljni da to objasne.
Korisni trag se pojavio tek nakon poredjenja latencije aplikacije sa external network RTT-om.
To je otkrilo arhitektonsku zavisnost koju je lako propustiti: dve aplikacije koje rade unutar istog Kubernetes klastera komunicirale su preko javnog endpoint-a.
Javna hostname imena unutar konfiguracije aplikacije nisu nuzno problem. Postaju problem kada server-side saobracaj nepotrebno nasledi dostupnost i latenciju spoljne mrežne putanje.
Kada se trazi uzrok povremene latencije aplikacije u Kubernetes-u, zato vredi proveriti ne samo do kog servisa zahtev stize, vec i kako do njega stize.