כשמבצעים אופטימיזציה של ביצועי אפליקציה, כדאי לקחת בחשבון את השימוש שלה ב-NDB. לדוגמה, אם אפליקציה קוראת ערך שלא נמצא במטמון, הקריאה הזו אורכת זמן. יכול להיות שאפשר להאיץ את האפליקציה על ידי ביצוע פעולות ב-Datastore במקביל לפעולות אחרות, או על ידי ביצוע כמה פעולות ב-Datastore במקביל זו לזו.
ספריית הלקוח של NDB מספקת הרבה פונקציות אסינכרוניות (async).
כל אחת מהפונקציות האלה מאפשרת לאפליקציה לשלוח בקשה ל-Datastore. הפונקציה מחזירה מיד אובייקט Future. האפליקציה יכולה לבצע פעולות אחרות בזמן ש-Datastore מטפל בבקשה.
אחרי ש-Datastore מטפל בבקשה, האפליקציה יכולה לקבל את התוצאות מאובייקט Future.
שימוש בממשקי API אסינכרוניים וב-Futures
כמעט לכל פונקציה סינכרונית של NDB יש פונקציה מקבילה _async. לדוגמה, put() כולל put_async().
הארגומנטים של הפונקציה האסינכרונית תמיד זהים לארגומנטים של הגרסה הסינכרונית.
ערך ההחזרה של שיטה אסינכרונית הוא תמיד Future או (עבור פונקציות 'multi') רשימה של Future.
Future הוא אובייקט ששומר את המצב של פעולה שהופעלה אבל אולי עדיין לא הושלמה. כל ממשקי ה-API האסינכרוניים מחזירים אובייקט אחד או יותר מסוג Futures.
אפשר לקרוא לפונקציה Future של get_result()
כדי לבקש ממנה את התוצאה של הפעולה שלה;
הפונקציה Future חוסמת את עצמה, אם צריך, עד שהתוצאה זמינה,
ואז היא מעבירה אותה אליכם.
הפונקציה get_result() מחזירה את הערך שמוחזר על ידי הגרסה הסינכרונית של ה-API.
הערה:
אם השתמשתם ב-Futures בשפות תכנות אחרות, יכול להיות שתחשבו שאפשר להשתמש ב-Future כתוצאה ישירות. אי אפשר לעשות את זה כאן.
בשפות האלה נעשה שימוש ב
futures משתמעים, וב-NDB נעשה שימוש ב-futures מפורשים.
מתקשרים אל get_result() כדי לקבל את התוצאה של FutureNDB.
מה קורה אם הפעולה מעלה חריגה? זה תלוי במועד שבו מתרחשת החריגה. אם NDB מזהה בעיה כששולחים בקשה (למשל, ארגומנט מסוג שגוי), השיטה _async() מעלה חריגה. אבל אם החריגה מזוהה על ידי שרת Datastore, השיטה _async() מחזירה Future, והחריגה תופעל כשהאפליקציה שלכם תקרא ל-get_result() שלה. אל תדאגו יותר מדי לגבי זה, בסופו של דבר הכל מתנהג בצורה די טבעית. יכול להיות שההבדל הכי גדול הוא שאם יודפס traceback, תראו חלקים מהמנגנון האסינכרוני ברמה הנמוכה.
לדוגמה, נניח שאתם כותבים אפליקציה של ספר אורחים. אם המשתמש מחובר, רוצים להציג דף עם הפוסטים האחרונים בספר האורחים. בדף הזה צריך להופיע גם הכינוי של המשתמש. האפליקציה צריכה שני סוגי מידע: פרטי החשבון של המשתמש המחובר ותוכן הרשומות בספר האורחים. הגרסה ה'סינכרונית' של האפליקציה הזו עשויה להיראות כך:
יש כאן שתי פעולות קלט/פלט בלתי תלויות: קבלת הישות Account ואחזור הישויות האחרונות Guestbook. כשמשתמשים ב-API הסינכרוני, הפעולות האלה מתבצעות אחת אחרי השנייה. אנחנו מחכים לקבל את פרטי החשבון לפני שאנחנו מאחזרים את ישויות ספר האורחים. אבל האפליקציה לא צריכה את פרטי החשבון באופן מיידי. אנחנו יכולים לנצל את היתרון הזה ולהשתמש בממשקי API אסינכרוניים:
בגרסה הזו של הקוד, קודם נוצרים שני Futures (acct_future ו-recent_entries_future), ואז המערכת ממתינה להם. השרת מטפל בשתי הבקשות במקביל.
כל קריאה לפונקציה _async() יוצרת אובייקט Future ושולחת בקשה לשרת Datastore. השרת יכול להתחיל לטפל בבקשה באופן מיידי. התשובות מהשרת עשויות להתקבל בכל סדר שרירותי. האובייקט Future מקשר בין התשובות לבקשות התואמות שלהן.
הזמן הכולל (האמיתי) שמושקע בגרסה האסינכרונית שווה בערך לזמן המקסימלי של הפעולות. הזמן הכולל שחלף בגרסה הסינכרונית חורג מסכום זמני הפעולה. אם אתם יכולים להריץ יותר פעולות במקביל, פעולות אסינכרוניות יעזרו לכם יותר.
שימוש ב-Tasklets
טסקלט של NDB הוא קטע קוד שיכול לפעול במקביל לקוד אחר. אם כותבים tasklet, האפליקציה יכולה להשתמש בו בדומה לשימוש בפונקציית NDB אסינכרונית: היא קוראת ל-tasklet, שמחזיר Future. מאוחר יותר, קריאה לשיטת get_result() של Future מחזירה את התוצאה.
Tasklets הם דרך לכתוב פונקציות מקבילות בלי להשתמש ב-threads. הם מופעלים על ידי לולאת אירועים ויכולים להשהות את עצמם כדי לחכות לקלט/פלט או לפעולה אחרת באמצעות הצהרת yield. המושג של פעולה חוסמת מופשט לתוך המחלקה Future, אבל יכול להיות ש-tasklet גם yield RPC כדי להמתין לסיום ה-RPC הזה.
אם ל-tasklet יש תוצאה, הוא raise חריגה מסוג ndb.Return. לאחר מכן, NDB משייך את התוצאה ל-Future שyield קודם לכן.
כשכותבים טסקלט של NDB, משתמשים ב-yield וב-raise בצורה לא רגילה. לכן, אם תחפשו דוגמאות לשימוש בהם, סביר להניח שלא תמצאו קוד כמו NDB tasklet.
כדי להפוך פונקציה ל-tasklet של NDB:
- מוסיפים את הקישוט
@ndb.taskletלפונקציה, - להחליף את כל הקריאות הסינכרוניות למאגר הנתונים ב
yieldקריאות אסינכרוניות למאגר הנתונים, - הפונקציה 'מחזירה' את ערך ההחזרה שלה עם
raise ndb.Return(retval)(לא נדרש אם הפונקציה לא מחזירה כלום).
אפליקציה יכולה להשתמש ב-tasklets כדי לשלוט טוב יותר בממשקי API אסינכרוניים. לדוגמה, נניח שיש לכם את הסכימה הבאה:
...
כשמציגים הודעה, הגיוני להציג את הכינוי של המחבר. הדרך ה'סינכרונית' לאחזור הנתונים כדי להציג רשימה של הודעות עשויה להיראות כך:
לצערנו, הגישה הזו לא יעילה. יכול להיות שתראו את דפוס המדרגות הבא.
החלק הזה של התוכנית יפעל מהר יותר אם פעולות ה-GET יוכלו לחפוף.
אפשר לשכתב את הקוד כדי להשתמש ב-get_async, אבל קשה לעקוב אחרי הבקשות וההודעות האסינכרוניות ששייכות זו לזו.
האפליקציה יכולה להגדיר פונקציה משלה מסוג async על ידי הפיכתה ל-tasklet. כך תוכלו לארגן את הקוד בצורה פחות מבלבלת.
בנוסף, במקום להשתמש ב-acct = key.get() או ב-acct = key.get_async().get_result(), הפונקציה צריכה להשתמש ב-acct = yield key.get_async().
הפקודה yield אומרת ל-NDB שזה מקום טוב להשהות את ה-tasklet הזה ולאפשר ל-tasklets אחרים לפעול.
הוספת הדקורטור @ndb.tasklet
לפונקציית גנרטור גורמת לפונקציה להחזיר Future במקום
אובייקט גנרטור. בתוך ה-tasklet, כל yield של Future ממתין לתוצאה של Future ומחזיר אותה.
לדוגמה:
חשוב לשים לב שלמרות שהפונקציה get_async() מחזירה Future, מסגרת ה-tasklet גורמת לביטוי yield להחזיר את התוצאה של Future למשתנה acct.
הפונקציה map() calls callback() מופעלת כמה פעמים.
אבל yield ..._async() ב-callback()
מאפשר לתזמן של NDB לשלוח הרבה בקשות אסינכרוניות לפני שהוא מחכה שאחת מהן תסתיים.
אם תבדקו את זה ב-Appstats, יכול להיות שתופתעו לגלות שהפעולות האלה לא חופפות זו לזו, אלא מתבצעות כולן באותה בקשה. NDB מטמיע autobatcher. ה-autobatcher מאגד כמה בקשות ב-RPC באצווה אחת לשרת; הוא עושה את זה כך שכל עוד יש עוד עבודה לעשות (יכול להיות שעוד קריאה חוזרת תפעל) הוא אוסף מפתחות. ברגע שאחת מהתוצאות נדרשת, הכלי לאיחוד בקשות שולח את ה-RPC של הקבוצה. בניגוד לרוב הבקשות, השאילתות לא "מצורפות".
כשמפעילים tasklet, מרחב השמות שמוגדר לו כברירת מחדל נקבע לפי מה שהיה מוגדר כברירת מחדל כשה-tasklet נוצר, או לפי מה שהוגדר כברירת מחדל על ידי ה-tasklet בזמן ההפעלה. במילים אחרות, מרחב השמות שמוגדר כברירת מחדל לא משויך להקשר ולא מאוחסן בו, ושינוי מרחב השמות שמוגדר כברירת מחדל במשימה אחת לא משפיע על מרחב השמות שמוגדר כברירת מחדל במשימות אחרות, למעט אלה שנוצרו ממנה.
Tasklets, Parallel Queries, Parallel Yield
אפשר להשתמש ב-tasklets כדי שכמה שאילתות יאחזרו רשומות בו-זמנית. לדוגמה, נניח שיש לאפליקציה דף שבו מוצגים התוכן של עגלת קניות ורשימה של מבצעים מיוחדים. הסכימה עשויה להיראות כך:
פונקציה 'סינכרונית' שמקבלת פריטים בעגלת קניות ומבצעים מיוחדים עשויה להיראות כך:
בדוגמה הזו נעשה שימוש בשאילתות כדי לאחזר רשימות של פריטים בעגלת הקניות ושל מבצעים. לאחר מכן, המערכת מאחזרת פרטים על פריטי המלאי באמצעות get_multi().
(הפונקציה הזו לא משתמשת ישירות בערך ההחזרה של get_multi(). היא קוראת ל-get_multi() כדי לאחזר את כל פרטי המלאי למטמון, כדי שאפשר יהיה לקרוא אותם במהירות בהמשך). get_multi
משלב הרבה בקשות Get לבקשה אחת. אבל השאילתות מתבצעות אחת אחרי השנייה. כדי שהאחזור יתבצע בו-זמנית, צריך לחפוף בין שתי השאילתות:
הקריאה get_multi()
עדיין נפרדת: היא תלויה בתוצאות השאילתה, ולכן אי אפשר לשלב אותה עם השאילתות.
נניח שהאפליקציה הזו צריכה לפעמים את עגלת הקניות, לפעמים את המבצעים ולפעמים את שניהם. אתם רוצים לארגן את הקוד כך שתהיה פונקציה להצגת העגלה ופונקציה להצגת המבצעים. אם האפליקציה קוראת לפונקציות האלה יחד, באופן אידיאלי השאילתות שלהן יכולות 'להחפוף'. כדי לעשות את זה, צריך להפוך את הפונקציות האלה ל-tasklets:
ההגדרה yield x, y חשובה, אבל קל לפספס אותה. אם אלה היו שתי הצהרות נפרדות של yield, הן היו מתבצעות ברצף. אבל yield של טאפל
של tasklets הוא parallel yield: ה-tasklets יכולים לפעול במקביל
וה-yield מחכה שכולם יסיימו וחוזר עם התוצאות. (בשפות תכנות מסוימות, זה נקרא מחסום).
אם הופכים קטע קוד אחד ל-tasklet, סביר להניח שתרצו לעשות זאת שוב בקרוב. אם אתם מבחינים בקוד 'סינכרוני' שיכול לפעול במקביל ל-tasklet, כדאי להפוך אותו גם ל-tasklet.
אחר כך אפשר להריץ אותו במקביל באמצעות yield מקביל.
אם תכתבו פונקציית בקשה (פונקציית בקשה של webapp2, פונקציית תצוגה של Django וכו') שתהיה tasklet, היא לא תפעל כמו שאתם רוצים: היא תניב אבל אז תפסיק לפעול. במקרה כזה, צריך להוסיף את הקישוט @ndb.synctasklet לפונקציה.
@ndb.synctasklet דומה ל-@ndb.tasklet אבל הוא שונה כדי להתקשר אל get_result() ב-tasklet.
הפעולה הזו הופכת את ה-tasklet לפונקציה שמחזירה את התוצאה שלה בדרך הרגילה.
Query Iterators ב-Tasklets
כדי לבצע איטרציה על תוצאות של שאילתה ב-tasklet, משתמשים בתבנית הבאה:
זוהי הגרסה המתאימה ל-tasklet של הפקודה הבאה:
שלוש השורות המודגשות בגרסה הראשונה הן המקבילה הידידותית ל-tasklet של השורה המודגשת היחידה בגרסה השנייה.
אפשר להשהות Tasklets רק במילת מפתח yield.
לולאת ה-for ללא yield לא מאפשרת להפעיל tasklets אחרים.
יכול להיות שתשאלו למה הקוד הזה משתמש באיטרטור של שאילתה במקום לאחזר את כל הישויות באמצעות qry.fetch_async().
יכול להיות שיש באפליקציה כל כך הרבה ישויות שהן לא נכנסות ל-RAM.
יכול להיות שאתם מחפשים ישות מסוימת ותוכלו להפסיק את החיפוש ברגע שתמצאו אותה, אבל לא תוכלו להביע את קריטריוני החיפוש רק באמצעות שפת השאילתה. אפשר להשתמש באיטרטור כדי לטעון ישויות לבדיקה, ואז לצאת מהלולאה כשמוצאים את מה שרוצים.
Async Urlfetch with NDB
ל-NDB Context יש פונקציה אסינכרונית urlfetch() שפועלת במקביל בצורה טובה עם tasklets של NDB, לדוגמה:
לשירות אחזור של כתובות אתרים יש API משלו לבקשות לא סנכרוניות. הוא בסדר, אבל לא תמיד קל להשתמש בו עם tasklets של NDB.
שימוש בעסקאות אסינכרוניות
יכול להיות שהעסקאות יתבצעו גם באופן אסינכרוני. אפשר להעביר פונקציה קיימת אל ndb.transaction_async() או להשתמש במעצב @ndb.transactional_async.
בדומה לפונקציות אסינכרוניות אחרות, הפונקציה הזו תחזיר NDB Future:
אפשר להשתמש בעסקאות גם עם tasklets. לדוגמה, אפשר לשנות את הקוד update_counter ל-yield בזמן ההמתנה לחסימת בקשות RPC:
שימוש ב-Future.wait_any()
לפעמים רוצים לשלוח כמה בקשות אסינכרוניות ולחזור כשמסתיימת הראשונה מביניהן.
אפשר לעשות זאת באמצעות שיטת המחלקה ndb.Future.wait_any():
לצערנו, אין דרך נוחה להפוך את זה ל-tasklet. מחכה מקבילה yield עד שכל Futures מסתיימים – כולל אלה שלא רוצים לחכות להם.