מערכות תגובתיות בג'אווה

1. הקדמה

במדריך זה נבין את יסודות יצירת מערכות תגובתיות בג'אווה באמצעות Spring וכלים ומסגרות אחרים.

בתהליך נדון כיצד תכנות תגובתי הוא רק מניע ליצירת מערכת תגובית. זה יעזור לנו להבין את הרציונל ליצירת מערכות תגובתיות ומפרטים שונים, ספריות וסטנדרטים ששימשו בהשראת הדרך.

2. מהן מערכות תגובתיות?

במהלך העשורים האחרונים, נוף הטכנולוגיה ראה מספר שיבושים שהובילו לשינוי מוחלט באופן בו אנו רואים ערך בטכנולוגיה. עולם המחשוב לפני האינטרנט מעולם לא יכול היה לדמיין את הדרכים והאמצעים שבהם הוא ישנה את ימינו הנוכחיים.

עם הגעת האינטרנט להמונים והחוויה המתפתחת כל העת, אדריכלי יישומים צריכים להיות על האצבעות כדי לענות על דרישתם.

ביסודו של דבר, פירוש הדבר שלעולם לא נוכל לעצב יישום באופן שקודם לכן. א יישום רספונסיבי מאוד אינו עוד מותרות אלא הכרח.

גם זה מול כשלים אקראיים ועומס בלתי צפוי. הצורך של השעה הוא לא רק להשיג את התוצאה הנכונה אלא להשיג אותה במהירות! זה די חשוב להניע את חוויות המשתמש המדהימות שאנחנו מבטיחים לספק.

זה מה שיוצר את הצורך בסגנון אדריכלי שיכול לתת לנו מערכות תגובתיות.

2.1. מניפסט תגובתי

עוד בשנת 2013, צוות מפתחים, בראשותו של ג'ונאס בונר, התכנס להגדרת מכלול עקרונות הליבה במסמך המכונה מניפסט תגובתי. זה מה שהניח את הבסיס לסגנון אדריכלות ליצירת מערכות תגובתיות. מאז המניפסט הזה זכה להתעניינות רבה מצד קהילת המפתחים.

בעיקרון, מסמך זה קובע המתכון של מערכת תגובית להיות גמישה, מצמידים באופן רופף וניתנת להרחבה. זה הופך מערכות כאלה לקלות לפיתוח, סובלניות לכשלים, והכי חשוב בתגובה רבה, לתשתית לחוויות משתמש מדהימות.

אז מהו המתכון הסודי הזה? ובכן, זה כמעט לא סוד! המניפסט מגדיר את המאפיינים הבסיסיים או העקרונות של מערכת תגובית:

  • מגיב: מערכת תגובית אמורה לספק זמן תגובה מהיר ועקבי ומכאן איכות שירות עקבית
  • מִתאוֹשֵׁשׁ מַהֵר: מערכת תגובית צריכה להישאר בתגובה במקרה של כשלים אקראיים באמצעות שכפול ובידוד
  • אֵלַסטִי: מערכת כזו צריכה להישאר בתגובה תחת עומסי עבודה בלתי צפויים באמצעות מדרגיות חסכונית
  • מונע הודעות: עליו להסתמך על הודעה אסינכרונית המועברת בין רכיבי המערכת

עקרונות אלה נשמעים פשוטים והגיוניים, אך לא תמיד קלים יותר ליישמם בארכיטקטורה ארגונית מורכבת. במדריך זה נפתח מערכת דוגמאות בג'אווה עם עקרונות אלה בראש!

3. מהי תכנות תגובתי?

לפני שנמשיך, חשוב להבין את ההבדל בין תכנות תגובתי למערכות תגובתיות. אנו משתמשים בשני המונחים הללו לעתים קרובות למדי ובקלות לא מבינים אחד את השני. כפי שראינו קודם, מערכות תגובתיות הן תוצאה של סגנון אדריכלי ספציפי.

בניגוד, תכנות תגובתי הוא פרדיגמת תכנות שבה מתמקדים בפיתוח רכיבים אסינכרוניים ולא חוסמים. הליבה של תכנות תגובתי היא זרם נתונים שנוכל להתבונן בו ולהגיב אליו, אפילו להפעיל לחץ אחורי גם כן. זה מוביל לביצוע שאינו חוסם ומכאן להרחבה טובה יותר עם פחות שרשורי ביצוע.

עכשיו, זה לא אומר שמערכות תגובתיות ותכנות תגובתי אינן בלעדיות זו לזו. למעשה, תכנות תגובתי הוא צעד חשוב לקראת מימוש מערכת תגובית, אך זה לא הכל!

3.1. זרמים ריאקטיביים

זרמים ריאקטיביים היא יוזמה קהילתית שהחלה עוד בשנת 2013 עד לספק תקן לעיבוד זרם אסינכרוני עם לחץ אחורי לא חוסם. המטרה כאן הייתה להגדיר מערך ממשקים, שיטות ופרוטוקולים שיכולים לתאר את הפעולות והישויות הדרושות.

