paint-brush
Nuo Paryžiaus iki Berlyno: kaip sukurti grandinės pertraukiklius Kotlinepateikė@jesperancinha
428 skaitymai
428 skaitymai

Nuo Paryžiaus iki Berlyno: kaip sukurti grandinės pertraukiklius Kotline

pateikė João Esperancinha26m2025/01/23
Read on Terminal Reader

Per ilgai; Skaityti

Grandinės pertraukikliai šiais laikais naudojami siekiant išvengti per didelio užklausų kiekio į vieną ar kitą nereaguojančią paslaugą. Pavyzdžiui, kai paslauga dėl kokios nors priežasties išsijungia, grandinės pertraukiklis, kaip nurodo jo pavadinimas, turėtų nutraukti grandinę. Šiame straipsnyje apžvelgsime tris programiškai būdus, kaip įdiegti grandinės pertraukiklius naudojant AOP pagrįstą įgyvendinimą.
featured image - Nuo Paryžiaus iki Berlyno: kaip sukurti grandinės pertraukiklius Kotline
João Esperancinha HackerNoon profile picture
0-item

1. Įvadas

Circuit-breakers šiais laikais naudojami siekiant išvengti per didelio užklausų kiekio į vieną ar kitą nereaguojančią paslaugą. Pavyzdžiui, kai paslauga dėl kokios nors priežasties išsijungia, grandinės pertraukiklis, kaip nurodo jo pavadinimas, turėtų nutraukti grandinę. Kitaip tariant, esant 1 milijonui užklausų vienu metu, norint gauti žirgų lenktynių rezultatus, norime, kad šios užklausos būtų nukreiptos į kitą tarnybą, galinčią tai tvarkyti.


Ši kita paslauga gali būti jos kopija arba ji gali būti naudojama tik kitoms operacijoms, susijusioms su pradiniu paslaugos gedimu, atlikti. Galutinis tikslas visada yra nutraukti nereikalingus skambučius ir nukreipti srautą kitur. 2017 m. Michael Nygard iškėlė „Circuit Breaker“ dizaino modelį į programinės įrangos kūrimo projektavimo priešakį. Tai buvo padaryta jo leidinyje Release It!: Design and Deploy Production Ready Software (Pragmatic Programmers) 1st Edition.


circuit breaker dizaino modelį įkvėpė tikrosios elektroninės ir elektros grandinės. Tačiau, kalbant apie bendrą koncepciją, grandinės pertraukiklio idėją 1879 išrado Thomas Edison . Kaip ir tuo metu, reikia valdyti perpildytą srovę. Labai, labai paprastai tariant, šiuo atveju tai taikome programinės įrangos architektūrai. Pagrindinis tikslas – įsitikinti, kad sistema yra pakankamai atspari. Inžinierių, atsakingų už šio modelio įgyvendinimą, akyse iš tikrųjų atsižvelgiama į tai, koks jis turi būti atsparus ir koks jis turi būti fault-tolerant . Idėja yra ta, kad tam tikromis sąlygomis galime sklandžiai nukreipti tam tikros užklausos srautą į kitą labiau prieinamą srautą už to paties endpoint .


Tarkime, kad norime atlikti užklausą nuo A iki B . Kartkartėmis B nepavyksta, o C visada pasiekiamas. Jei B nepavyksta atsitiktinai, norime pasiekti C , kad mūsų paslauga būtų visiškai prieinama. Tačiau norėdami grąžinti užklausas B , norime įsitikinti, kad B vėl nepasiduos. Tada galime sukonfigūruoti savo sistemą taip, kad pateiktų atsitiktines užklausas B ir visiškai grįžti į B tik tada, kai gedimų dažnis sumažėja iki tam tikro lygio.


Galime pateikti užklausą C dėl klaidos, bet ir dėl delsos. Jei B yra labai lėtas, galime iš naujo išsiųsti visas užklausas C . Yra daug kitų galimų konfigūracijų, pvz., bandymas pasiekti C , jei po tam tikro skaičiaus bandymų, užklausų tipai, lygiagrečios gijos ir daugelis kitų parinkčių Tai taip pat žinoma kaip short-circuiting ir dažniausiai tai yra laikinas veiksmas.

Valstybės mašina


Norėdami geriau suprasti savo žinias apie tai, kas iš tikrųjų yra grandinės pertraukiklis, turime suprasti, kad grandinės pertrauka mūsų programoje veikia kaip vienetas. Yra trys pagrindinės grandinės pertraukiklio būsenos. Jis gali būti uždarytas, atviras arba pusiau atviras. Būsena uždaryta reiškia, kad mūsų programų srautai veikia normaliai. Galime drąsiai teikti užklausas tarnybai A, žinodami, kad visos užklausos pateks į paslaugą B. Atvira būsena reiškia, kad visos užklausos tarnybai B žlugs. Taisyklės, kurias apibrėžėme kaip gedimą, įvyko ir nebepasiekiame paslaugos B. Tokiu atveju visada grąžinama išimtis. half-open būsena yra tada, kai mūsų grandinės pertraukikliui nurodoma atlikti B paslaugos bandymus, kad patikrintų, ar jis vėl veikia.


