paint-brush
No Parīzes līdz Berlīnei: kā izveidot slēdžus Kotlināautors@jesperancinha
420 lasījumi
420 lasījumi

No Parīzes līdz Berlīnei: kā izveidot slēdžus Kotlinā

autors João Esperancinha26m2025/01/23
Read on Terminal Reader

Pārāk ilgi; Lasīt

Automātiskie slēdži mūsdienās tiek izmantoti, lai izvairītos no pārmērīga pieprasījumu skaita uz vienu vai otru nereaģējošo pakalpojumu. Ja, piemēram, pakalpojums kāda iemesla dēļ tiek pārtraukts, ķēdes pārtraucējam, kā norādīts tā nosaukumā, jāpārtrauc ķēde. Šajā rakstā mēs aplūkosim trīs programmatiskus ķēdes slēdžu ieviešanas veidus, izmantojot uz AOP balstītu ieviešanu.
featured image - No Parīzes līdz Berlīnei: kā izveidot slēdžus Kotlinā
João Esperancinha HackerNoon profile picture
0-item

1. Ievads

Circuit-breakers mūsdienās tiek izmantoti, lai izvairītos no pārmērīga pieprasījumu skaita uz vienu vai otru nereaģējošo pakalpojumu. Ja, piemēram, pakalpojums kāda iemesla dēļ tiek pārtraukts, ķēdes pārtraucējam, kā norāda tā nosaukums, jāpārtrauc ķēde. Citiem vārdiem sakot, situācijā, kad vienlaikus tiek veikts 1 miljons pieprasījumu, lai iegūtu zirgu skriešanās sacensību rezultātus, mēs vēlamies, lai šie pieprasījumi tiktu novirzīti uz citu dienestu, kas to var apstrādāt.


Šis cits pakalpojums var būt tā kopija, vai arī to var izmantot, lai veiktu citas darbības saistībā ar sākotnējo pakalpojuma kļūmi. Gala mērķis vienmēr ir pārtraukt nevajadzīgus zvanus un vadīt plūsmu kaut kur citur. 2017 gadā Michael Nygard izvirzīja Circuit Breaker dizaina modeli programmatūras izstrādes dizaina priekšplānā. Tas tika darīts viņa publikācijā Release It!: Design and Deploy Production Ready Software (Pragmatic Programmers) 1st Edition.


circuit breaker dizaina modelis ir iedvesmots no faktiskajām elektroniskajām un elektriskajām ķēdēm. Tomēr, runājot par vispārīgu koncepciju, ķēdes pārtraucēja ideju 1879 gadā izgudroja Thomas Edison . Tāpat kā tajā laikā, ir jārīkojas ar pārplūdušo strāvu. Ļoti, ļoti vienkārši izsakoties, šajā gadījumā mēs to attiecinām uz programmatūras arhitektūru. Galvenais mērķis ir pārliecināties, ka sistēma ir pietiekami izturīga. To inženieru acīs, kas ir atbildīgi par šī modeļa ieviešanu, patiešām nosaka, cik tam jābūt izturīgam un cik fault-tolerant . Ideja ir tāda, ka noteiktos apstākļos mēs varam nemanāmi novirzīt noteikta pieprasījuma plūsmu uz citu pieejamāku plūsmu aiz tā paša endpoint .


Pieņemsim, ka mēs vēlamies izpildīt pieprasījumu no A līdz B . Ik pa laikam B neizdodas un C vienmēr ir pieejams. Ja B nejauši neizdodas, mēs vēlamies sasniegt C , lai mūsu pakalpojums būtu pilnībā pieejams. Tomēr, lai nosūtītu pieprasījumus atpakaļ uzņēmumam B , mēs vēlamies nodrošināt, lai B atkārtoti neizdodas. Pēc tam mēs varam konfigurēt savu sistēmu, lai veiktu nejaušus pieprasījumus B un pilnībā atgrieztos pie B tikai tad, kad atteices līmenis ir samazinājies par noteiktu līmeni.


