fbpx

Jak testować i debugować wysoce współbieżny kod?

Created with Sketch.

Jak testować i debugować wysoce współbieżny kod?

Post-23

Istnieją takie paradygmaty i języki programowania, w których klasyczne podejście do debugowania i testowania nie ma zbyt wiele sensu. W ramach alternatywy, takie środowiska udostępniają własne mechanizmy, aby ułatwić taki proces.

Jednym z takich środowisk jest Erlang, który słynie z mechanizmu śledzenia procesów (ang. tracing). Do czego wykorzystuje się takie języki i jak wyglądają takie mechanizmy – postaram się odpowiedzieć na to pytanie w tym artykule.

Współbieżność wszystko komplikuje…

Pozwólcie, że zacznę od historii. Po wdrożeniu na produkcję nowej wersji oprogramowania na całej flocie maszyn w czterech różnych regionach świata (ok. 500 maszyn wirtualnych w sumie), wszystko wyglądało w porządku. Po obserwacji metryk oraz sprawdzeniu logów nie znaleźliśmy nic, co mogłoby wskazywać na jakikolwiek problem. Odtrąbiliśmy sukces i zabraliśmy się za kolejne zadania.

Dwa dni później otrzymaliśmy zgłoszenie z działu obsługi klienta do zbadania. Jeden ze zintegrowanych partnerów skarży się na dużą ilość błędów zwracanych z naszego API, w dwóch różnych centrach danych. Zbadaliśmy metryki dla tego konkretnego partnera — wyglądały w porządku, ilość błędów — faktycznie większa, jednak dalej znajdowała się w przyjętej przez nas normie. Po krótkiej wymianie zdań, dotarliśmy do kolejnej wskazówki — odkryliśmy, że faktycznie API w pewnych przypadkach zamiast odpowiedzieć w prawidłowy sposób, wyrzucało błąd. Działo się to na tyle rzadko, że nie aktywowało to żadnego z alarmów (dlaczego statyczne progi przy monitorowaniu w takich środowiskach to temat na osobny artykuł, jeśli ktoś chciałby o tym posłuchać – dajcie znać). Po zebraniu wszystkich faktów, postanowiliśmy zbadać ten problem na żywym organizmie. Czy testowaliśmy nasze hipotezy na produkcji.

Zatrzymajmy się na chwilę i pomyślmy co normalnie robi się w takiej sytuacji. Najpewniej obniżamy poziom błędów tak, aby większość akcji zapisywać do logów aplikacji. Potem tylko mozolne czesanie tej tony wyprodukowanego tekstu i być może znajdziemy przyczynę. Możemy podmienić wersję aplikacji, która posiada dodatkowe punkty instrumentacji, nowe metryki, przy odrobinie odwagi (YOLO) może nawet spróbujemy podpiąć się debuggerem lub nawet nieśmiertelnym gdb do działającej aplikacji.

Postąpiliśmy inaczej i skorzystaliśmy z tego, co daje nam maszyna wirtualna Erlanga. Włączyliśmy mechanizm śledzenia procesów i zdarzeń dla tego konkretnego komponentu. I co się okazało? Bez większego wysiłku, produkcyjna maszyna i aplikacja wraz z włączonym podsystemem do debugowania były w stanie przetwarzać ruch z produkcyjnego środowiska. Kilkaset transakcji na sekundę. To przypominało diagnozowanie problemów z samochodem na autostradzie. Z tą różnicą, że mogliśmy bezpiecznie patrzeć pod maskę pędzącego samochodu nie mając żadnego kursu kaskaderskiego za sobą.

Po ustawieniu kilku punktów śledzenia i oczekiwaniu na ponowne wystąpienie sytuacji znaleźliśmy tą konkretną ścieżkę wykonania. Winę za problemy wewnątrz aplikacji ponosiła zależność, zewnętrzna biblioteka – konkretnie klient HTTP, który oczekiwał, że po otrzymaniu statusu 204 (No Content) serwer nie odsyła żadnego bajtu w odpowiedzi jako body. Założenie słuszne, jeśli spojrzycie w definicję RFC dla HTTP 1.1. Jednak standardy swoją szosą, a życie swoja – w przypadku tego konkretnego partnera ich serwer zwracał 204 a w treści dodatkowo odsyłał … tekst No Body. Tak dla pewności. Klient nie będąc na to przygotowanym wyrzucał wyjątek. W zasadzie jest to ciekawy problem sam z siebie, polecam sprawdzić jak zachowuje się wasz klient, tylko nie sprawdzajcie tego na produkcyjnej instancji, no chyba, że korzystacie z Erlanga i możecie skorzystać tak jak my ze śledzenia procesów.

Czym jest śledzenie procesów?