Kiekviena sėkminga užklausa apdorojama įprastai, tačiau ji ir toliau teiks užklausas C. Jei B elgsis taip, kaip tikėtasi pagal mūsų nustatytas tikrinimo taisykles, mūsų grandinės pertraukiklis grįš į uždarą būseną, o paslauga A pradės teikti užklausų tik B tarnybai. Daugumoje programų grandinės pertraukiklis atitinka dekoratoriaus dizaino modelį. Tačiau jį galima įdiegti rankiniu būdu, todėl apžvelgsime tris programinius grandinės pertraukiklių diegimo būdus ir galiausiai naudojant AOP pagrįstą įgyvendinimą. Kodas pasiekiamas GitHub .

Diagrama

2. Automobilių patikros

Paskutiniame šio straipsnio punkte apžvelgsime automobilių lenktynių žaidimą. Tačiau prieš tai, kai tai padarysime, norėčiau paaiškinti kai kuriuos programos, veikiančios su circuit breaker kūrimo aspektus.

2.1. Kystrix (nuo Paryžiaus iki Berlyno-kystrix-runnable-app)

Kystrixs , kaip mažas DSL , yra nuostabi biblioteka, kurią išrado ir sukūrė Johan Haleby . Biblioteka suteikia daug galimybių, įskaitant integraciją su Spring ir Spring WebFlux. Įdomu tai pažiūrėti ir šiek tiek pažaisti:

 <dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-core</artifactId> </dependency> <dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-spring</artifactId> </dependency>


Sukūriau pavyzdį ir jis yra modulyje nuo paris iki berlino-kystrix-runnable-app, esančiame GitHub . Pirmiausia pažvelkime į kodą:

 @GetMapping("/{id}") private fun getCars(@PathVariable id: Int): Mono<Car> { return if (id == 1) Mono.just(Car("Jaguar")) else { hystrixObservableCommand<Car> { groupKey("Test2") commandKey("Test-Command2") monoCommand { webClient.get().uri("/cars/carros/1").retrieve().bodyToMono<Car>() .delayElement(Duration.ofSeconds(1)) } commandProperties { withRequestLogEnabled(true) withExecutionTimeoutInMilliseconds(5000) withExecutionTimeoutEnabled(true) withFallbackEnabled(true) withCircuitBreakerEnabled(false) withCircuitBreakerForceClosed(true) } fallback { Observable.just(Car("Tank1")) } }.toMono() } }

Šis kodas reiškia 2 pavyzdžio komandą. Patikrinkite 1 komandos kodą. Čia yra tai, kad mes apibrėžiame norimą komandą naudodami monoCommand . Čia apibrėžiame metodą, kurį turime iškviesti. commandProperties apibrėžiame taisykles, kurios verčia circuit-breaker būseną pakeisti į atidarymą. Mes aiškiai atidedame savo skambutį, kad jis truktų tiksliai 1 sekundę.


Tuo pačiu metu mes nustatome 5000 milisekundžių skirtąjį laiką. Tai reiškia, kad mes niekada nepasieksime skirtojo laiko. Šiame pavyzdyje galime skambinti naudodami Id . Kadangi tai tik bandymas, darome prielaidą, kad Id=1 yra automobilio, „Jaguar“, kuriam nereikia circuit-breaker Id . Tai taip pat reiškia, kad niekada negausime Tank1, kaip apibrėžta atsarginiame metode. Jei dar nepastebėjote, atidžiai peržiūrėkite atsarginį metodą. Šis metodas naudoja Observable . Nors WebFlux yra įdiegtas pagal Observable dizaino modelį, Flux nėra tiksliai stebimasis.


Tačiau hystrix palaiko abu. Paleiskite programą ir atidarykite naršyklę adresu http://localhost:8080/cars/2 , kad tai patvirtintumėte. Svarbu suprasti, kad jei pradėsite skambinti labai anksti paleidžiant Spring Boot, galiausiai galite gauti Tank1 pranešimą. Taip yra todėl, kad paleidimo delsa gali labai lengvai viršyti 5 sekundes, priklausomai nuo to, kaip vykdote šį procesą. Antrame pavyzdyje mes trumpai sujungsime savo pavyzdį su 2 baku:

 @GetMapping("/timeout/{id}") private fun getCarsTimeout(@PathVariable id: Int): Mono<Car> { return if (id == 1) Mono.just(Car("Jaguar")) else { hystrixObservableCommand<Car> { groupKey("Test3") commandKey("Test-Command3") monoCommand { webClient.get().uri("/cars/carros/1").retrieve().bodyToMono<Car>() .delayElement(Duration.ofSeconds(1)) } commandProperties { withRequestLogEnabled(true) withExecutionIsolationThreadInterruptOnTimeout(true) withExecutionTimeoutInMilliseconds(500) withExecutionTimeoutEnabled(true) withFallbackEnabled(true) withCircuitBreakerEnabled(false) withCircuitBreakerForceClosed(true) } fallback { Observable.just(Car("Tank2")) } }.toMono() } }

