מדיניות ניסיונות חוזרים בספריות הלקוח של C++‎

בדף הזה מתואר מודל הניסיונות החוזרים שבו משתמשות ספריות הלקוח C++‎.

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

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

שגיאות שאפשר לנסות שוב לתקן ופעולות שאפשר לנסות שוב לבצע

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

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

הגדרת לולאות של ניסיונות חוזרים

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

  • הפרמטר *IdempotencyPolicy קובע אם בקשה מסוימת היא אידמפוטנטית. רק בקשות כאלה ינסו לשלוח מחדש.
  • הפונקציה *RetryPolicy קובעת (א) אם שגיאה מסוימת צריכה להיחשב ככשל זמני, ו-(ב) כמה זמן (או כמה פעמים) ספריית הלקוח מנסה לשלוח מחדש בקשה.
  • הערך *BackoffPolicy קובע כמה זמן ספריית הלקוח ממתינה לפני שהיא שולחת מחדש את הבקשה.

מדיניות ברירת המחדל בנושא אידמפוטנטיות

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

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

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

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

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

מדיניות ברירת המחדל לניסיון חוזר

בהתאם להנחיות שמפורטות ב-aip/194, רוב ספריות הלקוח של C++‎ מנסות לבצע שוב רק UNAVAILABLE שגיאות gRPC. הן ממופות לכתובת StatusCode::kUnavailable. מדיניות ברירת המחדל היא לנסות שוב לשלוח בקשות למשך 30 דקות.

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

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

מדיניות ברירת המחדל להשהיה לפני ניסיון חוזר

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

שינוי מדיניות ברירת המחדל לגבי ניסיון חוזר והשהיה לפני ניסיון חוזר (backoff)

כל ספרייה מגדירה מבנה *Option כדי להגדיר את המדיניות הזו. אפשר לספק את האפשרויות האלה כשיוצרים את הכיתה *Client או אפילו בכל בקשה.

לדוגמה, כך משנים את מדיניות הניסיון החוזר וההשהיה של לקוח Cloud Pub/Sub:

namespace pubsub = ::google::cloud::pubsub;
using ::google::cloud::future;
using ::google::cloud::Options;
using ::google::cloud::StatusOr;
[](std::string project_id, std::string topic_id) {
  auto topic = pubsub::Topic(std::move(project_id), std::move(topic_id));
  // By default a publisher will retry for 60 seconds, with an initial backoff
  // of 100ms, a maximum backoff of 60 seconds, and the backoff will grow by
  // 30% after each attempt. This changes those defaults.
  auto publisher = pubsub::Publisher(pubsub::MakePublisherConnection(
      std::move(topic),
      Options{}
          .set<pubsub::RetryPolicyOption>(
              pubsub::LimitedTimeRetryPolicy(
                  /*maximum_duration=*/std::chrono::minutes(10))
                  .clone())
          .set<pubsub::BackoffPolicyOption>(
              pubsub::ExponentialBackoffPolicy(
                  /*initial_delay=*/std::chrono::milliseconds(200),
                  /*maximum_delay=*/std::chrono::seconds(45),
                  /*scaling=*/2.0)
                  .clone())));

  std::vector<future<bool>> done;
  for (char const* data : {"1", "2", "3", "go!"}) {
    done.push_back(
        publisher.Publish(pubsub::MessageBuilder().SetData(data).Build())
            .then([](future<StatusOr<std::string>> f) {
              return f.get().ok();
            }));
  }
  publisher.Flush();
  int count = 0;
  for (auto& f : done) {
    if (f.get()) ++count;
  }
  std::cout << count << " messages sent successfully\n";
}

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

השלבים הבאים