מאגרי כתיבה וגבול העמידות¶
נניח שהתוכנית כתבה קובץ, קיבלה הצלחה, ונהרגה. בהרצה הבאה הקובץ קיים ולכן אנחנו מסיקים שהמידע עמיד. הניסוי הזה בדק רק חלק מהמערכת: הקרנל וההתקן המשיכו לעבוד אחרי מות התהליך, וייתכן שהשלימו כתיבות שהיו עדיין במטמון. כדי להגדיר durability צריך לציין איזה כשל אנחנו רוצים לשרוד ובאיזו נקודה אנחנו מודיעים על הצלחה.
נפריד בין כמה מאגרים. הטיפוס BufWriter יכול להחזיק בייטים בזיכרון התהליך בלי להעבירם לקרנל. אחרי כתיבה לקרנל, הנתונים יכולים להיות ב־page cache. בהמשך הם עוברים דרך מערכת הקבצים ושכבת ההתקן, שגם בה יכולים להיות מטמונים ותורי עבודה. המילה flush מופיעה בכמה שכבות, אבל אינה מתארת אותה התחייבות בכל אחת.
התוכנית הבאה יוצרת תיקיית תרגיל חדשה וקובץ חדש, כותבת דרך BufWriter ומבצעת סנכרון מפורש. היא מיועדת למערכת קבצים מקומית שתומכת בחוזי הסנכרון הנדרשים.
use std::{fs::{self, File, OpenOptions}, io::{self, BufWriter, Write}};
fn main() -> io::Result<()> {
fs::create_dir("durability-lab")?;
let file = OpenOptions::new().write(true).create_new(true)
.open("durability-lab/state")?;
let mut writer = BufWriter::with_capacity(8192, file);
writer.write_all(b"generation=1\nvalue=ready\n")?;
writer.flush()?;
writer.get_ref().sync_all()?;
File::open("durability-lab")?.sync_all()?;
File::open(".")?.sync_all()?;
println!("data and newly created names synchronized");
Ok(())
}
הקריאה flush דוחפת את הבאפר של האפליקציה אל הכותב שמתחתיו. היא אינה לבדה בקשת סנכרון של הקובץ לאחסון. הקריאה sync_all על הקובץ מבקשת לסנכרן תוכן ומטא־נתונים בהתאם לחוזה המערכת. סנכרון הספרייה מטפל בשם הקובץ שנוצר בתוכה, וסנכרון הספרייה ההורית בדוגמה מטפל גם בשם תיקיית הניסוי החדשה.
הבחירה ב־sync_data במקום sync_all יכולה לצמצם עבודה על metadata שאינו נחוץ כדי לקרוא את התוכן. היא אינה מבטלת את הצורך לשמור גודל קובץ כאשר הוא חלק מהמידע הנחוץ לקריאה. גם במקרה הזה שם חדש בספרייה הוא אחריות נפרדת, ולכן אין להסיק שסנכרון הקובץ לבדו מפרסם שם עמיד.
ניסוי strace יכול להראות את סדר הקריאות, אבל אינו מוכיח שההתקן באמת שמר נתונים מול אובדן חשמל. הוא מוכיח שהאפליקציה ביקשה את הפעולות בסדר שראינו. בדיקות מערכת כוללות גם את מערכת הקבצים, אפשרויות mount, שכבת הווירטואליזציה וההתקן. אם שכבה מדווחת השלמה בלי לקיים את החוזה שלה, תוכנית המשתמש אינה יכולה לתקן זאת באמצעות שם אחר לאותה קריאה.
שגיאה מ־fsync אינה אומרת שכל הכתיבה התבטלה. חלק מהנתונים יכולים להיות עמידים, חלק לא, וייתכן שאין לנו דרך לדעת מה בדיוק נשמר. לכן מצב העסקה אחרי כשל קלט ופלט עשוי להיות לא ידוע. החזרת שגיאה למשתמש חשובה, אבל אם המשתמש ינסה שוב פעולה עסקית שאינה idempotent נצטרך גם פרוטוקול זיהוי ושחזור.
גם ניסיון סנכרון שני שמצליח אינו בהכרח מוחק את משמעות השגיאה הראשונה. ייתכן שהמערכת כבר איבדה מידע שהיה אמור להיכתב, והקריאה השנייה אינה כותבת מחדש את כל התוכן שנעלם. מערכת שמחייבת עמידות צריכה להגדיר מעבר למצב כשל, עצירת אישורים חדשים ובדיקת התאוששות, במקום להמשיך כאילו קיבלנו רק הפרעה זמנית חסרת משמעות.
ההשמדה של BufWriter מנסה לטפל בבאפר לפי החוזה שלה, אבל אינה ערוץ מתאים לדיווח על כשל כתיבה למשתמש. לכן אין להסתמך על יציאה מה־scope כתחליף ל־flush מפורש כאשר ההצלחה צריכה להיות נצפית. גם Drop של File אינו נותן לנו תוצאת סנכרון עמידות.
לשם מדידה נפריד בין latency של קבלת בייטים, latency של flush מהאפליקציה ו־latency של התחייבות עמידות. אפשר לקבץ כמה פעולות לפני סנכרון כדי לחלק את העלות, אבל אז צריך לומר אילו פעולות עדיין לא אושרו. ביצועים טובים שמתקבלים על ידי אישור מוקדם יותר הם שינוי חוזה, לא רק אופטימיזציה פנימית.
החוזה של סנכרון קובץ וספרייה מופיע בעמוד fsync, והבאפר של ראסט מתועד בתיעוד BufWriter. בכל פרוטוקול כתיבה נסמן את הרגע שבו מותר להחזיר למשתמש אישור עמיד.