Mēs varētu vēlēties iesniegt pieprasījumu C kļūdas, bet arī latentuma dēļ. Ja B darbojas ļoti lēni, mēs, iespējams, vēlēsimies atkārtoti nosūtīt visus pieprasījumus uz C . Ir arī daudzas citas iespējamās konfigurācijas, piemēram, mēģinājums sasniegt C , ja pēc noteikta mēģinājumu skaita, pieprasījumu veidi, vienlaikus pavedieni un daudzas citas iespējas. To sauc arī par short-circuiting , un tā lielākoties ir īslaicīga kustība.

Valsts mašīna


Lai labāk izprastu mūsu zināšanas par to, kas patiesībā ir ķēdes pārtraucējs, mums ir jāsaprot, ka ķēdes pārtraukums mūsu lietojumprogrammā darbojas kā vienība. Slēdžiem ir trīs galvenie statusi. Tas var būt aizvērts, atvērts vai pusatvērts. Statuss aizvērts nozīmē, ka mūsu lietojumprogrammu plūsmas darbojas normāli. Mēs varam droši iesniegt pieprasījumus pakalpojumam A, zinot, ka visi pieprasījumi tiks nosūtīti pakalpojumam B. Atvērts stāvoklis nozīmē, ka visi pieprasījumi pakalpojumam B neizdosies. Noteikumi, ko esam definējuši, lai attēlotu kļūmi, ir notikuši, un mēs vairs nesasniedzam pakalpojumu B. Šajā gadījumā vienmēr tiek atgriezts izņēmums. half-open stāvoklis ir tad, kad mūsu ķēdes pārtraucējam tiek uzdots veikt pakalpojuma B pārbaudes, lai redzētu, vai tas atkal darbojas.


Katrs veiksmīgais pieprasījums tiek apstrādāts kā parasti, taču tas turpinās nosūtīt pieprasījumus C. Ja B darbojas, kā paredzēts saskaņā ar mūsu noteiktajiem verifikācijas noteikumiem, mūsu ķēdes pārtraucējs atgriezīsies slēgtā stāvoklī un pakalpojums A sāks veikt. pieprasa tikai pakalpojumam B. Lielākajā daļā lietojumu ķēdes pārtraucējs atbilst dekoratora dizaina shēmai. Tomēr to var ieviest manuāli, un mēs apskatīsim trīs programmatiskus veidus, kā ieviest ķēdes pārtraucējus un visbeidzot izmantot uz AOP balstītu ieviešanu. Kods ir pieejams vietnē GitHub .

Diagramma

2. Automašīnu pārbaudes

Šī raksta pēdējā punktā mēs apskatīsim automašīnu sacīkšu spēli. Tomēr, pirms mēs tur nonākam, es vēlos jums sniegt norādījumus par dažiem aspektiem, kā izveidot lietojumprogrammu, kas darbojas ar circuit breaker .

2.1. Kystrix (no Parīzes līdz Berlīnei-kystrix-runnable-app)

Kystrixs kā mazs DSL ir pārsteidzoša bibliotēka, ko izgudroja un izveidoja Johan Haleby . Bibliotēka sniedz daudz iespēju, tostarp integrāciju ar Spring un Spring WebFlux. Ir interesanti to apskatīt un mazliet paspēlēties:

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


Esmu izveidojis piemēru, un tas atrodas GitHub modulī no-paris-to-berlin-kystrix-runnable-app . Pirmkārt, mēs aplūkojam kodu:

 @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 kods apzīmē piemēra 2. komandu. Pārbaudiet 1. komandas kodu. Šeit notiek tas, ka mēs definējam vajadzīgo komandu ar monoCommand . Šeit mēs definējam metodi, kas mums jāizsauc. Programmā commandProperties mēs definējam noteikumus, kas liek circuit-breaker mainīt stāvokli uz atvērtu. Mēs nepārprotami kavējam zvanu, lai tas ilgtu precīzi 1 sekundi.


