Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Hibatűrő elosztott rendszerek Hystrix Javanica alapokon

Szűrő megjelenítése

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:

  1. 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.

  2. 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.

  3. 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 Hystrix a GitHubon

A Hystrix Javanica a GitHubon

A Leaflethez készült Bridge Hystrix support implementáció

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.