מאז הופיעו כמה יישומים במספר שפות תכנות התואמות את מפרט הזרמים התגובתיים. אלה כוללים את Akka Streams, Ratpack ו- Vert.x עד כמה שם.

3.2. ספריות ריאקטיביות עבור Java

אחת המטרות הראשוניות מאחורי הזרמים התגובתיים הייתה לכלול בסופו של דבר כספרייה רגילה של Java. כתוצאה מכך, מפרט הזרמים התגובתיים שווה ערך סמנטי לספריית Java Flow, שהוצגה ב- Java 9.

מלבד זאת, יש כמה אפשרויות פופולריות ליישם תכנות תגובתי בג'אווה:

  • הרחבות תגובתיות: הידועות בכינויו ReactiveX, הן מספקות API לתכנות אסינכרוני עם זרמים נצפים. אלה זמינים עבור שפות תכנות ופלטפורמות מרובות, כולל Java שם היא מכונה RxJava
  • כור הפרויקט: זוהי ספרייה תגובתית נוספת, המבוססת על מפרט הזרמים התגובתיים, המיועדת לבניית יישומים לא מקוונים ב- JVM. זה במקרה גם היסוד של הערימה התגובתית במערכת האקולוגית האביבית

4. יישום פשוט

לצורך מדריך זה, נפתח יישום פשוט המבוסס על ארכיטקטורת מיקרו-שירותים עם חזית מינימאלית. בארכיטקטורת היישומים צריכים להיות מספיק אלמנטים כדי ליצור מערכת תגובתית.

ליישום שלנו, נאמץ תכנות תגובתי מקצה לקצה ודפוסים וכלים אחרים להשגת המאפיינים הבסיסיים של מערכת תגובית.

4.1. ארכיטקטורה

נתחיל בהגדרה ארכיטקטורת יישומים פשוטה אשר לא בהכרח מציגה את המאפיינים של מערכות תגובתיות. משם ואילך נערוך את השינויים הדרושים להשגת מאפיינים אלה בזה אחר זה.

אז ראשית, נתחיל בהגדרת ארכיטקטורה פשוטה:

זוהי ארכיטקטורה פשוטה למדי שיש בה חבורה של מיקרו-שירותים כדי להקל על מקרה השימוש במסחר בו אנו יכולים לבצע הזמנה. יש לו גם חזית לחוויית משתמש, וכל התקשורת מתרחשת כ- REST באמצעות HTTP. יתר על כן, כל מיקרו-שירות מנהל את הנתונים שלהם בבסיסי נתונים בודדים, נוהג המכונה מסד נתונים לשירות.

נמשיך ליצור יישום פשוט זה בפרקי המשנה הבאים. זה יהיה שלנו בסיס להבנת הכשלים של אדריכלות זו ודרכים ואמצעים לאמץ עקרונות ופרקטיקות כדי שנוכל להפוך זאת למערכת תגובתית.

4.3. מלאי מיקרו-שירות

שירות המלאי המלאי יהיה אחראי על ניהול רשימת המוצרים והמלאי הנוכחי שלהם. זה יאפשר גם לשנות את המלאי בזמן עיבוד ההזמנות. נשתמש באביב אתחול עם MongoDB כדי לפתח שירות זה.

נתחיל בהגדרת בקר שיחשוף כמה נקודות קצה:

@GetMapping רשימה ציבורית getAllProducts () {return productService.getProducts (); } @PostMapping הזמנה ציבורית של הזמנת תהליך הזמנה (@ הזמנת הזמנה להזמנה) {החזר productService.handleOrder (הזמנה); } @DeleteMapping הזמנה ציבורית revertOrder (@RequestBody הזמנת הזמנה) {החזר productService.revertOrder (הזמנה); }

ושירות לתמצית ההיגיון העסקי שלנו:

@ Transactional Order OrderOrder (Order Order) {order.getLineItems () .forEach (l -> {Product> p = productRepository.findById (l.getProductId ()) .orElseThrow (() -> RuntimeException חדש ("לא יכול היה למצוא המוצר: "+ l.getProductId ())); אם (p.getStock ()> = l.getQuantity ()) {p.setStock (p.getStock () - l.getQuantity ()); productRepository.save ( p);} אחרת {זרוק RuntimeException חדש ("המוצר אזל מהמלאי:" + l.getProductId ());}}); הזמנת order.setOrderStatus (OrderStatus.SUCCESS); } @ Transactional Public Order revertOrder (סדר הזמנה) {order.getLineItems () .forEach (l -> {Product p = productRepository.findById (l.getProductId ()) .orElseThrow (() -> RuntimeException חדש ("לא יכול היה למצוא המוצר: "+ l.getProductId ())); p.setStock (p.getStock () + l.getQuantity ()); productRepository.save (p);}); הזמנת order.setOrderStatus (OrderStatus.SUCCESS); }

שים לב שאנחנו להתמיד בישויות במסגרת עסקה, אשר מבטיח כי אין מצב לא עקבי במקרה של חריגים.

מלבד אלה, נצטרך גם להגדיר את ישויות הדומיין, ממשק המאגר וחבורה של שיעורי תצורה הדרושים כדי שהכל יעבוד כמו שצריך.