Tajā pašā laikā mēs definējam 5000 milisekundes taimautu. Tas nozīmē, ka mēs nekad nesasniegsim taimautu. Šajā piemērā mēs varam veikt zvanus ar Id . Tā kā šis ir tikai tests, mēs pieņemam, ka Id=1 ir automašīnas, Jaguar, kam nav nepieciešams circuit-breaker Id . Tas nozīmē arī to, ka mēs nekad nesaņemsim Tank1, kā noteikts atkāpšanās metodē. Ja vēl neesat pamanījis, rūpīgi apskatiet atkāpšanās metodi. Šī metode izmanto Observable . Lai gan WebFlux ir ieviests saskaņā ar Observable dizaina modeli, Flux nav gluži novērojams.


Tomēr hystrix atbalsta abus. Lūdzu, palaidiet lietojumprogrammu un atveriet pārlūkprogrammu vietnē http://localhost:8080/cars/2 , lai to apstiprinātu. Ir svarīgi saprast, ka, ja sākat zvanīt ļoti agri, startējot Spring Boot, jūs galu galā varat saņemt Tank1 ziņojumu. Tas ir tāpēc, ka startēšanas aizkave var ļoti viegli pārsniegt 5 sekundes atkarībā no tā, kā jūs veicat šo procesu. Otrajā piemērā mēs īssavienosim savu piemēru ar tvertni 2:

 @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() } }

Šajā piemērā mūsu circuit-breaker kā atbilde pāries atvērtā stāvoklī, atgriešanās tvertni 2. Tas ir tāpēc, ka mēs arī šeit radām 1 s aizkavi, taču mēs norādām, ka mūsu ķēdes pārtraukuma stāvoklis tiek aktivizēts pēc 500 ms atzīmes. Ja jūs zināt, kā hystrix darbojas, jūs atklāsit, ka kystrix nav nekas atšķirīgs, virzoties uz priekšu. Tas, ko hystrix man šajā brīdī nenodrošināja, bija nevainojams, bez piepūles veids, kā nodrošināt to, kas man bija nepieciešams spēles izveidei. Šķiet, ka Kystrix strādā uz klienta pamata. Tas nozīmē, ka mums ir jādeklarē savs kods, pirms pieprasām pakalpojumus, kas ir mūsu galvenā pakalpojuma pamatā.

2.2. Izturība4J

Šķiet, ka Resilience4J daudzi atsaucas uz ļoti pilnīgu ķēdes pārtraucēja ieviešanu. Mani pirmie izmēģinājumi bija daži svarīgi ķēdes pārtraucēju aspekti. Proti, es gribēju redzēt ķēdes pārtraucēju, kas varētu darboties, pamatojoties uz taimautu un veiksmīgu pieprasījumu biežumu. Resilience4J ļauj konfigurēt dažāda veida short-circuiting moduļus. Tie ir sadalīti 6 dažādās kategorijās: CircuitBreaker , Bulkhead , Ratelimiter , Retry un Timelimiter . Tie visi ir arī dizaina modeļu nosaukumi. CircuitBreaker modulis nodrošina pilnīgu šī modeļa ieviešanu.


Mums ir daudz parametru, ko varam konfigurēt, taču būtībā CircuitBreaker modulis ļauj mums konfigurēt to, ko mēs atpazīstam kā kļūdu, cik pieprasījumu mēs pieļaujam pusatvērtā stāvoklī un bīdāmu logu, ko var konfigurēt pēc laika vai count, kur mēs saglabājam slēgtā stāvoklī veikto pieprasījumu skaitu. Tas ir svarīgi, lai aprēķinātu kļūdu biežumu. Būtībā mēs varētu teikt, ka šis CircuitBreaker modulis mums palīdzēs noteikt pieprasījumu ātrumu, taču tas ne vienmēr ir taisnība.


Tas ir atkarīgs no tā, kā jūs to interpretējat. Šķiet, ka tas ir labāks veids, kā to uzskatīt par vienkāršu veidu, kā tikt galā ar kļūmēm. Neatkarīgi no tā, vai tie nāk no taimauta vai izņēmuma, šeit tie tiek apstrādāti un kā pieprasījumus var nemanāmi pāradresēt kaut kur citur. Bulkhead modulis ir paredzēts, lai apstrādātu vienlaikus pieprasījumus. Tas nav ātruma ierobežotājs.


