18 Feb

Circuit breaker, avagy mi van akkor, ha nem megy?


Amikor szoftvert írunk, akkor törekszünk arra, hogy bolondbiztos legyen. Amikor webre fejlesztünk sem történik ez másként. Minden tőlünk telhetőt megteszünk annak érdekében, hogy bizony a felhasználó mindig megkapja, amit akar, hiszen nekünk közvetve belőlük van pénzünk. Azonban akadnak olyan esetek, mikor külső tényezők is befolyásolhatják a szolgáltatást, amit nyújtunk? Akkor bizony tudni kell esni. Ahogy manapság egyre nő a microservice hype, egyre gyakrabban fogunk találkozni azzal, hogy bizony, ki kell hívni egy külső service felé, ami nem mindig végződik egy boldog 200-as válasszal. Cikkünkben a Circuit breaker patternről lesz szó és arról, hogy is segíthet ez nekünk, ha külső szolgáltatásokra épül az oldalunk.


A szoftverfejlesztésben igen gyakori, hogy valami külső szolgáltatást használunk, nem kell feltétlenül REST/SOAP hívásra gondolni, hiszen aki már használt valamilyen SMTP alapú mailert is egy külső szolgáltatást használt. Ezek a remote hívások az esetek döntő többségében gyorsak és megbízhatóak, de sajnos ez nem mindig van így.

A circuit breaker alapötlete pofonegyszerű. Az előző cikkünkben használtuk a webserviceX egyik SOAP service-ét. Tegyük fel, hogy egy webáruházat készítünk, ahol a checkout page kirenderelésekor egy hasonló megoldással megyünk egy külső service felé, ami jelen esetben egy InventoryService lesz. Innen le tudjuk kérdezni azt, hogy valóban hány termék van raktáron, mert az eddig cache-elt érték a nagy forgalom miatt időközben változhatott. De tegyük fel, hogy ez a hívás egy szolid 30 sec-es timeoutra fut. A felhasználónk csak vár és vár, aztán az oldal elszáll egy hibával. Na és akkor képzeljük el, hogy 1000 user várja a kis 30 másodpercet. 1000 nyitott kapcsolat a PaymentService felé. Szerencsétlen így is döglődik, mi meg tovább rugdossuk. A support próbálja újraéleszteni, de mikor újraindítják, rögtön lehal, mert mi DoS-oljuk azt. Sok mindennek lehet nevezni, csak nem egészségesnek.

Akkor hogy jön ide a circuit breaker? Fogjuk ezt az InventoryService-t és elwrappeljük azt egy ilyen Circuit Breaker Proxyval. Ez annyit fog csinálni, hogy lesi a hibákat és amikor elér egy szintet, akkor átvált, zárt állapotról nyitott állapotra. Ilyen állapotban pedig rögtön visszadob egy hibát, nem fogja megint megpróbálni a hívást. Na de nézzük meg, hogy is működik ez az egész!

Az első lépés egy composer csomag lesz, mégpedig a

composer require letscodehu/php-circuit-breaker

Ha ezzel megvolnánk, akkor első körben szükségünk lesz valami olyan service-re, amit tudunk piszkálgatni. Ehhez létrehozunk egy FakeService osztályt, ami a guzzlehttp segítségével kihív lokálban egy olyan endpointra, amit tudunk majd befolyásolni, hogy szimuláljuk a kimaradásokat.
use GuzzleHttp\Client;

class FakeService {

    private $client;

    function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function slowCall() {
        return $this->client->get("http://cb.localhost.hu/slow")->getBody();
    }

}

Na most akkor hozzuk létre azt az endpointot, amit itt meglövünk. Én a példákban ezt egy Laravel 5.3-ban végzem:
Route::get("/slow", function() {
    // itt jól működik, gyors is

    return "100";
});

Route::get("/slow", function() {
    sleep(2); // a lassulást ezzel fogjuk szimulálni

    abort(500); // valamint 500-as hibát is dobunk

});

A második verziót egyelőre kommenteljük ki, most a jó működésre vagyunk kiváncsiak.

Az alapeset ugye annyi, hogy simán meghívjuk az egészet, circuit breaker nélkül:
Route::get("/home", function() {

    $fakeService = new \App\Services\FakeService(new \GuzzleHttp\Client());
    $content = $fakeService->slowCall();
    return $content. "-asért a kilója az almának!";

});

Ez tök jó, egészen addig, amíg tökéletesen működik a másik oldal. Na most írjuk át az endpointokat és az eddig kikommentezett /slow üzemeljen most. Hát igen, elkezd lassulni az egész, dobálja a hibákat. Persze bekerülhet az egész egy try-catch blokkba, de minden alkalommal meg fogja próbálni, tehát a try minden alkalomal 2 másodpercig várakoztatja a felhasználót, holott az X. alkalommal már lehet sejteni, hogy bizony valami nem kóser. Itt kellene elővenni a siege nevű linux programot, amivel meg tudjuk nézni, hogy mennyi felhasználót tudunk így kiszolgálni. Először is telepítsük azt:
sudo apt-get install siege