Šiame pavyzdyje mūsų circuit-breaker kaip atsakas pereis į atvirą būseną, grąžindamas 2 baką. Taip yra todėl, kad čia taip pat sukeliame 1 s delsą, bet nurodome, kad grandinės pertraukos sąlyga suveikia po 500 ms žymos. Jei žinote, kaip veikia hystrix , pamatysite, kad kystrix nesiskiria. Tai, ko šiuo metu „hystrix“ man nepateikė, buvo sklandus ir lengvas būdas suteikti tai, ko man reikia žaidimui sukurti. Panašu, kad Kystrix dirba pagal klientą. Tai reiškia, kad prieš pateikdami užklausas pagrindinėms paslaugoms teikiamoms paslaugoms, turime deklaruoti savo kodą.

2.2. Atsparumas4J

Atrodo, kad daugelis Resilience4J vadina labai išsamiu grandinės pertraukiklio įgyvendinimu. Mano pirmieji bandymai buvo susiję su kai kuriais svarbiais grandinės pertraukiklių aspektais. Būtent, norėjau pamatyti grandinės pertraukiklį, kuris veiktų pagal skirtąjį laiką ir sėkmingų užklausų dažnumą. Resilience4J leidžia konfigūruoti įvairių tipų short-circuiting modulius. Jie suskirstyti į 6 skirtingas kategorijas: CircuitBreaker , Bulkhead , Ratelimiter , Retry ir Timelimiter . Visa tai taip pat yra dizaino modelių pavadinimai. CircuitBreaker modulis visiškai įgyvendina šį modelį.


Turime daug parametrų, kuriuos galime konfigūruoti, bet iš esmės, CircuitBreaker modulis leidžia sukonfigūruoti, ką atpažinsime kaip gedimą, kiek užklausų leidžiame pusiau atviroje būsenoje ir slankiojantį langą, kurį galima konfigūruoti pagal laiką arba count, kur išlaikome uždaros būsenos užklausų skaičių. Tai svarbu norint apskaičiuoti klaidų dažnį. Iš esmės galėtume sakyti, kad šis CircuitBreaker modulis padės mums nustatyti užklausų dažnį, tačiau tai nebūtinai yra tiesa.


Tai priklauso nuo to, kaip jį interpretuojate. Atrodo, kad tai yra geresnis būdas galvoti apie tai kaip apie paprasčiausią būdą pašalinti gedimus. Nesvarbu, ar jie gaunami pasibaigus skirtajam laikui, ar dėl išimties, čia jie yra sprendžiami ir kaip užklausos gali būti sklandžiai nukreipiamos kažkur kitur. Modulis Bulkhead yra skirtas nagrinėti tuo pačiu metu vykstančias užklausas. Tai nėra greičio ribotuvas.


Vietoj to, jis įgyvendina Bulkhead dizaino modelį, kuris naudojamas siekiant išvengti per didelio apdorojimo viename galutiniame taške. Šiuo atveju Bulkhead leidžia apdoroti užklausas taip, kad jos būtų paskirstytos visuose galimuose galutiniuose taškuose. Pavadinimas Bulkhead kilęs iš skirtingų sandarių skyrių, kuriuose didelis laivas paprastai turi vengti nuskęsti, jei įvyktų nelaimingas atsitikimas, ir, kaip ir laivų atveju, turime apibrėžti, kiek siūlų bus siūlų telkinyje ir jų nuomos laiką. .


RateLimiter modulis yra skirtas tvarkyti užklausų dažnį. Skirtumas tarp šio ir Bulkhead modulio yra esminis, nes norime būti tolerantiški tarifams iki tam tikro taško. Tai reiškia, kad mums nereikia sukelti nesėkmės. Mes tiesiog sakome, kad projekte netoleruojame normos, viršijančios tam tikrą vertę. Be to, galime peradresuoti užklausą arba laikyti ją sulaikyta, kol bus suteiktas leidimas vykdyti užklausą. Retry modulis tikriausiai yra lengviausiai suprantamas, nes jis neturi daug bendro su kitais moduliais.


Mes iš esmės aiškiai deklaruojame pakartojimų skaičių iki tam tikro galutinio taško, kol pasieksime savo apibrėžtą slenkstį. Timelimiter modulis gali būti vertinamas kaip CircuitBreaker modulio supaprastinimas, nes jie abu turi galimybę konfigūruoti skirtąjį laiką. Tačiau Timelimiter nepriklauso nuo kitų parametrų, pvz., stumdomų langų, be to, jame nėra integruoto gedimo slenksčio skaičiavimo.