Tā vietā tiek ieviests Bulkhead dizaina modelis, kas tiek izmantots, lai novērstu pārāk daudz apstrādes vienā galapunktā. Šajā gadījumā Bulkhead ļauj mums apstrādāt mūsu pieprasījumus tā, lai tie tiktu sadalīti visos pieejamajos galapunktos. Nosaukums Bulkhead cēlies no dažādiem aizzīmogotiem nodalījumiem, kuros lielam kuģim parasti ir jāizvairās no nogrimšanas, ja notiek negadījums, un, tāpat kā kuģu gadījumā, mums ir jādefinē, cik pavedienu būs pieejams pavedienu baseinā un to nomas laiks. .


RateLimiter modulis ir paredzēts, lai apstrādātu pieprasījumu ātrumu. Atšķirība starp šo un Bulkhead moduli ir būtiska, lai mēs vēlamies būt iecietīgi pret likmēm līdz noteiktam brīdim. Tas nozīmē, ka mums nav jāizraisa neveiksme. Mēs tikai projektā sakām, ka mēs nepieļaujam likmi, kas pārsniedz noteiktu vērtību. Turklāt mēs varam vai nu pāradresēt pieprasījumu, vai paturēt to aizturēt, līdz tiek piešķirta atļauja izpildīt pieprasījumu. Modulis Retry , iespējams, ir visvieglāk saprotams, jo tam nav daudz kopīga ar citiem moduļiem.


Mēs būtībā skaidri deklarējam atkārtotu mēģinājumu skaitu līdz noteiktam beigu punktam, līdz sasniedzam mūsu noteikto slieksni. Timelimiter moduli var uzskatīt par CircuitBreaker moduļa vienkāršojumu, jo tiem abiem ir kopīga iespēja konfigurēt taimautus. Tomēr Timelimiter nav atkarīgs no citiem parametriem, piemēram, bīdāmiem logiem, un tam nav arī iebūvēta atteices sliekšņa aprēķina.


Tātad, ja mūs interesē tikai noildzes apstrāde, zvanot noteiktam pakalpojumam, un mēs neņemam vērā citas iespējamās kļūdas, iespējams, mums ir labāk izmantot Timelimiter .

2.2.1. Resilience4J ar Kotlin and No Spring ietvaru (no Parīzes līdz Berlīnei-resilience4j-runnable-app)

Šajā modulī esmu nolēmis izmantot tikai resilience4j kotlin bibliotēku:

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


Šī ieviešana ir pieejama repo pakalpojumā GitHub. Vispirms apskatīsim TimeLimiter modeli:

 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") }

Šajā gadījumā mēs izrotājam savu function getPrivateCar ar TimeLimiter funkcionalitāti, izmantojot funkciju decorateSuspendFunction . Tas izraisīs taimautu, ja funkcija, kuru mēs izsaucam, aizņem pārāk ilgu laiku, mēs saņemsim Opel Corsa, nevis Lancya. Lai to izmēģinātu, mēs varam vienkārši palaist lietojumprogrammu un atvērt http://localhost:8080/cars/timelimiter/normal/1 .


Aplūkojot ieviešanu, mēs redzam, ka mēs nekad nevarēsim iegūt Lancya . Un tas ir tāpēc, ka mēs apzināti nogaidām 10s , pirms atgriežam to atpakaļ. Mūsu TimeLimiter ir daudz mazāks taimauts, tāpēc tas nekad nedarbosies. TimeLimiter ir diezgan vienkārši saprotams. No otras puses, CircuitBreaker var būt atšķirīgs stāsts. Šis ir piemērs tam, kā to var izdarīt:

 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") }