Utána a használata már nem bonyolult:
siege http://cb.localhost.hu/home --time=1M

Ezzel egy percen át 15 konkurens felhasználót szimulálunk, akik folyamatosan ostromolják az oldalunkat. Az eredmény pedig hasonló lesz:
** SIEGE 3.0.8
** Preparing 15 concurrent users for battle.
The server is now under siege...
Lifting the server siege... done.

Transactions: 340 hits
Availability: 100.00 %
Elapsed time: 59.65 secs
Data transferred: 0.01 MB
Response time: 2.05 secs
Transaction rate: 5.70 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 11.68
Successful transactions: 340
Failed transactions: 0
Longest transaction: 3.05
Shortest transaction: 2.01

A lényeg a response time és a shortest-longest transaction, ami a leggyorsabb és leglassabb választ takarja. Láthatjuk, hogy mindenhol kivárta a két másodpercet, amíg a túloldal elfailelt. Akkor most vessük be azt a bizonyos circuit breakert! A library cache-t használ, de írhatunk saját adaptert is, ha az APC/memcached/Redis közül egyik sem nyerő. Mi a példában a Redist fogjuk használni, amihez szükségünk lesz a predis/predis library-re:
composer require predis/predis

Első körben hozzuk létre magát a CircuitBreakert:
$circuitBreakerFactory = new \Ejsmont\CircuitBreaker\Factory();
$redisClient = new \Predis\Client(); // szükségünk lesz egy Redis kliensre is

$circuitBreaker = $circuitBreakerFactory->getRedisInstance($redisClient, 5, 20);

A fenti kódban létrehozunk egy factory-t, amivel tudjuk majd példányosítani a CircuitBreakerünket. A factory metódusban átadjuk a Redis klienst, valamint megmondjuk neki, hogy az 5. hibás hívás után tekintse a szolgáltatást offline-nak és 20 másodperc után próbálkozzon újrahívni azt, hátha életre kelt.
$fakeService = new \App\Services\FakeService(new \GuzzleHttp\Client());
if ($circuitBreaker->isAvailable("FakeService")) {
    try {
        $content = $fakeService->slowCall();
        $circuitBreaker->reportSuccess("FakeService");
    } catch (\GuzzleHttp\Exception\ServerException $e) {
        $content = 0;
        $circuitBreaker->reportFailure("FakeService");
    }
    return $content. "-asért a kilója az almának!";
} else {
    return "A FakeService sajnos most nem elérhető, kérjük jöjjön vissza később!";
}

Itt ugye létrehozzuk a service-t, aztán első körben ellenőrízzük, hogy offline-e, ha nem, akkor megpróbálkozunk ráhívni, ha sikerül, akkor jelentjük, hogy a szolgáltatás újra él. Ellenben ha hibára futunk, akkor azt is jelentsük.

Ha azt látjuk, hogy a szolgáltatás nem működik, akkor valami alternatív dolgot meg tudunk jeleníteni a frontenden. Persze ha az oldalunk működése szempontjából kritikus a szolgáltatás megléte, akkor nem vagyunk sokkal előrébb.

Mi is ez az egész circuit breaker? Ha az áramköröknél használt biztosítékra gondolunk, akkor itt is hasonló a működés, ún. nyitott és zárt állapot. Zárt állapotban van összezárva az áramkör, tehát keresztülfolyik rajta az áram. Nyitott állapotban viszont nem. Alapból zárt állapotban indul az egész. Hívjuk a szolgáltatást és amíg sikerül, addig nem történik semmi. Viszont elkezd kimaradozni és jelentjük hogy hibádzik. Ha elérjük a megadott limitet, akkor az áramkör nyit. Itt már nem megyünk ki a szolgáltatás felé, egészen addig, amíg le nem telik a megadott reset timeout. Ez egy harmadik, ún. félig nyitott állapot. Ekkor megint rendesen kimegyünk a szolgáltatás felé, mintha zárt állapotban lenne az áramkör, de ha hibára futunk, akkor nem várjuk meg az X hibát, hanem rögtön nyitjuk az áramkört és megint csak a reset timeout lejárta után próbálkozunk újra.

Próbálgassuk most kézzel és látni fogjuk, hogy igen hamar az alternatív szöveget fogjuk látni. A siege-re pedig így reagál:
Transactions: 1670 hits
Availability: 100.00 %
Elapsed time: 59.67 secs
Data transferred: 0.15 MB
Response time: 0.04 secs
Transaction rate: 27.99 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 1.20
Successful transactions: 1670
Failed transactions: 0
Longest transaction: 4.07
Shortest transaction: 0.00

