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.