Šajā gadījumā mēs sakām, ka mēs vēlamies, lai mūsu ķēdes pārtraucējs aizvērtu ķēdi, tiklīdz atteices līmenis ar īpašumu ir mazāks par 20% . Lēniem zvaniem arī būs slieksnis, taču šajā gadījumā tas būs mazāks par 50%. Mēs sakām, ka lēnam zvanam ir jāilgst ilgāk par 1 s, lai tas tiktu uzskatīts par tādu. Mēs arī precizējam, ka pusatvērtā stāvokļa ilgumam jābūt 1 s. Tas praksē nozīmē, ka mums būs atvērts stāvoklis, daļēji atvērts vai slēgts stāvoklis.


Mēs arī sakām, ka mēs pieļaujam ne vairāk kā 500 daļēji atvērtus pieprasījumus. Lai veiktu kļūdu aprēķinus, ķēdes pārtraucējam ir jāzina, pie kuras atzīmes tas to darīs. Tas ir svarīgi, lai noteiktu, kad ķēde ir jāaizver. Mēs sakām, ka šim aprēķinam būs nepieciešami vismaz 2 pieprasījumi ar rekvizītu minimumNumberOfCalls . Atcerieties, ka pusatvērts ir brīdis, kad mēs turpināsim mēģināt panākt, lai ķēde tiktu aizvērta, ja pieprasījumi sasniegs drošu atteices slieksni?


Šajā konfigurācijā tas nozīmē, ka mums ir jāveic vismaz 2 pieprasījumi bīdāmajā logā, lai aprēķinātu kļūdu biežumu un noteiktu, vai atgriezties aizvērtā stāvoklī. Tas ir precīzs visu mūsu konfigurēto mainīgo lielumu nolasījums. Kopumā tas nozīmē, ka mūsu lietojumprogramma, iespējams, veiks vairākus zvanus uz alternatīvo dienestu, ja tāds būs, tas nepārslēgsies no atvērta uz slēgtu stāvokli, ņemot vērā, ka veiksmes rādītājam, lai to izdarītu, ir jābūt 80% pusgadā. ir jābūt atvērtiem stāvokļiem un atvērtā stāvokļa taimautai.


Ir daudz veidu, kā norādīt šādus taimautus. Mūsu piemērā mēs sakām, ka maxDurationInHalfOpenState ir 1 s. Tas nozīmē, ka mūsu CircuitBreaker statuss būs atvērts tikai tad, ja mūsu pārbaude neatbilst slēgtā stāvokļa nosacījumam vai ja šis taimauts vēl nav iestājies. Šajā CircuitBreaker definētajai darbībai var būt grūti sekot un paredzēt tikai tāpēc, ka konkrētus dīkstāves, ātrumus un citas pieprasījumu funkcijas vienkārši nav iespējams precīzi replicēt, taču, ja šim galapunktam izpildīsim vairākus pieprasījumus, mēs redzēsim, ka iepriekš aprakstītā uzvedība atbilst mūsu pieredzei.


Tātad, mēģināsim izpildīt vairākus pieprasījumus galapunktiem: http://localhost:8080/cars/circuit/1 un http://localhost:8080/cars/circuit/2 . Beidzas ar 1 ir veiksmīgas automašīnas izguves beigu punkts, un, kas beidzas ar 2, ir galapunkts, kas beidzas, ja netiek iegūta noteikta automašīna. Aplūkojot kodu, mēs redzam, ka viss, kas nav 2, nozīmē, ka mēs saņemam Lancya kā atbildi. A 2 nozīmē, ka mēs nekavējoties izlaižam darbības laika izņēmumu, kas nozīmē, ka mēs saņemam Opel Corsa kā atbildi.


Ja mēs tikai iesniegsim pieprasījumus galapunktam 1 , mēs turpināsim redzēt Lancya kā atbildi. Ja sistēma sāk kļūdīties, tas ir, kad jūs veicat pieprasījumus 2,\; jūs redzēsiet, ka atgriešanās pie Lancya pēc kāda laika nebūs pastāvīga. System informēs, ka tā ir atvērtā stāvoklī un ka vairs nav atļauti pieprasījumi.

 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


