Menu

מצב WAL ומקביליות ב-SQLite: קוראים, כותבים ו-Checkpoints

איך ה-write-ahead logging של SQLite משנה את תמונת המקביליות: קוראים וכותבים מפסיקים לחסום זה את זה, ומה בעצם עושים קובצי ה-wal וה-shm.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

מצב ברירת המחדל והמגבלות שלו

כברירת מחדל, SQLite משתמש ב-rollback journal. כשכותבים, SQLite מעתיק את הדפים המקוריים לקובץ -journal, משנה את מסד הנתונים הראשי, ומוחק את ה-journal בזמן ה-commit. אם התהליך קורס באמצע כתיבה, ה-journal מופעל לאחור כדי לבטל את השינוי החלקי.

זה פשוט ובטוח, אבל יש לזה תכונה כואבת אחת: כותבים וקוראים נלחמים על אותו קובץ. כל עוד כותב מחזיק את נעילת מסד הנתונים, אף קורא לא יכול להתחיל טרנזקציה חדשה. כל עוד יש קוראים פעילים, הכותב מחכה. באפליקציה עמוסה, נניח שרת ווב עם כמה בקשות מקביליות, תראו שגיאות SQLITE_BUSY מהר יותר ממה שהייתם רוצים.

מצב WAL משנה את זה.

מה WAL עושה בפועל

Write-ahead logging הופך את המודל. במקום לשנות את קובץ מסד הנתונים הראשי במקום, הכותב מוסיף דפים שנשמרו לקובץ נפרד עם הסיומת -wal. קוראים ממשיכים לקרוא את הקובץ הראשי, אבל הם גם מציצים ב-WAL כדי לראות גרסאות חדשות יותר של דפים שהם צריכים.

התוצאה: כותב אחד וכל מספר של קוראים יכולים להיות פעילים באותו רגע. כל קורא רואה תמונת מצב עקבית מהרגע שבו הטרנזקציה שלו התחילה, והכותב עסוק בהוספה ל-WAL בלי לגעת במה שהקוראים מסתכלים עליו.

ה-pragma היחיד הזה מעביר את מסד הנתונים למצב WAL. המצב קבוע: הוא נשמר בכותרת הקובץ, כך שכל חיבור עתידי מקבל WAL אוטומטית. לא צריך להריץ את זה בכל חיבור, רק פעם אחת כשמקימים את מסד הנתונים (או במריץ המיגרציות שלכם).

ה-pragma מחזיר את המצב החדש. אם הוא מחזיר wal, הכול מוכן. אם הוא מחזיר משהו אחר, כנראה שמערכת הקבצים לא תומכת בזיכרון משותף (עוד על זה בהמשך).

הפעלה ואימות

אפשר לוודא מה המצב הנוכחי בכל רגע:

הקריאה הראשונה מפעילה WAL ומחזירה את המצב החדש. הקריאה השנייה (בלי =) רק שואלת עליו. אחרי זה, התיקייה של messages.db תכיל שלושה קבצים כשיש פעילות: messages.db, messages.db-wal ו-messages.db-shm. שני האחרונים מופיעים ונעלמים לפי השאלה אם יש חיבורים פתוחים.

הקבצים -wal ו-shm

עם WAL מגיעים שני קבצים נוספים, וכדאי לדעת מה הם עושים:

  • -wal מכיל טרנזקציות שנשמרו ועוד לא מוזגו בחזרה למסד הנתונים הראשי. הוא גדל כשיש כתיבות ומתכווץ (או מתאפס) בזמן checkpoint.
  • -shm הוא קובץ זיכרון משותף. זה אינדקס לתוך ה-WAL, כך שכל החיבורים מסכימים איזה דף נמצא איפה בלי לסרוק את ה-WAL בכל שאילתה.

ההשלכה המעשית: אף פעם אל תעתיקו מסד נתונים במצב WAL על ידי העתקה של קובץ ה-.db בלבד. הנתונים העדכניים ביותר נמצאים ב--wal, ובלעדיו העותק שלכם ישן או פגום. או שתעתיקו את שלושת הקבצים כשאף חיבור לא כותב, או, הרבה יותר טוב, השתמשו ב-backup API של SQLite (מוסבר בפרק הבא).

