ייעול הכנסת נתונים מנורמלים לבסיס הנתונים בFireStats

במסגרת מקצה אופטימיזציות לFireStats מימשתי מנגנון נוסף לקליטת כניסות.
המנגנון הרגיל בו כולם משתמשים היום בודק שהכניסה לא צריכה להיות מסוננת (כניסה של רובוט ידוע או IP מסונן למשל), ומכניס את הנתונים בצורה מנורמלת.
נירמול בסיס נתונים נועד למנוע כפילויות, מה שעוזר במניעת אנומליות ובחיסכון במקום. בFireStats הנרמול מתבטא בכך שכל כתובת נשמרת פעם אחת בטבלאת הURLים, כל UserAgent בטבלאת הUserAgents וכדומה. כאשר מכניסים את הנתון העיקרי של כל הכניסה, משתמשים במזהה של כל נתון מנורמל.
המשמעות של זה בזמן הכנסת הנתונים היא כזו:
הגיעה כניסה עם כתובת מסויימת, מפנה מסויים ודפדפן (UserAgent) מסויים. לכל אחד מהנתונים האלו מבצעים פחות או יותר את סדרת הפעולות הבאה:
הכנס לטבלא הרלוונטית עם INSERT IGNORE, מה שמונע שגיאה במקרה שהנתון כבר נמצא שם (הטבלאות מוגדרות לא לקבל רשומות כפולות).
בדוק מה המזהה של הנתון שהכנסנו (אם הוא נכנס, אז זה יהיה מזהה חדש, אם לא זה יהיה המזהה שנבחר בפעם הראשונה שהכנסנו את הנתון לטבלא).
לבסוף, הכנס שורה לטבלאת הכניסות תוך שימוש במזהים שמצאנו בצעדים הקודמים.
זה קצת יותר מורכב מזה כי גם צריך לזהות ולסנן כניסות מסויימות כאמור.

כל התהליך הוא איטי למדי.
יצרתי קובץ CSV עם 100,000 כניסות (מfirestats.cc) והשתמשתי בו למדידת הביצועים:
בשיטה של הכנסה מנורמלת של כל כניסה, הקצב מתחיל די לא רע עם 80 כניסות לשניה, אבל ככל שבסיס הנתונים מתמלא הוא יורד עד שמתייצב על 7-8 כניסות לשניה.
למרות שקצב כזה בהחלט מספיק לכל בלוג מצוי, הוא ממש לא מספק לאתרים רציניים יותר או לבלוגיות עתירות בלוגים.

כשהוספתי תמיכה בWPMU בFireStats 1.4, חזיתי (חודשים לפני שביצעתי את המדידות) שקצב הכניסות יכול להיות בעיה בבלוגיות והוספתי שיטה חדשה לקליטת נתונים.
במקום לקלוט את הנתונים בצורה מנורמלת, הנתונים נקלטים לטבלאת כניסות ממתינות בצורה לא מנורמלת עם INSERT DELAYED. המשמעות של הDELAYED היא שבסיס הנתונים מכניס את הנתונים בזמנו הפנוי, ולא מחזיק את הקורא עד שהנתון הוכנס ממש.
עדיין צריך לנרמל את הנתונים, ולכל כתבתי סקריפט PHP פשוט שעובר על הכניסות הלא מנורמלות בטבלא אחת אחת, ולכל אחת קורא לפונקציה הרגילה שקולטת נתונים ומנרמלת אותם, ולבסוף מוחק את הכניסה מטבלאת הכניסות הממתינות. (מנהל המערכת אחראי לדאוג שהסריפט ירוץ בפרקי זמן סבירים, למשל באמצעות cron).
אני שומע אתכם צועקים: כן, אבל זה יהיה איטי לפחות כמו קודם, אם לא יותר!
זה נכון, אבל לפחות זה מאפשר לתזמן את העיבוד של הכניסות לזמנים פחות עמוסים כמו הלילה.
חיסרון נוסף הוא שהמשתמשים כבר לא מקבלים את הנתונים בזמן אמת, אלא נתונים שנכונים נכון לזמן העיבוד האחרון של הכניסות בטבלאת הממתינים.

אחרי שמדדתי את הזמן שדרוש כדי להכניס 100,000 כניסות בשיטה הרגילה, כמובן שמדדתי את הזמן שדרוש בשיטה המעוכבת, שנועדה לשפר ביצועים.
מסתבר שבשיטה המעוכבת, FireStats קולט בסביבות ה1000 כניסות לשניה, אבל המילכוד הוא שהנתונים עדיין דורשים עיבוד כדי שיהיו שימושיים, והעיבוד יקר בדיוק כמו הטיפול הרגיל.

