יום ראשון, 16 בפברואר 2014

מנוע מבני-נתונים: 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 של שרתי האפליקציה. אם שרתי האפליקציה נפרצו... לא ברור עם הגנה קשיחה יותר על רדיס תעזור.


תגובה 1:

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

    השבמחק