עמידות ופורמט נתונים¶
נניח שהשרת כתב תשובה "נשמר" ללקוח ואז המחשב נעצר. לאחר האתחול הרשומה חסרה. השאלה הראשונה היא מה בדיוק הבטיחה התשובה: שהבקשה נכנסה לתור, ש־WriteFile הסתיימה או שהמידע הגיע לנקודת עמידות שהמערכת הגדירה. בלי חוזה כזה אי אפשר להכריע אם ההתנהגות היא תקלה.
נבנה יומן כתיבה מקדים, או WAL. הרעיון הוא לתעד שינוי בפורמט שניתן לבדיקה לפני שמאשרים אותו, ולהחליט בזמן recovery אילו עסקאות שלמות. היומן אינו קסם שמבטל כשל אחסון; הוא פרוטוקול שמחבר bytes, סדר כתיבה ותשובה ללקוח.
פורמט שאינו תלוי במבנה C++¶
לא נכתוב struct גולמי לקובץ. יישור, padding, רוחב טיפוסים וסדר בתים עלולים להשתנות. נגדיר רשומה בת שלושים ושניים בתים עם שדות מפורשים, ונחשב checksum על התוכן שלפני שדה הבדיקה.
התוכנית הבאה בונה רשומה ובודקת שינוי בית וקטיעה. היא מממשת CRC32 לצורכי זיהוי פגמים מקריים, ואינה מנגנון אימות נגד שינוי מכוון:
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <span>
using Record = std::array<std::byte, 32>;
std::uint32_t crc32(std::span<const std::byte> bytes) {
std::uint32_t crc = 0xffffffffu;
for (auto byte : bytes) {
crc ^= std::to_integer<std::uint8_t>(byte);
for (int bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u)));
}
return ~crc;
}
void put32(Record& record, std::size_t offset, std::uint32_t value) {
for (unsigned i = 0; i < 4; ++i)
record[offset + i] = std::byte((value >> (i * 8)) & 0xffu);
}
std::uint32_t get32(std::span<const std::byte> bytes, std::size_t offset) {
std::uint32_t value = 0;
for (unsigned i = 0; i < 4; ++i)
value |= std::uint32_t(std::to_integer<unsigned char>(
bytes[offset + i])) << (i * 8);
return value;
}
Record encode(std::uint32_t sequence, std::uint32_t key,
std::uint32_t value, unsigned char kind) {
Record record{};
record[0] = std::byte{'A'};
record[1] = std::byte{'W'};
record[2] = std::byte{'L'};
record[3] = std::byte{'1'};
record[4] = std::byte{kind};
record[5] = std::byte{1};
record[6] = std::byte{32};
put32(record, 8, sequence);
put32(record, 16, key);
put32(record, 20, value);
put32(record, 28, crc32(std::span(record).first(28)));
return record;
}
bool valid(std::span<const std::byte> bytes) {
if (bytes.size() != 32) return false;
if (bytes[0] != std::byte{'A'} || bytes[1] != std::byte{'W'} ||
bytes[2] != std::byte{'L'} || bytes[3] != std::byte{'1'} ||
bytes[5] != std::byte{1} || bytes[6] != std::byte{32} ||
bytes[7] != std::byte{0}) return false;
const auto kind = std::to_integer<unsigned char>(bytes[4]);
if (kind != 1 && kind != 2) return false;
for (std::size_t i : {12u, 13u, 14u, 15u, 24u, 25u, 26u, 27u})
if (bytes[i] != std::byte{0}) return false;
return get32(bytes, 28) == crc32(bytes.first(28));
}
int main() {
auto record = encode(1, 7, 99, 1);
assert(valid(record));
assert(get32(record, 16) == 7);
record[20] ^= std::byte{1};
assert(!valid(record));
assert(!valid(std::span(record).first(17)));
}
הגרסה הזו משתמשת במספר רצף בן שלושים ושניים ביט ומסרבת במימוש היומן להתקדם מעבר לטווח שלו. השדות השמורים חייבים להיות אפס בגרסה הנוכחית, כדי ששינוי פורמט לא יתפרש בטעות כגרסה ישנה תקינה. בדיקת האורך קודמת לכל גישה לשדה.
עסקה מורכבת משני שלבים לוגיים¶
נגדיר רשומת data עם סוג אחד ורשומת commit עם סוג שתיים ואותו מספר רצף. במעבדה נאפשר עסקה אחת פעילה בכל רגע. בזמן recovery מחילים שינוי רק כאשר שתי הרשומות תקינות, מתאימות ומופיעות לפי הפרוטוקול.
במסלול הכתיבה כותבים את רשומת הנתונים, אחריה את רשומת ה־commit, ואז מבצעים FlushFileBuffers ומחכים להצלחה לפני תשובת אישור. לולאת כתיבה צריכה לטפל במספר בתים קטן מן המבוקש ולא להניח ש־WriteFile אחת תמיד מספיקה. כשל flush הוא מצב שבו אין לנו רשות לאשר עמידות, גם אם חלק מן המידע אולי כבר נשמר.
החוזה מניח שה־filesystem והתקן האחסון מכבדים את בקשות ההתמדה הרלוונטיות. תצורת cache שאינה מוגנת או אינה מכבדת flush משנה את ההבטחה. אין ב־checksum יכולת לשחזר bytes שאבדו אחרי שההתקן דיווח על הצלחה בניגוד לחוזה.
אטומיות ועמידות הן תכונות שונות¶
אטומיות אומרת שהשינוי הלוגי מוחל בשלמותו או לא מוחל. עמידות אומרת ששינוי שאושר נשמר מול סוגי הכשל שהוגדרו. אפשר לבנות פורמט שמזהה עסקה חלקית בלי להבטיח שעסקה שאושרה באמת שרדה, ואפשר לשמור bytes רבים בלי לדעת לאיזו עסקה הם שייכים.
בזמן recovery נבחין בין סיומת חלקית שנוצרה מקריסה בזמן append לבין פגם באמצע יומן שאמור להכיל עסקאות מאושרות. השלכת כל מה שאחרי כל checksum פגום יכולה להסתיר אובדן של מידע מאושר. מדיניות מעבדה יכולה לקבל סיומת לא שלמה, אך צריכה לדווח על השחתה פנימית ולא להכריז על recovery מוצלח ללא תנאי.
אישור שהלך לאיבוד¶
נניח שה־flush הצליח והשרת קרס לפני שהלקוח קיבל תשובה. העסקה עמידה, אבל הלקוח אינו יודע זאת. ניסיון חוזר יכול לבצע שינוי פעמיים אם אין מזהה בקשה ומדיניות idempotency. הפרק האחרון בנושא IPC יחבר את היומן לתוצאה שהלקוח רשאי להסיק.
הניסוי הבטוח הוא קריסת תהליך עובד במעבדה אחרי נקודות מוגדרות, תוך שמירת קבצי בדיקה חדשים. קריסת תהליך אינה מדמה בהכרח אובדן חשמל, ולכן הדוח צריך להפריד בין סוגי הכשל. אין צורך לנתק כונן או לסכן נתונים אמיתיים.
קריאה להעמקה¶
התיעוד של העברת מידע מאוגד לאחסון ושל FlushFileBuffers מגדיר את פעולת המערכת. פרוטוקול הרשומות וה־recovery הוא באחריות היישום.