Taigi, jei mums rūpi tik skirtasis laikas, kai skambiname į tam tikrą paslaugą, ir neatsižvelgiame į kitus galimus gedimus, tikriausiai geriau naudoti Timelimiter .

2.2.1. Resilience4J su Kotlin and No Spring framework (nuo Paryžiaus iki Berlyno-resilience4j-runnable-app)

Šiame modulyje nusprendžiau naudoti tik resilience4j kotlin biblioteką:

 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-kotlin</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-retry</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> </dependency>


Šis įgyvendinimas pasiekiamas per atpirkimą „GitHub“. Pirmiausia pažvelgsime į TimeLimiter modelį:

 var timeLimiterConfig: TimeLimiterConfig = TimeLimiterConfig.custom() .timeoutDuration(Duration.ofMillis(100)) .build() var timeLimiter: TimeLimiter = TimeLimiter.of("backendName", timeLimiterConfig) private suspend fun getPublicCar(): Car { return timeLimiter.decorateSuspendFunction { getPrivateCar() }.let { suspendFunction -> try { suspendFunction() } catch (exception: Exception) { Car("Opel Corsa") } } } private suspend fun getPrivateCar(): Car { delay(10000) return Car("Lancya") }

Šiuo atveju mes papuošiame savo function getPrivateCar su TimeLimiter funkcionalumu, naudodami funkciją decorateSuspendFunction . Tai sukels skirtąjį laiką, jei funkcija, kurią iškviečiame, užtruks per ilgai, vietoj Lancya gausime Opel Corsa. Norėdami tai išbandyti, galime tiesiog paleisti programą ir atidaryti http://localhost:8080/cars/timelimiter/normal/1 .


Žvelgdami į įgyvendinimą matome, kad niekada negalime gauti Lancya . Taip yra todėl, kad mes sąmoningai laukiame 10s , kol grąžinsime atgal. Mūsų TimeLimiter skirtasis laikas yra daug mažesnis, todėl jis niekada neveiks. „ TimeLimiter “ yra gana paprasta suprasti. Kita vertus, CircuitBreaker gali būti kitokia istorija. Tai pavyzdys, kaip tai galima padaryti:

 val circuitBreakerConfig = CircuitBreakerConfig.custom() .failureRateThreshold(20f) .slowCallRateThreshold(50f) .slowCallDurationThreshold(Duration.ofMillis(1000)) .waitDurationInOpenState(Duration.ofMillis(1000)) .maxWaitDurationInHalfOpenState(Duration.ofMillis(1000)) .permittedNumberOfCallsInHalfOpenState(500) .minimumNumberOfCalls(2) .slidingWindowSize(2) .slidingWindowType(COUNT_BASED) .build() val circuitBreaker = CircuitBreakerRegistry.of(circuitBreakerConfig).circuitBreaker("TEST") private suspend fun getPublicCar(id: Long): Car { return circuitBreaker.decorateSuspendFunction { getPrivateCar(id) }.let { suspendFunction -> try { suspendFunction() } catch (exception: Exception) { Car("Opel Corsa") } } } private fun getPrivateCar(id: Long): Car { if (id == 2L) { throw RuntimeException() } return Car("Lancya") }

Šiuo atveju mes sakome, kad norime, kad mūsų grandinės pertraukiklis uždarytų grandinę, kai gedimų dažnis bus mažesnis nei 20% . Lėti skambučiai taip pat turės slenkstį, tačiau šiuo atveju jis bus mažesnis nei 50%. Sakome, kad lėtas skambutis turi trukti ilgiau nei 1 s, kad būtų laikomas vienu. Taip pat nurodome, kad pusiau atviros būsenos trukmė turėtų būti 1 s. Praktiškai tai reiškia, kad turėsime atvirą, pusiau atvirą arba uždarą būseną.


Taip pat sakome, kad leidžiame ne daugiau kaip 500 pusiau atvirų užklausų. Norėdami apskaičiuoti klaidas, grandinės pertraukiklis turi žinoti, prie kurio ženklo jis tai padarys. Tai svarbu norint nustatyti, kada uždaryti grandinę. Sakome, kad šiam skaičiavimui reikės mažiausiai 2 užklausų, kurių ypatybė minimumNumberOfCalls . Prisiminkite, kad pusiau atvira yra tada, kai bandysime uždaryti grandinę, jei užklausos pasieks saugų nesėkmės slenkstį?


Šioje konfigūracijoje tai reiškia, kad turime pateikti bent 2 užklausas slankiojančiame lange, kad apskaičiuotume klaidų dažnį ir nuspręstume, ar grįžti į uždarą būseną, ar ne. Tai yra tikslus visų mūsų sukonfigūruotų kintamųjų skaitymas. Apskritai, tai reiškia, kad mūsų programa tikriausiai kelis kartus skambins į alternatyviąją tarnybą, jei tokia būtų, ji nebus labai lengvai persijungusi iš atviros būsenos į uždarą, nes sėkmės rodiklis turi būti 80 % per pusę. -atviros būsenos ir atviros būsenos skirtasis laikas turi būti įvykęs.


