מדיניות ניסיונות חוזרים בספריות הלקוח של 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. גם האידמפוטנטיות של הפעולה חשובה.

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

כברירת מחדל, רוב הספריות משתמשות באסטרטגיית השהיה מעריכית קטועה לפני ניסיון חוזר, עם תנודות. ההשהיה הראשונית לפני ניסיון חוזר היא שנייה אחת, ההשהיה המקסימלית לפני ניסיון חוזר היא 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";
}

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

השלבים הבאים