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.
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 .
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.
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ą.
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
.
Š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.
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
.
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.
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ą.
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ų.
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.
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!