Yra daug būdų, kaip nurodyti tokius skirtuosius laikus. Mūsų pavyzdyje sakome, kad maxDurationInHalfOpenState yra 1 s. Tai reiškia, kad mūsų CircuitBreaker būsena išliks atidaryta, tik jei mūsų patikrinimas neatitiks uždaros būsenos sąlygos arba jei šis skirtasis laikas dar nepasibaigė. Šiame CircuitBreaker aprašytą elgesį gali būti sunku sekti ir nuspėti vien dėl to, kad konkrečių prastovų, dažnių ir kitų užklausų ypatybių tiesiog neįmanoma tiksliai atkartoti, bet jei atliksime keletą užklausų šiam galutiniam taškui, pamatysime, kad aukščiau aprašytas elgesys atitinka mūsų patirtį.


Taigi, pabandykime atlikti keletą užklausų galutiniams taškams: http://localhost:8080/cars/circuit/1 ir http://localhost:8080/cars/circuit/2 . Pasibaigus 1 yra sėkmingo automobilio gavimo galutinis taškas, o baigiasi 2 yra galutinis taškas, kai nepavyksta gauti nurodyto automobilio. Žvelgdami į kodą matome, kad viskas, kas nėra 2, reiškia, kad kaip atsakymą gauname Lancya . A 2 reiškia, kad iš karto pateikiame veikimo laiko išimtį, o tai reiškia, kad galiausiai gauname „ Opel Corsa kaip atsaką.


Jei tik pateiksime užklausas dėl 1 galutinio taško, atsakymą matysime Lancya . Jei sistema pradeda trikdyti, tai yra tada, kai pateikiate užklausas 2,\; pamatysite, kad po kurio laiko sugrįžimas į Lancya nebus nuolatinis. System informuos, kad ji yra atviros būsenos ir kad daugiau užklausų neleidžiama.

 2021-10-20 09:56:50.492 ERROR 34064 --- [ctor-http-nio-2] .fcbrrcCarControllerCircuitBreaker : io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'TEST' is OPEN and does not permit further calls


Po sėkmingos užklausos mūsų grandinės pertraukiklis pereis į pusiau atvirą būseną, o tai reiškia, kad turėsime atlikti keletą užklausų iki 1, kol jis normalizuosis. Kelis kartus keisime Lancya į Opel Corsa , kol vėl gausime Lancya . Šį skaičių apibrėžėme kaip 2. Tai yra minimalus klaidų skaičiavimo dydis. Jei sukelsime tik vieną gedimą ir toliau skambinsime į galutinį tašką, kuriame nėra klaidų, galime susidaryti aiškesnį vaizdą apie tai, kas vyksta:

 2021-10-20 11:53:29.058 ERROR 34090 --- [ctor-http-nio-4] .fcbrrcCarControllerCircuitBreaker : java.lang.RuntimeException 2021-10-20 11:53:41.102 ERROR 34090 --- [ctor-http-nio-4] .fcbrrcCarControllerCircuitBreaker : io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'TEST' is OPEN and does not permit further calls

Šis atviros būsenos pranešimas, nors ir buvo teisingas, įvyko po to, kai pateikiau 2 užklausas galutiniam taškui, kurio nepavyko pasiekti. Štai kodėl valstybė esą pusiau atvira.

2.2.2. Resilience4J su Spring Boot ir be AOP (nuo Paryžiaus iki Berlyno-resilience4j-spring-app)

Ankstesniame segmente matėme, kaip tai įgyvendinti labai programiškai, nenaudojant jokios Spring technologijos. Mes naudojome „Spring“, bet tik norėdami teikti „ WebFlux MVC tipo paslaugas. Be to, nieko nekeitėme dėl pačių paslaugų. Šioje programoje išnagrinėsime šias bibliotekas:

 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-all</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-reactor</artifactId> </dependency>


Pažiūrėję, kaip daromas kodas, matome gana didelį skirtumą:

 @RestController @RequestMapping("/cars") class CarController( private val carService: CarService, timeLimiterRegistry: TimeLimiterRegistry, circuitBreakerRegistry: CircuitBreakerRegistry, bulkheadRegistry: BulkheadRegistry ) { private var timeLimiter: TimeLimiter = timeLimiterRegistry.timeLimiter(CARS) private var circuitBreaker = circuitBreakerRegistry.circuitBreaker(CARS) private var bulkhead = bulkheadRegistry.bulkhead(CARS) @GetMapping("/{id}") private fun getCars(@PathVariable id: Int): Mono<Car> { return carService.getCar() .transform(TimeLimiterOperator.of(timeLimiter)) .transform(CircuitBreakerOperator.of(circuitBreaker)) .transform(BulkheadOperator.of(bulkhead)) .onErrorResume(TimeoutException::class.java, ::fallback) } @GetMapping("/test/{id}") private fun getCarsTest(@PathVariable id: Int): Mono<Car> { return carService.getCar() .transform(TimeLimiterOperator.of(timeLimiter)) .transform(CircuitBreakerOperator.of(circuitBreaker)) .transform(BulkheadOperator.of(bulkhead)) .onErrorResume(TimeoutException::class.java, ::fallback) } @GetMapping("/carros/{id}") private fun getCarros(@PathVariable id: Long): Mono<Car> { return Mono.just(Car("Laborghini")) } private fun fallback(ex: Throwable): Mono<Car> { return Mono.just(Car("Rolls Royce")) } }