Láthatjuk, hogy az átlag válaszidő 0.04 másodperc volt, tehát döntő többségében ki se mentünk a service felé. Most próbáljuk újra a siege-el a dolgot, de menet közben kapcsoljuk vissza a jól működő route-ot, ami nem várakoztat és nem fut hibára.
Transactions: 1754 hits
Availability: 100.00 %
Elapsed time: 59.75 secs
Data transferred: 0.10 MB
Response time: 0.02 secs
Transaction rate: 29.36 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 0.67
Successful transactions: 1754
Failed transactions: 0
Longest transaction: 2.02
Shortest transaction: 0.00

Látható, hogy közel ugyanolyan eredményt értünk el.

De most jöjjön a feketemágia része!

Ha már Laravelben megy ez az egész, akkor miért ne csinálhatnánk mindezt a lehető legegyszerűbben? Hogy tudnánk ezt az egészet elrejteni, netán automatizálni?

Első körben vegyük fel az AppServiceProviderünkben a CircuitBreaker-t:
$this->app->singleton(CircuitBreaker::class, function() {
    $factory = new Factory();
    $client = new Client();
    return $factory->getRedisInstance($client, 5, 20);
});

Ez még nem ördöngősség. Viszont a használt library tud még egy olyan gonosz dolgot, hogy dinamikus proxy-t épít egy osztály köré és annak metódusait elburkolja a fent is látott if és try-catch blokkal.

A mágiát egy ún. CircuitBreakerProxyFactory osztály végzi, annak is a create metódusa:
$this->app->bind(FakeService::class, function() {
    return CircuitBreakerProxyFactory::create(FakeService::class, $this->app->make(CircuitBreaker::class), [new \GuzzleHttp\Client()]);
});

A lényeg, hogy a kreált proxy ugyanabból az osztályból származik, mint az amit elburkol, ennélfogva ahol az előbbit tudtuk használni, ott a proxyt is tudjuk majd.

A create 3+1 opcionális paramétert vár. Az első az a service neve, ahol én praktikusan az osztály nevét használom. Ezt azért kell elkülöníteni, mert ez alapján válogatja szét a Circuit Breaker a cache-ben a kulcsokat. A második paraméter az maga a CircuitBreaker, amit használni fog a háttérben. A harmadik paraméter egy tömb, amiben az osztály példányosításához szükséges paramétereket adjuk át, abban a sorrendben, ahogy a konstruktorban is vannak. A negyedik opcionális paraméterről majd némileg később ejtenék szót. A proxy létrehozása viszonylag erőforrás igényes feladat, körülbelül 0.00007 másodpercet vesz igénybe, de általában egy rendszer nem használ többet 2-3 ilyennél. A proxy osztályt cache-eljük, ha változtatunk az osztályon, csak akkor kell újragenerálni azt. Na de most bebindoltuk az osztályt, ennélfogva próbáljuk is ki!
Route::get("/home", function(\App\Services\FakeService $fakeService) {
    $content = $fakeService->slowCall();
    if ($content == null) {
        return "A FakeService sajnos most nem elérhető, kérjük jöjjön vissza később!";
    } else {
        return $content. "-asért a kilója az almának!";
    }
});

Egyetlen if-el megúsztuk az egészet, mivel ha nem sikerül a hívás, akkor a visszatérési érték üres marad, ez alapján tudjuk eldönteni, hogy működik-e a szolgáltatás vagy sem.
Fontos: egyetlen CircuitBreaker több szolgáltatáshoz/proxyhoz is felhasználható, amíg a kulcsok mentén elkülönítjük őket.

Na de nézzük csak mi történik itt a mélyben!



Van egy ún. ProxyMethodHook osztályunk, ami végzi a piszkos munkát. Amikor ilyen proxy-t gyártunk, akkor ezeken át érjük el az elburkolt osztályt. Amikor bejön egy hívás, először is ellenőrízzük, hogy tartozik-e hozzá ilyen:
/**
 * Does this hook support this method
 *
 * @param ReflectionMethod $method
 * @return boolean
 */
public function supports(ReflectionMethod $method)
{
    // ignores the magic functions

    return !in_array($method->getName(), array("__construct", "__destruct","__isset", "__invoke","__clone","__debugInfo", "__unset", "__sleep","__toString", "__wakeup" , "__call", "__get", "__set", "__callStatic"));
}

A magic metódusok és a constructor kivételével bármire rá van húzva mindez alapesetben. Ha a supports true-val tér vissza, akkor az adott metódus esetében ennek az osztálynak az invoke metódusa lefut:
/**
 * Called instead of the original method
 *
 * @param mixed $proxy the proxy object
 * @param ReflectionMethod $method original method
 * @param array $args original methods arguments
 */
