Afish
2020-01-15 07:07

Takie tam luźne przemyślenia.

Pisanie kodu odpornego na błędy nie jest łatwe, a obsługa wyjątków nie należy do czegoś, co programiści często robią poprawnie. Nawet nie chodzi o połknięcie wyjątku bez logów (bo to łatwo poprawić), ale o sytuacje, gdy finally się nie wykonuje (bo tak skonstruowano platformę), albo wyjątku nie da się złapać (bo nie i już), albo nie da się go obsłużyć od strony technicznej (bo wątek się kładzie albo nie ma miejsca na stosie), albo trzeba uważać, żeby nie złapać wyjątku używanego do sterowania aplikacją (niektóre języki przerywają pętlę wyjątkiem).

Dlatego zamiast ratować proces za wszelką cenę, lepiej jest dać mu umrzeć, a potem zacząć od nowa. Implementujemy tak zwany "self healing", czyli system może się psuć, ale ma się sam odratować. Brzmi prosto: stawiamy watchdoga, jak proces zdechnie, to go restartujemy i po sprawie. No ale tu pojawia się masa ciekawych spraw:

  • Skąd wiemy, że proces zdechł? Może działa wolno? Może czeka na mutex? Może młóci jakąś pętlę nieskończoną? Rozwiązanie brzmi prosto — dodajemy pinga — no ale tu tylko zaczynają się schody.
    • Jak obsłużyć pinga? Jeżeli odpalimy dodatkowy wątek, to skąd wiemy, że nie jest tak, że tylko wątek z pingiem działa, a wątek główny umarł? Trzeba odpowiednio ogarnąć to wątkiem działającym w tle
    • Co jeżeli ping ma zadyszkę? Być może trzeba dać mu kilka szans, a dopiero po jakimś czasie ubijać.
    • Jak z tym pingiem się skomunikować? Socket? Named pipe? Plik?
  • No dobra, wyłapaliśmy, że proces nie odpowiada, pora go ubić. Jak to zrobić?
    • Jeżeli chcemy go ubić ładnie, to pewnie to nie wyjdzie, w końcu proces nie odpowiada.
    • Ale jak ubijemy go brzydko, to wtedy nie sprzątnie zasobów
    • A co, jeżeli proces jest pod debuggerem — nie możemy go wtedy ubić ot tak, bo system nie pozwala
    • A co, jeżeli proces wpadł w jakimś system raportowania w Windowsie (WER) — wtedy też nie da się go ubić
    • A co, jeżeli proces zrobił się jakimś dziwnym zombie i nie da się go utłuc?
    • A co, jeżeli proces jest na zdalnej maszynie i straciliśmy połączenie?
  • Okej, ubiliśmy jakoś, to teraz restartujemy
    • A co, jeżeli zasoby ciągle są zablokowane?
    • A co, jeżeli ubity proces miał mutex? Chcielibyśmy go przejąć, ale nie wiemy, czy dane są poprawne
    • A co, jeżeli proces umiera deterministycznie, bo mamy jakiś poison message? Ile razy go restartujemy? Robimy backoff? Jakieś inne cuda?
  • A co, jeżeli watchdog zdechnie?

I tak dalej, i tak dalej.

W jednym projekcie klepałem coś takiego, zaczęło się niewinnie, a wyszedł mi spory system rozproszony:

  • Watchdog komunikował się z procesami przez named pipe
  • Każdy proces miał deep ping
  • Jak ping nie pyknął 3 razy z rzędu, to watchdog restartował proces
  • W określonych sytuacjach watchdog automatycznie robił zrzut pamięci procesu, żeby ułatwić debugowanie
  • Procesy pod spodem też obserwowały watchodga, jak umarł, to popełniały seppuku
  • Zamiast używać systemowych muteksów używałem własnej implementacji, żeby łatwo śledzić właściciela (PID i TID)
  • Każdy muteks był brany z timeoutem (to jest chyba kluczowe, jeżeli kiedykolwiek bierzesz mutex z nieskończonym oczekiwaniem, to prędzej czy później "masz deadlocka")
  • Jak proces A wykrył, że proces B trzyma mutex zbyt długo, to proces A robił zrzut pamięci B, a następnie ubijał B i brał jego locka
  • Trochę nadpisywania ustawień systemowych, żeby wyłączyć jakieś raportowanie błędów, automatyczne debugowanie itp, a to po to, żeby łatwo ubić proces w razie potrzeby
  • Do tego oczywiście logowanie, metryki, token bucket dla wyjątków i takie tam