Pēc veiksmīga pieprasījuma mūsu ķēdes pārtraucējs pāries pusatvērtā stāvoklī, un tas nozīmē, ka mums būs jāveic daži pieprasījumi, kas jāatgriež līdz 1, pirms tas normalizējas. Mēs pāris reizes pārslēgsimies no Lancya uz Opel Corsa pirms atkal iegūsim Lancya . Mēs definējām šo skaitli kā 2. Tas ir kļūdu aprēķina minimums. Ja mēs izraisīsim tikai vienu kļūdu un turpinām izsaukt galapunktu, kas nav neveiksmīgs, mēs varam iegūt skaidrāku priekšstatu par notiekošo:

 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

Lai gan šis atvērtā statusa ziņojums ir patiess, tas notika pēc tam, kad es veicu 2 pieprasījumus galapunktam bez kļūmes. Tāpēc valsts esot pusatvērta.

2.2.2. Resilience4J ar Spring Boot un bez AOP (no-paris-to-berlin-resilience4j-spring-app)

Iepriekšējā segmentā mēs redzējām, kā to ieviest ļoti programmatiski, neizmantojot nekādu Spring tehnoloģiju. Mēs izmantojām Spring, bet tikai, lai nodrošinātu WebFlux MVC veida pakalpojumu. Turklāt mēs neko nemainījām attiecībā uz pašiem pakalpojumiem. Šajā lietojumprogrammā mēs izpētīsim šādas bibliotēkas:

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


Aplūkojot, kā kods tiek darīts, mēs varam redzēt diezgan lielu atšķirību:

 @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")) } }

Šajā un turpmākajā piemērā mēs galvenokārt aplūkosim noildzes īpašības. Pašas CircuitBreaker sarežģītības ir mazāk svarīgas, jo šis ir ievadraksts. Šeit ir svarīgi saprast, cik viegli mēs varam to īstenot ar Resilience4J nodrošinātajiem dekoratoriem. Lai gan joprojām ir programmatical , mēs varam viegli izrotāt savu sākotnējo izdevēju, kuru iegūstam no carService.getCar() , ar vēlamajiem short-circuit veidiem.


Šajā piemērā mēs reģistrējam TimeLiter , BulkHead un CircuitBreaker . Visbeidzot, mēs definējam atkāpšanās funkciju, kas jāaktivizē, tiklīdz ir noticis TimeoutException. Mums vēl ir jāredz, kā tas viss ir konfigurēts. Mēs konfigurējam Resilience4J pavasarī tāpat kā jebkuru citu konfigurējamu moduli. Mēs izmantojam 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 fails ir parauga fails, kas ņemts no viņu repo un attiecīgi pārveidots par manu piemēru. Kā mēs redzējām iepriekš, dažādu veidu limiters/short-circuit gadījumiem ir nosaukums. Nosaukums ir ļoti svarīgs, ja jums ir daudz dažādu reģistru un dažādu ierobežotāju. Piemēram, tāpat kā iepriekš minēts, mūs interesē laika ierobežotājs. Mēs redzam, ka tas ir ierobežots līdz 2 s. Ja aplūkojam pakalpojuma ieviešanas veidu, mēs redzam, ka mēs piespiežam notikt taimautu:

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

Sāksim lietojumprogrammu un pārlūkprogrammā dodieties uz: http://localhost:8080/cars/test/2 . Tā vietā, lai iegūtu Fiat , mēs iegūsim Rolls Royce . Šādi mēs definējām taimautu. Tādā pašā veidā mēs varam viegli izveidot CircuitBreaker .

3. Lieta

Līdz šim mēs esam redzējuši trīs būtiskus veidus, kā ieviest CircuitBreakers un saistītos ierobežotājus. Tālāk mēs apskatīsim manu iecienītāko ķēdes pārtraucēju ieviešanas veidu, izmantojot manis izveidoto lietojumprogrammu, kas ir ļoti vienkārša spēle, kurā mēs vienkārši noklikšķiniet uz kvadrātiem, lai nokļūtu no Parīzes uz Berlīni. Spēle ir izveidota, lai saprastu, kā to īstenot. Tajā nav daudz teikts par to, kur to īstenot. Šis ir tikai gadījums, ko esmu izstrādājis, lai dalītos ar jums zinātībā.


