עומס חסימות ושרידות¶
נניח שהשרת מסוגל לבצע מאה בקשות בשנייה, אבל מקבל אלף. אם כל בקשה נשמרת בתור, המערכת לא פתרה את העומס; היא הפכה אותו לזיכרון, לזמן המתנה ולתוצאות שכבר אינן שימושיות. מנוע אסינכרוני יכול להחזיק יותר פעולות ממתינות, ולכן הוא זקוק להגבלת קבלה ברורה אפילו יותר.
נפריד בין מספר הבקשות לבין משקלן. ארבע בקשות עם buffer של מגה־בית אינן שקולות לארבע בקשות של עשרה בתים. התקציב צריך להתאים למשאבים שהמערכת באמת מחזיקה, כולל בקשות שבוטלו לוגית אבל עדיין מחכות להשלמה פיזית.
קבלה עם בעלות על רישיון¶
הדוגמה הבאה מגבילה גם מספר עבודות וגם בתים. רישיון נשמר עד שהעבודה מסיימת להשתמש במשאבים, ומשחרר את התקציב בהרסו:
#include <cassert>
#include <cstddef>
#include <mutex>
#include <optional>
#include <utility>
class Gate {
public:
class Lease {
Gate* owner_;
std::size_t bytes_;
friend class Gate;
Lease(Gate& owner, std::size_t bytes)
: owner_(&owner), bytes_(bytes) {}
public:
Lease(const Lease&) = delete;
Lease& operator=(const Lease&) = delete;
Lease(Lease&& other) noexcept
: owner_(std::exchange(other.owner_, nullptr)),
bytes_(other.bytes_) {}
~Lease() {
if (owner_) owner_->release(bytes_);
}
};
Gate(std::size_t max_jobs, std::size_t max_bytes)
: max_jobs_(max_jobs), max_bytes_(max_bytes) {}
std::optional<Lease> try_acquire(std::size_t bytes) {
std::lock_guard lock(mutex_);
if (jobs_ == max_jobs_ || bytes > max_bytes_ - bytes_)
return std::nullopt;
++jobs_;
bytes_ += bytes;
return Lease(*this, bytes);
}
private:
std::mutex mutex_;
std::size_t max_jobs_, max_bytes_;
std::size_t jobs_ = 0, bytes_ = 0;
void release(std::size_t bytes) {
std::lock_guard lock(mutex_);
assert(jobs_ != 0 && bytes <= bytes_);
--jobs_;
bytes_ -= bytes;
}
};
int main() {
Gate gate(2, 64);
auto first = gate.try_acquire(48);
auto rejected = gate.try_acquire(32);
auto second = gate.try_acquire(16);
assert(first && !rejected && second);
assert(!gate.try_acquire(0));
first.reset();
auto third = gate.try_acquire(32);
assert(third);
}
ה־Gate נוצר לפני כל הרישיונות ולכן נהרס אחריהם. בדיקת שני התקציבים והחיוב נעשים תחת אותה נעילה. שימוש בשני מונים אטומיים נפרדים לא היה הופך את ההחלטה כולה לאטומית: שני threads היו יכולים לאשר יחד עבודה שאין לה מקום.
הקוד אינו מקצה את ה־buffer עצמו. לאחר קבלת רישיון, הקצאת המשאב יכולה עדיין להיכשל, ואז הרס הרישיון מחזיר את התקציב. המערכת צריכה להגדיר האם החיוב הוא לפי גודל מבוקש, קיבולת בפועל או חסם שמרני. אחרת אפשר לעמוד במספר הרשום ולחרוג בפועל.
לחץ חוזר מגיע עד למקור¶
כאשר אין רישיון, אפשר לדחות בקשה מיד, להמתין בתור מוגבל או להפסיק לקרוא זמנית ממקור שתומך בכך. אם מפסיקים לקרוא מן הרשת, הלחץ יכול להתגלגל דרך buffers ופרוטוקול התעבורה, אך יש לבדוק את גבולות כל שכבה. תור בלתי מוגבל לפני ה־Gate פשוט עוקף את המגבלה.
נגדיר deadline כתאריך יעד של בקשה, ולא כמשך המתנה חדש בכל שלב. אם בקשה חיכתה בתור כמעט את כל התקציב שלה, אין להתחיל שוב timeout מלא בעת ההגשה. מדידת זמן מונוטוני מתאימה לחישובי משך; שעון קיר יכול להשתנות ואינו בהכרח בסיס נכון להחלטה.
ביטול לקוח אינו מחזיר מיד רישיון של I/O פעיל. כל עוד ה־buffer חי והפעולה לא הושלמה, המשאב עדיין תפוס. שחרור מוקדם מאפשר לקבל עוד בקשות ולחרוג מן הגבול דווקא תחת סערת ביטולים.
ארבע דרכים שונות לא להתקדם¶
ב־deadlock יש מעגל המתנה שלא יכול להיפתר מתוך המשתתפים. ב־livelock המשתתפים פועלים ומשנים מצב, אבל הפעולה הרצויה אינה מסתיימת. ב־starvation משתתף מסוים אינו מקבל הזדמנות מספקת להתקדם. ב־priority inversion עבודה חשובה ממתינה למשאב שבידי עבודה בעדיפות נמוכה יותר, בזמן שעבודה אחרת יכולה להפריע לה לשחררו.
ההבחנה קובעת מה מודדים. עבור deadlock נחקור stacks ומי מחזיק כל נעילה. עבור livelock נבדוק קצב ניסיונות חוזרים לעומת הצלחות. עבור starvation נשווה זמני המתנה לפי בקשה או עובד, ולא רק ממוצע כללי. שינוי priority בלי להבין את התלות יכול להחמיר את המצב.
מאגר עובדים שאיבד את כל העובדים¶
דמיינו ארבעה workers שמריצים callbacks, וכל callback מגיש עבודה לאותו מאגר ואז ממתין לתוצאתה. אם אין עובד פנוי להריץ את העבודה הפנימית, המערכת יכולה להיתקע למרות שיש CPU פנוי. הוספת עובדים עשויה להסתיר את העיצוב הפגום עד העומס הבא.
נעדיף המשך אסינכרוני, הפרדה בין עבודה חוסמת לעיבוד קצר או חוזה שאוסר המתנה לתוצאה שתלויה באותו מאגר. ה־dispatcher של IOCP צריך לצרוך השלמות במהירות סבירה; עיבוד כבד תחתיו יכול לעכב שחרור buffers ולהגדיל את התקציב החי.
בחקירה נשלב זמני קבלה, התחלה, השלמה ופרסום. אם זמן ההמתנה בתור גדל אבל המעבד פנוי, נבדוק חסימות וקיבולת עובדים. אם זמן העיבוד עצמו גדל והמעבד רווי, נבדוק עבודה חישובית ומקומיות זיכרון. מספר threads גדול כשלעצמו אינו אבחון.
קריאה להעמקה¶
ההסבר על תזמון ותורי השלמות מספק את בסיס מודל העובדים. כלי WPR ו־WPA בפרק האבחון יאפשרו למדוד את ההשערות במקום להסיק אותן ממונה CPU יחיד.