דרישות וארכיטקטורת מערכת¶
נבנה שירות שמקבל עבודת חישוב על קובץ ומחזיר מזהה שניתן לבדוק אחרי restart. כבר למדנו ליצור תהליכים, להשתמש ב־epoll, לכתוב יומן ולהגדיר namespaces. כעת השאלה אינה איזה API להוסיף, אלא איזו הבטחה המערכת נותנת ומי אחראי לקיים אותה. אם אין הגדרה של "העבודה התקבלה", לא נוכל להחליט היכן נדרש fsync או מה להחזיר אחרי timeout.
ננסח דרישה מדידה: השירות מקבל לכל היותר 32 בקשות ממתינות, מפעיל ארבעה workers, ומסרב לבקשה גדולה מ־1 MiB לפני הקצאת payload. לקוח שקיבל Accepted יכול לברר מצב אחרי הפעלה מחדש. ניתוק לקוח אינו מבטל עבודה שכבר התקבלה. אלו החלטות מוצר שנוגעות ישירות לבעלות, לא רק לסעיף תיעוד.
נחשב תקציב התחלתי לפני שנכתוב שרת. הקוד הבא הוא תוכנית מלאה שמפרידה זיכרון של בקשות ממתינות, בקשות בעיבוד ותקציב קבוע של השירות.
fn maximum_payload_memory(queue: usize, workers: usize, max_job: usize)
-> Option<usize>
{
queue.checked_add(workers)?.checked_mul(max_job)
}
fn main() {
let payload = maximum_payload_memory(32, 4, 1024 * 1024).unwrap();
let fixed = 16 * 1024 * 1024usize;
let planned = payload.checked_add(fixed).unwrap();
println!("payload={payload}, planned={planned}");
assert_eq!(payload, 36 * 1024 * 1024);
}
החישוב עדיין חסר אם כל connection מחזיק גם buffer לפני admission, אם retry משכפל payload או אם ה־journal cache מחזיק עותק נוסף. זאת בדיוק מטרתו: לחשוף מי מחזיק נתונים בכל מצב. נצייר מסלול מן socket אל parser, תור, worker, commit ותוצאה. בכל חץ נציין אם עבר ownership או נוצר עותק, ומתי מותר לשחרר את האחסון הקודם.
הגבול בין crates מאפשר להפריד חוזים ולבדוק רכיבים, אבל אינו גבול הגנה מפני קוד לא מהימן בתוך אותו תהליך. גבול תהליך מאפשר הרשאות, מרחב כתובות ותקציב נפרדים, במחיר serialization ותיאום כשלים. אם worker מפענח פורמט מורכב שאינו מהימן, נרצה לבדוק אם יש הצדקה להוציאו לתהליך עם capabilities מצומצמות. אם כל העבודה היא סכום byte-ים קטן, חלוקה כזאת עשויה להיות יקרה יותר מן התועלת שלה.
נגדיר בעלים אחד של היומן. ה־worker מחשב תוצאה, אבל אינו מחליט לבדו שהלקוח קיבל אישור עמיד. רכיב האחסון מבצע את commit ומחזיר תוצאה שמבדילה כתיבה לזיכרון, כתיבה ל־kernel והשלמת durability לפי החוזה. שכבת הפרוטוקול יכולה אז להחליט איזו תשובה מותר לשלוח. כך אין צורך שכל worker יבין מחדש את כללי fsync ו־directory synchronization.
ה־latency נמדד בנקודות ברורות. זמן בקשה מן הלקוח כולל המתנה ברשת ובתור, בעוד זמן worker יכול להיות קטן בהרבה. עבור יעד p99 נגדיר עומס קבלה, גודל קלט, חומרה, מצב cache וחלון מדידה. דרישה כמו "פחות מ־20 מילישניות" בלי תנאים אינה מאפשרת לבדוק אם המערכת עומדת בה. נגדיר גם התנהגות ברוויה: תשובת Busy מדודה יכולה להיות תקינה יותר מהמתנה בלתי מוגבלת.
ניצור טבלת עקיבות שבה לכל דרישה יש רכיב, invariant ובדיקת קבלה. עבור "אין איבוד עבודה מאושרת" הראיה היא ניסוי קריסה אחרי כל גבול commit והתאוששות. עבור "אין גידול בלתי מוגבל" הראיה היא חישוב תקציב ובדיקת עומס שמדגימה rejection. עבור "worker אינו קורא סוד של השירות" נרצה בדיקת שלילה בתוך סביבת ההרשאות שלו. התיעוד על cgroup v2 נותן מנגנון אכיפה, אבל החוזה של המוצר צריך להיכתב לפני שנבחר ערך ל־memory.max.
נבחן תרחיש קונקרטי: ארבעת ה־workers עסוקים, התור מלא ולקוח נוסף שולח header שמצהיר על MiB אחד. אם נקרא את כל הגוף לפני בדיקת capacity, כל לקוח נוסף יכול להוסיף עוד MiB למרות שהתור נשאר בגודל 32. התיקון הוא להחליט אחרי header קטן אם להקצות גוף, ולסגור או להחזיר Busy לפי הפרוטוקול כאשר אין permit. אם רוצים לאפשר ללקוח להמתין, גם מספר הממתינים וה־header-ים שלהם צריך להיות מוגבל. כך מסמך הארכיטקטורה הופך לשאלה שניתן לענות עליה בקוד: באיזו שורה נרכש התקציב, ובאיזו שורה הוא חוזר כאשר הלקוח נעלם באמצע הגוף.