public function invoke($proxy, ReflectionMethod $method, array $args)
{
    $returnValue = null;
    $oldTimeout = ini_get("default_socket_timeout");

    if ($this->circuitBreaker->isAvailable($this->serviceName)) {
        try {
            ini_set("default_socket_timeout", $this->timeout);
            $returnValue = $method->invokeArgs($proxy, $args);
            $this->circuitBreaker->reportSuccess($this->serviceName);
        } catch(\Exception $e) {
            $this->circuitBreaker->reportFailure($this->serviceName);
        }

    }
    ini_set("default_socket_timeout", $oldTimeout);

    return $returnValue;
}

Na itt van az igazi feketemágia. Először is beállítjuk a returnValue-t nullra, utána a socket_timeout-ot kinyerjük, hogy később vissza tudjuk állítani. Mivel remote hívásokkal dolgozunk, ezért lehet ezt is alacsonyan akarjuk tartani. Ezután ugyanúgy ellenőrízzük, hogy az adott service elérhető-e. Beállítjuk az alacsony timeoutot, meghívjuk a metódust, ami a proxy mögé van bújtatva és ha nem volt hiba, akkor jelentsük vagy ha volt, akkor azt is. Ezután visszaállítjuk az eredeti timeoutot és visszatérünk a visszatérési értékkel. Ami itt fontos lehet és amiért érdemes lehet saját MethodHook implementációkat létrehozni az az, hogy itt nem tudjuk, hogy mi is történik az elburkolt osztályban, ennélfogva minden Exceptiont a külső service hibájának hiszünk, pedig lehet, hogy nálunk van a baj. Valamint érdemes lehet logolást is elhelyezni itt, hogy lássuk, ha a külső szolgáltatások nem mentek.

Persze ilyenkor jönnek azok a vélemények, miszerint a Reflectiont egyenesen az ördög teremtette, mert nagyon erőforrásigényes, stb. Ez teljesen valid, ellenben inkább tartson tovább egy request 0.0001 másodperccel, mint tök feleslegesen pingeljek egy 3rd party service-t :)

Hozzászólások betöltése
2014-2017 © Letscode.hu. Minden jog fenntartva. Build verzió: 1.2.37

Mik azok a sütik?


As is common practice with almost all professional websites this site uses cookies, which are tiny files that are downloaded to your computer, to improve your experience. This page describes what information they gather, how we use it and why we sometimes need to store these cookies. We will also share how you can prevent these cookies from being stored however this may downgrade or 'break' certain elements of the sites functionality.

Áltálnos információkat nyújthat ez a Wikipedia cikk a HTTP sütikről...

Hogy használjuk a sütiket?


We use cookies for a variety of reasons detailed below. Unfortunately is most cases there are no industry standard options for disabling cookies without completely disabling the functionality and features they add to this site. It is recommended that you leave on all cookies if you are not sure whether you need them or not in case they are used to provide a service that you use.

Sütik kikapcsolása


You can prevent the setting of cookies by adjusting the settings on your browser (see your browser Help for how to do this). Be aware that disabling cookies will affect the functionality of this and many other websites that you visit. Disabling cookies will usually result in also disabling certain functionality and features of the this site. Therefore it is recommended that you do not disable cookies.

A sütik amiket mi használunk


This site offers newsletter or email subscription services and cookies may be used to remember if you are already registered and whether to show certain notifications which might only be valid to subscribed/unsubscribed users.

In order to provide you with a great experience on this site we provide the functionality to set your preferences for how this site runs when you use it. In order to remember your preferences we need to set cookies so that this information can be called whenever you interact with a page is affected by your preferences.


Harmadik féltől származó sütik


In some special cases we also use cookies provided by trusted third parties. The following section details which third party cookies you might encounter through this site.

This site uses Google Analytics which is one of the most widespread and trusted analytics solution on the web for helping us to understand how you use the site and ways that we can improve your experience. These cookies may track things such as how long you spend on the site and the pages that you visit so we can continue to produce engaging content.

For more information on Google Analytics cookies, see the official Google Analytics page.

The Google AdSense service we use to serve advertising uses a DoubleClick cookie to serve more relevant ads across the web and limit the number of times that a given ad is shown to you.

For more information on Google AdSense see the official Google AdSense privacy FAQ.

We also use social media buttons and/or plugins on this site that allow you to connect with your social network in various ways. For these to work the following social media sites including; Facebook, will set cookies through our site which may be used to enhance your profile on their site or contribute to the data they hold for various purposes outlined in their respective privacy policies.


More Information


Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site. However if you are still looking for more information then you can contact us through one of our preferred contact methods.

Email: fejlesztes@letscode.hu

Bezárás