הצעד הבא הוא כמובן לשפר את הביצועים של העיבוד הנ"ל.
כבר מהרגע הראשון שכתבתי אותו, היה לי ברור שדרושה פה אופטימיזציה רצינית, והיה לי גם ברור שהיא תהיה מסובכת.
האופטימיזציה מתבססת על התובנה הבאה:
בתוך קבוצת כניסות שנקלטו בפרק זמן מסויים, יהיו חזרות רבות מאוד של כתובות, מפנים ודפדפנים.
מה אם במקום לטפל בהם אחד אחד, נטפל בהם בקבוצות? נניח 1000 כניסות בכל קבוצה?
במקום 1000 כתובות ו1000 מפנים, יהיו לנו משהו כמו 300 כתובות (שכוללים מפנים).
במקום משהו כמו 1000 דפדפנים יהיו לנו משהו כמו 50 או 100 דפדפנים.
עכשיו נצטרך להכניס את כל מה שחדש לבסיס הנתונים, לחלץ את המזהים שלהם, ולבסוף להכניס את הכניסות עצמן תוך שאנחנו משתמשים במזהים מקודם, רק שהפעם נכניס 1000 כניסות במכה במקום כניסה אחת.
המשמעות של זה היא בעצם לממש את הפונקציה שמכניסה כניסה בודדת – אבל לרוחב, כך שתהיה ברוחב של k כניסות (קבוע כלשהו).
המימוש של זה מורכב, ולמעשה לקח יותר מפי עשר שורות קוד מהמימוש התמים הקודם, אבל הביצועים סוכר:
מעיבוד של 8 כניסות בשניה, עליתי לעיבוד של 600 כניסות לשניה.
שיפור של פי 75!

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

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

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

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

Facebook Comments

11 תגובות בנושא “ייעול הכנסת נתונים מנורמלים לבסיס הנתונים בFireStats”

  1. אני לא DBA וכו'
    1. טבלאות שמוגדרות עם BACKEND שונה יגיבו שונה. נסה INNODB לעומת MYISAM למשל
    2. טבלאות עם יותר אינדקסים מאיטות INSERT ומאיצות SELECT. נסה לשנות את המינון, בדוק אולי אם אפשר להגדיר KEYS יותר יעילים לUPDATE במקום INSERT. יש עוד תוכנות לשמירת לוגים בטבלאות MYSQL וצריך לראות פשוט איך הם עושים את זה.
    3. מישהו אמר stored procedures? אפשר אולי לבנות טבלאות שמעדכנות את עצמן בתלות עם המידע הישן. כלומר הולכות לשורה הנכונה בטבלה ועושות "+1" לשדה הנכון במחיר נמוך.

  2. יפה מאוד!

    בשביל הבדיקה, הייתי מנסה גם להשוות ביצועים מול stored procedure עם subqueries ו/או cursors.
    (למרות שאני מניח שלמשתמשים רבים לא יהיה MySQL 5 כך שעדיין יהיה צורך לעבוד עם PHP. מצד שני סביר שלמשתמשים ה"כבדים" שדורשים את הביצועים המשופרים כן תהיה אפשרות להשתמש בבסיס הנתונים העדכני ביותר).

    אודי

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

  4. אחחחח

    אני במקומך הייתי ממשיך לשחק בWii 🙂

    אגב, יש ברוקבוקס Frozen Bubble! אני מכור! קשות!

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

  6. עמרי, בוודאי שאפשר לבחור מינון. המיניום הוא אינדקס אחד שהוא הKEY אבל אפשר להוסיף ולהוריד אינדקסים מלבדו, כולל אינדקסים שכולים יותר משדה אחד בהגדרתם.

    וחוק מספר אחד (לדעתי) – אף פעם לא קיים מושג של יותר מדי אופטימיזציה 🙂

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

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

  8. אתה לא הבנת אותי אם כך. מה שאמרתי בתגובה הראשונה היה שאולי יש לך יותר מדי אינדקסים וזה מאיט INSERT. המערכת שלך מוציאה הרבה יותר INSERTים מאשר SELECTים, לעומת מערכת כמו בלוג שמכניסה מעט INSERTים ומוציאה המוני SELECT. לכן יכול להיות שעדיף לשלם בSELECT ללא אינדקס למען INSERT יותר מהיר.

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

  9. אוי ווי… אני רואה שבחצי השעה האחרונה המניה שלך בנאזדא"ק נפלה 10%. אני אזהר בפעם הבאה לפני הכרזות כאלו 🙂

סגור לתגובות.