הבלוג של ארתיום
בלוג על לינוקס, תוכנה חופשית, מוזיקה, סלסה, ומה לא!
בחירת בסיס נתונים משובץ... או לא כל הנוצץ זהב.
מי שעוקב אחרי התפתחות של CppCMS יודע, שבתחילת דרכי, רציתי לעבוד עם בסיס נתונים מיוחד Berkeley DB. אחד היתרונות הגדולים שלו זה ביצועים מדהימים, לפחות לפי הנתונים של Oracle.
בזמנו, התלבטתי והחלטתי להפסיק להשתמש בו. אלה היו הסיבות:
- אומנם BDB היה מהיר יותר, אבל לא כל כך משמעותית.
- התחזוקה של בסיס נתונים הייתה מאוד מסובכת (בייחוד שדרוג של מבנה טבלאות).
- ה־API היה, נוראי בלשון המעטה שדרש מעטפת נוספת שהכתיבה שלה בפני עצמה הייתה לא מי יודע מה פשוטה.
אבל היום, עלה לי צורך ספציפי, בו הייתי זקוק לבסיס נתונים מאוד פשוט, לא מבוסס על SQL שיעשה את העבודה הפשוטה: ניהול Sessions.
מה בדיוק צריך לשמור:
- מפתח ייחודי לכל אחת.
- שעת התפוגה --- מתי אפשר למחוק רשומה מהטבלה.
- הנתונים עצמם.
אפשר היה להשתמש ב־RDBS כמו MySQL, אבל עבור המשימה הספציפית, זה היה נראה כבד מידי. הדרישות היו די בסיסיות:
- תחזוקה מינימלית.
- מהירות הכנסה גבוהה.
- זמינות גבוהה בהפצהות ומערכות הפעלה שונות.
- דרישות שרידות בסיסיות: רצוי שבסיס הנתונים לא ייפגע עם התכנה תקרוס, או מחשב יפול, אבל, אם ייאבדו כמה רשומות, זה לא ממש משנה.
אז היו לי מספר אפשרויות:
- בסיס נתונים מאוד פשוט של מפתח/ערך כמו gdbm עם מנגון "ניקוי".
- בסיס נתונים מורכב כמו Berkeley DB שתומך אינדקסים, כבר היה לי ניסיון איתו.
- מערכת קבצים --- כל ערך שמור בקובץ נפרד, גם פה צריך מנגנון "ניקוי" של ערכים ישנים.
- בסיס נתונים משובץ sqlite3.
אז בואו נתחיל.
בחירת תשתית
הניסיון הראשון --- GDBM
על gdbm ויתרתי מהר מאוד: תיעוד ה־API שלו היה לוקה בחסר, תמיכה ב־multithreading לא הייתה מתועדת בכלל. תמיכה בגישה ממספר תהליכים שונים בכלל לא הייתה. במילים אחרת. הבנתי די מהר, זה לא בשבילי.
Berkeley DB
אז חזרתי ל־BDB הישן והטוב. בניתי backend, הוא עבד יפה. החלטתי להריץ בדיקה עומס פשוטה והתחלתי לקבל תופעות מוזרות:
- פתאום הייתי מקבל DB::put נכשל.
- התכנה הייתה נתקעת בכלל.
התחלתי לחקור --- אחרי הרבה חיפושים גיליתי: כשאני עובד במספר threadים, בסיס הנתונים נפגע!
בדקתי עוד פעם --- עבודה multi-threaded מוגדרת, לפי התיעוד נראה שכל הפרמטרים מוגדרים. אחרי כמה שעות של חיפוש גיגול וכאב ראש, התחלתי להבין איפה קבור הכלב.
הפעלת דגל multi-threading נותן גישה חופשית לאובייקט, כך אפשר לעבוד עם מספר threads כל עוד רק thread אחד כותב... WTF@#$%@#$. כן, מסתבר שצריך להפעיל עוד מנגנון lockים נוסף, אבל בתיעוד שלהם אין דוגמאות איך להשתמש בו. (וחייב להגיד התיעוד שלהם לא עד כדי כך גרוע).
אז עשיתי סריאליזציה ע"י mutex פשוט, וזה עבד. אבל התחלתי לחשוש: "אולי אני עדיין מפספס משהו? אולי יש עוד הגדרות ששכחתי?" שלא לדבר על הבעיות של הרישיון של BDB שדומה ל־GPL. כך בין כה לכה, הייתי צריך אפשרות של backend אחר.
הערה: כשהבנתי את האי־בהירות בנושא thread-safety של BDB, עברה בי צמרורת: היה לי בלוג שרץ על BDB והכיל באג שהיה עלול הרוס לי את כל בסיס הנתונים.
Sqlite3
אז בדקתי גם אותו. החלטתי להשתמש ב־API שלו ישירות, כדי לא לגרור תלויות נוספות כמו libdbi ואחרים.
פה המצב היה די פשוט --- זה עובד. אבל... הביצועים כברירת מחדל על הפנים.
הסבר: SQLite3 תואם באופן מלאה דרישות ACID. כמובן יש לזה מחיר כבד והוא מתבטא בקריאת fsync בגמר כל טרנזקציה כדי להבטיח את "ה־D" --- לשפוך את כל המידע לדיסק. בנוסף, כדי להבטיח עבודה של מספר תהליכים, הוא מבצע פתיחה וסגירה של קובץ בסיס הנתונים בגמר כל פעולה. לא נחמד. אבל...
יש שתי דרכים לעקוף את זה:
- לאחד את כל הפעולות לטרנזקציה בודדת, אפשרי כל עוד מדובר בתהליך יחיד (בין כה־לכה, sqlite מבצע נעילה של בסיס הנתונים לכתיבה באופן בלעדי).
- לבטל מוד סינכרוני, במצב כזה, יש סיכון של פגיעה בבסיס הנתונים במקרה של תקלת חשמל או נפילת חומרה.
כמובן, נשארה הבעיה של ביצועים של מנוע SQLי באופן כללי.
מערכת קבצים
זאת האופציה הפשוטה ביותר, אבל, היא דורשת מנגנון ניקוי אוטומטי שרץ פעם בה כמה זמן, סורק את כל הקבצים ומנקה את המיותר.
זה דורש הפעלה של thread נוסף שמתעורר מעת־לעת ומנקה את הכל, אבל, למנגנון הפשוט הזה היה יתרון גדול:
- אי־תלות בספריית צד ג'.
- יכולת ביזור פשוטה מעל NFS (כמובן אם האחרונה תומכת ב־lockים).
מבחן המציאות
אחרי הרבה התלבטויות, חפירה בעשרות דפי תיעוד ובדיקה של לפחות 5 בסיסי נתונים (gdbm, qdbm, berkeleydb, Tokyo Cabinet ו־Sqlite3) התכנסתי למספר פתרונות אפשריים, התחלתי להריץ benchmarks. כאשר, בניגוד לכל ה־benchmarks הקודמים, הפעם בדקתי פעולות "Insert" של 10,000 ערכים מ־10 threadים שונים בו זמנית.
מערכת הקבצים
פה היו הפתעות:
FS: XFS ext3
Writes/s: 322 20,000
מצד אחד, מאוד התאכזבתי מהתנהגות של XFS, שאני משתמש בה כל הזמן. מצד שני, הופתעתי לטובה מביצועי ext3, והבנתי ששמירה במערכת קבצים זאת אופציה לא רעה בכלל.
השוואת בסיסי נתונים
Data Base \ Indexes: Key Only Key+Timeout
-----------------------------------------------
Berkeley DB 34,400 1,450
Sqlite No Sync 4,600 3,400
Sqlite Delayed Commit 20,800 11,700
מה שמעניין ש־BDB שנחשב לבסיס נתונים מאוד מהיר, ניצח רק ב־עבודה עם אינדקס HASH יחיד. עבודה עם אינדקס כפול הייתה משמעותית יותר איטית, שלא לדבר על העובדה שבאמת, יש צורך באינדקס נוסף. כמו כן, הסתבר, ש־Sqlite עם בחירת גישה נכונה יכול לעבוד הרבה יותר מהר.
למשל, בעבודה עם Delayed Commit כאשר על פעולות באותו תהליך מתבצעות במסגרת של טרנזקציה בודדת, אפשר להגיע למהירויות מאוד יפות.
בנוסף, התיעוד המעולה של Sqlite גם עושה הבדל משמעותי ונותן הרבה יותר בטחון במימוש.
סיכום
- שוב נזכרתי למה נטשתי את Berkeley DB --- כדי לדעת להשתמש בו ברמה טובה, כנראה אתה צריך להשקיע מספר חודשים של לימוד ה־API שלו והדקויות בניהול שלו.
- הופתעתי לטובה מ־Sqlite3 והתאכזבתי מ־BDB. בסופו של דבר --- Sqlite3 ניצח.
אני אשתמש בשתי אפשרויות עיקריות: קבצים ו־Sqlite3 כתשתית לשמירת מידע על Sessions כאשר פתרון "עוגיות" לא מספיק טוב.
תגובות
ההבדלים בין xfs ל־ext3 הם בגלל ש־xfs יותר שמרן בכתיבה לדיסק.
ר' הערות ל: http://juliank.wordpress.com/2008/12/19/filesystems
בפרט: http://juliank.wordpress.com/2008/12/19/filesystems/#comment-311
הגישה הנ"ל בעייתית כאשר רוצים לעבור משרת אחד למספר שרתים. ראה http://www.sqlite.org/faq.html#q5 (גם שאלה מספר 6 תעניין אותך)
הבעיה נובעת מנעילות ברמת מערכת הקבצים על NFS. מימשתי פעם מנגנון נעילה כזה על לינוקס, והכי טוב שמצאתי במחקר זה שיצירת hard link ב NFS היא אולי פעולה אטומית. ובאמת, אצלנו היה תיעוד בלוגים של מספר פעמים מצומצם שבהם הנעילה לא עבדה. (יכול להיות שזה היה באג במימוש אבל מעולם לא התעכבנו לבדוק, עברנו מאז לשמירת נתונים ב DB)
אז אתה יכול להגדיר שיהיה DB מקומי על כל שרת (או בקבצים, להשתמש בספרייה מקומית), אבל אז אתה צריך להשתמש ב load balancer עם session affinity. לא אסון, אבל לא כל LB תומך בזה.
במובן הזה RDBMS קלאסי מאפשר לך scaling up פשוט יותר.
עידו, אף אחד לא מתכוון לשים בסיס הנתונים של Sqlite3 מעל NFS.
הדרך בא אני מתכוון לעשות scaling זה לשים שכבת TCP פשוטה (כמו שיש לי עם מנגנון cache יש לי פתרון בסגנון memcached) שתבזר גישה למספר בסיסי נתונים.
כך במקום לגשת לקובץ מקומי זה יעבוד מעל רשת.
הוסף תגובה:
חובה לאפשר JavaScript כדי להגיב.