Efekt? System leczy się sam całkiem nieźle. Nawet jak coś się sypnie konkretnie (ostry brak pamięci, CPU zjechane na 100% przez zewnętrzny proces), to system potrafi wrócić do działania, deadlocków nie ma, a zrzuty pamięci robią się same. A to wszystko dlatego, że czasami nie da się obsłużyć wyjątku.

Pipes

Watchdog brzmi jak supervisor w Erlangu / Elixirze. Cała reszta jest analogiczna (z wyjątkiem braku locków), tyle że na erlang vm masz inne lżejsze procesy.

Shalom

@Afish: pytanie tylko czy ten watchdog nie jest teraz np. 10x większ, bardziej złożony i kosztowny od tego miał monitorować ;)

Silv

@Afish: Jak zrobiłeś podwójne punkty – czarne i białe? — Co to znaczy: (…) wyjątku nie da się złapać (bo nie i już) (…)?

Afish

@Pipes: Tak, to samo. Wzorce są te same.
@Shalom: Na szczęście nie, ale nie wiem, co wyjdzie za rok ;)
@Silv: Podwójne punkty robisz dwoma gwiazdkami ze spacją między nimi. Co to znaczy: (…) wyjątku nie da się złapać (bo nie i już) (…)? Niektóre technologie nie pozwalają łapać wszystkich wyjątków (na przykład .NET i StackOverflowException) albo nie da się tego sensownie zrobić (przykładowo wywałka JVM), albo w ogóle nie da się tego zrobić (segfault).
@Koziołek: Ja bym powiedział, że tak działają sensowne systemy informatyczne, język nie ma nic do tego. No i w erlangu supervisor działa wewnątrz procesu (o ile się nie mylę, dawno tam nie grzebałem), co oczywiście nie jest tym, czego potrzebowałem.

Silv

Dzięki. :) Dalej nie bardzo rozumiem – na czym to niepozwalanie miałoby polegać. Ale może nie będę o to pytać, bo chyba za dużo musiałbyś mi wyjaśniać, jako nieobytemu z tymi platformami. ;)

Kamil Żabiński

@Silv: np. w języku Rust "wyjątki" nie da się złapać, bo tak język zaprojektowano. "Wywołanie" makra panic! składa wątek. Nie da się tego obejść nawet na potrzeby testów jednostkowych dlatego każdy test jest uruchamiany w osobnym wątku. Dzięki temu jest pewność że "wyjątki" będą używane tylko do celów wyjątkowych :D

Silv

@Kamil Żabiński: OK, dzięki, jeszcze widać muszę się o wyjątkach douczyć.

Afish

@Silv: Po prostu catch Exception nie łapie niektórych sytuacji. No i segfault oczywiście jest poza kontrolą, wtedy system ubija proces i nie przejmuje się, czy jest jakiś catch/trap itp. jak jesteś dotnetowcem, to proponuję https://youtu.be/NUz212O79tE (autopromocja).

Kamil Żabiński

@Afish: zawsze można robić catch Throwable. Z jakiegoś powodu jest to uważane za błąd w Javie, ale np. w Scali już nie. Tam łapie się NonFatal Throwable. A Fatal są tylko VirtualMachineError | _: ThreadDeath | _: InterruptedException | _: LinkageError | _: ControlThrowable

Afish

@Kamil Żabiński: W javie tak, ale to nie zmienia meritum. Jak dostaniesz w javie segfault, to nic z nim nie zrobisz. Nie da się zagwarantować obsługi wszystkich błędów, to jest kluczowe, a szczegóły już nie są istotne.

WeiXiao

