W ostatnim czasie bardzo dużo się u mnie technologicznie dzieje. Wypływam na różne nowe obszary, co też rodzi sporo sytuacji stykowych, a w konsekwencji: bóle głowy, wyrwane włosy i myśli czarne, jak niebo nad Mordorem.
Wypadkową tych wycieczek w nieznane mi obszary jest pomysł na nową serię: fixit, w której mam zamiar dokumentować ciekawe przypadki napotkanych błędów (z rozwiązaniami, rzecz jasna). Niniejszym otwieram kategorię wpisem o problemach z błędem 499 na nginx w konfiguracji kubernetesowej.
Jakiś czas temu, klienci aplikacji nad którą pracuję, zaczęli dostawać niecodzienne błędy timeoutu na jednej z końcówek. Sytuacja niepożądana, szczególnie, że w większości przypadków oznacza grubsze problemy. Inwestygacja wykazała, że jeden ze skryptów, odpalanych w (nazwijmy to) “mikroserwisie” 3rd-part, zaczął niebezpiecznie puchnąć, przez co konsumował zbyt dużo czasu na wykonanie. Brak dostępu do tej części aplikacji zmusiło nas do tymczasowego podniesienia limitu czasu wykonania… tylko na którym z serwerów?!
Oto, jak mniej więcej prezentowała się infrastruktura, z którą mieliśmy do czynienia:
Jak widać, mamy tutaj dwa gorące miejsca:
1) Ingress Controller do zarządzania ruchem wewnątrz klastra 2) Serwer nignx, który pracuje jako load balancer i steruje ruchem również poza klaster, m.in. do skryptu powodującego timeout.
Powstało pytanie: na którym serwerze mamy problem? Życie przyniosło szybką odpowiedź: na obu.
Oto sytuacja widziana oczami klienta:
Tymczasem po stronie serwera wygląda to nieco inaczej:
time/consuming/script/*
wskazuje na skrypt spoza klastra.Najsensowniejszym rozwiązaniem wydaje się podbicie timeoutu na Load Balancerze, żeby zaczekał nieco dłużej i przekazał do ingressa rezultat, a nie ucinał requesta. No, ale niezupełnie. Jeśli ustawimy tam 75 sekund, Ingress Controller wciąż zwraca 504!
Szybki rzut oka do logów ingressa wskazuje winnego: 499 na Load Balancerze.
Ale zaraz… jak to możliwe?! Błąd HTTP 499 oznacza dosłownie client closed request
. Jaki klient?! Przecież nasz, czerwony od złości klient sprzed komputera, choć niecierpliwy, czeka na wykonanie requesta ponad 60 sekund. Zgoda. Natomiast, jeśli rzucimy okiem na początkowy diagram zobaczymy, że tak naprawdę klientem Load Balancera jest… Ingress Controller. I to właśnie Ingress Controller jest “klientem”, który zamyka połączenie jeszcze przed jego zakończeniem.
Oto jak wygląda sytuacja w chwili obecnej:
time/consuming/script/*
wskazuje na skrypt spoza klastra.Mamy winnego, wysoki sądzie, można się rozejść.
Rozwiązanie jest proste - trzeba podnieść limit czasu żądania na ingress controllerze. Możemy to zrobić w definicji naszego ingressa na kubernetesie za pomocą anotacji przekazywanych później do nginksa. Z uwagi jednak na to, że zwiększony timeout wystawia naszą aplikację na niebezpieczeństwo niestabilności czy ataków DDoS, chcielibyśmy uniknąć sytuacji w której każda końcówka naszego API będzie miała wydłużony czas wykonania.
Domyślnie, uproszczona konfiguracja może wyglądać w następujący sposób:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: sample-ingressannotations:kubernetes.io/ingress.class: "nginx"spec:tls:- hosts:- sample.com- www.sample.comrules:- host: sample.comhttp:paths:- backend:service:name: load-balancer-nginxport:number: 80path: /pathType: Prefix
To, co chcielibyśmy zrobić, to zapewnić, że tylko na ścieżce /time/consuming/script/*
timeout będzie zwiększony. Nie możemy tego zrobić tą samą regułą, ponieważ zostałaby zaaplikowana do wszystkich zdefiniowanych w niej ścieżek, dlatego dodamy nową, która dokładnie opisze nasze potrzeby:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: ingress-timeoutannotations:kubernetes.io/ingress.class: "nginx"nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"nginx.ingress.kubernetes.io/proxy-send-timeout: "300"nginx.ingress.kubernetes.io/proxy-read-timeout: "300"spec:tls:- hosts:- sample.com- www.sample.comrules:- host: sample.comhttp:paths:- backend:service:name: load-balancer-nginxport:number: 80path: /time/consuming/script/pathType: Prefix
To wszystko! Aplikujemy naszą konfigurację za pomocą kubectl i po chwili requesty, które trafiają na /time/consuming/script/
zostają wykonane ze zwiększonym limitem czasowym.
Mam nadzieję, że powyższy case uratuje Wam trochę czasu w zmaganiach z błędem 499 w przyszłości!