אך מכיוון שמדובר בעיקר בתנור, אנו נימנע מלעבור אותם, וניתן להתייחס אליהם במאגר GitHub המופיע בחלק האחרון של מאמר זה.

4.4. משלוח מיקרו-שירות

גם שירותי המשלוח המשלוחים לא יהיו שונים מאוד. זה יהיה אחראי לבדיקה אם ניתן ליצור משלוח עבור ההזמנה וליצור אחד במידת האפשר.

כמו בעבר נגדיר בקר לחשוף את נקודות הקצה שלנו, למעשה רק נקודת קצה אחת:

תהליך הזמנה ציבורי @ PostMapping (הזמנת הזמנה ציבורית @ @ RequestBody) {returnService.handleOrder (הזמנה); }

ושירות לתמצית ההיגיון העסקי הקשור למשלוח הזמנות:

Order Order Order (סדר הזמנה) ציבורי {LocalDate shippingDate = null; אם (LocalTime.now (). isAfter (LocalTime.parse ("10:00")) && LocalTime.now (). isBefore (LocalTime.parse ("18:00"))) {shippingDate = LocalDate.now () .plusDays (1); } אחר {זרוק RuntimeException חדש ("הזמן הנוכחי אינו מוגבל לביצוע הזמנה."); } shipmentRepository.save (משלוח חדש () .setAddress (order.getShippingAddress ()) .setShippingDate (shippingDate)); החזר order.setShippingDate (shippingDate) .setOrderStatus (OrderStatus.SUCCESS); }

שירות המשלוח הפשוט שלנו פשוט בודק את חלון הזמן התקף לביצוע הזמנות. נימנע מלדון בשאר קוד ה- boilerplate כמו קודם.

4.5. הזמינו מיקרו-שירות

לבסוף, נגדיר מיקרו שירות הזמנה שיהיה אחראי על יצירת סדר חדש מלבד דברים אחרים. מעניין שזה גם ינגן כשירות תזמורת שבו הוא יתקשר עם שירות המלאי ושירות המשלוח להזמנה.

בואו נגדיר את הבקר שלנו עם נקודות הקצה הנדרשות:

@PostMapping סדר ציבורי ליצור (@RequestBody הזמנת הזמנה) {הזמנה processOrder = orderService.createOrder (הזמנה); אם (OrderStatus.FAILURE.equals (processingOrder.getOrderStatus ())) {זרוק RuntimeException חדש ("עיבוד ההזמנה נכשל, נסה שוב מאוחר יותר."); } להחזיר מעובד סדר; } @GetMapping הרשימה הציבורית getAll () {החזר orderService.getOrders (); }

וגם שירות למעטת את ההיגיון העסקי הקשור להזמנות:

סדר סדר ציבורי (סדר הזמנה) {הצלחה בוליאנית = אמת; הזמנה להציל סדר = orderRepository.save (סדר); הזמנת מלאי תגובה = null; נסה את {inventoryResponse = restTemplate.postForObject (inventoryServiceUrl, order, Order.class); } לתפוס (Exception ex) {success = false; } הזמין shippingResponse = null; נסה את {shippingResponse = restTemplate.postForObject (shippingServiceUrl, order, Order.class); } לתפוס (Exception ex) {success = false; HttpEntity deleteRequest = HttpEntity חדש (סדר); ResponseEntity deleteResponse = restTemplate.exchange (inventoryServiceUrl, HttpMethod.DELETE, deleteRequest, Order.class); } אם (הצלחה) {savedOrder.setOrderStatus (OrderStatus.SUCCESS); savedOrder.setShippingDate (shippingResponse.getShippingDate ()); } אחר {savedOrder.setOrderStatus (OrderStatus.FAILURE); } להחזיר orderRepository.save (savedOrder); } רשימת ציבורים getOrders () {return orderRepository.findAll (); }

הטיפול בהזמנות בהן אנו מתזמרים שיחות לשירותי מלאי ומשלוח רחוק מלהיות אידיאלי. מופץ עסקאות עם מיקרו-שירותים מרובים הוא נושא מורכב בפני עצמו ומעבר לתחום הדרכה זו.

עם זאת, נראה בהמשך הדרכה זו כיצד מערכת תגובתית יכולה למנוע את הצורך בעסקאות מבוזרות במידה מסוימת.

כמו בעבר, לא נעבור על שאר קוד ה- boilerplate. עם זאת, ניתן להתייחס לכך ברפיונו של GitHub.

4.6. חזיתי

בואו נוסיף גם ממשק משתמש כדי להשלים את הדיון. ממשק המשתמש יתבסס על Angular ויהיה יישום פשוט של עמוד יחיד.

נצטרך צור רכיב פשוט ב- Angular לטיפול בכדי ליצור ולהביא הזמנות. חשיבות ספציפית היא החלק בו אנו קוראים ל- API שלנו ליצירת ההזמנה:

createOrder () {let headers = new HttpHeaders ({'Content-Type': 'application / json'}); let options = {headers: headers} this.http.post ('// localhost: 8080 / api / orders', this.form.value, options). subscribe ((response) => {this.response = response}, (שגיאה) => {this.error = error})}