oj tam się danymi przejmujesz, ubijasz process (force), a dane jak trzeba będzie odzyskać, to będą w logach http :D :D :D jakiś intern na pewno się ucieszy, gdy będzie musiał napisać konsolówkę do przemielenia logów

Afish

@WeiXiao: O ile proces da się ubić (a nie zawsze się da) i o ile dobrze obsługujesz porzuconego muteksa (a tu łatwo się walnąć) ;)

WeiXiao

ubij cały kontener i po sprawie :D

superdurszlak

@WeiXiao: a po jakimś czasie intern dostarczy narzędzie i wtedy ze złośliwym uśmiechem na twarzy oznajmisz mu, że jest coś takiego jak Sumo Logic i cała jego robota jest w sumie g***o warta. Win-win

WeiXiao

@superdurszlak: korzystać z 3rd party? ja to wole mieć kontrole nad procesorem

superdurszlak

@WeiXiao: jeśli używasz kupnego CPU, już jesteś niewolnikiem

Dziduszka

a próbowaliście po prostu wyłączyć i włączyć?

vpiotr

Robiłem coś takiego prywatnie na swój użytek (C++/Win - watchdog, klka instancji tego samego procesu, joby, workqueue, shared memory, centralny log, CLI via REST/Python..., przy okazłi programowanie genetyczne i autotrading, wyszło jakieś 200k LOC :)
Może powinienem to opublikować, bo leży odłogiem, ale to C++98, całkiem niemodne.

System przeżywa kilowanie workerów, blokowanie komunikacji i brak prądu (pliki transakcyjne plus job scheduler jako serwis w Windows).

WeiXiao

to kiedy upublicznisz te konkurencję dla k8sa? czy to jest amazon_internal_tooling (chociaż bezos ewidentnie mówił, aby wystawiać API :D)

Afish

Nie planuję upubliczniać, ale nie jest to amazonowy kod, to mój prywatny projekt. 3 lata w tym dłubię, pierwsza wersja powstała w dwa wieczory, a teraz rozrosło się już tak bardzo i napisałem już tyle własnych mechanizmów systemowych i rozproszonych, że obawiam się, że niedługo własnego paxosa będę implementował :D

Shalom

@Afish nie jest to amazonowy kod, to mój prywatny projekt hmm a czy czasem nie masz w umowie że wszystko jest własnością firmy? ;)

WeiXiao

@Shalom: hej szalom, dalej twoja firma wyświetla mi reklamy viagry?

WeiXiao

@Afish: czemu nie? nie chciałbyś, aby ktoś z tego korzystał? czy planujesz monetyzować? a może nie chcesz dostawać PRów od nobuw?

Afish

@Shalom: Nie.
@WeiXiao: Nie chce mi się wydzielać tego kodu, bo i po co. Ciekawsze rzeczy mam opisane na blogu, reszta jest skrojona pode mnie i nie widzę potrzeby, aby to wywlekać na światło dzienne.

Shalom

@WeiXiao: cośty, już dawno nie :P Chociaż jakiś pozostały po mnie kod, może i tak.

superdurszlak

@WeiXiao: jak ciągle dostajesz reklamy viagry, to być może problem nie leży w firmie :]

WeiXiao

@superdurszlak: racja, czas chyba zrezygnować z Google i przejść na DDG lub Binga.

Afish

@WeiXiao: Swoją wyszukiwarkę napisz :P

Cr0w

smieszkowanie, ale stworzenie laptopa od verilog/migen/vhdl, przez obudowe, po wlasny minix i przegladarke w Rust'cie brzmi fajnie :]

superdurszlak

@Cr0w: ja temu nie przeczę ;) nie mówiąc o tym że trzeba być niezłym zawodnikiem żeby coś takiego w ogóle skleić

alagner

Paradoksalnie trudniej może być z własnym systemem niż hardwarem. ;)

vpiotr

Meh, wlasny system to pestka (na 65xe). A wlasne kompy skladaja dzis nawet dzieci (costam bits).

alagner

@vpiotr: raczej nie od poziomu projektu procka w VHDLu/Verilogu (nie znam projektu stąd pytanie)?

vpiotr

@alagner: littlebits, wyglada strawnie