אני, חוקר את היציבות של אחסון נתונים במערכות ענן, החלטתי לבדוק את עצמי, כדי לוודא שאני מבין את הדברים הבסיסיים. אני על מנת להבין אילו ערבויות לגבי התמדה של נתונים (כלומר, ערבויות שהנתונים יהיו זמינים לאחר כשל במערכת) תן לנו דיסקי NMVe. הסקתי את המסקנות העיקריות הבאות: אתה צריך לשקול את הנתונים שנפגעו מרגע מתן פקודת כתיבת הנתונים ועד לרגע כתיבתם לאמצעי האחסון. עם זאת, ברוב התוכניות, קריאות מערכת משמשות בצורה בטוחה למדי לכתיבת נתונים.
במאמר זה, אני חוקר את מנגנוני ההתמדה המסופקים על ידי ממשקי API של קבצי לינוקס. נראה שהכל צריך להיות פשוט כאן: התוכנית קוראת לפקודה write(), ולאחר השלמת פעולת הפקודה הזו, הנתונים יאוחסנו בצורה מאובטחת בדיסק. אבל write() מעתיק רק נתוני יישום למטמון הליבה שנמצא ב-RAM. על מנת לאלץ את המערכת לכתוב נתונים לדיסק, יש להשתמש בכמה מנגנונים נוספים.
באופן כללי, החומר הזה הוא אוסף של הערות המתייחסות למה שלמדתי בנושא שמעניין אותי. אם נדבר בקצרה על החשוב ביותר, מתברר שכדי לארגן אחסון נתונים בר-קיימא, אתה צריך להשתמש בפקודה fdatasync() או לפתוח קבצים עם דגל O_DSYNC. אם אתה מעוניין ללמוד עוד על מה שקורה לנתונים בדרך מקוד לדיסק, תסתכל על מאמר.
תכונות של שימוש בפונקציה write().
שיחת מערכת write() מוגדר בתקן כניסיון לכתוב נתונים לתיאור קובץ. לאחר סיום עבודה מוצלח write() פעולות קריאת נתונים חייבות להחזיר בדיוק את הבתים שנכתבו קודם לכן, לעשות זאת גם אם הגישה לנתונים מתבצעת מתהליכים או שרשורים אחרים ( הסעיף המקביל בתקן POSIX). , בסעיף על האינטראקציה של שרשורים עם פעולות קבצים רגילות, יש הערה שאומרת שאם שני שרשורים כל אחד מתקשרים לפונקציות האלה, אז כל קריאה חייבת לראות את כל ההשלכות המצוינות שביצוע השיחה האחרת מוביל אליה, או לא רואה בכלל אין השלכות. זה מוביל למסקנה שכל פעולות ה-I/O של הקבצים חייבות להחזיק נעילה על המשאב שעליו עובדים.
האם זה אומר שהמבצע write() הוא אטומי? מנקודת מבט טכנית, כן. פעולות קריאת נתונים חייבות להחזיר את כל מה שנכתב או אף אחד ממנו write(). אבל המבצע write(), בהתאם לתקן, לא חייבת להסתיים, לאחר שרשמה את כל מה שהתבקשה לרשום. מותר לכתוב רק חלק מהנתונים. לדוגמה, ייתכן שיש לנו שני זרמים שמוסיפים כל אחד 1024 בתים לקובץ המתואר על ידי אותו מתאר קובץ. מנקודת המבט של התקן, התוצאה תהיה מקובלת כאשר כל אחת מפעולות הכתיבה יכולה לצרף בית אחד בלבד לקובץ. פעולות אלו יישארו אטומיות, אך לאחר שהן יסתיימו, הנתונים שהם כותבים לקובץ יתבלבלו. דיון מעניין מאוד בנושא זה ב-Stack Overflow.
פונקציות fsync() ו-fdatasync().
הדרך הקלה ביותר לשטוף נתונים לדיסק היא לקרוא לפונקציה . פונקציה זו מבקשת ממערכת ההפעלה להעביר את כל הבלוקים ששונו מהמטמון לדיסק. זה כולל את כל המטא נתונים של הקובץ (זמן גישה, זמן שינוי קובץ וכן הלאה). אני מאמין שיש צורך במטא נתונים אלה לעתים רחוקות, אז אם אתה יודע שזה לא חשוב לך, אתה יכול להשתמש בפונקציה fdatasync(). בתוך על fdatasync() הוא אומר שבמהלך הפעולה של פונקציה זו, כמות כזו של מטא נתונים נשמרת בדיסק, שהיא "הכרחי לביצוע נכון של פעולות קריאת הנתונים הבאות." וזה בדיוק מה שחשוב לרוב האפליקציות.
בעיה אחת שיכולה להתעורר כאן היא שמנגנונים אלו אינם מבטיחים שניתן למצוא את הקובץ לאחר תקלה אפשרית. בפרט, כאשר נוצר קובץ חדש, יש להתקשר fsync() עבור הספרייה שמכילה אותו. אחרת, לאחר קריסה, עלול להתברר שהקובץ הזה לא קיים. הסיבה לכך היא שתחת UNIX, עקב השימוש בקישורים קשיחים, קובץ יכול להתקיים במספר ספריות. לכן, כשמתקשרים fsync() אין דרך לקובץ לדעת אילו נתוני ספרייה צריכים גם להיות מרוחק לדיסק ( אתה יכול לקרוא עוד על זה). נראה שמערכת הקבצים ext4 מסוגלת להחיל fsync() לספריות המכילות את הקבצים המתאימים, אך ייתכן שזה לא המקרה במערכות קבצים אחרות.
ניתן ליישם מנגנון זה בצורה שונה במערכות קבצים שונות. השתמשתי כדי ללמוד אילו פעולות דיסק משמשות במערכות קבצים ext4 ו-XFS. שניהם מוציאים את פקודות הכתיבה הרגילות לדיסק הן עבור תוכן הקבצים והן עבור יומן מערכת הקבצים, שוטפים את המטמון ויוצאים על ידי ביצוע FUA (Force Unit Access, כתיבת נתונים ישירות לדיסק, עקיפת המטמון) כתיבה ליומן. הם כנראה עושים בדיוק את זה כדי לאשר את עובדת העסקה. בכוננים שאינם תומכים ב-FUA, הדבר גורם לשתי שטיפות מטמון. הניסויים שלי הראו את זה fdatasync() קצת יותר מהר fsync(). תוֹעֶלֶת blktrace מעיד על כך fdatasync() בדרך כלל כותב פחות נתונים לדיסק (ב-ext4 fsync() כותב 20 KiB, ו fdatasync() - 16 KiB). כמו כן, גיליתי ש-XFS מעט מהיר יותר מ-ext4. וכאן בעזרת העזרה blktrace הצליח לגלות זאת fdatasync() שוטף פחות נתונים לדיסק (4 KiB ב-XFS).
מצבים מעורפלים בעת שימוש ב-fsync()
אני יכול לחשוב על שלושה מצבים לא ברורים fsync()שבה נתקלתי בפועל.
התקרית הראשונה כזו התרחשה ב-2008. באותו זמן, ממשק Firefox 3 "קפא" אם מספר רב של קבצים נכתב לדיסק. הבעיה הייתה שהטמעת הממשק השתמשה במסד נתונים של SQLite כדי לאחסן מידע על מצבו. לאחר כל שינוי שהתרחש בממשק, הפונקציה נקראה fsync(), מה שנתן ערבויות טובות לאחסון נתונים יציב. במערכת הקבצים ext3 שהייתה בשימוש אז, הפונקציה fsync() שטף לדיסק את כל הדפים ה"מלוכלכים" במערכת, ולא רק אלו שהיו קשורים לקובץ המתאים. משמעות הדבר היא שלחיצה על כפתור בפיירפוקס עלולה לגרום לכתיבת מגה-בייט של נתונים לדיסק מגנטי, מה שעלול להימשך שניות רבות. הפתרון לבעיה, למיטב הבנתי החומר, היה להעביר את העבודה עם מסד הנתונים למשימות רקע אסינכרוניות. משמעות הדבר היא שפיירפוקס נהג ליישם דרישות קשיחות אחסון מחמירות יותר ממה שהיה נחוץ באמת, ותכונות מערכת הקבצים ext3 רק החריפו את הבעיה הזו.
הבעיה השנייה אירעה ב-2009. לאחר מכן, לאחר קריסת מערכת, משתמשים במערכת הקבצים החדשה ext4 גילו שהרבה קבצים שזה עתה נוצרו היו באורך אפס, אבל זה לא קרה עם מערכת הקבצים ext3 הישנה יותר. בפסקה הקודמת, דיברתי על איך ext3 זרק יותר מדי נתונים על הדיסק, מה שהאט מאוד את הקצב. fsync(). כדי לשפר את המצב, ext4 שוטפת רק את אותם דפים "מלוכלכים" שרלוונטיים לקובץ מסוים. והנתונים של קבצים אחרים נשארים בזיכרון הרבה יותר זמן מאשר עם ext3. זה נעשה כדי לשפר את הביצועים (כברירת מחדל, הנתונים נשארים במצב זה למשך 30 שניות, אתה יכול להגדיר זאת באמצעות ; אתה יכול למצוא מידע נוסף על זה). משמעות הדבר היא שכמות גדולה של נתונים יכולה ללכת לאיבוד באופן בלתי הפיך לאחר קריסה. הפתרון לבעיה זו הוא להשתמש fsync() ביישומים שצריכים לספק אחסון נתונים יציב ולהגן עליהם ככל האפשר מהשלכות של כשלים. פוּנקצִיָה fsync() עובד הרבה יותר יעיל עם ext4 מאשר עם ext3. החיסרון של גישה זו הוא שהשימוש בה, כמו בעבר, מאט חלק מהפעולות, כמו התקנת תוכניות. ראה פרטים על זה и .
הבעיה השלישית לגבי fsync(), מקורו ב-2018. לאחר מכן, במסגרת פרויקט PostgreSQL, התברר כי אם הפונקציה fsync() נתקל בשגיאה, הוא מסמן דפים "מלוכלכים" כ"נקיים". כתוצאה מכך, השיחות הבאות fsync() לא לעשות כלום עם דפים כאלה. בשל כך, דפים שהשתנו מאוחסנים בזיכרון ולעולם לא נכתבים לדיסק. זהו אסון אמיתי, כי האפליקציה תחשוב שחלק מהנתונים נכתבים לדיסק, אבל למעשה זה לא יהיה. כישלונות כאלה fsync() הם נדירים, היישום במצבים כאלה לא יכול לעשות כמעט דבר כדי להילחם בבעיה. בימים אלה, כשזה קורה, PostgreSQL ויישומים אחרים קורסים. , במאמר "האם יישומים יכולים להתאושש מכשלי fsync?", בעיה זו נחקרת בפירוט. נכון לעכשיו הפתרון הטוב ביותר לבעיה זו הוא להשתמש ב-I/O ישיר עם הדגל O_SYNC או עם דגל O_DSYNC. בגישה זו, המערכת תדווח על שגיאות שעלולות להתרחש בעת ביצוע פעולות כתיבת נתונים ספציפיות, אך גישה זו מחייבת את האפליקציה לנהל את המאגרים בעצמה. קרא עוד על זה и .
פתיחת קבצים באמצעות הדגלים O_SYNC ו-O_DSYNC
נחזור לדיון על מנגנוני לינוקס המספקים אחסון נתונים מתמשך. כלומר, אנחנו מדברים על שימוש בדגל O_SYNC או דגל O_DSYNC בעת פתיחת קבצים באמצעות שיחת מערכת . בגישה זו, כל פעולת כתיבת נתונים מבוצעת כאילו לאחר כל פקודה write() המערכת ניתנת, בהתאמה, פקודות fsync() и fdatasync(). בתוך זה נקרא "השלמה של שלמות קבצי I/O מסונכרנים" ו"השלמת שלמות נתונים". היתרון העיקרי של גישה זו הוא שצריך לבצע רק קריאת מערכת אחת כדי להבטיח שלמות הנתונים, ולא שתיים (לדוגמה - write() и fdatasync()). החיסרון העיקרי של גישה זו הוא שכל פעולות הכתיבה באמצעות מתאר הקובץ המתאים יסונכרנו, מה שיכול להגביל את היכולת לבנות את קוד האפליקציה.
שימוש ב-I/O ישיר עם הדגל O_DIRECT
שיחת מערכת open() תומך בדגל O_DIRECT, אשר נועד לעקוף את המטמון של מערכת ההפעלה, לבצע פעולות I/O, תוך אינטראקציה ישירה עם הדיסק. משמעות הדבר, במקרים רבים, היא שפקודות הכתיבה שיונפקו על ידי התוכנית יתורגמו ישירות לפקודות שמטרתן לעבוד עם הדיסק. אבל, באופן כללי, מנגנון זה אינו תחליף לפונקציות fsync() או fdatasync(). העובדה היא שהדיסק עצמו יכול פקודות מתאימות לכתיבת נתונים. וחמור מכך, במקרים מיוחדים מסוימים, פעולות ה-I/O שבוצעו בעת השימוש בדגל O_DIRECT, לתוך פעולות מאוחסנות מסורתיות. הדרך הקלה ביותר לפתור בעיה זו היא להשתמש בדגל כדי לפתוח קבצים O_DSYNC, מה שיגרום לכך שכל פעולת כתיבה תגרור קריאה fdatasync().
התברר שמערכת הקבצים XFS הוסיפה לאחרונה "נתיב מהיר" עבור O_DIRECT|O_DSYNC-רשומות נתונים. אם הבלוק מוחלף באמצעות O_DIRECT|O_DSYNC, אז XFS, במקום לשטוף את המטמון, יבצע את פקודת הכתיבה FUA אם ההתקן תומך בה. אימתתי זאת באמצעות כלי השירות blktrace במערכת Linux 5.4/Ubuntu 20.04. גישה זו צריכה להיות יעילה יותר, מכיוון שהיא כותבת את כמות הנתונים המינימלית לדיסק ומשתמשת בפעולה אחת, לא שתיים (כתיבה ושטיפה של המטמון). מצאתי קישור ל קרנל 2018 שמיישם את המנגנון הזה. יש דיון מסוים על החלת אופטימיזציה זו על מערכות קבצים אחרות, אך עד כמה שידוע לי, XFS היא מערכת הקבצים היחידה שתומכת בה עד כה.
פונקציית sync_file_range()
ללינוקס יש קריאת מערכת , המאפשר לך לשטוף רק חלק מהקובץ לדיסק, לא את כל הקובץ. קריאה זו יוזמת שטיפה אסינכרונית ואינה ממתינה לסיום. אבל בהתייחסות ל sync_file_range() אומרים שהפקודה הזו היא "מסוכנת מאוד". לא מומלץ להשתמש בו. תכונות וסכנות sync_file_range() מתואר היטב ב חוֹמֶר. בפרט, נראה שהקריאה הזו משתמשת ב- RocksDB כדי לשלוט כאשר הקרנל שוטף נתונים "מלוכלכים" לדיסק. אבל באותו הזמן שם, כדי להבטיח אחסון נתונים יציב, הוא משמש גם fdatasync(). בתוך ל- RocksDB יש כמה הערות מעניינות בנושא זה. לדוגמה, זה נראה כמו השיחה sync_file_range() בעת שימוש ב-ZFS אינו שוטף נתונים לדיסק. הניסיון אומר לי שקוד בשימוש נדיר עשוי להכיל באגים. לכן, הייתי ממליץ לא להשתמש בשיחת מערכת זו אלא אם כן הכרחי.
קריאות מערכת כדי לסייע בהבטחת התמדה של הנתונים
הגעתי למסקנה שיש שלוש גישות שניתן להשתמש בהן לביצוע פעולות I/O מתמשכות. כולם דורשים קריאת פונקציה fsync() עבור הספרייה שבה נוצר הקובץ. אלו הגישות:
- קריאת פונקציה
fdatasync()אוfsync()לאחר פונקציהwrite()(עדיף להשתמשfdatasync()). - עבודה עם מתאר קובץ נפתח עם דגל
O_DSYNCאוO_SYNC(עדיף - עם דגלO_DSYNC). - שימוש בפקודה
pwritev2()עם דגלRWF_DSYNCאוRWF_SYNC(רצוי עם דגלRWF_DSYNC).
הערות ביצועים
לא מדדתי בקפידה את הביצועים של המנגנונים השונים שחקרתי. ההבדלים ששמתי לב אליהם במהירות העבודה שלהם קטנים מאוד. זה אומר שאני יכול לטעות, ושבתנאים אחרים אותו דבר עשוי להראות תוצאות שונות. ראשית, אדבר על מה שמשפיע יותר על הביצועים, ולאחר מכן, על מה שמשפיע פחות על הביצועים.
- החלפת נתוני קובץ מהירה יותר מאשר הוספת נתונים לקובץ (הרווח הביצועים יכול להיות 2-100%). צירוף נתונים לקובץ דורש שינויים נוספים במטא נתונים של הקובץ, גם לאחר קריאת המערכת
fallocate(), אך עוצמת ההשפעה הזו עשויה להשתנות. אני ממליץ, לביצועים הטובים ביותר, להתקשרfallocate()להקצות מראש את השטח הנדרש. אז יש למלא את החלל הזה במפורש באפסים ולקרואfsync(). זה יגרום לסימון הבלוקים המתאימים במערכת הקבצים כ"מוקצים" במקום "לא מוקצים". זה נותן שיפור קטן בביצועים (כ-2%). כמו כן, ייתכן שלדיסקים מסוימים פעולת גישה חסימה ראשונה איטית יותר מאחרים. המשמעות היא שמילוי החלל באפסים יכול להוביל לשיפור ביצועים משמעותי (כ-100%). בפרט, זה יכול לקרות עם דיסקים. (אלה נתונים לא רשמיים, לא יכולתי לאשר אותם). אותו דבר לגבי אחסון. (וזה כבר מידע רשמי, שאושר בבדיקות). מומחים אחרים עשו את אותו הדבר קשור לדיסקים שונים. - ככל שפחות שיחות מערכת, כך הביצועים גבוהים יותר (הרווח יכול להיות כ-5%). זה נראה כמו שיחה
open()עם דגלO_DSYNCאו שיחהpwritev2()עם דגלRWF_SYNCשיחה מהירה יותרfdatasync(). אני חושד שהנקודה כאן היא שבגישה הזו, העובדה שצריך לבצע פחות קריאות מערכת כדי לפתור את אותה משימה (קריאה אחת במקום שתיים) משחקת תפקיד. אבל ההבדל בביצועים הוא קטן מאוד, כך שאתה יכול בקלות להתעלם ממנו ולהשתמש במשהו באפליקציה שלא מוביל לסיבוך של ההיגיון שלה.
אם אתה מעוניין בנושא אחסון נתונים בר-קיימא, הנה כמה חומרים שימושיים:
- - סקירה כללית של היסודות של מנגנוני קלט/פלט.
- - סיפור על מה שקורה לנתונים בדרך מהאפליקציה לדיסק.
- - התשובה לשאלה מתי להגיש בקשה
fsync()עבור ספריות. בקצרה, מסתבר שצריך לעשות זאת בעת יצירת קובץ חדש, והסיבה להמלצה זו היא שבלינוקס יכולות להיות הפניות רבות לאותו קובץ. - - הנה תיאור של האופן שבו אחסון נתונים מתמשך מיושם ב-SQL Server בפלטפורמת לינוקס. יש כאן כמה השוואות מעניינות בין קריאות מערכת של Windows ו-Linux. אני כמעט בטוח שבזכות החומר הזה למדתי על אופטימיזציית FUA של XFS.
האם איבדת אי פעם נתונים שחשבת שאוחסנו בצורה מאובטחת בדיסק?
מקור: www.habr.com