מקביליות: כותב אחד, קוראים רבים

WAL לא נותן כתיבות מקביליות. SQLite עדיין מבצע אותן בזו אחר זו: בכל רגע, בדיוק טרנזקציה אחת מחזיקה את נעילת הכתיבה. מה שהשתנה הוא שכתיבות לא חוסמות קריאות וקריאות לא חוסמות כתיבות.

אז אפליקציית ווב טיפוסית שרצה על WAL מתנהגת כך:

  • נקודות קצה שעיקרן קריאה רצות במקביל בלי תחרות.
  • נקודות קצה של כתיבה עומדות בתור לזמן קצר זו אחרי זו, אבל לא חוסמות קריאות.
  • קוראים ארוכים (שאילתות אנליטיקה, ייצואים) לא גורמים לכותבים לחכות.

אם שני חיבורים מנסים לכתוב באותו זמן, השני מקבל SQLITE_BUSY. הפתרון הוא בדרך כלל busy timeout הגיוני: להגיד ל-SQLite לחכות קצת לפני שהוא מוותר:

busy_timeout=5000 פירושו "אם נעילה תפוסה, חכה עד 5 שניות לפני שאתה מעלה שגיאה". בשילוב עם WAL, זה מטפל בתחרות שרוב האפליקציות באמת פוגשות. הצורה BEGIN IMMEDIATE לוקחת את נעילת הכתיבה בתחילת הטרנזקציה במקום בכתיבה הראשונה, וכך נמנעת קבוצה של קיפאונות שדרוג כשכמה חיבורים מתכוונים לכתוב.

Checkpoints: קיפול ה-WAL בחזרה

קובץ ה-WAL לא יכול לגדול לנצח. Checkpointing הוא התהליך של לקיחת הדפים שנשמרו ב-WAL, כתיבתם למסד הנתונים הראשי, ואיפוס ה-WAL.

SQLite מבצע checkpoint אוטומטית כשה-WAL עובר בערך 1000 דפים (ברירת המחדל של wal_autocheckpoint). ברוב האפליקציות אפשר להשאיר את זה כמו שזה. אם רוצים לכוונן את זה או להפעיל checkpoint ידנית:

ה-pragma wal_checkpoint מקבל מצב:

  • PASSIVE: checkpoint של כמה שיותר בלי להפריע לקוראים או לכותבים. ברירת המחדל.
  • FULL: מחכה שכותבים פעילים יסיימו, ואז מבצע checkpoint לכל מה שנשמר.
  • RESTART: כמו FULL, ובנוסף חוסם קוראים חדשים מלהשתמש ב-WAL הישן.
  • TRUNCATE: כמו RESTART, ובנוסף מכווץ את קובץ ה-WAL חזרה לאפס בייטים.

רוב השרתים אף פעם לא צריכים לקרוא לזה ידנית. אם אתם משחררים אפליקציית דסקטופ שרוצה לשמור על גודל קבצים מסודר בזמן סגירה, checkpoint מסוג TRUNCATE לפני סגירת החיבור האחרון הוא הרגל סביר.

כמה Pragmas שמשתלבים טוב עם WAL

WAL לבדו הוא טוב. WAL עם עוד כמה הגדרות הוא בדרך כלל מה שאפליקציות בפרודקשן משתמשות בו:

סיור קצר:

  • synchronous=NORMAL הוא השילוב המומלץ עם WAL. הוא בטוח מפני קריסות של האפליקציה ושל מערכת ההפעלה. רק הפסקת חשמל ברגע הלא נכון יכולה לאבד את הטרנזקציות האחרונות, וגם אז מסד הנתונים נשאר עקבי. ברירת המחדל FULL בטוחה יותר אבל איטית יותר באופן מורגש.
  • את busy_timeout כיסינו קודם.
  • foreign_keys=ON לא קשור ל-WAL, אבל כדאי להגדיר אותו בכל חיבור: SQLite משאיר את אכיפת המפתחות הזרים כבויה כברירת מחדל לשם תאימות לאחור.

ההגדרות האלה הן לכל חיבור בנפרד (חוץ מ-journal_mode, שנשאר). הריצו אותן מיד אחרי פתיחת החיבור בקוד האפליקציה.

מתי WAL הוא לא הבחירה הנכונה