Część z was pewnie nie wierzy w powyższą opowieść. Takie środowisko, gdzie bez konsekwencji wydajnościowych można włączyć tryb debugowania nie istnieje. Spójrzcie jednak na to z innej strony — przy całkowicie współbieżnym modelu pracy nie mamy innej opcji, świat nie poczeka na nas, aż skorzystamy z breakpointa. Osoby, które próbowały namierzyć problem klasycznym debuggerem w wielowątkowym środowisku lub skomplikowanym systemie rozproszonym (ekhm, mikroserwisy, ekhm) wiedzą o czym mówię.

Tylko, że takie środowisko istnieje — wewnątrz maszyny wirtualnej Erlanga, zwanej bardzo często BEAM, mamy do dyspozycji taki mechanizm zwany śledzeniem procesów. Na poziomie środowiska uruchomieniowego możemy włączyć i wyłączyć generowania pewnych zdarzeń, które są wystawione z poziomu VM. Przykładem takich wywołań są wywołania funkcji, wysłanie wiadomości do innego procesu, stworzenie nowego procesu oraz odśmiecanie pamięci. Wszystko to na wyciągnięcie ręki, z pełną mocą języka programowania oraz zdalnej konsoli:

1> redbug:start("module:function->return").
{156,1}
 
2> module:function().

% 12:13:10 <0.42.0>({erlang,apply,4})
% module:function(some, parameters)

% 12:13:10 <0.42.0>({erlang,apply,4})
% module:function/2 -> ok
 
ok
3>

W powyższym przykładzie nasłuchujemy na wywołania funkcji function z modułu module i oprócz wywołania interesuje nas także wartość zwracana.

Innymi słowy: mamy do dyspozycji w każdej aplikacji zewnętrzny komponent, który generuje strumień zdarzeń, historię akcji z wykonania naszej aplikacji do konkretnego odbiorcy.

Przypomina to obserwowanie układu elektronicznego mając do dyspozycji różnego rodzaju próbniki, narzędzia (np. multimetr lub oscyloskop). W większości przypadków narzut tych narzędzi jest pomijalny i nie wpływa na obiekt obserwacji. Oczywiście, w przypadku błędu operatora, może dojść do małej katastrofy, ale czynnika ludzkiego nie możemy tutaj uniknąć.

Dlaczego w ogóle istnieje śledzenie procesów?

Część z Was na pewno zastanawia się, co kierowało twórcami maszyny wirtualnej, aby dodać taką funkcjonalność na poziomie maszyny wirtualnej. Odpowiedź na to pytanie jest szalenie ciekawa i warto się nad tym zastanowić.

Ponad 30 lat temu, kiedy Ericsson zainwestował czas i pieniądze w stworzenie Erlanga potrzebowali oni platformy, którą będzie można zdalnie debugować. Dlaczego? Ponieważ znali biznes telekomunikacji od podszewki i wiedzieli, że wszystkich problemów nie da się uniknąć, ani tym bardziej przewidzieć. A nie można odciąć ludzi od dostępu do telefonu, zwłaszcza jeśli chodzi o tak krytyczną infrastrukturę np. jak połączenia alarmowe.

Tworząc nowy język programowania oraz wirtualną maszynę udało im się spełnić to założenie. Jedno z najważniejszych dla nich w dawnych czasach. Współbieżna obsługa połączeń telefonicznych w switchu telefonicznym to niełatwe zadanie, dlatego zainwestowano więcej czasu w wbudowanie obsługi współbieżności. Centrala telefoniczna to idealny przykład modelu aktorowego i Erlang VM wewnętrznie czerpie z tego paradygmatu. To jednak temat na osobny artykuł.

Jeśli spojrzymy na wymagania firm telekomunikacyjnych 30 lat temu oraz nasze obecne wymagania jeśli chodzi o systemy informatyczne widzimy same podobieństwa. Wiele z nich, nawet jeśli nie są one tak krytyczne jak połączenia alarmowe, musi działać 24/7 z jak najmniejszym przestojem. Mimo tych wymagań i upływu lat, w dalszym ciągu chcemy mieć możliwość debugowania w czasie rzeczywistym jeśli coś pójdzie nie tak. I na pewno nie muszę wam tłumaczyć z jak skomplikowanymi systemami musimy sobie radzić.

Permanentna inwigilacja

Śledzenie procesów może być nieocenionym narzędziem. Szczególnie w sytuacji podbramkowej na produkcji, ale także i w codziennym programowaniu, kiedy próbujemy wyśledzić własne błędy już na pierwszym etapie testów naszej implementacji. To na co chciałbym zwrócić uwagę, to jakie wyzwania stawia praca z takimi systemami i jak różni się to od klasycznego podejścia związanego z testowaniem i zapewnieniem jakości. Zaręczam Wam, że to zaledwie wierzchołek góry lodowej i jeśli jesteście zainteresowani to bardzo chętnie pociągnę temat dalej. Dajcie znać w komentarzach czy jesteście zainteresowani tym tematem.