Šiame pavyzdyje ir kituose pavyzdžiuose daugiausiai nagrinėsime skirtojo laiko ypatybes. Patys CircuitBreaker gudrybės yra mažiau aktualios, nes tai yra įvadinis straipsnis. Čia svarbu suvokti, kaip lengvai galime tai įgyvendinti naudodami Resilience4J pavasariui suteiktus dekoratorius. Nors ir vis dar programmatical , galime lengvai papuošti savo pradinį leidėją, kurį gauname iš carService.getCar() , norimais short-circuit tipais.


Šiame pavyzdyje registruojame TimeLiter , BulkHead ir CircuitBreaker . Galiausiai apibrėžiame atsarginę funkciją, kuri bus suaktyvinta, kai įvyksta TimeoutException. Mums dar reikia pamatyti, kaip visa tai sukonfigūruota. „Resilience4J“ konfigūruojame pavasarį, kaip ir bet kurį kitą konfigūruojamą modulį. Mes naudojame application.yml :

 resilience4j.circuitbreaker: configs: default: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 recordExceptions: - org.springframework.web.client.HttpServerErrorException - java.util.concurrent.TimeoutException - java.io.IOException ignoreExceptions: # - io.github.robwin.exception.BusinessException shared: slidingWindowSize: 100 permittedNumberOfCallsInHalfOpenState: 30 waitDurationInOpenState: 1s failureRateThreshold: 50 eventConsumerBufferSize: 10 ignoreExceptions: # - io.github.robwin.exception.BusinessException instances: cars: baseConfig: default roads: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 10 permittedNumberOfCallsInHalfOpenState: 3 waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 # recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate resilience4j.retry: configs: default: maxAttempts: 3 waitDuration: 100 retryExceptions: - org.springframework.web.client.HttpServerErrorException - java.util.concurrent.TimeoutException - java.io.IOException ignoreExceptions: # - io.github.robwin.exception.BusinessException instances: cars: baseConfig: default roads: baseConfig: default resilience4j.bulkhead: configs: default: maxConcurrentCalls: 100 instances: cars: maxConcurrentCalls: 10 roads: maxWaitDuration: 10ms maxConcurrentCalls: 20 resilience4j.thread-pool-bulkhead: configs: default: maxThreadPoolSize: 4 coreThreadPoolSize: 2 queueCapacity: 2 instances: cars: baseConfig: default roads: maxThreadPoolSize: 1 coreThreadPoolSize: 1 queueCapacity: 1 resilience4j.ratelimiter: configs: default: registerHealthIndicator: false limitForPeriod: 10 limitRefreshPeriod: 1s timeoutDuration: 0 eventConsumerBufferSize: 100 instances: cars: baseConfig: default roads: limitForPeriod: 6 limitRefreshPeriod: 500ms timeoutDuration: 3s resilience4j.timelimiter: configs: default: cancelRunningFuture: false timeoutDuration: 2s instances: cars: baseConfig: default roads: baseConfig: default

Šis failas yra pavyzdinis failas, paimtas iš jų atpirkimo ir atitinkamai pakeistas į mano pavyzdį. Kaip matėme anksčiau, skirtingų tipų limiters/short-circuit egzemplioriai turi pavadinimą. Pavadinimas yra labai svarbus, jei turite daug skirtingų registrų ir skirtingų ribotuvų. Pavyzdžiui, kaip minėta anksčiau, mus domina laiko ribotuvas. Matome, kad jis apsiriboja 2 s. Jei pažvelgsime į tai, kaip įdiegėme paslaugą, pamatysime, kad verčiame įvykti skirtąjį laiką:

 @Component open class CarService { open fun getCar(): Mono<Car> { return Mono.just(Car("Fiat")).delayElement(Duration.ofSeconds(10)); } }

Paleiskite programą ir naršyklėje eikite į: http://localhost:8080/cars/test/2 . Užuot įsigiję Fiat , gausime Rolls Royce . Taip apibrėžėme skirtąjį laiką. Lygiai taip pat galime lengvai sukurti CircuitBreaker .

3. Byla

Iki šiol matėme tris pagrindinius būdus, kaip įdiegti CircuitBreakers ir susijusius ribotuvus. Be to, apžvelgsime mano mėgstamiausią grandinės pertraukiklių diegimo būdą, naudodami mano sukurtą programą, kuri yra labai paprastas žaidimas, kuriame tiesiog spustelėjame kvadratus, kad patektume iš Paryžiaus į Berlyną. Žaidimas sukurtas siekiant suprasti, kaip jį įgyvendinti. Jame nėra daug pasakyta, kur tai įgyvendinti. Tai tik atvejis, kurį sukūriau norėdamas pasidalinti su jumis žiniomis.


