Testowanie ruchem produkcyjnym. Mirroring – Nginx vs Envoy

Opublikowane przez admin w dniu

Cześć. Pisząc następny wpis do serii CI/CD pipeline z użyciem Kubernetesa, AWS, Azure i .NET Core natknąłem się na problem z Octopus Deploy, o którym poinformowałem jego deweloperów tutaj. Został on już naprawiony, jednak w międzyczasie popełniłem ten wpis. Wyskoczmy trochę z “hype trainu” k8s.

Wstęp

Jak wiemy, zmiany w aplikacji bywają trudne. Często po wdrożeniu “ulepszonej” wersji aplikacji dostajemy obuchem w głowę, bo nie była ona przetestowana ruchem produkcyjnym. Zrobiliśmy, co mogliśmy: testy jednostkowe, integracyjne, manualne itd., jednak obciążenie aplikacji prawdziwym ruchem często prowadzi do powstania sytuacji, których nie przewidzieliśmy. Aby przetestować ją ruchem produkcyjnym możemy użyć tzw. mirroringu requestów.

 

Czym jest mirroring ?

Mirroring sam w sobie jest bardzo prostym rozwiązaniem. Polega na skopiowaniu requestu i wysłaniu go do innego serwera i zignorowaniu odpowiedzi. Skopiowany ruch możemy przekierować do infrastruktury, gdzie chcemy przetestować nową wersję aplikacji. Może to być bardzo fajne narzędzie do testów nowych wersji aplikacji bez wpływania na działanie aktualnej.

O czym trzeba pamiętać

Samo skopiowanie requestu zwykle nie wystarczy. Każda aplikacja korzysta z zewnętrznych zależności. Nie możemy po prostu użyć konfiguracji aplikacji produkcyjnej. Powyższe testy będą wymagały oddzielnego środowiska testowego zawierającego oddzielną bazę danych, szynę itp. Może się to okazać kosztowne, np. w przypadku, gdy chcemy przetestować aplikację na realnym ruchu, ale także na realnych danych z bazy. Będziemy musieli odtworzyć jej backup na nowym środowisku. Dodatkowo, aby testy były miarodajne, ważne by zapewnić dobre logowanie, śledzenie i metryki w obu środowiskach (prod i mirror) po to, by móc w jakiś sposób porównać/przeanalizować, czy czegoś nie popsuliśmy.  Popularne narzędzia do tzw. observability to oczywiście Jaeger, Prometheus, Graphana czy ELK. W .NET Core możemy użyć np. Convey, który bardzo ułatwia wdrożenie powyższych do naszej aplikacji. Niestety, mirroringiem możemy przetestować części systemu, które aktualnie przyjmują już jakiś ruch z zewnątrz. Nie przetestujemy tym nowych endpointów, dlatego mirroring najlepiej nadaje się do testów istniejących rozwiązań, np. gdy chcemy przepisać/zrefaktorować istniejący system.

Mirroring Nginx vs Envoy

Przed przystąpieniem do implementacji tego rozwiązania przeprowadziłem testy dwóch popularnych proxy Nginx i Envoya. Stworzyłem dwie proste aplikacje w .NET Core. Pierwsza z nich posiada 4 proste endpointy. Cała konfiguracja potrzebna do testów obydwu rozwiązań znajduje się na moim githubie.

Każda z nich zwraca prosty tekst odpowiadający temu jak zachowa się mirror. Druga aplikacja posiada odpowiedniki powyższych endpointów.

Końcówka normal nie robi nic, bugs rzuca wyjątek, a slow czeka 5 sekund, aż zwróci wynik. Dodatkowo, oba kontrolery posiadają końcówkę POST. Przetestujemy dzięki niej, czy przepisanie całego body requestu wpłynie jakoś na wydajność rozwiązania.

Scenariusze testowe z użyciem nginxa oraz envoya:

  1. Ruch bez mirroringu
  2. Normalny mirroring GET /normal
  3. Normalny mirroring POST
  4. Mirroring, gdzie mirror rzuca wyjątek /bugs
  5. Mirroring, gdzie mirror odpowiada wolno  /slow
  6. Mirroring, gdzie mirror zostanie wyłączony