Zināšanas par to, kad es to atstāju jums, lai izlemtu vēlāk. Būtībā mēs vēlamies izveidot vairākas automašīnas un izveidot maršrutu uz Berlīni. Dažādās šī maršruta vietās mēs nokļūsim pilsētās, kur nejauši radīsim problēmas. Mūsu circuit breakers izlems, cik daudz laika mums būs jāgaida, pirms varēsim doties tālāk. Pārējām automašīnām nav problēmu, un mums vienkārši jāizvēlas pareizais maršruts.


Mums ir atļauts pārbaudīt kustības grafiku, kurā tiek reģistrēts, kad pilsētā noteiktā minūtē notiks kāda problēma. Minūtes atzīme ir derīga tās 0 indeksētās pozīcijās. Tas nozīmē, ka 2 nozīmē, ka every 2, 12, 22, 32, 42, 52 minūšu atzīme pulkstenī būs derīga, lai radītu šo problēmu. Problēmas būs divu veidu: ERROR un TIMEOUT . Kļūdas kļūme radīs 20 sekunžu aizkavi.


Taimauts nodrošinās 50 sekunžu aizkavi. Katrai pilsētas maiņai ikvienam ir jāgaida 10 sekundes. Tomēr pirms gaidīšanas automašīna jau atrodas pie nākamās pilsētas iebraukšanas, kad tas tiek darīts ar rezerves metodēm. Šajā gadījumā nākamā pilsēta tiek izvēlēta nejauši.

Spēles lapa

4. Īstenošana

Mēs jau iepriekš esam redzējuši, kā konfigurēt mūsu resilience4j reģistru, izmantojot application.yml . Kad tas ir izdarīts, apskatīsim dažus piemērus, kā izrotāt mūsu 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) }


Kā redzam, oriģinālie servisa izsaukumi ir tieši noformēti, izmantojot anotācijas! To var izdarīt tikai tāpēc, ka paketē ir AOP modulis:

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

AOP jeb Aspect Oriented Programming ir vēl viena programmēšanas paradigma, kuras pamatā ir OOP . Tas tiek uzskatīts par OOP papildinājumu, un tas ir tieši tas, cik daudz anotāciju darbojas. Tas ļauj aktivizēt citas funkcijas ap, pirms vai pēc sākotnējās metodes precīzos griezuma punktos. Kā redzams piemērā, mēs vai nu ģenerējam taimautus vai kļūdas. BlockageException tiek ģenerēts arī atkāpšanās metodes ietvaros. Tas nerada problēmu. Izņemot atbildi.


Tomēr lietojumprogramma darbojas WebSockets, un tāpēc šī kļūda lietojumprogrammā nebūs redzama. Līdz šim tā bija spēle. Es to ieviesu, lai parādītu, kā anotāciju izmantošana var ievērojami atvieglot mūsu dzīvi, ieviešot elastīgu lietojumprogrammu. Mums ir ieviesti ne tikai CircuitBreakers, bet arī citas tehnoloģijas, piemēram WebSockets , Spring WebFlux , Docker , NGINX , typescript un vairākas citas. Tas viss tika darīts, lai redzētu, kā CircuitBreakers varētu darboties lietojumprogrammā. Ja vēlaties spēlēt ar šo lietojumprogrammu, lūdzu, dodieties uz projekta sakni un palaidiet:

 make docker-clean-build-start


Pēc tam palaidiet šo komandu:

 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":[]}'


Šī pieprasījuma lietderīgā slodze tiek ģenerēta, izmantojot moduli from-paris-to-berlin-city-generator . Ja ieskatīsieties šajā modulī, jūs redzēsit, ka to ir diezgan vienkārši saprast un ka varat izveidot savu karti spēlei! Visbeidzot, dodieties uz vietni http://localhost:9000, un jūsu lietojumprogrammai vajadzētu darboties! Tagad jums vienkārši jānoklikšķina uz pareizajiem laukumiem, lai spēlētu spēli. Vienkārši neklikšķiniet uz sarkanajiem, ja vēlaties laimēt. Ja tomēr vēlaties redzēt ķēdes pārtraucēju darbībā, lūdzu, palaidiet lietojumprogrammu žurnālus:

 docker logs from_paris_to_berlin_web -f

