ביג דאטה: מחסני נתונים, אגמי נתונים, ומרכולי נתונים – סדר בבלאגן

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

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

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

כאשר אנו רוצים לבצע ניתוחים מקיפים על הנתונים במערכת האופרטיבית – יש בעיה אמיתית פשוט "להריץ שאילתות על בסיס הנתונים האופרטיבי":

  • האינדקסים ומבני-הנתונים בבסיס הנתונים האופרטיבי מותאמים לשימושים האופרטיביים (למשל: הכנסה ועדכון של רשומות בתכיפות גבוהה), ופחות לצרכים האנליטיים (למשל: שאילתות גדולות עם הרבה joins). התוצאה: השאילתות יכולות לקחת זמן רב מאוד.
  • הפעלת שאילתות כבדות על בסיס-הנתונים האופרטיבי יכולות לפגוע בתפקוד השוטף. אמנם גם ניתוח נתונים הוא חשוב, אך הוא כמעט-תמיד פחות דחוף מהמשך הפעולה התקין של המערכת האופרטיבית.
Data Reservoir, השם הקודם (או הפחות נפוץ) של Data Lake. מקור: O'Reilly Radar.
יש הטוענים ש"מאגר נתונים" הוא מונח מדויק יותר, אך אין ספק ש "אגם נתונים" הוא המונח הפופולרי.

התמודדות עם בסיס נתונים אופרטיבי "גדול מדי"

מה עושים?

בתור שלב ראשון אפשר לייצר Replica של בסיס הנתונים האופרטיבי – ולהריץ עליה את השאילתות.
אבל:

  1. רפליקה משכפלת את מבני-הנתונים והאינדקסים מבסיס הנתונים האופרטיבי – שכבר ציינו שאינם אופטימליים לשאילתות האנליטיות.
  2. אם נעמיס את הרפליקה יותר מדי, אנו עלולים לגרום להאטה גם בבסיס הנתונים הראשי – שעסוק בלהמתין לרפליקה שתקבל את העדכונים שלו.

השלב הבא בהתפתחות הוא ליצור מחסן נתונים (Data Warehouse, או בקיצור DWH או DW) – בסיס נתונים בו מאוחסן עותק של הנתונים מבסיס הנתונים האופרטיבי (ייתכן שב delay מסוים, נאמר שעה עד יממה) – אבל עם החופש ליצור סכמה שונה, ואינדקסים שונים.

מקור: הבלוג של James Serra
ה ETL (קיצור של Extract, Transform, and Load) הוא התהליך ששלוף נתונים מבסיס הנתונים האופרטיבי, ממיר אותם לסכמה של בסיס הנתונים האנליטי וטוען אותם לשם.

כעת אנו יכולים לבצע שאילתות אנליטיות "כבדות" על ה DWH. הן:

  • ירוצו מהר יותר – כי מבנה הנתונים / האינדקסים מותאמים לשאילתות.
  • לא ישפיעו על הביצועים או ה Stability של בסיס הנתונים האופרטיבי (!Hurray)
בנוסף ב DWH מקובל:
  • לשמור נתונים לטווח ארוך – וכך לשחרר את המערכת התפעולית מלאחסן אותם.
    נאמר: ה DWH יכיל נתונים עשר שנים אחורה, בעוד בסיס הנתונים האופרטיבי – נתונים של חצי שנה אחרונה בלבד.
  • עבור הנתונים שיש הרבה מהם – שומרים לרוב סיכומים, ולא את כל הנתונים המקוריים שהיו במערכת התפעולית.
    למשל: במקום סכום של כל עסקה – שומרים רק את סכום העסקאות היומי, מה שיכול לצמצם את כמות הנתונים בסדרי-גודל, ובהתאמה להקל על השאילתות האנליטיות (שרצות עכשיו על פחות נתונים).
    יש לזה מחיר: אנו מאבדים נתונים – היכולת להבין את הפיזור המדויק של סכומי העסקאות, למשל.
  • להשתמש במהדורה מעט שונה של Database Server שמתאימה בצורה טובה יותר לצרכים אנליטיים. למשל: Vertica, Greenplum, Sybase IQ – הם בסיסי נתונים שמיועדים לשמש כ DWH.

מהפיכת הביג דאטה

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

הגידול בכמות הנתונים לחץ על מערכי ה DWH הקיימים, וטכנולוגיות ה NoSQL/BigData החדשות – יצרו הזדמנות לשינוי.

מקור: הבלוג של James Serra

ה DWH היה בשלב זה כבר פתרון מקובל, אך היו לחצים שהלכו וגברו:

  • חלק מהנתונים החלו לנהל בבסיסי נתונים לא-רלציונים – שלא נתמכים / נתמכים בצורה חלקית ע"י כלי ה ETL המסורתיים. מצבים של אי-עקביות בנתונים (inconsistency) וסכמה דינמית (למשל ב Document Databases) הפכו את מלאכת ה ETL ל"גיהינום" עבור ה DBAs.
  • כמות הנתונים במערכת האופרטיבית גדלו בצורה דרמטית – מה שגם הקשה על ה ETL להתבצע בזמנים טובים ו / או להישאר מעודכן לכל סוגי הנתונים שנוספו.
  • ברגע שה DWH לא מעודכן בכל הנתונים האחרונים, מערכות ה BI הן כבר פחות טובות – והמשתמשים הזקוקים לנתונים מתחילים למצוא "דרכי-מעקף" על מנת להשיג ולנתח את הנתונים שלהם.
בעזרת אכסון "אינסופי" וזול – נוצר מודל חדש לאיסוף וניתוח נתונים: אגם הנתונים (Data Lake):
  • כל המידע נאסף, בצורה גולמית וללא עיבוד ("as-is"), מהמערכות התפעוליות – ונשמר על גבי שטח האכסון האינסופי (Hadoop, S3, או Azure Data Lake).
    • נתונים שהמערכת התפעולית לא יכולה להמשיך ולשמור (משיקולי ביצועים) – נשמרים ולא נזרקים.
    • נגמר המירוץ של ה DBAs לעקוב ולהתאים את ה ETL לשינויי הסכמה במערכת התפעולית. המנגנון החדש יודע לאסוף את כל הנתונים.
  • מאוחר יותר, כשיהיה צורך מעשי בנתונים – ינקו אותם, יסכמו אותם, וינתחו אותם, באותו הרגע.
    • ה Stack של Hadoop – נבנה ממש לצרכים שכאלו.
תהליך העברת הנתונים ל Data Lake נקרא ELT (קיצור של Extract, Load…. ואז Transform), כאשר שלב ה Transform הוא לרוב ארגון מאוד בסיסי של הנתונים, כמו סידור שלהם בתיקיות / המרת פורמט הקבצים לכאלו שקל יותר לסרוק.
ב Hadoop למשל, נהוג להמיר את הנתונים (שמגיעים לרוב כהרבה קבצי JSON או CSV) לאחד מהפורמטים הבאים:
  • Avro – פורמט כללי, שדוחס את הנתונים ומאפשר לחלק קובץ ל-2 מבלי לקרוא את כולו (כי הנתונים שמורים במבנה ידוע). זו תכונה חשובה ל Map-Reduce.
  • Parquet – פורמט ששומר את הנתונים בצורה Columnar ומאפשר לקרוא מהדיסק את עמודות ספציפיות של הנתונים. הדחיסה כאן היא אפילו גבוהה יותר, כי הדמיון בין ערכים באותה עמודה – הוא באופן טבעי גבוה יותר.
המוטיבציה לדחוס את הנתונים היא: א. עלות אכסון  ב. צמצום זמן ה I/O שנדרש לטעון את הנתונים לזיכרון.
Hadoop, למשל, לא מתנהג יפה עם הרבה קבצים קטנים – ולכן יש להכין לו קבצים גדולים יותר, המסכמים "אצווה של רשמות".
תהליך ELT. מקור: www.ironsidegroup.com
ה Data Lake הוא בעצם גישה הפוכה לגישת ה DWH:
  • מהגישה החרוצה: ניקוי, ארגון, וסיכום הנתונים ואז שמירה שלהם מה DWH – המאפשרת שליפה קלה.
  • לגישה העצלה: שמירת כל הנתונים המלוכלכים, raw, עם כפילויות וחוסרי התאמות ב Data Lake. ניקוי וסידור הנתונים יתבצע – רק ברגע שרוצים לתשאל אותם.
מקור: Martin Fowler
מי שבקיא ברזי ה Agile יודע בוודאי שדחיינות ועצלות הם Best Practices. זה כנראה נשמע רעיון טוב לאמץ את הרעיונות החשובים הללו בעולם הנתונים.
מי שהלך רחוק עם רעיונות ה laziness ב Data Lake שלו, עלול לגלות שהוא יצר לעצמו ביצת הנתונים (Data Swamp): מצב בו הנתונים ב Data Lake כ"כ מבולגנים ו"מלוכלכים" – שהערך מניתוחם לא מצדיק את העלות הגדולה בניקוי והסידור שלהם בדיעבד. הנתונים הם שם, שמורים ב Data Lake – אבל בעצם לא משתלם להתחיל ולהתעסק איתם (במקרים מסוימים – כמובן).
סידור נתונים בשלב מוקדם הוא לעתים רבות זול ופשוט הרבה יותר מסידור שלהם בדיעבד. למשל:
  • הסכמה וצורת שמירת הנתונים השתנו עשרות פעמים בתקופה – ואין לנו דרך לדעת בדיוק ממתי השינוי. שמירת שדה "version" על הנתונים במקור – היה יכול להיות השקעה קטנה שתחסוך זמן רב בעתיד.
  • נתונים שחסר איזה מפתח או נתון לקשר ביניהם. אם היינו מוסיפים זאת במקור – זו הייתה תוספת קטנה מאוד, אבל כיום צריך להתחיל להריץ יוריסטיקות וניתוחים – רק על מנת לקשר את הנתונים שהיו עד לא מזמן "במרחק נגיעה".
  • שמירת נתונים בצורה אחידה, למשל שמירת תאריכים בפורמט אחיד (האם 1/4/12 זה אפריל או ינואר?) – או הקפדה על שמירת ה timezone הרלוונטי. נתונים שלא נשמרו כך – בדיעבד קשה לנקות ולסדר.
  • שמירת פרטים על המערכת ממנה מגיעים כל מקבץ-נתונים – יכול לסייע מאוחר יותר לקבוע את האיכות שלהם, או לשפר אותה.
  • לאסוף "חוסרי-נתונים". יש סיפור על חברת טלקום שבנתה מערך Big-Data מרשים, אבל שכחה לאסוף אינדיקטור על ניתוק שיחה (שלא נשמר בבסיס הנתונים האופרטיבי – אני מניח). אחרי שנה+ של צבירת נתונים, היא לא הייתה מסוגלת לנתח בעיות במערכת – כי היה לה רק "מידע חיובי".
    נקודה זו מתייחסת גם לנתונים שהמערכת האופרטיבית "תיקנה", למשל – כאשר היא קובעת "ערך מקסימום" לסוג נתון כלשהו, עדיין עשוי להיות מעניין מה היה הערך המקורי.
  • כדאי לעקוב ולתעד את השמירה של מידע רגיש (מספרי טלפון של משתמשים, למשל) – כדי שיהיה ניתן להגן עליו. לא כל אנליסט עסקי אמור להיות מסוגל לגשת לנתונים הללו.
הנה דוגמה לארגון מקובל של נתונים בתוך ה Data Lake:

ה RAW הם נתונים שהגיעו מהמערכות התפעוליות ללא כל עיבוד, כאשר ה Gold הוא storage של הנתונים לאחר עיבוד מסוים. ה Work הוא אזור העבודה של ה Data Scientists, ויש אזור Sensitive אליו מעבירים את כל הנתונים הרגישים (שמחליטים לשמור. לפעמים פשוט מטשטשים אותם).

נוטים לרוב להבחין בין אנשי BI / Data Analysts שעובדים מול נתונים נקיים יחסית ("Gold") ל Data Scientist – שמבינים טוב יותר את הבעיות השונות של נתונים (חוסר עקביות, פורמטים לא-אחידים, סתירות), ויכולים אף לכתוב custom code בכדי "לנקות נתונים קשים".

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

חלוקת הנתונים בארגון

כמו תמיד, הארכיטקטורה הנקייה, בה יש DWH אחד ו Data Lake אחד ממנו כל הארגון צורך נתונים, היא טובה – בעיקר כתיאוריה.

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

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

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

מקור: datamartist.com

מתוך כך צצו, עוד בימי ה DWH – "מרכולי הנתונים" (Data Marts), שהם סוג של DWH קטן וממוקד.

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

ישנן עוד כמה שאלות הנוגעות לקשר בין ה DWH ל Data Marts. למשל: האם קודם יוצרים את ה DWH ואז גוזרים ממנו Data Marts, או האם נותנים ליחידות Data Marts ואז אוספים אותם ליצירת DWH ארגוני? אם ישנם נתונם שנכונים ל-2 Data Marts, האם להחזיק אותם ב Data Mart שלישי או ב DWH? וכו'…

מקור: datamartist.com
באופן דומה ל DWH, גם ב Data Lake צפוי שיהיו אזורים מיועדים ליחידות / רמות עסקיות שונות. כל אחד – והצרכים הייחודיים שלו. מרטין פאוולר, למשל, קרא להם "Lakeshore Data Marts".
מה ההבדל בין Data Mart ל "Data Silos" – מאגרי מידע שיחידות בארגון שומרות לעצמן ולא משתפות עם אחרים?
אני מניח שאלו הבדלים של נקודות מבט: החצי המלא והחצי הריק של אותה הכוס.

סיכום

כמה Best-practices שכדאי לשקול בעבודת ארגון הנתונים:
איסוף וארגון נתונים:
  • להשתדל לאסוף ל Data Lake נתונים ברזולוציה של אירועים בעלי משמעות עסקית (קנייה, הצעת מחיר, רישום, וכו').
    • זו הרזולוציה שלרוב יהיה מעניין לנתח.
  • לשלוח את הנתונים ל Data Lake לא כשהם ממש Raw, אלא לאחר עיבוד-קל ("Medium-Rare"):
    • להשתדל "לשטח" לתוך האירוע נתונים חשובים שיהיה קשה לקשר אותם מאוחר יותר. למשל: באירוע הצימוד בין נהג ונוסע, מעניין לשמור את המיקומים המדויקים שלהם באותו הרגע, כפי שהם ידועים לשרת. חלק הקוד שמפיק את אירוע-הנתונים יכול לספק נתון כזה בקלות יחסית, אבל להגיע לתובנות כאלו בדיעבד – זה יכול להיות קשה, ואפילו לא מדויק. הבנה עמוקה של הביזנס – היא כמובן המפתח להחלטה.
    • להוסיף metadata שבסבירות גבוה יכול להיות שימושי בעתיד: גרסה של מבנה הנתונים, מה לא נשמר ומאיזו סיבה, ציון תיקונים שנעשו בנתונים או ציון נתונים רגישים, וכו'
  • להחליט ע"פ הצורך אילו נתונים רוצים להכין / "לנקות" ב Data Lake בצורה יזומה ("Golden Data" או "tidy data") – כדי שיהיו זמינים לניתוח מהיר. אלו לרוב הנתונים שמתארים את המדדים הביצועיים החשובים של הארגון.
  • לנסות ליצור סדר מובן ב Data Lake, ולא להפוך אותו ל Data Swamp. איזה סדר? מה שמתאים לצרכים הייחודיים של הארגון שלכם.
    • אנשים בתחום מדברים על תכון ה data pipeline (תהליך ניהול הנתונים) ו / או ה data lineage (המסלול והתחנות בו עוברים הנתונים לאורך חייהם).
  • (שנוי במחלוקת): נסו להוסיף נתונים ל Data Lake ב Pull (כלומר: ע"פ צורך ניתוח ממשי), ולא ב Push (כלומר: כי הם שם).
    • היתרון: יהיה לכם הרבה פחות "זבל" ו"בלאגן" ב Data Lake.
    • החיסרון: לא תוכלו לחקור נתונים שלא שמרתם.
שימוש בכלים:
  • "לשחק" בציר היכולות (ה trade-offs) בין Data Warehouse ובין Data Lake.
  • "לשחק" בציר היכולות בין ארגון מידע ריכוזי (DWH או Data Lake מרכזי) לארגון מידע מבוזר ע"פ צרכים (Data Marts או אזורים ספציפיים ב Data Lake).
בסופו של דבר, אלו הם כלים: הם לא "טובים" או "לא-טובים". הם פשוט שימושיים – למקרים וצרכים מסוימים.

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

—-
לינקים רלוונטיים
מרטין פאוולר על אגמי נתונים – http://martinfowler.com/bliki/DataLake.html

Spring-JDBC: שדרוג קטן לעבודה ב JDBC

בפוסט (קצר) זה אני רוצה לשתף בכלי שהתחלנו להשתמש בו לאחרונה – Spring JDBC.

נכון, לספרינג יש גם כלים ל JPA / חיבור ל Hibernate, – אבל לפעמים לא רוצים לעבוד עם ORM – אלא עם JDBC פשוט.

(כדרך אגב, ה Suite של ספרינג כולל רכיב בשם iBatis שהוא הפשטה / אוטומציה מעל JDBC. קצת יותר מ JDBC Templates אותם אנו סוקרים בפוסט זה –  וקצת פחות מ ORM. עקרון הבסיס שלו הוא קשירה של SQL Statements שכתב המפתח לאובייקטי Java וניהול ההפעלה שלהם.)

בכל מקרה אנו רוצים לעבוד עם הבסיס – עם Spring-JDBC וכלי ה JDBC Templates שלו.

JDBC Templates מסייעים בפישוט הגישה לבסיס הנתונים בכך שהם מטפלים בכמה נושאים:

  • פתיחה / סגירה של משאבים מול בסיס הנתונים (Connection, Statement, וכו\') – נושא שטיפול לא נכון בו יכול לכלות את ה connection pool ולגרום לקריסה של מערכות.
  • פישוט ניהול Transactions  – על בסיס Spring Transactions
  • עטיפה של Exceptions טכניים של JDBC ב Exceptions יותר ידידותיים. למשל: לקבל CannotGetJdbcConnectionException במקום SQLException עם ErrorCode סתום כלשהו.

 סה\"כ Spring-JDBC היא שכבת ריפוד קלה לעבודה כמעט ישירה מעל JDBC.

הקמת סביבת העבודה

צעד ראשון נדרש הוא שיהיה לנו חיבור לבסיס הנתונים (ושיהיה גם בסיס נתונים, כמובן. אני מניח שיש לכם). JDBC Templates עובדים מול DataSource – שהוא הממשק של ג\'אווה ל Factory המייצר DB Connections.
בואו נתחיל בהגדרת DataSource שיהיה מחובר לבסיס הנתונים.
הערה: חילקתי שלב זה ל2 וריאציות :אפליקציה שהיא Standalone, ואפליקציה שרצה על Application Server.

אפליקציית Standalone

ניתן תאורטית להשתמש בקובץ ה XML של ספרינג כקובץ קונפיגורציה. למשל: להגדרת הפרמטרים של החיבור לבסיס הנתונים.
בפועל – זה לא רעיון כ\"כ מוצלח: לקוח (איש IT) שיידרש לשנות קובץ יכול בקלות להסתבך או/ו להרוס משהו. קונפיגורציה כמו חיבור לבסיס הנתונים, שסביר שנצטרך לקנפג עבור כל התקנה – עדיף לשים בקובץ חיצוני ופשוט, כזה שיהיה זמין בקלות לכלי קונפיגורציה כמו chef או puppet. למשל: קובץ properties (המורכב מ keys ו values מופרדים ע\"י הסימן \"=\").

הנה דוגמה לכזו תצורה:

  1. אנחנו מבקשים מ Spring לטעון ל ApplicationContext קובץ Properties חיצוני. כל ערכי הקובץ יתווספו ל context של ספרינג. שימוש ב classpath יכול להכשל אם החיפוש של ספרינג החל ב root classpath שונה, למשל של ספריית ה test – ושימוש ב *classpath יחפש אחר הקובץ בכל ה roots של ה classpath.
  2. באפליקציה שהיא Standalone הייתי רוצה לקבל יכולות בסיסיות של Connection Pooling, אותן נוכל לקבל מ Apache Commons ע\"י שימוש ב BasicDataSource.
  3. בעת חיסול האפליקציה נקרא ל ()DataSource.close שגורם לסגירה מסודרת של כל ה connections שנותרו.
  4. השמת משתני ה \"Connection String\" ל dataSource נעשית ע\"י ספרינג שמשתמש ב Setters הקיימים של מחלקת ה BasicDataSource בכדי לקבוע את הערכים, שבשלב 1 נטענו לסביבה.
ובכדי להשלים את התמונה – הנה קובץ ה properties:
סביבת Application Server
אם אתם לא הקוד הראשון שרץ על ה Application Server – סביר שמישהו כבר רשם DataSource, טיפל ב connection pooling וכו\', ורשם אותו ל JNDI. אתם רק צריכים להתייחס אליו. למשל:
פשוט!
מרגע זה נועל להתחיל לעבוד עם Spring-JDBC.

JdbcTemplate

היכולות הבסיסיות של Spring-JDBC נגישות דרך שימוש במחלקה JdbcTemplate. מחלקה זו מנהלת את ה Connection, Statement וה ResultSet של JDBC ועוטפת את ניהול השגיאות.

נגדיר bean של JDBC Template ונשתמש ב DataSource שהגדרנו קודם:

הנה דוגמת קוד לשימוש ב JdbcTemplate כחלק מ DAO (אובייקט גישה לנתונים, קיצור של Data Access Object):

  1. נגדיר SQL Statement של INSERT עם מקום להכנסת ערכים (סימני השאלה).
  2. KeyHolder (או המימוש הפשוט GeneratedKeyHolder) היא מחלקה של Spring-JDBC המשמשת כמבנה נתונים שמחזיק key של הטבלה (מספר columns, כמו Primary Key). בפעולות Insert – ערך ההחזרה הוא ה key שניתן לרשומה החדשה.
  3. המתודה update משמשת לכל פעולת SQL של עדכון (יצירה, מחיקה, עדכון). במקרה שלנו – יצירה (INSERT).
  4. אנו מצהירים מהם ה colums שאנו רוצים לקבל בחזרה מפעולת העדכון. זהו ה primary key שנקבע ע\"י בסיס הנתונים.
  5. אנו מבצעים השמה מתוך ה Domain Object (במקרה שלנו: Person) לתוך ה prepared Statement, כדי למלא את ערכי ה ?,?,? ב SQL Statement – ואת הטיפוסים שלהם.

שימו לב שאין לנו בקוד טיפול בשגיאות, ואין את סגירת ה connection / statement – שהם error prone, וטיפול לא נכון בהם יכולים לפגוע ביציבות המערכת. כל אלו מטופלים עבורנו ע\"י המחלקה JdbcTemplate.

NamedParameterJdbcTemplate

אני רוצה להציג בקצרה, Template של Spring-JDBC שהוא טיפה יותר \"מפנק\".
מעצבן אתכם לראות את הקוד של השמה של ערכים ל prepared Statement עם טיפוסים? הנה דרך מעט יותר אלגנטית לעשות זאת.

בכדי לגוון (ולהראות את ה RowMapper) בחרתי בשאילתה שמחזירה נתונים (SELECT) ולא מבצעת עדכון.

  1. במקרה הזה אין לנו ?,?,? בשאילתה, אלא placeholders המתחילים בסימן \":\".
  2. SqlParameterSource הוא מבנה נתונים של Spring המתאים ל NamedParameterTemplate. הוא משמש כפרמטר לפעולות השונות של ה NamedParameterJdbcTemplate.
  3. הפעם אנו לא עושים פעולת update כי אם query. ה RowMapper היא מחלקה שתפקידה לתרגם את ה ResultSet ל Domain Object. במקרה זה יצרנו אותה כ Anonymous Class.
  4. השמה \"סיזיפית\" של ערכים. אני לא מכיר דרך יעילה בהרבה לעשות זאת.
שיהיה בהצלחה!

שלום, מונגו! (MongoDB)

לפני כחודשיים, בערך, התרחש מהפך קטן-גדול.לא, אני לא מדבר על מה שקרה (וקורה, בעת כתיבת הפוסט) באוקראינה. גם לא על נתח השוק שמאבדת מייקרוסופט בהתמדה, או על החדירה של אינטל ל high end האמיתי של החומרה. דווקא החברה שמפנה במקרה זה את מקומה היא לא אחרת מאשר הענק הכחול – IBM, והיא עשויה לא-להיות האחרונה.

בסיס נתונים לא-רלציוני, בשם MongoDB תפס את המקום החמישי הנחשק במדד הפופולריות של DB-Engine, ובעצם הפך לבסיס הנתונים הלא-רלציוני הראשון שנכנס לחמישייה הפותחת [א].

יש היגיון מסוים בכך שדווקא MongoDB הוא הראשון מבין בסיסי הנתונים מסוג NoSQL לפרוץ לרשימת בסיסי הנתונים הרלציוניים: מבחינות מסוימות הוא קצת יותר דומה להם: יש לו (יחסית ל NoSQL DB) עושר רב של יכולות, וותק, וזמינות של תמיכה מסודרת (לאחרונה גם בישראל) – הכוללת "גרסאות Enterprise".

האופן בו אנשי מונגו אוהבים להציג את בסיס הנתונים: מעט פחות יכולות והרבה יותר Scalability מ RDBMS + הרבה-הרבה יותר יכולות מבסיסי נתונים NoSQL אחרים…

מקור השם של מונגו הוא מהמילה humongous (גדול, אדיר), אולם היכולת לטפל בכמויות גדולות של Data היא כנראה לא התכונה הבולטת, המושכת אליו כ"כ הרבה מפתחים. בעולם ה Internet Scale דווקא אני שומע קולות שמחשיבים את Mongo כבסיס נתונים בעל יכולות Scale מתונות.

הפופולריות של MongoDB (בקיצור: "מונגו") נובעת מהקלות שהוא מספק למפתחים בעזרת הסכֶמה (db schema) הגמישה שלו. לא עוד Migrations, או יותר גרוע: תקלות migration על סביבת production (המוכרות כנראה היטב לכל מי שעבד עם SQL בסביבת ה Enterprise).
מונגו הופך להיות מועמד לקחת את מקומו של MySQL כבסיס נתונים זול ויעיל ל"משימות רגילות" – ויש הרבה מאוד כאלו.

הבהרה חשובה: מונגו הוא לא תחליף ישיר ל RDBMS. הוא שונה. כאשר זקוקים להרבה joins על נתונים, ופעולות רוחביות ("SELECT") על הרבה אובייקטים – מונגו (או Document-Based Database אחר) כנראה לא יתאים. מונגו מתאים כאשר יש אובייקטים עצמאיים ("לקוח", "אתר", "משלוח"), ורוב הפעולות נעשות על האובייקטים הבודדים – ורק מיעוט הפעולות הן רוחביות ("כל המשלוחים שיצאו אתמול ויגיעו מחר").

סקר המראה מה מושך מפתחים ב NoSQL: לא, זהו לא ה Scale במקום הראשון – אלא דווקא הגמישות. מקור

בפוסט זה אינני מתכוון לגעת בנושאים של Scalability או Availability של מונגו או בכלל ,לא הקדמה כללית ל NoSQL (נתתי כזו פעם, היא עדיין רלוונטית דייה) ולא דיון על ה CAP theorem.

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

… עוד קיטלוג של בסיסי נתונים NoSql

מונגו DB – מי זה הבחור הזה? 

ראשית הייתי רוצה להזכיר בכמה מילים את מודל ה Document-Based Database בכלל, ואת זה של מונגו בפרט.
אם נשווה את המודל של Mongo לבסיס נתונים רלציוני ההשוואה תראה בערך כך:

מונגו מנהל מספר בסיסי נתונים, ממש כמו RDBMS. יש אוספים של "מסמכים" (ניתן לחשוב עליהם בשלב זה כמחרוזות של JSON). בתוך ה JSON יש ערכי key:value (= שדות) שמונגו מודע להם (בניגוד לבסיס נתונים K/V – שם הוא לא).

ניתן ליישם הדמייה ל KVDB (בסיסי נתונים מסוג K/V) על גבי RDBMS בכך שמייצרים טבלה עם 2 עמודות: ID ו BLOB של נתונים (למשל: קובץ JSON).

באופן עקרוני, בסיס הנתונים הרלציוני יספק את היכולת הבסיסית של KVDB, אך ללא הביצועים / Scalability / זמינות שניתן לקבל מבסיס נתונים KVDB. הוא עתיד "להתפרק" אחרי מאות מיליונים ספורים של רשומות והקצב בו הוא ישרת את לקוחותיו לא הולך להיות מרשים במיוחד.

בסיס נתונים מבוסס מסמכים (בקיצור DBDB) הוא כמו[ב] KVDB עם 2 הבדלים משמעותיים נפוצים:

  • הוא מכיר את תוכן המסמך, והפורמט שלו – והוא מאנדקס חלקים ממנו, לצורך Queries רוחביים יעילים ו/או מתוחכמים.
  • לעתים קרובות: הוא מאפשר הגדרת קשרים (למשל: היררכי) בין המסמכים.
אפשר גם "לקמבן" התנהגות כזו בעזרת RDBMS בעזרת כמה stored procedures, מה שעשוי ליצור יישום יעיל אפילו פחות מ"חיקוי ה KVDB" שתארתי למעלה.

דייט ראשון – התקנה וכלים ראשוניים

ההתקנה של מונגו היא פשוטה למדי:

  • פתיחת ZIP הכולל את קבצי ה exe. לאיזו תיקיה (ב"חלונות").
  • יצירת תיקיה ריקה בשם data/db/ (יחסית לכונן בו מונגו מותקן) – התיקייה בה מונגו מאכסן את ה data שלו. ניתן לקנפג תיקיה אחרת, כמובן.
בין קובצי ה exe ניתן למצוא כמה מעניינים:
  • mongod.exe – השדון החרוץ (daemon [ג]) של בסיס הנתונים. כלומר: התהליך המרכזי.
  • mongo.exe – ה Admin Shell, דרכו ניתן לבצע פעולות רבות.
  • mongoexport.exe – פעולות import/export של קבצי נתונים (BSON).
  • mongodump.exe/mongorestore.exe – גיבוי / אחזור של בסיס הנתונים (כקובץ בינארי דחוס).
  • mongostat.exe – תהליך שאוסף נתונים על השימוש ב mongo.
בגדול, כדי להפעיל ולנסות את mongo בצורה קלה יש פשוט להפעיל את mongod.exe ואחריו את mogno.exe. דרך ה console ניתן לבצע / לנסות את רוב הפעולות המשמעותיות של בסיס הנתונים – ולקבל פידבק מהיר.

בואו ננסה משהו:

  1. ביקשנו לראות אלו בסיסי נתונים קיימים בעזרת פקודת show dbs. בסיס הנתונים היחידי הקיים הוא local, למרות שמונגו חיבר אותנו בכניסה ל test – בסיס נתונים ריק. מונגו הוא לעתים רבות עצלן, וייצור אובייקטים מבנים רק כאשר יש בהם תוכן ממשי.
  2. השתמשנו ב use בכדי לעבור לבסיס נתונים חדש (שעדיין לא קיים). מונגו יזכור אותו, אך עדיין לא ייצור אותו.
  3. פקודת db בודקת באיזה בסיס נתונים אנו כעת.
  4. נתחיל בהכנסות: נכניס לתוך אוסף שעדיין לא קיים (people), בבסיס הנתונים שעדיין לא קיים ("db" הוא האובייקט המייצג את בסיס הנתונים הנוכחי) רשומה – כלומר מסמך, המתאר את ג'ון.
    ברגע זה מונגו ייצור את בסיס הנתונים ואת האוסף (collection) בשם people – ויכניס לתוכו את הרשומה.שימו לב שמונגו יקצה קובץ בשם data/db/myNewDb.0 בו הוא יאכסן את בסיס הנתונים. למרות שיש לי רק מסמך אחד קטן – מונגו בחר להקצות אצלי כ 200MB בדיסק – מה שעשוי להראות קצת מבהיל. הסברים – בהמשך הפוסט.

    לאחר שלמונגו יש קובץ המייצג את בסיס הנתונים – הוא יכניס במהירות את המסמך השני – לו סכמה דומה אך רחבה יותר.

  5. בשלב זה אני בודק אלו אוספים יש בבסיס הנתונים: מכיוון שבסיס הנתונים נוצר – האוספים כבר קיימים (people ואוסף האינדקסים – שמכיל רשומות ראשוניות בלבד). בדומה ל RDBMS, מונגו משתמש בעצמו כדי לנהל מידע מערכת.
  6. אני אבצע שאילתה (מקבילה ל * SELECT) על אוסף האנשים.
    הממ… אני לא זוכר שהכנסתי שדה בשם id_, אתם זוכרים?!

סביבת ה shell, כדרך אגב, היא javaScript לכל דבר – וניתן להשתמש ביכולות ה JavaScript שאתם מכירים (var, פונקציות ועוד).

בלינק זה תוכלו למצוא עוד פקודות רבות שניתן להפעיל ב shell. בעיקרון ה shell הוא כלי מצוין ללמידה, ניסוי, או תפעול של mongoDB.

שדה ה id_

כפי ששמתם לב מונגו הוסיף לנו שדה בתוך המסמך בשם id_. הוא יוסיף שדה זה אם לא הגדרנו אותו בעצמנו (בד"כ נשאיר זאת לו).

מבנה ה id_

תכונה מעניינת של ה id_ (נקרא גם object id) היא שה timestamp הוא ראשון, מה שמאפשר לבצע מיון של אובייקטים ע"פ זמן היצירה, בקירוב. הזמן נשמר ע"פ epoch[ה] של unix.

בבסיסי נתונים מסוג KVDB ו DBDB יש חשיבות לא-מעטה לדרך בה בונים את ה ids/keys:

  • ה key הוא לעתים קרובות המפתח ל partitioning, ויכול להשפיע רבות על הביצועים.
  • יעילות של ה hash function יכול להיות משמעותי כאשר מדברים על המון פעולות בשנייה.
  • מה קורה כאשר שני ids מתמפים לאותו אובייקט? זה לא סביר כאשר יש אלפי ערכים – אך סביר יותר, כאשר יש מיליארדים.

חיפוש ואינדקסים

במונגו, ניתן לחפש אחר ערכים בתוך collection בעזרת פקודת find, למשל:
db.persons.find({ lastname: 'Smith' });
החיפוש נעשה ע"י "דוגמה" או prototype: אנו מספקים את הערכים שאנו מחפשים ומונגו יחזיר לנו cursor המצביע על האובייקטים שמכילים את הערכים הללו.
ניתן לעשות חיפוש קצת יותר כללי, בעזרת query modifiers המספקים יכולות מספריות / לוגיות, למשל:
db.persons.find( { childrenCount: { $gt: 3 } } );
gt$ הוא קיצור של greater than, כלומר: אנשים בעלי 3 ילדים או יותר. שדה שלא הוגדר באובייקט (כלומר: מסמך) מסוים, יהיה בעל ערך 0 – לצורך העניין. יש גם query modifiers נוספים כגון min$ או or$ ועוד.

אפשר לחפש על אובייקטים מקוננים, למשל address.city או להגדיר שאילותות מורכבות הכוללת מספר רב של שדות או query modifiers לדוגמה:

db.persons.find( { childrenCount: { $gt: 3, $lt 20 } } , { 'address.city': 'Holon' } );
הערה: הסיבה ששמתי את address.city בתוך מרכאות נובעת מג'אווהסקריפט. שימוש בנקודה איננו תקני במפתח של אובייקט.אפשר גם לצמצם את התשובה לשדות מסוים (בדומה ל RDBMS select) בעזרת אובייקט projection – אותו מגדירים בפרמטר השני של הפקודה find:

db.persons.find({ lastname: 'Smith' }, { tile:1, lastname: 1 });

בדוגמה זו אנו רוצים לקבל רק "תואר" ושם משפחה. ערכי ה "1" הם דרך מקוצרת לכתוב true – כלומר: החזר לי את השדה הזה. שימו לב: השדות צריכים להיות כולם true או כולם false – לא ניתן לערבב. יוצא הדופן היחיד הוא השדה id_, אותו ניתן להשמיט גם כאשר יש רשימה "פוזיטיבית" של שדות בהם מעוניינים.

מה עוד ניתן לעשות ב queries? ניתן לעשות הרבה. אציין שליפת מספר קבוע-מראש של ערכים מתוך מערך במסמך (נאמר תגובות בבלוג: רק 10 תגובות ראשונות מכל פוסט), או את היכולת לעשות שאילתות קיבוץ מורכבות (db.collection.group) – ולהוסיף להן פונקציות פילטור בג'אווהסקריפט, ad-hoc.

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

בעולם ה NoSQL עושים הפרדה בין "planned Queries" ו "Ad-hoc Queries".
Planned Queries הן כאלו שהתכוננו אליהן, לרוב בעזרת יצירת אינדקסים (פנימיים או חיצוניים לבסיס הנתונים), בעוד Ad-hoc Queries הן כאלו שעושים בצורה "ספונטנית" – ללא קיום של אינדקסים, pre-fetch או כל הכנה מקדימה.

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

אינדקסים

 
הגדרה של אינדקסים היא פשוטה למדי, דומה להגדרת החיפוש:
db.persons.ensureIndex( { firstname: 1 } );
ייצור אינדקס לשדה ה firstname.
db.person.ensureIndex( { "address.city" : 1 } );
ייצור אינדקס לשדה city בתוך תת-האובייקט (או המסמך) address. כמו כן:
db.person.ensureIndex( { lastname: 1, firstname: 1} );
ייצור אינדקס של מפתח המורכב מ2 שדות: שם פרטי, ושם משפחה.
התחביר ensureIndex מצביע על כך שאם אינדקס קיים – מונגו לא ישכפל אותו.
יש עוד אפשרויות אינדוקס רבות, עליהן ניתן לקרוא בתיעוד הרשמי.
מבני הנתונים המשמשים את מונגו. מקור

אכסון (Persistency)

שאלה שבוודאי מעניינת מאוד את מי שמגיע מרקע והבנה כיצד עובד RDBMS היא "מה קורה שם בתוך מונגו?" או "כיצד ממימשו זאת?".
בחלק זה אנסה לענות על שאלה זו בקיצור.מונגו כתוב ב ++C וש לו הפצות למערכות הפעלה שונות. מערכת ההפעלה אליה הוא תוכנן במקור היא Unix/Linux (כלומר: מערכת POSIX). יוצרי מונגו לקחו כמה החלטות Design מהותיות:

  1. מונגו יעבוד קודם כל עם זיכרון (עבור המהירות). מונגו אוהב זיכרון, והרבה!
    כל בסיס נתונים שמכבד את עצמו מרוויח מזיכרון, אך כשלמונגו חסר זיכרון – נפילת הביצועים היא משמעותית מאוד.
  2. עדכון של הזיכרון לדיסק יינתן (delegated) למערכת ההפעלה. התכנון המקורי התבסס על הדרך בה מערכת Linux עובדת, וספציפית פקודת mmap למיפוי קבצים – ישירות לתוך הזיכרון. המימוש עבור מערכת ההפעלה "חלונות" הוא כנראה דומה (במסגרת הכלים ש"חלונות" מספקת), אך ידוע כפחות יעיל.
    היתרונות של גישה זו היא קלות פיתוח שמונגו הרוויח, ואי שכפול caches בין מערכת ההפעלה למונגו (מה שמנצל את הזיכרון בצורה יעילה יותר). החיסרון: המנגנון מאחורי mmap הוא כללי ואינו האופטימלי-ביותר לצרכים של מונגו.
  3. למונגו יש רמות "durability" שונות הניתנות לקנפוג. האם כתיבה לבסיס הנתונים תכנס לתור של כתיבה לדיסק ("fire and forget" – דומה ל INSERT_DELAYED של MySQL) או האם כל כתיבה נכתבת במקום ל journal על הדיסק ("fully safe"). ברירת המחדל היא "fire and forget".

מונגו מאכסן כל Database בסדרת קבצים. הקבצים מתחילים בקובץ בגודל 64MB (למשל db.0) ומכפילים את עצמם בכל פעם (128MB – עבור db.1 ואז 256MB עבור db.2) וכו' עד שמגיעים לגודל של 2GB – ונשארים שם. מונגו מקצה יותר מקום ממה שנדרש למידע בפועל – כדי לא "להתקע" באמצע רצף של כתיבות. במערכת ההפעלה שלי הוא מקצה 200MB לכל בסיס נתונים שרק נוצר.

את הקבצים עצמם מונגו מחלק ל Extents – מן בלוקים הכוללים data או אינדקסים של collection מסוים (לא מערבבים סוגים ולא מערבבים collections).
גם בתוך extent של data, מונגו שומר לכל document מעט Padding – כלומר: יותר מקום ממה שנדרש, במידה והמסמך יגדל במעט. לדוגמה: עדכון של שדה לערך ארוך יותר או הוספה של שדה. המסמכים וה extents הם רציפים בדיסק / זכרון.

db.collection.dataSize();
יספק לנו מידע מה גודל ה collection בבתים, עבור המסמכים וה paddings שלהם.
db.collection.storageSize();
יספק לנו את גודל ה data extents של ה collection. כלומר ()dataSize ועוד מקום שהוקצה בדיסק ועדיין לא בשימוש.
db.collection.totalIndexSize();

יספק לנו את גודל ה index extents של ה collection, כולל שטחים ב extents שעדיין לא בשימוש.

סטטיסטיקות נוסף ניתן לקבל בעזרת אובייקט ה dbStat.

מונגו משתמש בפקודת mmap של לינוקס בכדי למפות extents (בלוקים בקבצים) לזיכרון. הקבצים הם כמו "גדר הגנה" בפני fragmentation שיכולה להיווצר באופן טבעי במערכת הקבצים.

כאשר מסמכים גדלים מעבר ל padding הקיים – הם מועברים למקום חדש עם padding חדש. מונגו לומד את קצב השינויים ב collection ולפיו מבסס את ה padding factor לפיו יקבע גודל padding חדש. גדילות תכופות של מסמכים –> paddings גדולים יותר.

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


fragments בתוך extents של מונגו. מקור.


הפתרון בעיקרון הוא לבצע de-fragmantation בעזרת פקודת compact.

הקושי:  למונגו יש thread יחיד לכתיבות, ופעולת compact תתקע את בסיס הנתונים כולו לזמן מה (כלומר: down time). אפשר לתזמן פעולות תחזוקה אלו או אפשר, בעזרת replication של nodes של מונגו – לעשות את זה offline.
זה כנראה לא מה שהייתם מצפים מ"בסיס הנתונים מהדור החדש" – אבל ככה זה.
עוד נקודה כואבת במונגו היא נעילות (לצורך consistency):
נעילות הן ברמה של בסיס נתונים (database). עד גרסה 2.1 – נעילות בכלל היו ברמת כל ה instance של מונגו. ישנן תוכניות לעתיד לשפר את רמת הנעילה במונגו.
המשמעות היא כמובן מגבלה משמעותית לביצוע כתיבות ב scale, לפחות כאשר אנו עובדים ברמת durability (או safety) גבוהה.
הברירה היא בין לוותר על מידה מסוימת של durability, לצמצם כתיבות, או להשתמש ב very-high-end SSD עם IOPS (פעולות IO בשנייה) גבוה במיוחד: איזה חצי מיליון IOPS, הייתי אומר. האפשרות השלישית – חסומה בחומרה הקיימת כיום, כמובן.
ה consistency במונגו מובטח רק ברמת פעולה על מסמך – ואין להניח על סדר הפעולות בכלל המערכת (isolation).
מה קורה כאשר רוצים לבצע שינוי במסמך א' בצורה אטומית (למשל: להגדיל ערך מספרי ב 3)?
או שמוכנים לספוג חוסר consistency מסוים, או שאפשר להשתמש בכלי מיוחד של מונגו בשם collection.findAndModify המקבלת הוראות ומבצעת שינויים במסמכים בצורה אטומית.
הפקודה תבצע שינוי אטומי במסמך אחר מסמך, אך כל פעם – במסמך בודד.
מה קורה כאשר רוצים לבצע שינוי במסמך ב' על בסיס של נתונים ממסמך א'?
לבעיה זו אין למונגו פתרון מובנה, ויש כל מיני פתרונות מקובלים / "design patterns" כיצד לפתור את הבעיה. בגדול סט הפתרונות מתבסס על יצירת מסמך שלישי זמני בשם (מפתיע:) transaction שמסייע לנהל את הפעולה.
נקודה אחרונה מעניינת היא נושא גודל המידע. לחוסר הסכמה של מונגו יש חסרון אחד ברור: יש לאכסן את ה keys בכל פעם, מה שיכול להגדיל את גודל המידע בצורה משמעותית הן בדיסק והן ברשת (מול ה clients של מונגו).
אמנם JSON הוא מבנה נתונים יחסית יעיל, וכאשר מונגו שולח / מאכסן מסמכים – הוא בעצם משתמש בצורה בינרית יותר יעילה שנקראת BSON [ד] (קיצור של Binary JSON), אך עדיין מדובר בבזבוז.

סיכום

מונגו הוא לא בסיס נתונים מושלם, אך כנראה שהוא מספיק טוב למערכות רבות, ויכול לסייע בקיצור זמני פיתוח ותחזוקה.
ב technology stacks כמו MEAN (קיצור של Mongo, Express, Angular and Node.js) – השימוש הטבעי של מונגו ב javaScript ו JSON מאפשר לכתוב פתרון קצה-אל-קצה בשפה יחידה: javaScript.

למרות שמונגו יחסית עשיר ביכולות, יש לא מעט ספריות ODM (קיצור של Object-Document Mapping), כגון mongoose או doctrine, המסייעות במידול הנתונים או בהעשרת היכולות בשימוש במונגו.

מונגו, ומודל ה documents בכלל, טבעי מאוד לפיתוח מערכות בהן היינו משתמשים בכלי ORM: מסמך (document) הוא סוג של אובייקט, ועבודת התרגום בין שפת התכנות לשפת בסיס הנתונים, ובחזרה – נחסכת מאיתנו. ביוחד כאשר יש לפרק/להרכיב "אובייקט" יחיד מטבלאות רבות.
מונגו הוא בד"כ מהיר מ RDBMSs, לא בגלל שהמפתחים שלו חכמים יותר – אלא בגלל שהם עשו בחירות תכנוניות שמעדיפות מהירות על דברים אחרים (למשל: ההחלטה ל durability לא מושלם, כברירת מחדל). בטלו את הבחירות הללו – והפערים יצטמצמו. בעוד מימוש של KVDB הוא יחסית פשוט על גבי RDBMSs קיים (עם scale מוגבל יותר), מימוש של DBDB על גבי RDBMS הוא קשה – בגלל תהליכי האינדוקס.

כמו בסיס NoSQL רבים אחרים, מונגו מוסיף אחריות על המפתח: לדאוג לשלמות ואחידות הנתונים. מונגו העלים סט בעיות אחד מהמפתח – אך חושף אותו לסט אחר (בשאיפה: מצומצם יותר) של בעיות אחרות.
כשאתם ניגשים לעבוד במונגו – על תצפו ל Internet Scale. הוא כנראה טוב ב Scale בהרבה מ RDBMS – אך לא טוב כמו Riak או Cassandra. כמו כן אל תצפו לבגרות של RDBMS – המבוססים על מודל שכבר בעבודה במשך עשרות שנים.

בכל זאת, אם אתם מתחילים לפתח מערכת חדשה, במיוחד מערכות ווב (במידה, "המגרש הביתי של מונגו") ובמיוחד כאלו שאינן מוגדרות היטב עדיין – יש סיכוי טוב שעם מונגו תתקדמו מהר וטוב יותר מאשר עם MySQL או PostgresDB.

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

זמן קצר לאחר שחרור הפוסט שוחררה גרסה משמעותית, 2.6, של מונגו. ניתן לקרוא highlights בלינק הבא.

נעשה עדכון ב 30/3 בכדי לחדד את הנקודה שמונגו הוא לא "RDBMS מהיר" או "גמיש יותר" – הוא שונה, והוא יעיל לסוגים מסויימים של בעיות.

לינקים רלוונטיים:
מצגת מעניינת על מידול נתונים במונגו
השוואה (מעט פרובוקטיבית) בין הביצועים של מונגו ו MS SQL

—-

[א] DB-Engines קיים "כולו" שנתיים ומשהו, אבל ע"פ כל מדד מקובל – לא נראה שבסיס נתונים לא-רלציוני היה איפשהו ברשימה הפותחת ב 30 שנה האחרונות.

עוד הערה: Cassandra ומונגו נמצאים ברשימת ה top10 מאז המדד החל, אולם Sybase ASE "נדחף" ל Cassandra באמצע והחזיר אותו למקום העשירי. הסיבה היא כנראה החיזוק ש ASE קיבל מרכישת SAP את Sybase, אולם אין בכך לגרוע מהמגמה הכללית: ASE צמח בעיקר על חשבון אורקל.

[ב] ב NoSQL, כמו ב NoSQL – אין הגדרות מוסכמות או חד-משמעיות. התייחסו לכל הגדרה כ "נקודת מבט אפשרית".

[ג] מקור המונח daemon הוא מניסוי מחשבתי בתחום התרמודינמיקה בשם Maxwell's demon בו יש שדון שעובד ללא הפסקה ברקע ופותח דלת קטנה לחלקיקים שיעברו דרכה… daemon היא צורת כתיבה עתיקה של המילה demon (שד).

[ד] פורמט BSON גם מאפשר ניהול טיפוסים שלא קיימים ב javaScript כגון object id או date כפרמיטיב.

[ה] epoch (תאריך בסיס) הוא נקודת ייחוס ממנה שומרים תאריך בצורה מספרית, בדרך כלל בשניות. MS DOS, היתה מערכת 16bit ולא רצו לשמור תאריך, בשניות, משנת 0 (או אפילו מ 1970, כמו unix) – ולכן ה epoch הוא 1/1/1980.

כדי לתאר את התאריך 2/1/1980 (יום אחרי) – יש לשמור בשדה התאריך את המספר 24*60*60 = 86,400.

במערכת הקבצים NTFS (חלונות NT) זמן הייחוס הוא שנת 1601 – השנה הראשונה במחזור 400-השנים של לוח השנה הגרוגאני בו, שימו לב: שוחררה חלונות NT!. אפל (כתגובה?) קבעה את ה epoch של OS X לשנת 2001 – השנה בה יצאה מערכת ההפעלה OS X. בגרסאות קודמות של MAC OS, ה epoch היה שנת 1904 – שנקבעה בגלל שזו הייתה "השנה המעוברת הראשונה במאה ה-20", ואולי כדי להתבשם בעובדה שזו הייתה מערכת שרצה על מעבדי 24 ביט (בזמן ש DOS הייתה תלויה בעבדי 16 ביט, ו epoch כזה היה מגביל אותה).

מנוע מבני-נתונים: Redis

ישנן משימות תכנות פשוטות למדי. למשל:

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

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

כאשר הצרכנים של השירות נמצאים על מחשבים אחרים (פיסית) – יש לחשוף להם את השירות:

  • לחשוף גישה ברשת (למשל Java Servlet).
  • להגדיר פרוטוקול/פורמט (למשל מבוסס REST, שיהיה פשוט) כיצד מבצעים קריאה לשירות ואיזה תשובה מקבלים.
  • לדאוג לטיפול במקביליות / בעיות consistency של הנתונים.
כל אלו יכולים להפוך משימה של דקות – למשימה של כמה שעות, ולדרוש תחזוקה גדולה יותר לאורך הזמן.
אם נתבונן במערכות מבוזרות, ניתן לראות שפעמים רבות החלק הגדול של הקוד הוא קוד שרץ מקומית – עם נקודות סנכרון בין מספר שרתים / שירותים מרוחקים. נקודות הסנכרון הללו דורשות לא מעט עבודה (יחסית לקוד מקומי דומה).הסנכרון נעשה לרוב ע"י מבני נתונים או ע"י הודעות (שגם הודעות לרוב מנהלים בעזרת מבני נתונים).

Redis (קיצור מעט מוזר של REmote DIrectory Server) הוא "מנוע מבני-נתונים" המספק לנו שירות של מספר מבני נתונים עם גישה מרוחקת, אטומיות ואפשרות של שמירת מבני-הנתונים לדיסק (Persistence). ל Redis יש ספריות client המאפשרות גישה קלה למדי במגוון רחב מאוד של שפות תכנות.
אם אתם משתמשים בשפת נישה שאינה ברשימה (למשל שפת Boo), פרוטוקול הגישה ל Redis הוא פשוט מספיק על מנת לממש Client בקלות יחסית.

ייתכן וזו הפעם הראשונה בה אתם שומעים על Redis, אולם בכל כדאי להכיר ש Redis משמש כבר היום כמה תשתיות רציניות למדי:

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

—-

התקנה (למי שמוכן "ללכלך" מעט את הידיים)

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

Redis תומכת רשמית ב OS X, Linux ו BSD Unix. מכיוון שאני עובד על "חלונות" אני משתמש בגרסה לא רשמית – אך טובה מספיק עבור פיתוח: https://github.com/MSOpenTech/redis.

את הגרסה מפתח ומתחזק צוות של מייקרוסופט ש"שומר שטכנולוגיות-מפתח לא יסגרו בפני משתמשי חלונות/Azure". חילופי תפקידים משעשע…

הנה הסבר כיצד להתקין את Redis על "חלונות" במהירות:

קולאג' הפעולות להתקנה מהירה של רדיס על "חלונות"
  1. הורידו את קובץ ה ZIP של כל ה Repository מתוך github (כפתור ה download הוא בפאנל הימני).
  2. פתחו את קובץ ה ZIP שירד.
  3. בתוך ה ZIP, עברו לתת-התיקיה redis-2.6/bin/release.
  4. פתחו את הקובץ redisbin64.zip (מכונות 64 ביט) או הקובץ השני (32 ביט) ו"שפכו" את תוכנו לאיזו תיקיה.
  5. הפעילו את השרת של redis.
  6. הפעילו את הלקוח CLI של redis.

—-

ניסיון ראשון עם רדיס

Redis בנוי כ Dictionary (כלומר "Hash Table") ענק של צמדי .

מנקודת מבט מסוימת ניתן לומר שזהו בסיס נתונים NoSql-י, מסוג Key/Value (בעצם Key/Data Structure) שפועל In-Memory. מנקודת מבט זו Redis הוא מהיר בצורה קיצונית[א] וקצת חריג בנוף של NoSQL Databases.

בדוגמה למעלה פתחתי 2 clients של Redis ובצעתי בהם מספר פעולות על מבנה הנתונים הפשוט ביותר: String.

  1. הכנסתי ערך של "!hello world" למפתח "messages:hello". הסימן ":" הוא קונבנציה מקובלת ל namespacing של ערכים, בדומה ל סימן "." לסימון packages בג'אווה.
  2. קראתי את הערך – ערך ההחזרה מוצג בשורה הבאה.
  3. ניסיתי לקרוא מפתח שלא הושם בו ערך – וקיבלתי nil.
  4. קראתי את הערך מה client השני – והערך זמין לו.
    אם זה לא היה קורה –  לא היה הרבה טעם ב Redis, כאשר אני מקבל את זה בקלות בג'אווה : )

את הפקודות, אגב, אני מקליד ב Capital letters לצורך ההדגשה, הפרוטוקול של redis מקבל אותן בכל case.
המפתחות הם (כמובן) Case Sensitive.

Lists

בואו נעבור למבנה נתונים מעט יותר מורכב: List.
המקביל בג'אווה (עולם מוכר?) ל List של Redis הוא <LinkedList<String, בערך. מדובר ברשימה משורשת עם השלכות הסיבוכיות הידועות (זול להכניס, יקר לחפש). היא מנהלת רק מחרוזות. בתיעוד של Redis מצוינת על כל פקודה הסיבוכיות שלה, למשל (O(n + הסבר מהו n.

במה List שונה מ <LinkedList<String של ה JDK? הנה 2 דוגמאות:

  • המימוש מעט שונה. למשל פקודת LINDEX (האות L עבור List) סורקת את הרשימה משני הכיוונים: פעם מימין, ופעם משמאל – מה שאומר שאם האיבר שאנו מחפשים הוא האחרון ברשימה – ניתן לצפות לזמן של (O(1.
  • בג'אווה יש רשימה "מסונכרנת" או רשימה "לא מסונכרנת". ב Redis זו אותה רשימה כאשר יש פעולת שליפה "מסונכרנת" או "לא מסונכרנת". הגישה של Redis היא מאוד לא-דפנסיבית (כמו ג'אווה), אלא יותר כמו של Unix ("אתה אחראי למה שאתה עושה").

בואו נשחק מעט עם List:

  1. גיא יוצר רשימה בשם guyList. הדרך ליצור רשימה ברדיס – היא פשוט להתחיל ולהכניס לה ערכים, במקרה זה: בעזרת פקודת LPUSH = "דחוף לרשימה" וגם "דחוף משמאל".
    ניתן לראות את ערך ההחזרה – 3, 3 ערכים נוספו לרשימה.
  2. עכשיו נדחוף ערך אחד מימין בעזרת RPUSH.
  3. LLEN בודק את אורך הרשימה. כצפוי: הוא 4.
  4. ניתן לקבל טווח של ערכים ברשימה ע"פ האינדקס שלהם, במקרה הזה – כל הרשימה שלנו.עכשיו נוסיף לסיטואציה את בן, שמעביר פריטים מהרשימה של גיא לרשימה משלו.
    הכוונה שלי היא לייצר מעט "דרמה", והיא לא לעסוק בנושאי אבטחה. מבחינת אבטחה: יש לנהל ניהול גישה (authentication) בצורה אפליקטיבית על שרת האפליקציה (ג'אווה, Haskell, רובי, Whatever) [ב].
  5. בן בוחר דווקא להשתמש בפקודות רדיס ב lower case – מותר.
    מכיוון שהוא הולך לבצע פקודה הנוגעת ל2 מבני נתונים שונים (כל פקודות רדיס אטומיות כל עוד מדובר במבנה נתונים אחד) – עליו לשמור על consistency והוא עושה זאת ע"י הפעלת transaction – פקודת multi, ומקבל אישור.
  6. הוא מסיר מצד ימין (rpop) את האבר האחרון ברשימה של גיא.
  7. הוא "דוחף" את ערך האיבר ("itemD") לרשימה חדשה משלו. ניתן לציין את הערכים עם או בלי מירכאות. מכיוון שאלו מחרוזות – התוצאה תהיה זהה.
  8. בעזרת exec בן מנסה לבצע "commit" לטרנזקציה – והוא מצליח.
  9. בעצם, מכיוון שתהליך העברת ערכים בין רשימות הוא נפוץ ברדיס, יש פקודה מקוצרת שעושה את 2 הפעולות הנ"ל בצורה אטומית (כלומר: לא צריך להפעיל טרנזקציה). הפקודה נקראת… (מפתיע!): rpoplpush ומקבלת את המקור והיעד. הערך שעכשיו יעבור הוא "itemA". ייתרון נוסף בפקודה ישירה הוא ביצוע roundtrip יחיד ברשת – ולא ארבעה.
    האם יש גם פקודת lpoprpush או lpoplpush ברדיס? בכן… לא. רדיס שומר על פשטות, לעתים במחיר הקלות למשתמש: לעתים צריך מעט יצירתיות בכדי למצוא את הדרך עם הפקודות המובנות של רדיס (או שאפשר להרחיב את הקיים בעזרת LUA – על כך בהמשך). זה, לטעמי, מעט חסרון של רדיס – והייתי שמח שתצוץ ספריית הרחבה (למי שמעוניין) שמשכללת את סט הפקודות שרדיס מכיר.
  10. אנו בוחנים את הרשימות ורואים את התוצאה הסופית.

מבני נתונים נוספים

Redis תומך בחמישה טיפוסים:

  • String (עד 512MB) – יכול להכיל כל אובייקט מקודד למחרוזת (json, תמונה וכו'). מכיוון של Redis אין indexing – אין טעם "לפרק" אובייקטים מורכב לחלקים קטנים יותר. אני מניח שניתן ליישם אינדוקס חיצוני – אם צריכים.
    ניתן לאכסן ב strings גם ערכים מספריים ולבצע עליהם פעולות אטומיות כגון INC (קיצור של increase – כמו בשפת פאסקל), INCBY ו INCBYFLOAT.
  • List שהוא בעצם <LinkedList<String, עליו דיברנו למעלה. ל List יש גם פעולות blocking שאם יבוצעו על רשימה ריקה "יתקעו" את ה client עד שיהיה ערך מסוים או שיעבור timeout שהוגדר בפעולה.
  • Sets שהוא בעצם <Set<String (כל איבר יכול להופיע פעם אחד בלבד). מאפשר לעשות פעולות יעילות על קבוצות כגון Union או intersection. פקודות של Set מתחילות באות "S".
  • SortedSet שהוא בעצם <SortedSet<String ומחזיק את הרשימה באופן ממוין-תמידית, מה שמאפשר לבצע פעולת Range (שליפה של טווח של איברים) בצורה יעילה. לכל ערך ב SortedSet יש ערך מספרי (score) וערך מחרוזת (value). הערך המספרי קובע את הסדר. על מבני נתונים אלו ניתן גם לעשות פעולות על קבוצות (כמו union) ואפילו לבצע פעולות חישוביות על ה scores (פקודת ZUIONSTORE). פקודות של SortedSets מתחילות באות "Z".
  • Hash – שהוא בעצם <Map<String (או <Dictionary<String למי שבא מ #C). כלומר: value של ה K/V store שלנו הוא K/V בעצמו. לא ניתן לקנן Hash מעבר לרמה אחת. פקודות של Hash מתחילות באות "H".

בעיות נפוצות לדוגמה שנפתרות בעזרת Redis:

  1. Cache מבוזר, המשותף לכמה שרתים. שיתוף זה מאפשר ששרת אחד יחדש את ה cache – וכל השאר יהנו מחידוש זה.
  2. ניהול State אשר מצד אחד הוא בזיכרון (כמו server session state) ומצד שני הוא משותף (כמו db session state) כך שאם המשתמש אינו יכול לחזור ל node האחרון שטיפל בו, ה node החדש יכול לגשת ל session של המשתמש ולא לרסט אותו. אם הנושא לא מוכר – ניתן ללמוד עליו מהספר של מרטין פאוולר שבקישורים.
  3. Pub/Sub – מערכת הודעות בין כמה שרתים.
  4. Job Queue לחלוקת עבודה במערכת מבוזרת.
  5. ספירה וניהול מבוזר של counters.
  6. פתרון בעיות של מערכות מבוזרות כגון Leader Election, בעיות הצבעה ובעיות שעון / סנכרון זמנים. ניתן למצוא בלינק הבא כמה רמזים / המלצות למימוש.

לבעיית ה Pub/Sub החליטו להציע פתרון מובנה – הנוח מאוד לשימוש.
לבעיות ה Cache ישנן פקודות כמו EXPIRE (מחיקת ערך לאחר זמן נקוב), TTL (לבדוק כמה זמן קצוב נותר לאיבר) או PERSIST (ביטול הקצבת הזמן).
לבעיית ה counting יש את משפחת פקודות ה INC.
וכו'

דוגמה לשימוש ב Redis בעזרת client (או driver) לג'אווה הנקרא Jedis

יכולות אחרות

ל Redis יש עוד כמה יכולות משמעותיות שכדאי להכיר:

Persistency
היכולת לשמור את מבני הנתונים לדיסק.
ברדיס יש שני מנגנוני שמירה:

  • RDB (קיצור של Redis Database) – שמירת כל מבני הנתונים בזיכרון ביחד לדיסק, ע"פ מדיניות קבועה / פעולה יזומה של המשתמש.
  • AOF (קיצור של Append Only File) – שמירת פעולות אחרונות ל Log file לצורך התאוששות במקרה של קריסה.
כדאי לציין זאת עכשיו: Redis הוא לא פתרון בסטנדרט גבוה של durability בשמירה לדיסק. אם אתם שומרים (בקיצוניות) מידע פיננסי – השתמשו במנגנון אחר בכדי לשמור אותו, לא ברדיס. ניתן להגיע עם רדיס לאמינות לא-רעה שמתאימה לשימושים רבים.
AOF ניתן להפעיל אותו עבור כל שינוי של מפתח (פגיעה קשה בביצועים) או כל שנייה (tradeoff סביר בין אמינות וביצועים – ברירת המחדל). ניתן גם לכבות יכולת זו בכלל או להשאיר את ה flushing למדיניות של מערכת ההפעלה, שזה סוג של כתיבה מדי זמן-מה.
RDB ניתן להפעלה ע"י פקודות (SAVE או BGSAVE) או קונפיגורציה. קונפיגורציה נעשית בקובץ redis.conf, בואו נתבונן ב section המתאים:
ברירת המחדל היא סבירה למדי:
  • שמירה לאחר 15 דקות לאחר שינוי של מפתח כלשהו.
  • שמירה לאחר 5 דקות אם השתנו 10 מפתחות או יותר.
  • שמירה לאחר דקה אם השתנו 10,000 מפתחות.
אם המערכת שלכם עובדת בעומסים נמוכים – ניתן לצמצם, נאמר, לשמירה לאחר דקה לאחר שינוי כלשהו (קרי save 60 1).
ניתן לקרוא עוד בנושא בתיעוד הרשמי של רדיס.

Transactions
כפי שהראנו למעלה בעזרת פקודות כמו MULTI ו EXEC ניתן לייצר טרנזקציות בצורה פשוטה.
ניתן לקרוא עוד בנושא בתיעוד הרשמי של רדיס.

Scripts
ניתן לכתוב בשפת LUA סקריפטים המבצעים סדרת פקודות – וכך להרחיב את סט הפקודות הזמין. הסקריפטים יכולים להישלח בכל קריאה (הפעלה של פקודת EVAL) או להישמר בקובץ ה redis.conf.
יתרונות ה Scripts דומים למחשבה על Stored Procedure ב Database – אנו חוסכים את ה latency בין קריאה לקריאה ובפקודה אחת ניתן לבצע את סט הפקודות ישירות ב DB (במקרה שלנו: Redis).

ניתן לקרוא עוד על סקריפטים בתיעוד של רדיס.

Clustering
Redis הוא כמעט-single-threaded. כל הפקודות יבוצעו ע"י thread יחיד ורק פעולות של שמירה לדיסק עשויות להיעשות ב thread נפרד. משמעות אחת היא שאם יש לכם שרת עם שמונה cores – יש להפעיל 8 תהליכים שונים של redis (ב multiplexing) על מנת לנצל את כח החישוב של המכונה כראוי.
שכפול מנועים הוא תסריט אפשרי לניהול cache – אבל בעייתי לכמעט כל תסריט אחר. לצורך כך יש ברדיס מנגנון של Partitioning ויש גם מנגנון של master/slave cluster.

מנגנונים אלו דורשים התעמקות על מנת להבין אותם היטב ולהגיע בעזרתם לביצועים מיטביים.

סיכום

Redis הוא מנוע שיכול לסייע לפתח מערכות מבוזרות ויעילות להפליא. הוא בנוי בפשטות ("עושה דבר אחד – אבל עושה אותו היטב") – מה שלעתים מותיר את המפתח עם "שפה פרימיטיבית" שהוא רוצה להרחיב. סביר למדי שתכתבו בשפת האפליקציה מעין Adapter שמתקשר בשפה ה"פרימיטיבית" של רדיס – ומציג לאפליקציה שפה "גבוהה" יותר.

כשתהיה לכם בעיה שכרוכה במספר מחשבים ("מבוזרת") – חשבו על Redis.

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

לינקים מעניינים

הארכיטקטורה של רדיס: http://www.enjoythearchitecture.com/redis-architecture ו http://pauladamsmith.com/articles/redis-under-the-hood.html

תבניות שימוש ברדיס: http://www.slideshare.net/dvirsky/kicking-ass-with-redis

רדיס בטוויטר: http://bit.ly/1pm7PsV

עוד פרטים על ה Persistency של רדיס: http://oldblog.antirez.com/post/redis-persistence-demystified.html

redsmin – כלי monitoring לרדיס: https://redsmin.com

[א] נו – הכל רץ בזיכרון. זה לא רציני לזרוק סתם כך מספרים ללא Use-case מדויק וחומרה עליה הבדיקה רצה, אבל בהערת צד אפשר לספר שמדברים על מספרים כגון "100,000tps" – מספר שמשאיר באבק כל "בסיס נתונים" אחר, בערך.
tps = transactions per seconds שאילתות בשנייה. ברור שקל יותר לשלוף ערך מתא בזיכרון מלבצע join על מידע ששמור על הדיסק.

[ב] יותר ספציפית, כן יכול להיות מצב שבו מישהו חדר ל Data Center – ואז רדיס הוא "פרוץ" לגישה. לרדיס יש אפשרות לבצע אימות גישה (Authentication) על בסיס ססמה שנקבעה מראש – לא פרדיגמה קשיחה במיוחד, אלא כזו שתחסום את התוקף המזדמן. כאשר זקוקים ליותר הגנה – מתקינים לרוב Firewall מקומי על השרת של רדיס שיאפשר תקשורת נכנסת רק מכתובות ה IP של שרתי האפליקציה. אם שרתי האפליקציה נפרצו… לא ברור עם הגנה קשיחה יותר על רדיס תעזור.

עשה זאת בעצמך: 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?