Dodatkowo, testy zakładają, że nas system posiada obciążenie na poziomie ~200 req/s. Do wytworzenia takiego ruchu użyję narzędzia hey. Wszystkie aplikacje można uruchomić z poziomu plików docker-compose. Oba proxy pozwalają na przekierowanie ruchu częściowo np. “kopiuj tylko 20% requestów”, jednak w poniższych testach kopiowany będzie cały ruch.

NGINX bez mirroringu

Konfiguracja:

 

Test /normal:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/normal

 

Wyniki:

 

 

Test endpointu POST:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 -m POST -d {"value1":1,"value2":"123"} -T application/json http://localhost:2001/api/mirroring

 

Wyniki:

 

Komentarz:

Dla pewności możemy sprawdzić, czy nic nie doszło do drugiej aplikacji:

docker logs nginx-app2

NGINX ze zwykłym mirroringiem

Konfiguracja:

 

Przebudowa kontenerów:

docker-compose up --build --force-recreate -d

 

Test /normal

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/normal

 

Test endpointu POST:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 -m POST -d {"value1":1,"value2":"123"} -T application/json http://localhost:2001/api/mirroring

 

Wyniki:

 

Komentarz:

Wyniki są w miarę podobne. Przy endpoincie GET wynik lekko spadł poniżej ~200 req/s.

NGINX z mirrorem który rzuca błędem

Test /bugs:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/bugs

 

Wyniki:

 

Komentarz:

Tutaj, jak widać spadek jest już znaczny, około 40% wydajności.

 

NGINX z mirrorem który jest wolny

Test /slow z zwiększonym timeoutem na 10 sekund:

hey -z 10s -q 210 -n 2100 -c 1 -t 10 http://localhost:2001/api/mirroring/slow

 

Wyniki:

 

Komentarz:

W przypadku, gdy nasz mirror działa wolno właściwie zabijamy naszą produkcje.

 

NGINX z mirrorem który został wyłączony

Ubijamy mirror:

docker kill nginx-app2

 

Test /normal, timeout 10 sekund:

hey -z 10s -q 210 -n 2100 -c 1 -t 10 http://localhost:2001/api/mirroring/normal

 

Wyniki:

 

Komentarz:

Gdy nasz mirror przestanie działać tak naprawdę ubijamy nasz system.

 

Envoy bez mirroringu

Konfiguracja:

 

Test /normal:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/normal

 

Wyniki:

 

Test endpointu POST:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 -m POST -d {"value1":1,"value2":"123"} -T application/json http://localhost:2001/api/mirroring

 

Wyniki:

 

Komentarz:

Do sprawdzenia, czy jakiś request doszedł do mirrora, możemy ponownie użyć:

docker logs envoy-app2

 

Envoy ze zwykłym mirroringiem

Konfiguracja:

 

Przebudowa kontenerów:

docker-compose up --build --force-recreate -d

 

Test /normal:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/normal

 

Test endpointu POST:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 -m POST -d {"value1":1,"value2":"123"} -T application/json http://localhost:2001/api/mirroring

 

Wyniki:

 

Komentarz:

Bez spadku, lepiej niż NGINX.

Envoy z mirrorem, który rzuca błędem

Test /bugs:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/bugs

 

Wyniki:

 

Komentarz:

Akceptowalny spadek.

Envoy z mirrorem który jest wolny

Test /slow:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/slow

 

Wyniki:

 

Komentarz:

Wszystko ok.

Envoy z mirrorem, który został wyłączony

Zabijamy drugą aplikację:

docker kill envoy-app2

 

Test /normal:

hey -z 10s -q 210 -n 2100 -c 1 -t 1 http://localhost:2001/api/mirroring/normal

 

Wyniki:

 

Komentarz:

Ponownie wszystko ok.

 

Podsumowanie

W powyższym teście mirroringu bezwzględnie wygrywa Envoy. Mirroring w Nginx  może z łatwością ubić nasz system. Sam mirroring jest fajnym rozwiązaniem, jednak przez koszty jego wdrożenia osobiście używam go do testowania tylko najbardziej wrażliwych części systemów.

Źródła

https://alex.dzyoba.com/blog/nginx-mirror/

https://norbinsh.github.io/nginx/2018/10/05/mirroring-http-requests-with-nginx.html

https://blog.markvincze.com/shadow-mirroring-with-envoy/