Obecně se při návrhu distribuovaných systému předpokládá, že síťové volání je rychlé a spolehlivé. Což je nesprávný předpoklad.
Kdysi v dávných dobách, ještě než začala být moderní microservisová architektura, se distribuované systémy propojovaly pomocí technologie CORBA. Jedna z hlavních myšlenek, která mě tehdy zaujala, byla: Programátor by neměl řešit, zda volání metody je lokální, nebo zda se pouští kód na vzdáleném serveru. A skutečně, v kódu se volala metoda na stub objektu a to, zda šlo o vzdálené volání se nastavovalo v konfiguraci. Tenkrát mi to připadalo jako dobrý nápad – napíšeme kód a z něho pomocí konfigurace uděláme distribuovaný systém. Problém se ale objevil záhy.
Síť jako zdroj problémů
Brzy jsme zjistili, že aplikace je velmi pomalá. Pokud programátor neví, že volá vzdálený systém a takové volání vloží do cyklu FOR, vznikne pomalý kód. Důvod je ten, že navázání spojení a transformace dat nějakou dobu trvá a pokud voláme metodu, jejíž zpracování je rychlé, může se snadno stát, že režie na síťové volání je vyšší než režie na samotné zpracování metody.
Síťové volání však přináší další problémy, které nejsou na první pohled zjevné. V případě poruchy sítě může dojít k zamrznutí systému, nebo dokonce k poruše integrity dat.
Zavoláme síťovou službu, spojení se naváže, ale už nedostaneme žádnou odpověď. A spojení je stále aktivní. Pokud nemáme nastaven správně timeout, můžete čekat neomezeně dlouho. Při použití nějakého Java web serveru je počet vláken omezen. Postupným snižováním volných vláken systém degraduje, až nakonec zatuhne.
Takto se mi podařilo shodit kompletně celý backend, včetně všech mobilních aplikací, které se na tento backend připojovaly. Zajímavé je, že externí monitorovací systém nic nehlásil, ačkoliv měl – programátor monitorovacího software zřejmě udělal stejnou chybu jako já – neměl ošetřený timeout.
Dalším problémem je, že v distribuovaném systému přicházíme o pohodlné databázové transakce. To se často podceňuje. Kvůli tomu stále hrozí ztráta konzistence dat. Jeden z příkladů: Pokud zavoláme vzdálenou službu, může se stát, že systém požadovanou akci zpracuje a při posílání odpovědi spadne spojení. Požadovaná akce se sice provede, ale my dostaneme chybu. Protože si myslíme, že akce neproběhla, zavoláme systém znovu a dojde k dvojitému volání.
Pokud volání navyšuje například kredit na účtu, tak jsme právě přišli o konzistenci. Jedno z řešení je posílat unikátní identifikaci požadavku (UUID) a vzdálený systém bude ignorovat volání, které již zpracoval. Řešení je jednoduché, ale je otázka, zda si toto nebezpečí programátor uvědomuje a umí ho správně ošetřit.
Dopad sítě na aplikaci
Čím více je aplikace provázána vzájemným voláním, tím větší je riziko, že problém na síťové vrstvě se projeví negativně na celé distribuované aplikaci. Pak si můžeme klást otázku, zda nějaký drobný problém v komunikaci nezpůsobí kaskádovitě kolaps celé aplikace. A pokud dojde k obnově spojení, zvládne se aplikace sama zotavit? A zůstane integrita dat v pořádku?
Na tyto otázky se nejlépe odpoví tak, že se to prostě zkusí. Problém je, že tyto nestandardní stavy sítě se špatně testují. Hledal jsem tedy nějakou možnost, jak jednoduchým způsobem tyto případy otestovat. Cílem bylo vytvořit proxy, která je mezi testovanou aplikací a okolním světem. A proxy by narušovala spojení a simulovala by možné problémy sítě.
Můj seznam požadavků na proxy:
- Simulovat výše zmíněné problémy, jako je zaseknuté spojení a chyba při odpovědi.
- Zpracovat a narušovat veškeré spojení na TCP vrstvě, tedy nejen HTTP, HTTPS, ale také spojení na databázi, RabbitMQ a podobně.
- Snadná instalace i bez root oprávnění. Nemělo by se zasahovat do síťového nastavení.
- Testovaná aplikace by se neměla speciálně upravovat pro toto testování. Měly by fungovat veškeré aplikace, ať jsou napsané v Javě, C++, nebo PHP.
- Podpora black box testování – ten, kdo testuje, nemusí znát detailně testovanou aplikaci. Jednoduše se podívá přes GUI proxy, kam se aplikace připojuje a spojení v reálném čase naruší.
- REST API ovládání pro automatizované JMeter testy.
SOCKS proxy
Po dlouhém zkoumání a prošlapávání slepých cest mě napadlo, že by se dala použít SOCKS proxy. SOCKS proxy je standard popsaný v RFC 1928. Proxy se používá především k zamaskování provozu, aby správce sítě neviděl, kam požadavky směřují. Toho se dá docílit s vyšší verzí SOCK5, která posílá přes proxy i DNS požadavky. SOCKS proxy implementaci nalezneme i v SSH klientu. Napadlo mě tedy, že pokud bych si napsal vlastní implementací SOCKS proxy, mohl bych do své implementace přidat kýžené rušení spojení.
Poslední problém byl, jak donutit testovanou aplikaci, aby pro navazování spojení použila SOCKS proxy a ne přímé spojení do internetu. A to bez zásahu do testované aplikace. Naštěstí existuje jednoduchá utilita proxychains-ng. Ta dělá přesně to, co potřebujeme. V konfiguraci zadáme číslo portu, kde se nachází naše SOCKS proxy a poté pustíme testovanou aplikaci jako parameter proxychains.
Spoiler Proxy
A tak už jen zbývalo napsat speciální SOCKS proxy, kterou jsem nazval Spoiler Proxy. Spoiler Proxy je zdarma ke stažení jako open source a je napsaná v Javě. Pokud nechcete instalovat JVM, můžete použít připravený Docker image.
Spoiler Proxy po startu otevře dva porty. Port 8484 slouží k ovládání proxy přes HTTP. Je tam také API popsané přes Swagger. Port 1080 je SOCKS5 proxy port, přes který se budou směrovat síťové požadavky testované aplikace.
Instalace Spoiler Proxy je snadná, po stažení aplikace se Spoiler Proxy pouští příkazem:
$ java -jar spoiler-proxy.war
Pokud nemáme nainstalovanou Javu, můžete pustit Docker variantu:
$ docker run --rm -p 1080:1080 -p 8484:8484 spoilerproxy/jdk8
Zde vidíme otevření dvou portů 1080 a 8484.
Nasazení proxychains
Dalším krokem je připravit proxychains, abychom donutili testovanou aplikaci použít Spoiler Proxy. Ačkoliv proxychains bývá součástí některých repozitářů (například Ubuntu), doporučuji nainstalovat nejnovější generaci proxychains-ng přímo z GitHub. Verze z repozitáře na Ubuntu mi totiž nefungovala dobře.
Instalace nejnovější verze proxychains lze takto:
$ sudo apt install -y git make gcc $ git clone https://github.com/rofl0r/proxychains-ng.git $ cd proxychains-ng $ make $ sudo make install $ cd .. $ rm -rf proxychains-ng
Poté musíme nakonfigurovat proxychains4
. Vytvoříme konfigurační soubor v domovském adresáři: $HOME/.proxychains/proxychains.conf
A do něj vložíme:
strict_chain proxy_dns tcp_read_time_out 150000 tcp_connect_time_out 80000 [ProxyList] socks5 127.0.0.1 1080
Poslední řádek nám určuje, kde nám běží Spoiler Proxy. Ve výchozím nastavení je to localhost na portu 1080. Nastavení proxy_dns
je důležité, abychom viděli DNS záznamy, kam se testovaná aplikace připojuje. Také je vhodné zvýšit timeouty, abychom mohli simulovat zaseknuté volání.
Spuštění testované aplikace
Závěrečným krokem je puštění naší aplikace, kterou chceme testovat. Zkusme pro začátek pustit nějaký prohlížeč, například:
$ proxychains4 firefox
Poté (ideálně v jiném prohlížeči) jděme na http://localhost:8484
a zkusme Firefoxu přerušovat spojení na určité servery a sledovat, co se stane.
Užitečný pomocník pro sebevědomé vývojáře
Spoiler Proxy je jednoduchý nástroj, jak testovat problémy se sítí, které sice nevyvstávají často, avšak mohou mít dalekosáhlé důsledky pro naši aplikaci. Nehrozí jen podivné zatuhnutí systému, ale v horším případě dokonce i ztráta integrity dat.
Spoiler Proxy je to vhodný nástroj ke snížení sebevědomí vývojářů, kteří opouští bezpečí monolitických aplikací a naskakují na aktuální módní microservice architekturu, aniž by tušili, jaká nebezpečí tam na ně možná čekají.