Un skaidri noklikšķiniet uz sarkanajiem kvadrātiem, lai izraisītu kļūmi.

5. Kā Kystrix un Resilience4J atšķiras

Kystrix ir ideāli piemērots gadījumos, kad jūsu lietojumprogramma ir maza, un jūs vēlaties pārliecināties, ka DSL lietojums ir patiešām zems. Šķiet, ka vienīgais trūkums ir tāds, ka tas nepiedāvā vienkāršu veidu, kā dekorēt metodes, kuras ietekmē ķēdes pārtraucējs. Šķiet, ka Resilience4J ir lieliska iespēja uzņēmuma darbam ar automātiskiem slēdžiem. Tas nodrošina uz anotācijām balstītu programmēšanu, izmanto visas AOP priekšrocības, un tā moduļi ir atdalīti. Savā ziņā to var stratēģiski izmantot arī lietojumprogrammas kritiskajiem punktiem. To var izmantot arī kā pilnīgu ietvaru, kas aptver daudzus lietojumprogrammas aspektus.

6. Secinājums

Neatkarīgi no mūsu izvēlētā zīmola mērķis vienmēr ir nodrošināt elastīgu pielietojumu. Šajā rakstā es parādīju dažus piemērus tam, kā es personīgi pieredzēju automātisko slēdžu izmeklēšanu un savus atklājumus ļoti augstā līmenī. Tas nozīmē, ka šis raksts patiešām ir rakstīts cilvēkiem, kuri vēlas uzzināt, kas ir automātiskie slēdži un ko Limiters var darīt.


Atklāti sakot, iespējas ir bezgalīgas, domājot par mūsu lietojumprogrammu uzlabošanu, izmantojot elastīguma mehānismus, piemēram, circuit breakers . Šis modelis ļauj precizēt lietojumprogrammu, lai labāk izmantotu mūsu rīcībā esošos resursus. Pārsvarā mākonī joprojām ir ļoti svarīgi optimizēt mūsu izmaksas un to, cik daudz resursu mums faktiski ir jāpiešķir.


CircuitBreakers konfigurēšana nav triviāls uzdevums, kā tas ir Limiters, un mums patiešām ir jāsaprot visas konfigurācijas iespējas, lai sasniegtu optimālu veiktspējas un noturības līmeni. Šī iemesla dēļ es nevēlējos iedziļināties šajā ievadrakstā par automātiskiem slēdžiem.


Circuit-breakers var izmantot dažādiem lietojumu veidiem. Lielākajai daļai ziņojumapmaiņas un straumēšanas lietojumprogrammu tas būs nepieciešams. Lietojumprogrammām, kas apstrādā lielus datu apjomus, kuriem arī jābūt ļoti pieejamiem, mēs varam un mums vajadzētu ieviest kādu ķēdes pārtraucēju. Lielajiem tiešsaistes mazumtirdzniecības veikaliem katru dienu ir jāapstrādā milzīgs datu apjoms, un agrāk Hystrix tika plaši izmantots. Pašlaik šķiet, ka mēs virzāmies Resilience4J virzienā, kas ietver daudz vairāk.

6. Atsauces

Paldies!

Ceru, ka jums patika šis raksts tikpat ļoti kā man tā tapšana! Lūdzu, atstājiet atsauksmi, komentārus vai jebkādas atsauksmes, ko vēlaties sniegt par kādu no sociālajiem tīkliem, kas atrodas tālāk esošajās saitēs. Esmu ļoti pateicīgs, ja vēlaties man palīdzēt uzlabot šo rakstu. Esmu ievietojis visu šīs lietojumprogrammas pirmkodu vietnē GitHub. Paldies, ka izlasījāt!

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

PAKARINĀT TAGUS

ŠIS RAKSTS TIKS PĀRSTRĀDĀTS...