WAL הוא ההמלצה ברירת המחדל, אבל יש כמה מצבים שמתנגדים:

  • מערכות קבצים ברשת. WAL מסתמך על זיכרון משותף (mmap) בין תהליכים שניגשים למסד הנתונים. NFS, SMB ודומיהם לא תומכים בזה באמינות. אם מסד הנתונים שלכם נמצא על כונן רשת משותף, הישארו עם ה-rollback journal, או, עדיף, אל תשימו SQLite על כונן רשת משותף.
  • מדיה לקריאה בלבד. WAL צריך לכתוב את הקבצים -wal ו--shm. מסד נתונים על CD-ROM או מדיה דומה חייב להשתמש במצב journal שלא כותב (או להיפתח לקריאה בלבד עם mode=ro).
  • משימות אצווה עם כותב יחיד ובלי קוראים מקביליים. WAL לא יזיק, אבל גם לא תרוויחו כלום. ה-rollback journal של ברירת המחדל בסדר.

ל-95% מהאפליקציות, כמו צד שרת של אתרים, אפליקציות דסקטופ, אפליקציות מובייל ומכשירים משובצים עם אחסון מקומי, WAL הוא הבחירה הנכונה.

הגדרה מציאותית

הנה הצורה שרוב ההגדרות של SQLite בפרודקשן לובשות, מכווצת ל-pragmas שאפשר להריץ:

temp_store=MEMORY שומר טבלאות ואינדקסים זמניים ב-RAM במקום על הדיסק: רווח קטן שבא בחינם אם יש לכם זיכרון פנוי.

חברו את זה פעם אחת בזמן החיבור, בהגדרת מסד הנתונים של האפליקציה, וטיפלתם ברוב מה שאפליקציה מבוססת SQLite צריכה כדי להתנהג טוב תחת עומס מקבילי.

הבא בתור: גיבוי ושחזור

עכשיו שלמסד הנתונים שלכם יש קבצים נלווים -wal ו--shm, העתקת הקובץ היא כבר לא אסטרטגיית גיבוי בטוחה. הפרק הבא מכסה את הדרך הנכונה לגבות מסד נתונים חי של SQLite: הפקודה .backup, ה-online backup API, ומה לעשות כשצריך תמונת מצב עקבית בלי להוריד את האפליקציה.

שאלות נפוצות

מה זה מצב WAL ב-SQLite?

WAL הוא קיצור של write-ahead logging. במקום לכתוב שינויים ישירות לקובץ מסד הנתונים הראשי ולהשתמש ב-rollback journal כדי לבטל אותם במקרה של כשל, SQLite מוסיף את השינויים לקובץ -wal נפרד וממזג אותם בחזרה מדי פעם. הרווח הגדול הוא מקביליות: קוראים וכותב אחד יכולים לעבוד באותו זמן בלי לחסום זה את זה.

איך מפעילים מצב WAL ב-SQLite?

מריצים PRAGMA journal_mode=WAL; פעם אחת. ההגדרה קבועה: היא נשמרת בכותרת של קובץ מסד הנתונים, כך שגם חיבורים עתידיים ישתמשו ב-WAL אוטומטית. לא צריך להגדיר אותה בכל חיבור. ה-pragma מחזיר את המצב החדש (wal) כשהוא מצליח.

האם מצב WAL מאפשר כתיבות מקביליות?

לא: SQLite עדיין מבצע את הכתיבות בזו אחר זו. רק כותב אחד יכול להחזיק את נעילת הכתיבה בכל רגע. מה ש-WAL כן משנה הוא שקוראים כבר לא חוסמים את הכותב, והכותב כבר לא חוסם קוראים. ברוב האפליקציות זה צוואר הבקבוק שבאמת היה חשוב.

מה הם הקבצים -wal ו-shm?

הקובץ -wal מכיל שינויים שנשמרו אבל עוד לא מוזגו בחזרה למסד הנתונים הראשי. הקובץ -shm הוא אינדקס קטן בזיכרון משותף שעוזר לחיבורים למצוא דפים בתוך ה-WAL במהירות. שניהם נוצרים מחדש אוטומטית, אבל אם מעתיקים מסד נתונים, חייבים להעתיק אותם יחד או להשתמש ב-backup API.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל