2013-02-12

עשה זאת בעצמך: NoSQL

מעוניינים לשדרג משהו? לעתים זול יותר ופשוט יותר לבצע שיפוץ קטן בעצמכם, במקום להשתמש בבעל מקצוע. ייתכן והתוצאה תהיה מוצלחת לא-פחות.
בפוסט זה אני רוצה לשתף במימוש מוצלח של "טכניקת NoSQL BIG DATA" שביצענו על גבי מערכת קיימת, מבלי לשנות אותה באופן מהותי ומבלי להחליף את בסיס הנתונים הרלציוני הקיים.

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




הבעיה

הערה: פרטי המקרה האמיתי פושטו ושונה כך שהפוסט יהיה קל יותר להבנה.
יש לנו מערכת שאחת הפונקציות שלה היא ניהול פרויקטים. במודול הפרויקטים החלו להשתמש בצורה קצת שונה ממה שתכננו, והחלו להיווצר פרויקטים רבים במערכת. כמה "רבים"? היה לקוח שהתעניין בניהול 2 מיליון "פרויקטים".

בדיקות שעשינו למערכת הראו שבסביבות 30-אלף פרויקטים, המערכת מתחילה להראות סימנים של שבירת הלינאריות ב scalability. כלומר: עד נקודה זו - אם רצו לנהל עוד פרויקטים היה ניתן להוסיף עוד חומרה ביחס ישר לגדילה בכמות הפרויקטים / הפעילות. מעבר לנקודה זו היה צריך להוסיף x וקצת חומרה ל x פעילות נוספת, וככל שהמספר גדל - העלות השולית הלכה וגדלה.
הבנו שהמערכת תוכל לטפל במשהו כמו 50 אלף עד 100 אלף פרויקטים, תלוי בכמות החומרה שהלקוח יסכים להקצות. באופן מעשי זהו בערך גבול ה Scalability שלנו ולכן ההמלצה ללקוחות הייתה לא ליצור מעל 50 אלף פרויקטים.

חשוב להבהיר שמדובר במערכת בת כ3 שנים - שעברה לאורך חייה לא מעט שיפורי performance ו scalability. בשלבים הראשונים של המערכת הצלחנו לבצע שיפור יחיד שהגדיל את ה Scalability ב 30% - אך ככל שהזמן עבר שיפרנו אלמנטים פחות משמעותיים (כיוון שהמשמעותיים כבר שופרו) והבנו שאנו מגיעים לקצה ה Scalability של הארכיטקטורה הקיימת.

פתרון אפשרי אחד היה לנסות לעבור לבסיס נתונים NoSQL, נוסח MongoDB או CouchDB - המתאימים יותר לשימוש הספציפי של המערכת, והיו יכולים בהחלט לשפר את המצב. הבעיה: שאר האלמנטים במערכת (מלבד הפרויקטים) התנהלו בצורה משביעת-רצון בבסיס הנתונים הרלציוני. מה עושים? עושים הסבה לכל הקוד לעבוד מול בסיס נתונים NoSql או דורשים מלקוחות לנהל 2 בסיסי-נתונים שונים במקביל?!


התוצאות

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

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


מקור הבעיה

ישנן סיבות שונות המצדיקות מעהר לNoSql Databases:
  • הרצון בסכמה גמישה, שלא דורשת migration בין גרסאות.
  • כמות נתונים (ב TB) שדורשת מעבר משרת אחת לכמה שרתים - מה שנקרא Scale Out.
  • בעיית Scalability מקומית. כלומר: מעל כמות נתונים מסוימת, זמן התגובה למשתמש הקצה הופך ללא-סביר.
    זו הבעיה איתה התמודדנו במערכת שלנו.

איך נוצרת בעיית Scalability?
בואו נביט על (הפשטה של) סכמת הנתונים של מערכת הפרויקטים:

האובייקטים החשובים, הבנויים בהיררכיה הם: פרויקט, נושא, דיון, תגובה ותכונה-של-תגובה.

אני רוצה להתמקד לרגע באובייקט קצת פחות טריוויאלי במודל הנתונים: Comment Attribute.
תכונה-של-תגובה (Comment Attribute) יכולה להיות דבר כמו: תאריך, שם המגיב, קישור לתמונה וכו'

בבסיס נתונים רלציוני ניתן לשמור תכונות כאלו ב-2 אופנים:
  • סכמה קשיחה: כעמודה (column) בטבלה. על כל תכונה אפשרית יוצרים עמודה חדשה.
    יתרונות: פשטות
    חסרונות: יש תכונות (כגון "deleted by admin") שמתרחשות לעתים נדירות - אך עדיין יש לשמור עבורן מקום בכל רשומה, הוספת שורה = הוספת סכמה.
  • סכמה גמישה: כטבלה נוספת, בתצורת master-detail, בה כל מפתח וערך של תכונה היא שורה נוספת.
    יתרונות: גמישות רבה ללא שינויי סכמה
    חסרונות: עוד טבלה לנהל, עוד קצת סיבוך.
המערכת הנ"ל השתמשה בסכמה גמישה.

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

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


חזרה לתיאוריה

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

הנה טבלה חשובה למדי:
מקור: גוגל









שתי נקודות שהייתי רוצה להדגיש:
  • קריאה מדיסק היא הפעולה היקרה ביותר ברשימה (נו, טוב - מלבד WAN בין-יבשתי), והיא יקרה משמעותית מפעולות מבוססות זיכרון (פי 100 עד פי 100-אלף, תלוי בתסריט).
    אם אנו מתבוננים על שכבת ה Persistence כקופסה שחורה, אזי כדאי לנו מאוד להפחית קריאות לדיסק, גם על חשבון הרבה פעולות בזיכרון. כלומר: להבין מתי בסיס הנתונים או שכבת ה ORM גורמות לקריאות לדיסק להתרחש - ולגרום לקריאות אלו לפחות ככל האפשר.
  • בניגוד לזיכרון, בו יש פער גדול בין גישה כלשהי (100ns) לקריאת 1MB של נתונים (250K ns, פי 2500), בקריאה מדיסק הפער הוא רק פי - 2. כלומר: להביא 1MB של נתונים רציפים לוקח כמו הבאה של 2 חתיכות של 4k.
    הסיבה לפער זה הוא שזמן גישה (seek time) בדיסק כוללת תנועה של זרועה מכנית וסיבוב הדיסק לנקודה הנכונה, משם קריאה רציפה היא כבר "לא סיפור".
    הערה: מגבלה זו השתפרה מאוד עם הצגת כונני ה SSD המודרניים. ניתן לקרוא עוד בנושא בפוסט מבט מפוכח על מהפכת ה SSD. עדיין, קריאה מדיסק ובמיוחד קריאה בלתי-רציפה, היא יקרה למדי. בפוסט הנ"ל ניתן לראות כונן SSD שקורא 180MB בשנייה באופן רציף, אך רק 18MB בשנייה כאשר המידע מפוזר. יחס קצב העברת-נתונים של פי 10-15 בין קריאה רציפה לקריאה אקראית הוא מאפיין שכיח בכונני SSD מודרניים. יחס זה הוא בערך פי 100-200 בכוננים קלאסיים - כך שמדובר בשיפור גדול.
כיצד נגרום לבסיס הנתונים לבצע משמעותית פחות קריאות לדיסק, מבלי לגעת בקוד של ה ORM או של בסיס הנתונים? כיצד נוכל לעשות זאת מבלי לשנות דרמטית את כל המערכת שלנו? התשובה בפסקה הבאה.


Aggregate-Based Data Storage

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

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

נניח מצב אופטימלי בו טבלאות הן רציפות בדיסק, ע"פ סדר הכנסת השורות.
כמה פעולות seek של הדיסק יש פה?
  • לפחות קריאה אחת עבור כל טבלה.
  • בעצם, יש אינדקסים שבהם ייתכן ויש להיעזר - כך שבפועל ייתכנו מספר קריאות לכל טבלה.
  • המידע מגיע מהדיסק בבלוקים של 4k או 16k. אם הרשומות בטבלה אינן "קרובות דיין" על מנת להיכנס לבלוק של 16k נתונים - ניאלץ "לדלג" (seek) שוב בתוך הטבלה.
    רשומות מסוג ה Comment יכולות להיכתב בהפרש זמנים ניכר אחת מהשנייה, שכן תגובות יכולות להגיע לאחר שבוע או חודש.
    רשומות מסוג ה Comment Attribute יכולות להיכתב בהפרש (כלומר, פיזור) נוסף, מאחר והן נוספות "רק ע''פ הצורך". לדוגמה: תכונת ה likesCount תיווצר רק בעת שנעשה ה Like הראשון ולא עם יצירת ה comment.

אין לי חישוב של מספר הפעולות בדיסק, אך יש לי בסיס להאמין שהוא יכול להסתיים בעשרות קריאות מהדיסק לכל discussion. השימוש ב ORM יכול להסתיר את העובדה שיצירת אובייקט "discussion" בזיכרון, רק בכדי לקחת את ה title ו lastUpdateDate - יכולה לגרום ליצירת אובייקטי ה comment והקריאה גם שלהן מהדיסק.

נקודה מעניינת, שמעצימה את הבעיה, היא שבמערכת עם הרבה מאוד פרויקטים ודיונים, הכמות הגבוהה של ה comments שמתווספים למערכת בשעה, יכולה לגרום לכך ש 2 תגובות במרחק של 5 דקות אחת מהשנייה - לא יהיו במרחק 16k בדיסק (מכיוון שנכתבו מאז הרבה comments אחרים ב discussions אחרים).

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

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

במקרים כאלו יש ייתרון ברור לאיגוד הנתונים ע"פ נקודת הייחוס המתאימה לשימוש הנפוץ במערכת, מה שנקרא document-based database או aggregate-database.

אילוסטרציה: כיצד פרויקטים יכולים להשמר בדיסק באופו רציף, כך שיהיה קל לקרוא פרויקט בודד.
ברור שפרויקטים שונים יהיו בגדלים שונים (לא הכללתי זאת בתרשים).

כיצד מממשים זאת?
פשוט מאוד: בוחרים נקודת ייחוס (למשל Topic, בכדי לצמצם מעט את גודל ה"קובץ" שנקרא בכל פעם), ומייצגים אותה ואת כל ההיררכיה של האובייקטים מתחתיה (למשל אובייקטי ה discussion) כרשומת JSON או XML אחת. את רשומה זו שומרים בבסיס הנתונים כ BLOB, כך שיהיה עדיין להינות משירותים של בסיס הנתונים (גיבוי, טרנזקציות וכו').

כעת, במקום להשתמש באובייקטי ה ORM לאובייקטי ה Topic ומה שמתחת - יש לכתוב מימוש מחדש, שיקרא את המידע המתאים מרשומת ה JSON ויטפל באותם הדברים שה ORM טיפל עבורנו עד כה.
במידה (כמו במקרה למעלה) שיצירה של אובייקט Topic גורמת ליצירת כל השאר - השימוש בבסיס הנתונים יהפוך ליעיל בהרבה: כל היררכית האובייקטים נקראת ממקטע רציף על הדיסק.


משמעויות נוספות
יש כמה משמעויות נוספות בגישה זו שכדאי להיות מודעים אליהן.
  • יש לממש לבד לוגיקה של קריאה / כתיבה של אובייקטים לתוך ה BLOB (מה שציינו למעלה).
  • אנו מאבדים את היכולת לעשות שאילתת SELECT על כל האובייקטים במערכת. למשל, למצוא את כל ה Comments שנכתבו בין 2 ל 4 בבוקר.
    אם אנו רוצים לבצע שאילתה שכזו - יהיה עלינו לקרוא את כל ה Topics מהדיסק, אחד אחרי השני, ולסרוק בעצמנו את המידע בתוך ה BLOB.
    אם אנו רוצים מהירות בסריקה (ויש לנו use-case ספציפי) אנו יכולים להשתמש במנועי indexing כגון Lucne.
  • השימוש בסכמה של "טקסט חופשי", כגון פורמט JSON, מאפשרת לנו לבצע שינויים לסכמת הנתונים בין הגרסאות של המוצר מבלי לבצע שינויים לסכמת בסיס הנתונים.
  • השימוש בסכמה של "טקסט חופשי" מאפשרת לנו לשלם על שדות "נדירים" רק כאשר משתמשים בהם (לדוגמה: isDeletedByAdmin) ועדיין להינות מביצועים נהדרים.


עדכון ספטמבר 2015:
הנה שני סיפורים דומים של חברות שבחרו ב"התאמה אישית" של בסיס נתונים רלציוני על פני מעבר ל NoSQL DB:



סיכום

בסיסי הנתונים הרלציונים נבנו תחת הנחה שהנתונים מתמפים יפה לטבלאות וקשרים ביניהן. צורת מיפוי זו לעתים איננה טבעית לנתונים שאנו נתקלים בהם בפועל.
בנוסף לכך, שמירה של מיליוני רשומות שאין קשר בניהן בטבלה אחת ("כל התמונות של כל מסמכי הוורד") - היא גישה שמשפיעה לרעה על ה Performance וה Scalability שלנו.

כדי להינות מ BIG DATA, מספיק לעשות שינוי קצת שונה בבסיס הנתונים הרלציוני הקיים שלנו. ברור שבסיס נתונים NoSQL ייעודי יכול לתת יותר - אך לא תמיד הפער הזה מצדיק את המעבר.

שיהיה בהצלחה!


אם אתם מעוניינים ללמוד קצת יותר על BIG DATA, אתם מוזמנים לקרוא את הפוסט מה הביג-דיל ב BIG DATA?