all

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:

1
https://api.example.com

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:

1
2
3
4
5
frontend
    -> /api/user
    -> /api/menu
    -> /api/products
    -> /api/permissions

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:

1
2
3
4
5
CPU                OK
Memory             OK
Database           OK
Pods               OK
Kubernetes network OK

dok korisnici i dalje imaju utisak sporog sajta.

Skrivena zavisnost je:

1
2
3
4
5
6
7
server-side request
public DNS
external network path
same Kubernetes cluster

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:

1
https://api.example.com

i Kubernetes service discovery za server-side komunikaciju:

1
http://api.backend.svc.cluster.local:8080

Na primer:

1
2
PUBLIC_API_URL=https://api.example.com
INTERNAL_API_URL=http://api.backend.svc.cluster.local:8080

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:

1
https://api.example.com/auth/*

ide ka jednom Service-u,

1
https://api.example.com/files/*

ide ka drugom,

a sve ostalo ide ka glavnom API-ju.

Na primer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: public-api
  namespace: application
spec:
  parentRefs:
    - name: public-gateway
      namespace: gateway-system

  hostnames:
    - api.example.com

  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /auth
      backendRefs:
        - name: auth-api
          port: 8080

    - matches:
        - path:
            type: PathPrefix
            value: /files
      backendRefs:
        - name: files-api
          port: 8080

    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: core-api
          port: 8080

U ovoj konfiguraciji, promena:

1
api.example.com

u:

1
core-api.application.svc.cluster.local

bila bi pogresna.

Zahtev:

1
https://api.example.com/auth/login

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.com nepromenjen 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:

1
2
3
api.example.com
public address

Unutar Kubernetes-a:

1
2
3
api.example.com
internal Gateway

Aplikacija i dalje zahteva potpuno isti URL:

1
https://api.example.com/auth/login

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:

1
api.example.com

pa HTTP zahtev i dalje nosi:

1
Host: api.example.com

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:

1
internal-api-gateway.gateway-system.svc.cluster.local

CoreDNS pravilo moze da mapira javno ime na njega:

1
2
3
4
rewrite stop {
    name exact api.example.com internal-api-gateway.gateway-system.svc.cluster.local
    answer auto
}

Lookup tada postaje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Application
    |
    | lookup api.example.com
    v
CoreDNS
    |
    | rewrite
    v
internal-api-gateway.gateway-system.svc.cluster.local
    |
    v
Gateway Service ClusterIP

Aplikacija ne mora da zna da se ovo desilo.

Pojednostavljena CoreDNS konfiguracija mogla bi da izgleda ovako:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.:53 {
    errors
    health
    ready

    rewrite stop {
        name exact api.example.com internal-api-gateway.gateway-system.svc.cluster.local
        answer auto
    }

    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }

    prometheus :9153

    forward . /etc/resolv.conf

    cache 30
    loop
    reload
    loadbalance
}

Sada se aplikacija i dalje konektuje na:

1
https://api.example.com

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:

1
https://api.example.com

interior endpoint mora da podrzava HTTPS na ocekovanom portu.

Ovo ne bi radilo:

1
2
3
https://api.example.com:443
api-service:8080 HTTP

DNS ne moze da pretvori HTTPS konekciju u HTTP.

Interni Gateway je ovde koristan zato sto moze da terminira TLS koristeci sertifikat validan za:

1
api.example.com

a zatim da prosledi zahteve na interne HTTP servise.

Na primer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: internal-api-gateway
  namespace: gateway-system
spec:
  gatewayClassName: example-gateway-class

  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: api.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-example-com-tls

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:

1
10.96.80.25

Iz application Pod-a:

1
2
3
curl \
  --resolve api.example.com:443:10.96.80.25 \
  https://api.example.com/auth/health

curl --resolve forsira konekciju ka specificiranoj IP adresi uz zadrzavanje hostname-a.

To znaci da test i dalje koristi:

1
Host: api.example.com

a TLS i dalje vidi:

1
api.example.com

To ga cini korisnim testom bas za ovaj scenario.

Testirajte sve vazne rute:

1
2
3
curl \
  --resolve api.example.com:443:10.96.80.25 \
  https://api.example.com/auth/health
1
2
3
curl \
  --resolve api.example.com:443:10.96.80.25 \
  https://api.example.com/files/health
1
2
3
curl \
  --resolve api.example.com:443:10.96.80.25 \
  https://api.example.com/v1/health

Tek nakon sto ti zahtevi stignu do ocekivanih backend-ova treba uvesti DNS override.

Proverite iz stvarnog application Pod-a

Nakon primene CoreDNS promene:

1
2
kubectl exec -n application deploy/frontend -- \
  getent hosts api.example.com

Vracena adresa sada bi trebalo da pripada internom putu, a ne javnom load balancer-u ili CDN-u.

Zatim testirajte pravi URL:

1
2
kubectl exec -n application deploy/frontend -- \
  curl -v https://api.example.com/auth/health

Ne bi trebalo vise da bude potrebe za --resolve.

Gateway ruta bi takodje trebalo da se proveri:

1
kubectl get httproute -A

i:

1
2
3
kubectl describe httproute \
  -n application \
  public-api

Za troubleshooting je vredno proveriti kompletan put, a ne stati nakon uspesnog DNS resolution-a:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DNS resolution
TCP connection
TLS handshake
Gateway hostname match
HTTPRoute path match
backend Service

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:

1
2
3
4
5
spec:
  hostAliases:
    - ip: "10.96.80.25"
      hostnames:
        - "api.example.com"

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:

1
2
3
4
5
api.example.com
internal-api-gateway.gateway-system.svc.cluster.local
current Service ClusterIP

To cuva Kubernetes service discovery.

ExternalName resava suprotan problem

ExternalName Service moze da natera Kubernetes Service ime da pokazuje na eksterni hostname:

1
2
3
4
5
6
7
apiVersion: v1
kind: Service
metadata:
  name: external-api
spec:
  type: ExternalName
  externalName: api.example.com

To daje:

1
2
3
external-api.application.svc.cluster.local
api.example.com

Nas zahtev je obrnut:

1
2
3
api.example.com
internal Kubernetes destination

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:

1
2
3
4
5
6
7
WAF
authentication
rate limiting
header modification
IP restrictions
URL rewrites
CDN behavior

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:

1
https://api.example.com

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.