קטע הקוד לעיל מצפה שנתוני ההזמנה יילכדו בצורה וזמינים במסגרת הרכיב. Angular מציעה תמיכה נהדרת ליצירת טפסים פשוטים ומורכבים באמצעות טפסים מגיבים ומונעי תבנית.

חשוב גם הוא החלק בו אנו מקבלים הזמנות שנוצרו בעבר:

getOrders () {this.previousOrders = this.http.get ('' // localhost: 8080 / api / orders '')}

שים לב שמודול ה- HTTP הזוויתי הוא אופי אסינכרוני ומכאן מחזיר RxJS נצפהס. אנו יכולים להתמודד עם התגובה לדעתנו על ידי העברתם דרך צינור אסינכרון:

ההזמנות שביצעת עד כה:

  • מזהה הזמנה: {{order.id}}, סטטוס הזמנה: {{order.orderStatus}}, הודעת הזמנה: {{order.responseMessage}}

כמובן ש- Angular ידרשו תבניות, סגנונות ותצורות כדי לעבוד, אך ניתן להתייחס אליהן במאגר GitHub. לידיעתך, ריכזנו כאן הכל ברכיב אחד, וזה אידיאלי לא משהו שאנחנו צריכים לעשות.

אבל, עבור הדרכה זו, חששות אלה אינם בתחום.

4.7. פריסת היישום

כעת לאחר שיצרנו את כל החלקים האינדיבידואליים של היישום, כיצד עלינו לפרוס אותם? ובכן, אנחנו תמיד יכולים לעשות זאת באופן ידני. אבל עלינו להיזהר שזה במהרה יכול להיות מייגע.

לצורך הדרכה זו, נשתמש ב- Docker Compose ל לבנות ולפרוס את היישום שלנו במכונת Docker. זה ידרוש מאיתנו להוסיף קובץ Docker סטנדרטי בכל שירות וליצור קובץ Docker Compose עבור היישום כולו.

בוא נראה איך זה docker-compose.yml נראה קובץ:

גרסה: שירותי '3': חזית: build: ./ יציאות frontend: - "80:80" שירות הזמנות: build: ./order- יציאות שירות: - "8080: 8080" שירות מלאי: build: ./inventory יציאות שירות: - "8081: 8081" שירות משלוח: build: ./ משלוח יציאות שירות: - "8082: 8082"

זו הגדרה די סטנדרטית של שירותים ב- Docker Compose ואינה דורשת שום תשומת לב מיוחדת.

4.8. בעיות בארכיטקטורה זו

כעת, כשיש לנו יישום פשוט עם מספר שירותים האינטראקציה זה עם זה, נוכל לדון בבעיות בארכיטקטורה זו. יש למה שננסה לטפל בסעיפים הבאים ובסופו של דבר להגיע למצב בו היינו הופכים את היישום שלנו למערכת תגובתי!

יישום זה אמנם רחוק מלהיות תוכנת ייצור ויש מספר בעיות, אך להתמקד בנושאים הנוגעים למניעים למערכות תגובתיות:

  • כשל בשירותי המלאי או בשירות המשלוח יכול להיות בעל השפעה מדורגת
  • השיחות למערכות חיצוניות ולמאגר מידע כולן חוסמות אופי
  • הפריסה אינה יכולה להתמודד עם כשלים ותנודות בעומסים באופן אוטומטי

5. תכנות תגובתי

חסימת שיחות בכל תוכנית לעיתים קרובות לגרום למשאבים קריטיים שרק מחכים שהדברים יקרו. אלה כוללים שיחות בסיס נתונים, שיחות לשירותי אינטרנט ושיחות מערכת קבצים. אם נוכל לשחרר חוטים של ביצוע מהמתנה זו ולספק מנגנון לחזור לאחור ברגע שהתוצאות יהיו זמינות, זה יניב ניצול משאבים טוב בהרבה.

זה מה שאימץ את פרדיגמת התכנות התגובתית עושה לנו. אמנם ניתן לעבור לספרייה תגובתית עבור רבות מהשיחות הללו, אך יתכן שלא יהיה אפשרי לכל דבר. מבחינתנו, למרבה המזל, אביב מקל על השימוש בתכנות תגובתי עם ממשקי API של MongoDB ו- REST:

ל- Spring Data Mongo יש תמיכה בגישה תגובתית דרך מנהל התקן Java של זרמי התגובה של MongoDB. זה מספק תגובתי MongoTemplate ו מאגר תגובתי, לשניהם פונקציונליות מיפוי נרחבת.

Spring WebFlux מספק את מסגרת האינטרנט המחסנית-ריאקטיבית עבור Spring, ומאפשרת קוד שאינו חוסם ולחץ אחורי של זרמים ריאקטיביים. הוא ממנף את הכור כספרייה התגובתית שלו. יתר על כן, הוא מספק WebClient לביצוע בקשות HTTP עם לחץ אחורי של זרמי תגובה. הוא משתמש בכור Netty כספריית לקוח HTTP.

5.1. שירות מלאי

נתחיל בשינוי נקודות הקצה שלנו לפליטת מפרסמים תגובתי:

@ GetMapping שטף ציבורי getAllProducts () {החזר productService.getProducts (); }
@ PostMapping ציבורי מונו processOrder (הזמנת הזמנה @ RequestBody) {החזר productService.handleOrder (הזמנה); } @ מחיקת מיפוי ציבורי מונו מחדש (הזמנת הזמנה @ RequestBody) {החזר productService.revertOrder (הזמנה); }

ברור שנצטרך לבצע שינויים נחוצים גם בשירות:

@ Transactional מונו ידית הזמנה (סדר הזמנה) {החזר Flux.fromIterable (order.getLineItems ()) .flatMap (l -> productRepository.findById (l.getProductId ())) .flatMap (p -> {int q = order. getLineItems (). stream () .filter (l -> l.getProductId (). שווה ל- (p.getId ())) .findAny (). get () .getQuantity (); if (p.getStock ()> = q) {p.setStock (p.getStock () - q); return productRepository.save (p);} else {return Mono.error (RuntimeException חדש ("המוצר אזל מהמלאי:" + p.getId ()) );}}). ואז (Mono.just (order.setOrderStatus ("הצלחה"))); } @ Transactional Mono revertOrderorder (סדר הזמנה) {return Flux.fromIterable (order.getLineItems ()) .flatMap (l -> productRepository.findById (l.getProductId ())) .flatMap (p -> {int q = order .getLineItems (). stream () .filter (l -> l.getProductId (). שווה ל- (p.getId ())) .findAny (). get () .getQuantity (); p.setStock (p.getStock ( ) + q); להחזיר productRepository.save (p);}). ואז (Mono.just (order.setOrderStatus ("הצלחה"))); }

5.2. שירות משלוחים

באופן דומה נשנה את נקודת הסיום של שירות המשלוחים שלנו:

@ PostMapping תהליך מונו ציבורי (הזמנת הזמנה @ RequestBody) {return shippingService.handleOrder (הזמנה); }

ושינויים מקבילים בשירות למינוף תכנות תגובתי:

handle מונו ציבורי ציבורי (הזמנת הזמנה) {החזר Mono.just (סדר) .flatMap (o -> {LocalDate shippingDate = null; אם (LocalTime.now (). isAfter (LocalTime.parse ("10:00")) && LocalTime .now (). isBefore (LocalTime.parse ("18:00"))) {shippingDate = LocalDate.now (). plusDays (1);} אחר {להחזיר Mono.error (RuntimeException חדש ("הזמן הנוכחי כבוי הגבולות לביצוע הזמנה. "));} החזר shipmentRepository.save (משלוח חדש () .setAddress (order.getShippingAddress ()) .setShippingDate (shippingDate));}) .מפה (s -> order.setShippingDate (s. getShippingDate ()) .setOrderStatus (OrderStatus.SUCCESS)); }

5.3. שירות הזמנות

נצטרך לבצע שינויים דומים בנקודות הקצה של שירות ההזמנות:

@PostMapping מונו ציבורי ליצור (@RequestBody הזמנת הזמנה) {return orderService.createOrder (order) .flatMap (o -> {if (OrderStatus.FAILURE.equals (o.getOrderStatus ())) {return Mono.error (RuntimeException new ( "עיבוד ההזמנות נכשל, אנא נסה שוב מאוחר יותר." + O.getResponseMessage ()));} אחרת {החזר Mono.just (o);}}); } @ GetMapping שטף ציבורי getAll () {return orderService.getOrders (); }

השינויים בשירות יהיו מעורבים יותר מכיוון שנצטרך להשתמש באביב WebClient להפעיל את המלאי ונקודות קצה תגובתיות למשלוח:

ציבורי מונו ציבורי (סדר הזמנה) {החזר מונו.סתם (סדר) .flatMap (orderRepository :: שמור) .flatMap (o -> {החזר webClient.method (HttpMethod.POST) .uri (inventoryServiceUrl) .body (BodyInserters.fromValue (o)) .exchange ();}) .onErrorResume (err -> {return Mono.just (order.setOrderStatus (OrderStatus.FAILURE) .setResponseMessage (err.getMessage ()));}) .flatMap (o -> {if (! OrderStatus.FAILURE.equals (o.getOrderStatus ())) {return webClient.method (HttpMethod.POST) .uri (shippingServiceUrl) .body (BodyInserters.fromValue (o)). exchange ();} אחר להחזיר Mono.just (o);}}). onErrorResume (err -> {return webClient.method (HttpMethod.POST) .uri (inventoryServiceUrl) .body (BodyInserters.fromValue (order)). retrieve () .bodyToMono (Order .class) .map (o -> o.setOrderStatus (OrderStatus.FAILURE) .setResponseMessage (err.getMessage ()));}) .map (o -> {if (! OrderStatus.FAILURE.equals (o.getOrderStatus ( ))) {return order.setShippingDate (o.getShippingDate ()) .setOrderStatus (OrderStatus.SUCCESS);} אחר {return order.setOrderStatus (OrderStatus.FAILURE) .setResponseMessage (o.getResponseMessage ()); }}) .flatMap (orderRepository :: save); } שטף ציבורי getOrders () {return orderRepository.findAll (); }

זֶה סוג של תזמור עם ממשקי API ריאקטיביים אינו תרגיל קל ולעתים קרובות מועד לטעויות וכן קשה לניפוי באגים. נראה כיצד ניתן לפשט זאת בחלק הבא.

5.4. חזיתי

כעת, מכיוון שממשקי ה- API שלנו מסוגלים להזרים אירועים תוך כדי התרחשותם, זה די טבעי שנוכל למנף את זה גם בחזית שלנו. למרבה המזל, Angular תומכת ב- EventSource, הממשק לאירועים שנשלחו על ידי השרת.

בואו נראה איך נוכל לעבד ולעבד את כל ההזמנות הקודמות שלנו כזרם אירועים:

getOrderStream () {return Observable.create ((observer) => {let eventSource = new EventSource ('// localhost: 8080 / api / orders') eventSource.onmessage = (event) => {let json = JSON.parse ( event.data) this.orders.push (json) this._zone.run (() => {observer.next (this.orders)})} eventSource.onerror = (error) => {if (eventSource.readyState = == 0) {eventSource.close () this._zone.run (() => {observer.complete ()})} אחר {this._zone.run (() => {observer.error ('שגיאת EventSource: '+ שגיאה)})}}})}

6. אדריכלות מונעת מסרים

הבעיה הראשונה בה אנו מתמודדים קשורה לתקשורת שירות לשירות. עכשיו, התקשורת הזו סינכרונית, מה שמציב כמה בעיות. אלה כוללים כשלים מדורגים, תזמור מורכב ועסקאות מבוזרות עד כמה שם.

דרך ברורה לפתור בעיה זו היא להפוך את התקשורת לאסינכרונית. א מתווך הודעות להקלת כל התקשורת בין שירות לשירות יכול לעשות את הטריק עבורנו. נשתמש בקפקא כמתווך המסרים שלנו ובמעיין קפקא לייצר ולצרוך הודעות:

נשתמש בנושא יחיד כדי לייצר ולצרוך הודעות הזמנה עם סטטוסי הזמנה שונים עבור שירותים להגיב.

בואו נראה כיצד כל שירות צריך להשתנות.

6.1. שירות מלאי

נתחיל בהגדרת יצרן המסרים עבור שירות המלאי שלנו:

@ KafkaTemplate פרטית אוטומטית kafkaTemplate; sendMessage בטל ציבורי (הזמנת הזמנה) {this.kafkaTemplate.send ("הזמנות", הזמנה); }

בשלב הבא נצטרך להגדיר צרכן הודעות לשירות מלאי שיגיב להודעות שונות בנושא:

@KafkaListener (נושאים = "הזמנות", groupId = "מלאי") ריק לצרוך (סדר הזמנה) זורק IOException {if (OrderStatus.RESERVE_INVENTORY.equals (order.getOrderStatus ())) {productService.handleOrder (order) .doOnSuccess (order). o -> {orderProducer.sendMessage (order.setOrderStatus (OrderStatus.INVENTORY_SUCCESS));}) .doOnError (e -> {orderProducer.sendMessage (order.setOrderStatus (OrderStatus.INVENTORY_FAILURE) (setResponse)) הירשם (); } אחרת אם (OrderStatus.REVERT_INVENTORY.equals (order.getOrderStatus ())) {productService.revertOrder (order) .doOnSuccess (o -> {orderProducer.sendMessage (order.setOrderStatus (OrderStatus.INVENTORY_REVERT_S)) e -> {orderProducer.sendMessage (order.setOrderStatus (OrderStatus.INVENTORY_REVERT_FAILURE) .setResponseMessage (e.getMessage ()));}). subscribe (); }}

זה גם אומר שאנחנו יכולים לבטל בבטחה חלק מנקודות הקצה המיותרות מהבקר שלנו עכשיו. שינויים אלה מספיקים בכדי להשיג תקשורת אסינכרונית ביישום שלנו.

6.2. שירות משלוחים

השינויים בשירות המשלוחים דומים יחסית למה שעשינו קודם עם שירות המלאי. יצרן ההודעות זהה, וצרכן המסר ספציפי להיגיון המשלוחים:

@KafkaListener (נושאים = "הזמנות", groupId = "משלוח") ריק לצרוך (סדר הזמנה) זורק IOException {אם (OrderStatus.PREPARE_SHIPPING.equals (order.getOrderStatus ())) {shippingService.handleOrder (סדר) .doOnSuccess () o -> {orderProducer.sendMessage (order.setOrderStatus (OrderStatus.SHIPPING_SUCCESS) .setShippingDate (o.getShippingDate ()))}} .doOnError (e -> {orderProducer.sendMessage (order.setOrderStatUS (OrderStat). (e.getMessage ()));}). subscribe (); }}

אנו יכולים להוריד בבטחה את כל נקודות הקצה בבקר שלנו מכיוון שאיננו זקוקים להם יותר.

6.3. שירות הזמנות

השינויים בשירות ההזמנות יהיו מעורבים קצת יותר מכיוון שכאן עשינו את כל התזמור קודם.

עם זאת, יצרן ההודעות נותר ללא שינוי, וצרכני המסרים מקבלים לוגיקה ספציפית לשירות ההזמנות:

@KafkaListener (נושאים = "הזמנות", groupId = "הזמנות") מבטל ריק לצרוך (סדר הזמנה) זורק IOException {אם (OrderStatus.INITIATION_SUCCESS.equals (order.getOrderStatus ())) {orderRepository.findById (order.getId () ) .מפה (o -> {orderProducer.sendMessage (o.setOrderStatus (OrderStatus.RESERVE_INVENTORY)); להחזיר o.setOrderStatus (order.getOrderStatus ()). setResponseMessage (order.getResponseMessage ());}) orderflat. : שמור). מנוי (); } אחר אם ("INVENTORY-SUCCESS" .equals (order.getOrderStatus ())) {orderRepository.findById (order.getId ()) .map (o -> {orderProducer.sendMessage (o.setOrderStatus (OrderStatus.PREPARE_SHIPPING)) ; להחזיר o.setOrderStatus (order.getOrderStatus ()) .setResponseMessage (order.getResponseMessage ());}). flatMap (orderRepository :: save). abonnement (); } אחר אם ("SHIPPING-FAILURE" .equals (order.getOrderStatus ())) {orderRepository.findById (order.getId ()) .map (o -> {orderProducer.sendMessage (o.setOrderStatus (OrderStatus.REVERT_INVENTORY)) ; להחזיר o.setOrderStatus (order.getOrderStatus ()) .setResponseMessage (order.getResponseMessage ());}). flatMap (orderRepository :: save). abonnement (); } אחר {orderRepository.findById (order.getId ()) .map (o -> {return o.setOrderStatus (order.getOrderStatus ()) .setResponseMessage (order.getResponseMessage ());}) .flatMap (orderRepository :: save ). מנוי (); }}

ה הצרכן כאן רק מגיב להזמנת הודעות עם סטטוסי הזמנה שונים. זה מה שנותן לנו את הכוריאוגרפיה בין שירותים שונים.

לבסוף, גם שירות ההזמנות שלנו יצטרך להשתנות כדי לתמוך בכוריאוגרפיה זו:

ציבורי מונו ציבורי (סדר הזמנה) {החזר מונו.סתם (סדר) .flatMap (orderRepository :: שמור). מפה (o -> {orderProducer.sendMessage (o.setOrderStatus (OrderStatus.INITIATION_SUCCESS)); החזר o;}). onErrorResume (err -> {return Mono.just (order.setOrderStatus (OrderStatus.FAILURE) .setResponseMessage (err.getMessage ()));}) .flatMap (orderRepository :: save); }

שים לב שזה הרבה יותר פשוט מהשירות שהיה לנו לכתוב עם נקודות קצה תגובתיות בחלק האחרון. אסינכרוני כוריאוגרפיה לעתים קרובות מביאה לקוד פשוט בהרבה, אם כי זה עולה במחיר של עקביות בסופו של דבר וניקוי וניטור מורכבים. כפי שאנו עשויים לנחש, חזיתנו כבר לא תקבל את המעמד הסופי של ההזמנה באופן מיידי.

7. שירות תזמור מכולות

החלק האחרון של הפאזל שאנו רוצים לפתור קשור לפריסה.

מה שאנחנו רוצים ביישום הוא עודף רב ונטייה להתמוטט כלפי מעלה או מטה בהתאם לצורך באופן אוטומטי.

כבר השגנו מיכל שירותים באמצעות Docker ומנהל תלות ביניהם באמצעות Docker Compose. אמנם מדובר בכלים פנטסטיים בפני עצמם, אך הם אינם עוזרים לנו להשיג את מבוקשנו.

מכאן, אנחנו זקוקים לשירות תזמור מכולות שיכול לדאוג ליתירות ולהרחבה ביישום שלנו. אמנם יש כמה אפשרויות, אך אחת הפופולריות כוללת את Kubernetes. Kubernetes מספקת לנו דרך אגנוסטית של ספק ענן להשיג פריסות מדרגיות ביותר של עומסי עבודה מכוליים.

Kubernetes עוטף מכולות כמו Docker לתוך Pods, שהם יחידת הפריסה הקטנה ביותר. יתר על כן, אנו יכולים להשתמש בפריסה כדי לתאר את המצב הרצוי באופן הצהרתי.

פריסה יוצרת ReplicaSets, האחראית באופן פנימי להעלאת התרמילים. אנו יכולים לתאר מספר מינימלי של תרמילים זהים שצריכים לפעול בכל נקודת זמן. זה מספק יתירות ומכאן זמינות גבוהה.

בואו נראה כיצד נוכל להגדיר פריסת Kubernetes ליישומים שלנו:

apiVersion: אפליקציות / v1 סוג: מטא נתונים של פריסה: שם: מפרט פריסת מלאי: העתקים: 3 בורר: matchLabels: שם: תבנית פריסת מלאי: מטא נתונים: תוויות: שם: מפרט פריסת מלאי: מכולות: - שם: תמונת מלאי: מלאי-שירות-אסינק: יציאות אחרונות: - מיכל נמל: 8081 --- api גרסה: אפליקציות / v1 סוג: פריט מטא נתונים: שם: מפרט פריסת משלוח: העתקים: 3 בורר: matchLabels: שם: תבנית פריסת משלוח: מטא נתונים: תוויות : שם: מפרט פריסת משלוח: מכולות: - שם: תמונת משלוח: shipping-service-async: יציאות אחרונות: - מיכל נמל: 8082 --- api גרסה: אפליקציות / v1 סוג: פריט מטא נתונים: שם: מפרט פריסת הזמנות: העתקים : 3 בורר: matchLabels: שם: תבנית פריסת הזמנה: מטא נתונים: תוויות: שם: מפרט פריסת הזמנות: מכולות: - שם: הזמנת תמונה: הזמנת שירות-אסינק: יציאות אחרונות: - מיכל נמל: 8080

כאן אנו מצהירים על פריסתנו לשמור על שלוש העתקים זהים של תרמילים בכל עת. זוהי דרך טובה להוסיף יתירות, אך ייתכן שהיא לא תספיק לעומסים משתנים. Kubernetes מספק משאב אחר המכונה Autoscaler Pod Horizontal שיכול הגדל את מספר התרמילים בפריסה בהתבסס על מדדים שנצפו כמו ניצול מעבד.

לידיעתך, כרגע כיסינו את היבטי המדרגיות של היישום המתארח באשכול Kubernetes. זה לא בהכרח מרמז שהאשכול הבסיסי עצמו ניתן להרחבה. יצירת אשכול Kubernetes בזמינות גבוהה היא משימה לא טריוויאלית ומעבר לתחום הדרכה זו.

8. מערכת תגובתי כתוצאה מכך

כעת, לאחר שביצענו מספר שיפורים בארכיטקטורה שלנו, הגיע הזמן להעריך זאת על פי ההגדרה של מערכת תגובתי. אנו נשמור על ההערכה על פי ארבעת המאפיינים של מערכות ריאקטיביות עליהם דנו קודם במדריך:

  • מגיב: אימוץ פרדיגמת התכנות התגובתית אמור לעזור לנו להשיג אי-חסימה מקצה לקצה ומכאן יישום מגיב
  • מִתאוֹשֵׁשׁ מַהֵר: פריסת Kubernetes עם ReplicaSet של המספר הרצוי של תרמילים אמורה לספק חוסן כשלים אקראיים
  • אֵלַסטִי: אשכול ומשאבי Kubernetes צריכים לספק לנו את התמיכה הדרושה כדי להיות אלסטיים מול עומסים בלתי צפויים
  • מונע הודעות: אם כל התקשורת בין שירות לשירות מטופלת בצורה אסינכרונית באמצעות מתווך קפקא אמורה לעזור לנו כאן

זה אמנם נראה מבטיח למדי, אבל זה רחוק מלהסתיים. למען האמת, ה החיפוש אחר מערכת תגובית אמיתית צריך להיות תרגיל מתמשך של שיפורים. לעולם איננו יכולים להקדים את כל מה שיכול להיכשל בתשתית מורכבת ביותר, שבה היישום שלנו הוא רק חלק קטן.

כך תהיה מערכת תגובית לדרוש אמינות מכל חלק שעושה את השלם. ממש מהרשת הפיזית לשירותי תשתית כמו DNS, כולם צריכים ליפול בתור כדי לעזור לנו להשיג את המטרה הסופית.

לעיתים קרובות, יתכן שלא נוכל לנהל ולספק את הערבויות הדרושות לכל החלקים הללו. וזה המקום בו תשתית ענן מנוהלת עוזרת להקל על הכאב שלנו. אנו יכולים לבחור מתוך מגוון שירותים כמו IaaS (Infeastrure-as-a-Service), BaaS (Backend-as-a-Service) ו- PaaS (Platform-as-a-Service) כדי להאציל את האחריות לגורמים חיצוניים. זה משאיר אותנו באחריות הבקשה שלנו ככל האפשר.

9. מסקנה

במדריך זה עברנו את היסודות של מערכות תגובתיות וכיצד זה משתווה לתכנות תגובתיות. יצרנו יישום פשוט עם מיקרו-שירותים מרובים והדגשנו את הבעיות שאנחנו מתכוונים לפתור בעזרת מערכת תגובית.

יתר על כן, המשכנו, הכנסנו תכנות תגובתי, ארכיטקטורה מבוססת מסרים ושירות תזמור מכולות בארכיטקטורה כדי לממש מערכת תגובית.

לבסוף, דנו בארכיטקטורה שנוצרה וכיצד היא נותרה מסע לעבר המערכת התגובתית! הדרכה זו אינה מציגה בפנינו את כל הכלים, המסגרות או הדפוסים שיכולים לעזור לנו ליצור מערכת תגובית, אך היא מציגה אותנו למסע.

כרגיל, ניתן למצוא את קוד המקור של מאמר זה ב- GitHub.


$config[zx-auto] not found$config[zx-overlay] not found