Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
Hibatűrő elosztott rendszerek Hystrix Javanica alapokon
Ahogy már a bevezetőben említettem, a Hystrix egy olyan library, mely a service-eink hálózati kommunikációját - mondhatni - felügyeli, és szabályozza. Alapvető működési elve az, hogy minden "káros", veszélyes kommunikációt megakadályoz, így biztosítva a rendszer stabilitását, ami akár egyébként értékes hálózati hívások elvetését is jelentheti. Kicsit talán ellentmondásosnak hathat ez a kijelentés, mégis, valóban a rendszer fenntartása érdekében teszi mindezt.
Miért jó nekünk a Hystrix?
Hogy kicsit érthetőbb legyen, nézzünk egy példát. Tegyük fel, van egy rendszerünk, az egyszerűség kedvéért legyen ez egy webáruház, mely megbízható, átfogó teljesítmény teszteken ment át, és tudjuk, hogy a rendszer maximális throughput-ja percenként 30 000 kérés körül mozog. A rendszer átlagos terhelése ennél jóval alacsonyabb, így alapesetben nem kell tartanunk a túlterheléstől. Microservice architektúrára épülő rendszerről beszélünk, a felhasználói folyamatok így több komponenst is érintenek, azok között pedig értelemszerűen hálózati hívások történnek REST API-okon keresztül. Van azonban a rendszernek egy gyenge pontja, legyen ez a fizetések kezelését végző komponens. Elérkezik a "fekete péntek" napja, a kérések száma jelentősen megnő, megközelíti vagy akár túl is lépi a tesztelt maximális throughputot. Tudjuk azt is, hogy ha tovább emelkedik a kérések száma, a fizetéseket kezelő service összeomolhat, ami jelentős kiesést okozhat az áruháznak. Ugyanakkor, amennyiben egy határérték alatt tudjuk tartani a kérések számát, a service stabil marad, legfeljebb némiképp lassabb lesz. Ekkor jön a képbe a Hystrix, amely automatikusan közbelép, amennyiben a kapcsolat túloldalán levő service például folyamatosan időtúllép. Ebben az esetben a "circuit breaker" kinyílik, és meghatározott ideig nem enged kéréseket továbbítani a kérdéses service felé. Hogy ez miért jó? Mert így nem az összes kérést (és az összes potenciális vásárlót) veszíti el az áruház (és őket sem végleg), csak egy kisebb részét. A service terhelése nem ugrik hirtelen az egekbe, nem omlik össze, így folytatni tudja a kiszolgálást.
A Hystrix alapelvei
A fenti példa mentén talán már érthető, miért fontos a Hystrix, vagy bármilyen más hasonló eszköz használata amennyiben több service közti kommunikációra van szükség (szóval a mai szoftvertrendek szerint gyakorlatilag mindig). A Hystrix alapvetően három "alapelvet" definiál:
-
Késleltetés és hibatűrés
Egyrészt a service stack a meghatározott időtúllépések miatt mindig reális határidőkön belül válaszol - még ha az sikertelen választ is jelent. Még mindig jobb felhasználói élményt jelent az, ha hamar tud róla a felhasználó, hogy most valami probléma van, mintha akár percekig várakoztatná a rendszer. A rendszerünk másrészt hibatűrő kell legyen: egy teljes komponens vagy annak egy példánya még nem feltétlenül (sőt általában inkább egyáltalán nem) kell jelentse a teljes rendszer használhatatlanná válását. A Hystrix a "sérült" komponensek felé elvágja a kommunikációt, így a még futó komponensek reagálni tudnak a kiesésre. Ezt szokás "graceful degradation" néven emlegetni. -
Valósidejű monitorozás
A Hystrix számos metrikát biztosít a kapcsolatok megfigyelésére. Különböző percentilisek, átlagok, számlálók segítségével képes a karbantartó / fejlesztő személy vagy csapat tudtára hozni, hogy egyes komponensek épp nincsenek a helyzet magaslatán. -
Konkurens hívások
Ezek mellett a Hystrix out-of-the-box támogatást ad konkurens hálózati hívások végrehajtására is.
Hogy jön tehát ide a Javanica?
Ahogy a cikkem címe is mutatja, mégsem a "natív" Hystrix-szel fogunk most foglalkozni, hanem az ahhoz készült, Javanica nevű extension-nel. Ennek pedig nagyon egyszerű oka van: a Hystrix egy szörnyen kényelmetlen library. Feltételezhetően a fejlesztése elején még senkinek nem volt az eszében az open-source kiadás gondolata, legalábbis az elkészült terméken ez látszik. Egyébként fontos megjegyezni, hogy a Netflix már "maintenance módba" rakta a Hystrixet, azt "elég stabilnak" ítélték ahhoz, hogy további új feature-ök implementálását már ne igényelje, így már csak legfeljebb hibajavításokat fog kapni. Ettől függetlenül egy remek eszközről beszélünk, csak a fejlesztésénél nem gondoltak például az IoC konténerben való használatra, ahol a leginkább kap tőle igazán fejfájást a fejlesztő. Hangsúlyozom, nem annyira a biztosított API bonyolultsága a probléma, sokkal inkább egyszerűen csak arról van szó, hogy kényelmetlen azt használni. Minden egyes hálózati hívást (így tehát magát a klienst) egy HystrixCommand
implementációban kell elhelyezni, melynek a paraméter nélküli, generikussal megadható visszatérésű run()
metódusában történik majd meg a tényleges kliens hívás (1-2 ilyen esetben nem tűnik ez nagy munkának, de ahogy elkezdjük elcsomagolni az összes hálózati hívást, egyre fájdalmasabbnak kezd érződni.
Szerencsére a Javanica fejlesztői is lusta, elkényeztetett programozók voltak, így megalkottak egy kiterjesztést a Hystrixhez, mely annotációk alapján térképezi fel a "commandjainkat", és aspektusokkal köti be azokat a Hystrix vérkeringésébe. A Javanica lényegében tényleg ennyit ad hozzá, minden mást a Hystrix végez továbbra is.
A Javanica használata
Először is, adjuk hozzá a projektünkhöz a Javanica-t. Ez minden szükséges függést húz magával, így másra nem lesz szükségünk. Tekintettel a library jegelt állapotára valószínűleg a verzió már nem fog igazán változni, de azért érdemes lehet ellenőrizni időnként, hátha van újabb kiadás.
<dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>1.5.18</version> </dependency>
A következő lépés - mely persze csak abban az esetben szükséges, ha Spring alatt szeretnénk használni a Hystrixet - a Hystrix aspektusának létrehozása beanként. Egyéb esetben a használt AOP library konfigurációját kell a megfelelő módon finomhangolni.
@Bean public HystrixCommandAspect hystrixCommandAspect() { return new HystrixCommandAspect(); }
Ezután érdemes legalább az alapértelmezéseket beállítani. Ehhez az alkalmazás classpath-ján helyezzünk el egy config.properties
nevű file-t - a Hystrix ugyanis az Archaius nevű library segítségével automatikusan felolvassa innen a beállításokat, illetőleg futásidőben is figyeli az említett file változásait (szintén a Netflix készítette). Alább egy példa konfiguráció látható, mely az alapértelmezéseket definiálja:
# Hystrix Command configuration - default hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1500 hystrix.command.default.execution.timeout.enabled=true
A fenti konfiguráció engedélyezi minden command implementáció számára az időtúllépés figyelését, mely alapértelmezett 1500 ms. Az összes szabályozható paraméter megtalálható a Hystrix dokumentációjában. A command-szintű felülbírálásokat a default
szó a command nevére történő lecserélésével tehetjük meg, például az alábbi felülbírálás a Leaflet front oldali alkalmazásában a regisztráció művelet várakoztatását emeli meg a mögötte levő ReCaptcha ellenőrzés miatt (ami sajnos kifejezetten sokáig tart).
hystrix.command.signUp.execution.isolation.thread.timeoutInMilliseconds=5000
Az alapértelmezések egyébként megadhatóak a @DefaultProperties
annotáció segítségével is (nagyjából bárhol elhelyezhető, persze érdemes egy konfigurációs osztályt, vagy a commandokat tartalmazó osztályt választani). Annyiból nem ajánlanám mégsem ezt a módszert, hogy a konfiguráció így nem lesz elválasztva a live kódtól, illetve a dinamikus, környezetfüggő felülbírálásokat is elveszítjük (mivel csak konstansok használhatók az annotációk attribútumaiban).
Az utolsó lépés a command definiálása, melyet a @HystrixCommand
annotációval tehetünk meg. Amennyiben a commandKey
attribútummal nem adtunk meg command nevet, az automatikusan az annotált metódus neve lesz. Hasonlóan az alapértelmezésekhez, itt is megadhatjuk a konfigurációt, de azt ez esetben sem javasolnám. Az annotált command végül így fog kinézni:
@HystrixCommand public ExtendedUserDataModel updateProfile(Long userID, UpdateProfileRequestModel updateProfileRequestModel) throws CommunicationFailureException { // ... }
Ezzel Hystrix-védetté tettük a klienst.
A Hystrix-execution-thread probléma
Egy érdekességről szeretnék még írni, ami alapvetően igényelt némi utánajárást, mikor a problémába belefutottam. Szóval a Bridge kliens implementációk mindegyike támaszkodik két nagyon fontos és értékes információ jelenlétére, melyet a Leaflet front alkalmazása a HttpServletRequest
objektumban bocsát rendelkezésükre. Ezek az eszköz (minden látogatóé egyedi) és a kliens alkalmazás azonosítói. A Hystrix minden esetben nyit egy új threadet, és a hálózati kérést azon indítja el, majd megvárja a válaszát (vagy eldobja azt, ha időtúllépés történik). A probléma ezzel az, hogy a request és response objektumok csak a container request threadjein élnek (thread-local objektumok), azok nem kerülnek átmásolásra a hívást indító threadekre - és ez így van jól. A gond ezzel csak az, hogy így lehetetlenné tesszük a Hystrix threadjei számára, hogy elérjék a HttpServletRequest
objektumot.
Azonban van rá megoldás, mivel megkérhetjük a Hystrix-et, hogy mielőtt megnyitná az executor threadet, másoljon át nekünk bizonyos objektumokat a hívó threadről. Többek között erre szolgálnak a HystrixCommandExecutionHook
-ok és a HystrixRequestVariable
-ök. Először is, létre kell hoznunk két HystrixRequestVariableDefault
példányt az alábbi módon:
public final class RequestAttributesHystrixRequestVariable { // ez a mező tartalmazza majd a request-ben levő attribútumokat, session ID-t, stb. ... private static final HystrixRequestVariableDefault<RequestAttributes> REQUEST_ATTRIBUTES = new HystrixRequestVariableDefault<>(); private RequestAttributesHystrixRequestVariable() { } public static HystrixRequestVariableDefault<RequestAttributes> getInstance() { return REQUEST_ATTRIBUTES; } }
public final class SecurityContextHystrixRequestVariable { // ... ez pedig a SecurityContext-et, hogy a Hystrix executor thread // is hozzáférhessen a benne tárolt aktív JWT tokenhez private static final HystrixRequestVariableDefault<SecurityContext> SECURITY_CONTEXT = new HystrixRequestVariableDefault<>(); private SecurityContextHystrixRequestVariable() { } public static HystrixRequestVariableDefault<SecurityContext> getInstance() { return SECURITY_CONTEXT; } }
Furcsának, sőt ijesztőnek tűnhet, hogy mindkét esetben teljesen statikus osztályokat definiáltunk, azonban a tárolást valójában megvalósító HystrixRequestVariableDefault
osztály a Hystrix saját védett kontextusában (thread-localon) helyezi el az objektumokat, így - amennyiben megfelelően kitakarítjuk azokat - adatszivárgás elvileg nem fordulhat elő. Ezen a ponton még persze nincsenek adatok az executor threaden, ahhoz még meg kell tennünk két lépést. Először is szükségünk lesz egy filterre, mely még a container request threadjéről le tudja olvasni az adatokat, és elhelyezni a fent definiált tárolókba:
public class HystrixContextFilter extends OncePerRequestFilter implements Ordered { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // először is thread-localon megnyitunk egy új request contextet // a másolt adatok lényegében ide kerülnek majd, enélkül exceptiont kapunk HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { // átmásoljuk a HttpServletRequest tartalmát RequestAttributesHystrixRequestVariable.getInstance().set(RequestContextHolder.getRequestAttributes()); // majd a SecurityContext tartalmát SecurityContextHystrixRequestVariable.getInstance().set(SecurityContextHolder.getContext()); filterChain.doFilter(request, response); } finally { // előfordulhat, hogy nem is történt Hystrix command hívás, de akkor // is le kell zárnunk a context-et az adatszivárgás elkerülése érdekében context.shutdown(); } } // ... }
Értelemszerűen definiáljuk beanként a fenti filtert az alkalmazás configjában (a @Component
is működik persze):
@Bean public HystrixContextFilter hystrixContextFilter() { return new HystrixContextFilter(); }
Végül implementáljuk az execution hook-ot:
public class BridgeSupportHystrixCommandExecutionHook extends HystrixCommandExecutionHook { @Override public <T> void onExecutionStart(HystrixInvokable<T> commandInstance) { // a command végrehajtás kezdetén az executor threadre másoljuk // a Hystrix request contextjében lévő adatokat RequestContextHolder.setRequestAttributes(RequestAttributesHystrixRequestVariable.getInstance().get()); SecurityContextHolder.setContext(SecurityContextHystrixRequestVariable.getInstance().get()); } @Override public <T> T onEmit(HystrixInvokable<T> commandInstance, T value) { // bármilyen visszatérése is lett a commandnak, takarítunk az execution threaden RequestContextHolder.resetRequestAttributes(); SecurityContextHolder.clearContext(); return value; } @Override public <T> Exception onError(HystrixInvokable<T> commandInstance, HystrixRuntimeException.FailureType failureType, Exception e) { // error esetén pedig főleg takarítunk RequestContextHolder.resetRequestAttributes(); SecurityContextHolder.clearContext(); return e; } }
Ezzel el is készültünk, így már el fogják érni a Hystrix execution threadek a request tartalmát. És ezzel a mai Hystrix gyorstalpaló végére is értünk, köszönöm a figyelmet!
A Leaflethez készült Bridge Hystrix support implementáció
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.