Circuit-breakers
משמשים בימינו כדי למנוע כמות מוגזמת של בקשות לשירות כזה או אחר שאינו מגיב. כאשר, למשל, שירות נכבה, מכל סיבה שהיא, מפסק צריך, כפי ששמו מציין, לשבור את המעגל. במילים אחרות, במצב בו נעשות מיליון בקשות בו-זמנית כדי לקבל תוצאות של מרוץ סוסים, אנו רוצים שהבקשות הללו יופנו לשירות אחר שיכול להתמודד עם זה.
שירות אחר זה יכול להיות העתק שלו, או שהוא יכול לשמש אך ורק לביצוע פעולות אחרות לגבי כשל השירות המקורי. המטרה הסופית היא תמיד לשבור שיחות מיותרות ולהוביל את הזרימה למקום אחר. בשנת 2017
, Michael Nygard
הביא את דפוס העיצוב Circuit Breaker לחזית עיצוב פיתוח התוכנה. זה נעשה בפרסום שלו Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers) Edition 1st Edition.
דפוס העיצוב circuit breaker
הוא בהשראת מעגלים אלקטרוניים וחשמליים בפועל. עם זאת, במונחים של מושג כללי, רעיון מפסק החשמל הומצא למעשה בשנת 1879
על ידי Thomas Edison
. בדיוק כמו בתקופה ההיא, צריך לטפל בזרם שעולה על גדותיו. במונחים מאוד מאוד פשוטים, זה מה שאנחנו מיישמים על ארכיטקטורת תוכנה במקרה זה. המטרה העיקרית היא לוודא שהמערכת עמידה מספיק. עד כמה היא חייבת להיות עמידה ועד כמה היא חייבת להיות fault-tolerant
היא באמת בעיני המהנדסים האחראים להקל על יישום הדפוס הזה. הרעיון מאחורי זה הוא שבתנאים מסוימים אולי נרצה לנתב בצורה חלקה את הזרימה של בקשה מסוימת לזרימה אחרת זמינה יותר מאחורי אותה endpoint
.
נניח שאנו רוצים לבצע בקשה מ- A
עד B
מעת לעת, B נכשל ו- C
תמיד זמין. אם B
נכשל באופן אקראי, אנו רוצים להגיע ל- C
כדי להפוך את השירות שלנו לזמין במלואו. עם זאת, כדי להחזיר בקשות ל- B
, אנו רוצים לוודא ש- B
לא ייכשל שוב כל כך. לאחר מכן נוכל להגדיר את המערכת שלנו לבצע בקשות אקראיות ל-B ולחזור במלואו ל- B
רק לאחר ששיעור הכשל ירד לרמה מסוימת.
אולי נרצה להגיש בקשה C על שגיאה אבל גם על חביון. אם B
איטי מאוד, ייתכן שנרצה לשלוח מחדש את כל הבקשות ל- C
. ישנן תצורות אפשריות רבות אחרות, כמו ניסיון להגיע ל- C
אם לאחר מספר מוגדר של ניסיונות, סוגי בקשות, שרשורים בו-זמניים ואפשרויות רבות אחרות. זה ידוע גם short-circuiting
, וזה בעיקר מהלך זמני.
על מנת להבין יותר את הידע שלנו על מהו בעצם מפסק, עלינו להבין שהפסק פועל כישות באפליקציה שלנו. ישנם שלושה סטטוסים עיקריים למפסק. זה יכול להיות סגור, פתוח או חצי פתוח. מצב סגור פירושו שזרימות האפליקציות שלנו פועלות כרגיל. אנו יכולים להגיש בבטחה בקשות לשירות א', בידיעה שכל הבקשות יעברו לשירות ב'. מצב פתוח פירושו שכל הבקשות לשירות ב' ייכשלו. הכללים שהגדרנו לייצג כשל התרחשו וכבר איננו מגיעים לשירות B. במקרה זה, תמיד מוחזר חריג. מצב half-open
הוא כאשר המפסק שלנו מקבל הוראה לבצע בדיקות בשירות B כדי לראות אם הוא פעיל שוב.
כל בקשה מוצלחת מטופלת כרגיל, אך היא תמשיך להגיש בקשות ל-C. אם B יתנהג כמצופה על פי כללי האימות שקבענו, המפסק שלנו יחזור למצב סגור, ושירות A יתחיל לבצע מבקש בלעדי לשירות B. ברוב היישומים, מפסק עוקב אחר דפוס העיצוב של הדקורטור. עם זאת, ניתן ליישם אותו באופן ידני, ונבחן שלוש דרכים פרוגרמטיות להטמעת מפסקים ולבסוף שימוש ביישום מבוסס AOP. הקוד זמין ב- GitHub .
בנקודה האחרונה של מאמר זה, נסתכל על משחק מירוץ מכוניות. עם זאת, לפני שנגיע לשם, ברצוני להדריך אותך בכמה מההיבטים של בניית אפליקציה הפועלת עם circuit breaker
.
Kystrixs
, כ- DSL
קטן, היא ספרייה מדהימה שהומצאה ויצרה על ידי Johan Haleby
. הספרייה מספקת אפשרויות רבות, כולל אינטגרציה עם Spring ו-Spring WebFlux. מעניין להסתכל על זה ולשחק קצת:
<dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-core</artifactId> </dependency> <dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-spring</artifactId> </dependency>
יצרתי דוגמה, והיא ממוקמת במודול מ-פריז-לברלין-kystrix-runnable-app ב- GitHub . ראשית, נסתכל על הקוד:
@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() } }
קוד זה מייצג פקודה 2 של הדוגמה. בדוק את הקוד של פקודה 1. מה שקורה כאן הוא שאנחנו מגדירים את הפקודה שאנחנו רוצים עם monoCommand
. כאן, אנו מגדירים את השיטה שאנו צריכים לקרוא. ב- commandProperties
, אנו מגדירים את הכללים שגורמים circuit-breaker
לשנות מצב להיפתח. אנו מעכבים במפורש את השיחה שלנו כדי שהיא תימשך שנייה אחת בדיוק.
במקביל, אנו מגדירים פסק זמן של 5000
אלפיות השנייה. זה אומר שלעולם לא נגיע לפסק זמן. בדוגמה זו, נוכל לבצע שיחות עם Id
. מכיוון שזהו רק מבחן, אנו מניחים Id=1
הוא Id
של מכונית, יגואר ללא צורך circuit-breaker
. זה גם אומר שלעולם לא נקבל את Tank1 כפי שמוגדר בשיטת ה-fallback. אם עדיין לא שמת לב, תסתכל מקרוב על שיטת ה-fallback. שיטה זו משתמשת ב- Observable
. למרות ש- WebFlux
מיושם על פי דפוס העיצוב Observable
, Flux
הוא לא בדיוק Observable.
עם זאת, Hystrix תומך בשניהם. אנא הפעל את היישום ופתח את הדפדפן שלך בכתובת http://localhost:8080/cars/2 , כדי לאשר זאת. חשוב להבין שאם תתחיל לבצע שיחות מוקדם מאוד בהפעלה של Spring Boot, ייתכן שבסופו של דבר תקבל הודעת Tank1. הסיבה לכך היא שעיכוב האתחול יכול לעלות על 5 שניות בקלות רבה, תלוי איך אתה מפעיל את התהליך הזה. בדוגמה השנייה, אנחנו הולכים לקצר את הדוגמה שלנו לטנק 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() } }
בדוגמה זו, circuit-breaker
שלנו יכנס למצב פתוח קצה החזר טנק 2 כתגובה. הסיבה לכך היא שאנו גורמים כאן גם לעיכוב של 1 שניות, אך אנו מציינים שמצב הפסקת החשמל שלנו מופעל לאחר סימון ה-500ms. אם אתה יודע איך hystrix
עובדת, תגלה kystrix
לא שונה מהתקדם. מה שהיסטריקס לא סיפקה לי בשלב זה, הייתה דרך חלקה וחסרת מאמץ לספק את מה שאני צריך כדי להפוך את המשחק. נראה כי Kystrix
עובדת על בסיס לקוח. משמעות הדבר היא שעלינו להצהיר על הקוד שלנו לפני ביצוע בקשות לשירותים מאחורי השירות הראשי שלנו.
נראה שרבים מתייחסים Resilience4J
כיישום שלם מאוד של מפסק זרם. הניסויים הראשונים שלי נסעו על חקר כמה היבטים חשובים של מפסקים. כלומר, רציתי לראות מפסק שיוכל לעבוד על בסיס פסקי זמן ותדירות של בקשות מוצלחות. Resilience4J
מאפשר להגדיר סוגים שונים של short-circuiting
. אלה מופרדים ל 6
קטגוריות שונות: CircuitBreaker
, Bulkhead
, Ratelimiter
, Retry
ומגביל Timelimiter
. כל אלה הם גם שמות של דפוסי עיצוב. מודול CircuitBreaker
מספק יישום מלא של דפוס זה.
יש לנו הרבה פרמטרים שאנחנו יכולים להגדיר, אבל בעצם, מודול CircuitBreaker
מאפשר לנו להגדיר מה אנחנו מזהים ככשל, כמה בקשות אנחנו מאפשרים במצב חצי פתוח וחלון הזזה, שניתן להגדיר לפי זמן או count, שבו אנו שומרים את ספירת הבקשות המתרחשות במצב סגור. זה חשוב כדי לחשב את תדירות השגיאה. בעיקרו של דבר, אנחנו יכולים לומר שמודול CircuitBreaker
זה יעזור לנו עם קצב הבקשות, אבל זה לא בהכרח נכון.
זה תלוי איך אתה מפרש את זה. נראה שזו דרך טובה יותר לחשוב על זה כעל דרך להתמודדות עם תקלות. בין אם הם מגיעים מפסק זמן או חריג, זה המקום שבו הם מטופלים וכיצד ניתן להפנות את הבקשות בצורה חלקה למקום אחר. מודול Bulkhead
נועד להתמודד עם בקשות במקביל. זה לא מגביל שיעור.
במקום זאת, הוא מיישם את דפוס העיצוב של Bulkhead
, המשמש למניעת התרחשות של עיבוד רב מדי בנקודת קצה אחת. במקרה זה, Bulkhead
מאפשרת לנו לעבד את הבקשות שלנו באופן שהן מופצות בכל נקודות הקצה הזמינות. השם Bulkhead
מגיע מהתאים האטומים השונים שספינה גדולה צריכה בדרך כלל להימנע מהטבעה, במקרה של תאונה, וכמו במקרה של ספינות, עלינו להגדיר כמה חוטים יהיו זמינים במאגר החוטים ואת זמן החכירה שלהם .
מודול RateLimiter
נועד לטפל בקצב הבקשות. ההבדל בין זה לבין מודול Bulkhead
הוא חיוני כדי שנרצה להיות סובלניים לתעריפים עד לנקודה מסוימת. זה אומר שאנחנו לא צריכים לגרום לכישלון בשביל זה. אנחנו רק אומרים, בעיצוב שאנחנו לא סובלים שיעור מעל ערך מסוים. בנוסף, אנו יכולים להפנות בקשה או להשאיר אותה בהמתנה עד למתן הרשאה לבצע את הבקשה. המודול Retry
הוא כנראה הקל ביותר להבנה מכיוון שאין לו הרבה במשותף עם המודולים האחרים.
בעצם אנו מצהירים במפורש על מספר הניסיונות החוזרים לנקודת קצה מסוימת, עד שנגיע לסף המוגדר שלנו. ניתן לראות את מודול Timelimiter
כפישוט של מודול CircuitBreaker
בכך ששניהם חולקים את האפשרות להגדיר פסקי זמן. עם זאת, Timelimiter
אינו תלוי בפרמטרים אחרים כמו חלונות הזזה, ואין לו גם חישוב סף כשל מובנה.
לכן, אם אנו מעוניינים אך ורק בטיפול בתקופות זמן קצובות בעת קריאה לשירות מסוים, ואיננו מביאים בחשבון תקלות אפשריות אחרות, אז כנראה שעדיף לנו עם Timelimiter
.
במודול זה, החלטתי להשתמש רק בספריית resilience4j
kotlin:
<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>
יישום זה זמין ב-repo ב- GitHub. תחילה נסקור את דפוס TimeLimiter
:
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") }
במקרה זה, אנו מקשטים את function
getPrivateCar
שלנו בפונקציונליות TimeLimiter
באמצעות הפונקציה decorateSuspendFunction
. מה שזה יגרום לפסק זמן, אם הפונקציה שאנו קוראים לה לוקח יותר מדי זמן נקבל אופל קורסה במקום לאנסיה. כדי לנסות זאת, נוכל פשוט להפעיל את האפליקציה ולפתוח את http://localhost:8080/cars/timelimiter/normal/1 .
כשבוחנים את היישום, אנו רואים שלעולם לא נוכל להשיג Lancya
. וזה בגלל שאנחנו בכוונה מחכים 10s
לפני שנחזיר אותו בחזרה. TimeLimiter
שלנו יש פסק זמן נמוך בהרבה כך שזה לעולם לא יעבוד. TimeLimiter
הוא די פשוט להבנה. CircuitBreaker
, לעומת זאת, יכול להיות סיפור אחר. זו דוגמה כיצד ניתן לעשות זאת:
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") }
במקרה זה, אנו אומרים שאנו רוצים שהמפסק שלנו יסגור את המעגל ברגע ששיעור הכשלים נמוך מ 20%
בנכס. גם לשיחות איטיות יהיה סף, אבל במקרה זה, הוא יהיה פחות מ-50%. אנו אומרים ששיחה איטית חייבת להימשך יותר מ-1 שניות כדי להיחשב כזו. אנו גם מציינים שמשך מצב חצי פתוח צריך להיות 1 שניות. זה אומר, בפועל, שיהיה לנו מצב פתוח, מצב פתוח למחצה או מצב סגור.
אנחנו גם אומרים שאנחנו מאפשרים לכל היותר 500 בקשות מדינה פתוחות למחצה. עבור חישובי שגיאה, המפסק צריך לדעת באיזה סימן הוא יעשה זאת. זה חשוב כדי לקבוע מתי לסגור את המעגל. אנו אומרים ש-2 בקשות יהיו המינימליות הכרחיות לחישוב זה, עם המאפיין minimumNumberOfCalls
. זוכרים שחצי פתוח זה הזמן שבו נמשיך לנסות לגרום למעגל להיסגר אם הבקשות יגיעו לסף כשל בטוח?
בתצורה זו, זה אומר שעלינו לבצע לפחות 2
בקשות, בתוך חלון ההזזה, כדי לחשב את תדירות השגיאה ולקבוע אם לחזור למצב סגור או לא. זוהי הקריאה המדויקת של כל המשתנים שקבענו. באופן כללי, המשמעות היא שהאפליקציה שלנו כנראה תבצע מספר שיחות לשירות האלטרנטיבי, אם תהיה כזו, היא לא תעבור בקלות רבה ממצב פתוח לסגור בהתחשב בכך ששיעור ההצלחה לעשות זאת חייב להיות 80% במהלך חצי -מצבים פתוחים וזמן הקצוב עבור המצב הפתוח חייבים להתרחש.
ישנן דרכים רבות לציין פסקי זמן כאלה. בדוגמה שלנו, אנו אומרים שה- maxDurationInHalfOpenState
הוא 1s. המשמעות היא שה- CircuitBreaker
שלנו ישמור על סטטוס פתוח, רק אם הבדיקה שלנו אינה עומדת בתנאי המצב הסגור או אם פסק הזמן הזה עדיין לא התרחש. ההתנהגות המוגדרת ב- CircuitBreaker
זה עשויה להיות קשה לעקוב ולחזות, אך ורק בגלל שזמני השבתה, תעריפים ותכונות אחרות של בקשות פשוט לא ניתן לשכפל בדיוק, אבל אם נבצע מספר בקשות לנקודת קצה זו, נראה כי ההתנהגות המתוארת לעיל תואמת את הניסיון שלנו.
אז בואו ננסה לבצע מספר בקשות לנקודות קצה: http://localhost:8080/cars/circuit/1 ו- http://localhost:8080/cars/circuit/2 . הסיום ב-1 הוא נקודת הסיום של שליפת מכונית מוצלחת, והסיום ב-2 הוא נקודת הסיום של כשל בהשגת מכונית מוגדרת. בהסתכלות על הקוד, אנו רואים שכל דבר מלבד 2 אומר שאנו מקבלים Lancya
כתגובה. A 2
, אומר שאנחנו מיד זורקים חריג זמן ריצה, מה שאומר שבסופו של דבר אנחנו מקבלים Opel Corsa
כתגובה.
אם רק נגיש בקשות לנקודת קצה 1
, נמשיך לראות Lancya
כתגובה. אם המערכת מתחילה להיכשל, זה כאשר אתה מגיש בקשות ל-2,\; אתה תראה שהחזרה Lancya
לא תהיה קבועה לאחר זמן מה. System
תודיע שהיא במצב פתוח ושלא מותרות בקשות נוספות.
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
המפסק שלנו יעבור למצב חצי פתוח לאחר בקשה מוצלחת, וזה אומר שנצטרך לבצע כמה בקשות חזרה ל-1 לפני שהוא יתנרמל. נעבור Lancya
Opel Corsa
כמה פעמים לפני שנקבל שוב Lancya
. הגדרנו את המספר הזה כ-2. זהו המינימום לחישוב שגיאה. אם נגרום רק לכשל אחד ונמשיך להתקשר לנקודת הקצה הלא נכשלת, נוכל לקבל תמונה ברורה יותר של מה שקורה:
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
הודעת הסטטוס הפתוח הזו, למרות שהיא אמיתית, קרתה לאחר ששלחתי 2 בקשות לנקודת הקצה שאינה נכשלת. זו הסיבה שאומרים שהמדינה חצי פתוחה.
בקטע הקודם ראינו איך מיישמים את זה בצורה מאוד פרוגרמטית, ללא שימוש בשום טכנולוגיית Spring. השתמשנו ב-Spring אבל רק כדי לספק שירות מסוג WebFlux
MVC
. יתר על כן, לא שינינו דבר לגבי השירותים עצמם. ביישום הבא, נסקור את הספריות הבאות:
<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>
כאשר בוחנים כיצד מתבצע הקוד, אנו יכולים לראות הבדל גדול למדי:
@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")) } }
בדוגמה זו ובדוגמה הבאה, אנו הולכים להסתכל בעיקר על מאפייני הזמן הקצוב. המורכבויות של CircuitBreaker
עצמם פחות רלוונטיות כי זהו מאמר מבוא. מה שחשוב כאן להבין הוא באיזו קלות נוכל ליישם זאת עם המעצבים שסופקו עבור Spring על ידי Resilience4J
. למרות שעדיין בצורה programmatical
, אנו יכולים בקלות לקשט את המפרסם הראשוני שלנו, זה שאנו מקבלים מ- carService.getCar()
, עם סוגי short-circuit
שאנו רוצים.
בדוגמה זו, אנו רושמים TimeLiter
, BulkHead
ו- CircuitBreaker
. לבסוף, אנו מגדירים את פונקציית ה-fallback שתופעל ברגע שהתרחשה חריגה של Timeout. מה שאנחנו עדיין צריכים לראות זה איך כל זה מוגדר. אנו מגדירים את Resilience4J באביב בדיוק כמו כל מודול אחר שניתן להגדרה. אנו משתמשים 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
הקובץ הזה הוא קובץ לדוגמה שנלקח מהריפו שלהם ושונה לדוגמא שלי בהתאם. כפי שראינו בעבר, למקרים של הסוגים השונים של limiters/short-circuit
יש שם. השם חשוב מאוד אם יש לך רישומים רבים ומגבילים שונים. לדוגמא שלנו, ובדיוק כמו שהוזכר קודם, אנו מעוניינים במגביל הזמן. אנחנו יכולים לראות שזה מוגבל ל-2 שניות. אם אנחנו מסתכלים על הדרך שבה אנחנו מיישמים את השירות, אנחנו רואים שאנחנו מאלצים להתרחש פסק זמן:
@Component open class CarService { open fun getCar(): Mono<Car> { return Mono.just(Car("Fiat")).delayElement(Duration.ofSeconds(10)); } }
בואו נתחיל את האפליקציה, ובדפדפן, עבור אל: http://localhost:8080/cars/test/2 . במקום לקבל Fiat
נקבל Rolls Royce
. כך הגדרנו את פסק הזמן. באותו אופן, אנו יכולים ליצור בקלות CircuitBreaker
.
עד עכשיו ראינו שלוש דרכים חיוניות ליישם CircuitBreakers
ומגבילים קשורים. בהמשך, נסתכל על הדרך האהובה עלי ליישם מפסקים על ידי מעבר על אפליקציה שהכנתי, שהיא משחק פשוט מאוד שבו אנחנו פשוט לוחצים על ריבועים כדי להגיע מפריז לברלין. המשחק נוצר כדי להבין איך ליישם. זה לא אומר הרבה על היכן ליישם את זה. זה רק מקרה שתכננתי כדי לחלוק איתך את הידע.
לדעת מתי אני משאיר לך להחליט מאוחר יותר. בעיקרו של דבר, אנחנו רוצים ליצור מספר מכוניות ולהקים נתיב לברלין. במקומות שונים במסלול זה, נגיע לערים שבהן ניצור בעיות באופן אקראי. circuit breakers
שלנו יחליטו כמה זמן נצטרך לחכות לפני שנאפשר לנו להמשיך הלאה. למכוניות האחרות אין בעיה, ואנחנו רק צריכים לבחור את המסלול הנכון.
מותר לנו לבדוק לוח זמנים שבו נרשם מתי תתרחש בעיה מסוימת בעיר בסימון דקה מסוים. סימן הדקה תקף ב-0 מיקומי האינדקס שלו. זה אומר ש-2 אומר every
סימן של 2, 12, 22, 32, 42, 52 דקות בשעון יהיה תקף ליצירת בעיה זו. הבעיות יהיו משני סוגים: ERROR
ו- TIMEOUT
. כשל בשגיאה ייתן לך עיכוב של 20 שניות.
פסק זמן ייתן לך עיכוב של 50 שניות. על כל שינוי עיר, כולם צריכים לחכות 10 שניות. אולם לפני ההמתנה, המכונית כבר נמצאת בכניסה לעיר הבאה כאשר הדבר נעשה בשיטות החלפה. במקרה זה, העיר הבאה נבחרת באופן אקראי.
ראינו בעבר כיצד להגדיר את הרישום resilience4j
שלנו באמצעות application.yml
. לאחר שעשינו זאת, בואו נסתכל על כמה דוגמאות כיצד לקשט את הפונקציות שלנו:
@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) }
כפי שאנו יכולים לראות, קריאות השירות המקוריות מעוטרות ישירות באמצעות הערות! זה יכול להיעשות רק עקב נוכחות של מודול AOP
בחבילה:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
AOP
, או Aspect Oriented Programming
היא פרדיגמת תכנות נוספת המבוססת על OOP
. זה נחשב כהשלמה ל- OOP
, וזה בדיוק כמה הערות עובדות. זה מאפשר להפעיל פונקציות אחרות סביב, לפני או אחרי השיטה המקורית בנקודות חיתוך מדויקות. כפי שניתן לראות מהדוגמה, אנו מייצרים פסקי זמן או שגיאות. ה- BlockageException
נוצר גם בשיטת ה-fallback. זה לא מייצג בעיה. חוץ מהתגובה.
עם זאת, האפליקציה פועלת על WebSockets,
ולכן שגיאה זו לא תופיע באפליקציה. עד כה, זה היה המשחק. יישמתי את זה מתוך דגש להראות כיצד שימוש בהערות יכול להפוך את חיינו לקלים הרבה יותר בעת יישום יישום גמיש. לא רק שהטמינו את CircuitBreakers, אלא גם טכנולוגיות אחרות, כמו WebSockets
, Spring WebFlux
, Docker
, NGINX
, typescript
ועוד כמה. כל זה נעשה כדי לראות כיצד CircuitBreakers יפעלו באפליקציה. אם ברצונך לשחק עם היישום הזה, אנא עבור לשורש הפרויקט והפעל:
make docker-clean-build-start
לאחר מכן הפעל את הפקודה הזו:
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":[]}'
המטען של בקשה זו נוצר באמצעות המודול from-paris-to-berlin-city-generator
. אם תסתכל על המודול הזה, תראה שהוא די פשוט להבנה ושאתה יכול ליצור מפה משלך עבור המשחק! לבסוף, עבור אל http://localhost:9000 והאפליקציה שלך אמורה לפעול! כעת, עליך פשוט ללחוץ על הריבועים הנכונים כדי לשחק את המשחק. רק אל תלחץ על האדומים אם אתה רוצה לזכות. אם אתה רוצה לראות את מפסק החשמל בפעולה, אנא הפעל את יומני היישום:
docker logs from_paris_to_berlin_web -f
ולחצו במפורש על הריבועים האדומים על מנת לגרום לכשל.
Kystrix
אידיאלית במקרים שבהם האפליקציה שלך קטנה, ואתה רוצה לוודא שהשימוש ב-DSL נמוך באמת. החיסרון היחיד שנראה הוא שהוא לא מציע דרך קלה לקשט שיטות שיושפעו ממפסק. נראה כי Resilience4J
היא אופציה מצוינת לעבודה ארגונית עם מפסקים. הוא אכן מספק תכנות מבוסס הערות, משתמש בכל היתרונות של AOP, והמודולים שלו מופרדים. במובן מסוים, זה יכול לשמש גם אסטרטגית עבור נקודות קריטיות באפליקציה. זה יכול לשמש גם כמסגרת שלמה לכיסוי היבטים רבים של יישום.
ללא קשר למותג שאנו בוחרים, המטרה היא תמיד לקבל יישום גמיש. במאמר זה, הראיתי כמה דוגמאות כיצד חוויתי באופן אישי את חקירת מפסקי החשמל ואת הממצאים שלי ברמה גבוהה מאוד. זה אומר שהמאמר הזה באמת נכתב עבור אנשים שרוצים לדעת מה הם מפסקי זרם ומהם Limiters יכולים לעשות.
האפשרויות הן למען האמת אינסופיות כשחושבים על שיפור היישומים שלנו עם מנגנוני גמישות כמו circuit breakers
. דפוס זה אכן מאפשר כוונון עדין של אפליקציה על מנת לנצל טוב יותר את המשאבים הזמינים שיש לנו. בעיקר בענן, עדיין חשוב מאוד לייעל את העלויות שלנו, וכמה משאבים אנחנו באמת צריכים להקצות.
הגדרת CircuitBreakers
היא לא משימה טריוויאלית כפי שהיא עבור ה-Limiters, ואנו באמת צריכים להבין את כל אפשרויות התצורה כדי להגיע לרמות אופטימליות של ביצועים וחוסן. זו הסיבה שלא רציתי להיכנס לפרטים במאמר המבוא הזה על מפסקי זרם.
ניתן ליישם Circuit-breakers
בהרבה סוגים שונים של יישומים. רוב סוגי יישומי העברת ההודעות והסטרימינג יזדקקו לכך. עבור יישומים שמטפלים בכמויות גדולות של נתונים שצריכים להיות גם זמינים מאוד, אנחנו יכולים וצריכים ליישם צורה כלשהי של מפסק החשמל. חנויות קמעונאיות מקוונות גדולות צריכות להתמודד עם כמויות אדירות של נתונים על בסיס יומי, ובעבר נעשה שימוש נרחב Hystrix
. נכון לעכשיו, נראה שאנחנו מתקדמים לכיוון של Resilience4J
שמקיף הרבה יותר מזה.
אני מקווה שנהניתם מהמאמר הזה כמו שאני עשיתי אותו! אנא השאירו ביקורת, הערות או כל משוב שתרצו לתת על כל אחד מהחברתיים בקישורים למטה. אני מאוד אסיר תודה אם אתה רוצה לעזור לי לשפר את המאמר הזה. שמתי את כל קוד המקור של האפליקציה הזו ב- GitHub. תודה שקראת!