Žinokite, kada paliksiu jums nuspręsti vėliau. Iš esmės norime sukurti daugybę automobilių ir nutiesti maršrutą į Berlyną. Įvairiose šio maršruto vietose pateksime į miestus, kur atsitiktinai sukelsime problemų. Mūsų circuit breakers nuspręs, kiek laiko turėsime laukti, kol galėsime judėti toliau. Kiti automobiliai neturi problemų, o mums tereikia pasirinkti tinkamą maršrutą.


Leidžiame patikrinti tvarkaraštį, kuriame registruojama, kada tam tikra problema įvyks mieste tam tikroje minutės žymoje. Minutės ženklas galioja 0 indeksuotų pozicijų. Tai reiškia, kad 2 reiškia, kad every 2, 12, 22, 32, 42, 52 minučių žyma laikrodyje galios šiai problemai sukurti. Problemos bus 2 rūšių: ERROR ir TIMEOUT . Klaidos gedimas suteiks jums 20 sekundžių delsą.


Skirtasis laikas suteiks jums 50 sekundžių delsą. Kiekvienam miesto pakeitimui kiekvienas turi palaukti 10 sekundžių. Tačiau prieš laukdamas automobilis jau yra kito miesto įvažiavime, kai tai daroma naudojant atsarginius metodus. Tokiu atveju kitas miestas pasirenkamas atsitiktinai.

Žaidimo puslapis

4. Įgyvendinimas

Anksčiau matėme, kaip sukonfigūruoti savo resilience4j registrą naudojant application.yml . Tai atlikę, pažvelkime į keletą pavyzdžių, kaip papuošti savo funkcijas:

 @TimeLimiter(name = CarService.CARS, fallbackMethod = "reportTimeout") @CircuitBreaker(name = CarService.CARS, fallbackMethod = "reportError") @Bulkhead(name = CarService.CARS) open fun moveToCity(id: Long): Mono<RoadRace> { val myCar = roadRace.getMyCar() if (!myCar.isWaiting()) { val destination = myCar.location.forward.find { it.id == id } val blockage = destination?.blockageTimeTable?.find { it.minute.toString().last() == LocalDateTime.now().minute.toString().last() } blockage?.let { roadBlockTime -> when (roadBlockTime.blockageType) { BlockageType.TIMEOUT -> return Mono.just(roadRace).delayElement(Duration.ofSeconds(10)) BlockageType.ERROR -> return Mono.create { it.error(BlockageException()) } BlockageType.UNKNOWN -> return listOf(Mono.create { it.error(BlockageException()) }, Mono.just(roadRace).delayElement(Duration.ofSeconds(10))).random() else -> print("Nothing to do here!") } } destination?.let { myCar.delay(10) myCar.location = it myCar.formerLocations.add(myCar.location) } } return Mono.just(roadRace) } private fun reportError(exception: Exception): Mono<RoadRace> { logger.info("---- **** error reported") roadRace.getMyCar().delay(20L) roadRace.getMyCar().randomFw() roadRace.errorReports.add("Error reported! at ${LocalDateTime.now()}") return Mono.create { it.error(BlockageException()) } } private fun reportTimeout(exception: TimeoutException): Mono<RoadRace> { logger.info("---- **** timeout reported!") roadRace.getMyCar().delay(50L) roadRace.getMyCar().randomFw() roadRace.errorReports.add("Timeout reported! at ${LocalDateTime.now()}") return Mono.just(roadRace) }


Kaip matome, originalūs serviso skambučiai yra tiesiogiai dekoruoti anotacijomis! Tai galima padaryti tik dėl to, kad pakete yra AOP modulis:

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

AOP arba Aspect Oriented Programming yra kita programavimo paradigma, pagrįsta OOP . Jis laikomas OOP papildymu ir tiksliai nurodo, kiek komentarų veikia. Tai leidžia suaktyvinti kitas funkcijas aplink, prieš arba po pradinio metodo tiksliuose pjovimo taškuose. Kaip matote iš pavyzdžio, mes generuojame skirtąjį laiką arba klaidas. „ BlockageException taip pat sugeneruojamas naudojant atsarginį metodą. Tai nėra problema. Išskyrus atsakymą.


Tačiau programa veikia WebSockets, todėl ši klaida programoje nebus matoma. Iki šiol tai buvo žaidimas. Įgyvendinau tai norėdamas parodyti, kaip komentarų naudojimas gali palengvinti mūsų gyvenimą diegiant atsparią programą. Įdiegę ne tik „CircuitBreakers“, bet ir kitas technologijas, pvz. WebSockets , Spring WebFlux , Docker , NGINX , typescript “ ir keletą kitų. Visa tai buvo padaryta siekiant pamatyti, kaip „CircuitBreakers“ veiktų programoje. Jei norite žaisti su šia programa, eikite į projekto šaknį ir paleiskite:

 make docker-clean-build-start


