תכנון וחקירה תחת לחץ זיכרון¶
נניח שכל בקשה בשירות מקצה באפר קטן, אבל קצב ההגעה גבוה מקצב העיבוד. כל בקשה בנפרד נראית סבירה, ובכל זאת התור גדל עד שהמערכת מאבדת זמינות. הבעיה אינה רק המקצה. המערכת קיבלה התחייבויות שאין לה תקציב לשמור לאורך זמן.
נבנה תקציב לוגי פשוט שמחויב לפני קבלת עבודה ומשוחרר כשהעבודה מסתיימת. הדוגמה חד־תהליכונית במכוון ומשתמשת ב־Rc<Cell<_>>, שאת החוזים שלהם למדנו בפרק השני. בפרק המקביליות נחליף את מנגנון הסנכרון כאשר התקציב יחולק בין תהליכונים.
use std::{cell::Cell, rc::Rc};
struct Budget { used: Rc<Cell<usize>>, limit: usize }
struct Ticket { used: Rc<Cell<usize>>, bytes: usize }
impl Budget {
fn new(limit: usize) -> Self { Self { used: Rc::new(Cell::new(0)), limit } }
fn reserve(&self, bytes: usize) -> Option<Ticket> {
let next = self.used.get().checked_add(bytes)?;
if next > self.limit { return None; }
self.used.set(next);
Some(Ticket { used: Rc::clone(&self.used), bytes })
}
}
impl Drop for Ticket {
fn drop(&mut self) { self.used.set(self.used.get() - self.bytes); }
}
struct Job { data: Vec<u8>, _ticket: Ticket }
fn make_job(budget: &Budget, length: usize) -> Result<Job, &'static str> {
let ticket = budget.reserve(length).ok_or("budget exceeded")?;
let mut data = Vec::new();
data.try_reserve_exact(length).map_err(|_| "allocation failed")?;
data.resize(length, 0);
Ok(Job { data, _ticket: ticket })
}
fn main() {
let budget = Budget::new(100);
let first = make_job(&budget, 70).unwrap();
assert!(make_job(&budget, 40).is_err());
assert_eq!(first.data.len(), 70);
drop(first);
assert!(make_job(&budget, 40).is_ok());
assert_eq!(budget.used.get(), 0);
}
ה־ticket נוצר לפני ההקצאה. אם ההקצאה נכשלת, החזרה המוקדמת משמידה אותו ומחזירה את התקציב. אם העבודה מתקבלת, ה־ticket עובר יחד איתה ונשאר חי כל עוד היא תופסת את המשאב הלוגי. אין פעולה ציבורית שמאפשרת להעתיק ticket ולהחזיר את אותו תקציב פעמיים.
המדד בדוגמה הוא מספר בייטים לוגי שהעבודה ביקשה, ולא RSS מדויק. המקצה יכול לעגל גדלים, לשמור metadata ולהחזיק קיבולת נוספת. גם try_reserve_exact אינו חוזה שכל שכבות האחסון הפיזיות צרכו בדיוק את המספר שביקשנו. אם המוצר צריך תקציב קשיח יותר, נגדיר יחידות חיוב שמכסות את ההקצאות בפועל או נשתמש במאגר קבוע מראש.
החזרת Err כשהתקציב נגמר היא רק בחירת מדיניות אחת. אפשר לדחות קבלה, להחזיר עומס ללקוח, להפיל עבודה פחות חשובה או להמתין בתור חסום. המתנה בתור בלתי מוגבל עבור הזכות להקצות רק מעבירה את אותה בעיה למקום אחר. גם הבקשות הממתינות צורכות זיכרון ומחזיקות משאבים.
לכן נמדוד גם אורך תור, מספר עבודות פעילות, זמן המתנה ושיעור דחייה. גרף RSS לבדו לא מספר אם השירות מועיל למשתמש. מערכת שמחזיקה את כל הבקשות אבל עונה אחרי דקות יכולה להיות פחות טובה ממערכת שמסרבת מוקדם לחלק מהעומס ושומרת על זמן תגובה לשאר.
המנגנון PSI של לינוקס מתאר זמן שבו משימות סובלות מלחץ על משאבים כגון זיכרון. הוא יכול לעזור לראות שהמערכת מבזבזת זמן על המתנה ופינוי גם לפני שהגיעה ל־OOM. המדד אינו מחליף את מדדי האפליקציה, אלא מחבר בין התנהגות השירות לבין מצב המערכת שמריצה אותו.
קראו את /proc/pressure/memory בסביבה שבה הממשק זמין, והשוו לתקופות עומס ידועות. הבדילו בין מצב שבו חלק מהמשימות מתעכבות לבין מצב שבו ההתקדמות הכוללת נפגעת בצורה רחבה יותר לפי פירוש השדות. אל תהפכו סף שנבחר ממכונה אחת לחוק כללי לכל עומס.
חקירה מסודרת מתחילה בהשערה אחת. למשל, התור גדל ולכן שומר באפרים גם כאשר העבודה לא יכולה להתקדם. נוסיף תקציב, נשמור את קצב ההגעה קבוע ונבדוק אם RSS וזמן תגובה מתייצבים תוך עלייה מדווחת בדחיות. אם רק החלפנו מקצה והמערכת עדיין מקבלת עבודה בלי גבול, ייתכן ששיפרנו סימפטום ולא את מקור הבעיה.
המידע על לחץ מתועד בתיעוד PSI של הקרנל. החוזה שנרצה ממערכת יציבה הוא שהיא יודעת להגביל התחייבויות לפני שהיא מאבדת שליטה, ומסוגלת להסביר ללקוח מה קרה לעבודה שלא התקבלה.