Tada paleiskite šią komandą:

 curl -X POST http://localhost:8080/api/fptb/blockage -H "Content-Type: application/json" --data '{"id":1,"name":"Paris","forward":[{"id":2,"name":"Soissons","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":3,"name":"Compiègne","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":4,"name":"Reims","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]}'


Šios užklausos naudingoji apkrova sugeneruojama naudojant modulį from-paris-to-berlin-city-generator . Jei pažvelgsite į šį modulį, pamatysite, kad jį suprasti gana paprasta ir kad galite sukurti savo žaidimo žemėlapį! Galiausiai eikite į http://localhost:9000 ir jūsų programa turėtų veikti! Dabar turėtumėte tiesiog spustelėti dešiniuosius kvadratus, kad galėtumėte žaisti. Jei norite laimėti, nespauskite ant raudonų. Jei norite matyti, kad grandinės pertraukiklis veikia, paleiskite programos žurnalus:

 docker logs from_paris_to_berlin_web -f

Ir aiškiai spustelėkite raudonus kvadratus, kad sukeltumėte gedimą.

5. Kuo skiriasi Kystrix ir Resilience4J

Kystrix puikiai tinka tais atvejais, kai jūsų programa yra maža ir norite užtikrinti, kad DSL būtų naudojamas tikrai mažas. Atrodo, kad vienintelis minusas yra tai, kad jis nesuteikia lengvo būdo papuošti metodus, kuriuos paveiks grandinės pertraukiklis. Atrodo, kad Resilience4J yra puikus pasirinkimas įmonės darbui su grandinės pertraukikliais. Jis teikia anotacijomis pagrįstą programavimą, naudoja visus AOP privalumus, o jo moduliai yra atskirti. Tam tikra prasme jis taip pat gali būti strategiškai naudojamas kritiniams programos taškams. Jis taip pat gali būti naudojamas kaip visapusė sistema, apimanti daugelį programos aspektų.

6. Išvada

Nepriklausomai nuo to, kokį prekės ženklą pasirenkame, tikslas visada yra turėti atsparų pritaikymą. Šiame straipsnyje aš parodžiau keletą pavyzdžių, kaip aš asmeniškai patyriau labai aukšto lygio grandinės pertraukiklius ir savo išvadas. Tai reiškia, kad šis straipsnis tikrai skirtas žmonėms, norintiems sužinoti, kas yra grandinės pertraukikliai ir ką gali padaryti Limiters.


Atvirai kalbant, galimybės yra begalinės, kai galvojame apie mūsų programų tobulinimą naudojant atsparumo mechanizmus, tokius kaip circuit breakers . Šis modelis leidžia tiksliai suderinti programą, kad būtų galima geriau panaudoti turimus išteklius. Dažniausiai debesyje vis dar labai svarbu optimizuoti savo išlaidas ir tai, kiek išteklių mums iš tikrųjų reikia skirti.


CircuitBreakers konfigūravimas nėra trivialus uždavinys, kaip ir Limiters, ir mes tikrai turime suprasti visas konfigūravimo galimybes, kad pasiektume optimalų našumo ir atsparumo lygį. Dėl šios priežasties nenorėjau detalizuoti šiame įvadiniame straipsnyje apie grandinės pertraukiklius.


Circuit-breakers gali būti taikomi daugeliui skirtingų tipų programų. To reikės daugeliui pranešimų ir srautinio perdavimo programų. Programoms, kurios apdoroja didelius duomenų kiekius, kurie taip pat turi būti labai prieinami, galime ir turėtume įdiegti tam tikrą grandinės pertraukiklį. Didelės internetinės mažmeninės prekybos parduotuvės turi kasdien tvarkyti didžiulius duomenų kiekius, o anksčiau Hystrix buvo plačiai naudojamas. Šiuo metu atrodo, kad judame Resilience4J kryptimi, kuri apima daug daugiau.

6. Literatūra

Ačiū!

Tikiuosi, kad jums patiko šis straipsnis taip pat, kaip man jį kuriant! Toliau pateiktose nuorodose palikite atsiliepimą, komentarus ar bet kokius atsiliepimus, kuriuos norite pateikti apie bet kurį iš socialinių tinklų. Esu labai dėkingas, jei norite padėti man patobulinti šį straipsnį. Visą šios programos šaltinio kodą įdėjau į „GitHub“. Ačiū, kad skaitėte!

L O A D I N G
. . . comments & more!

About Author

João Esperancinha HackerNoon profile picture
João Esperancinha@jesperancinha
Software Engineer for 10+ Years, OCP11, Spring Professional 2020 and a Kong Champion

PABAIGTI ŽYMES

ŠIS STRAIPSNIS BUVO PRISTATYMAS...