Skip to content

שוב הפרופיירלר עושה בעיות #3329

Description

@amirbiron

הלוגים מראים קריסה ברמת האפליקציה (Python/asyncio) שנובעת מניסיון לכפות ריצה סינכרונית (חוסמת) על קוד שהוא ביסודו אסינכרוני, בעת ניסיון לטעון את עמוד ה-Profiler (/admin/profiler).

הנה ניתוח של שרשרת הכשלים המופיעה בלוג:

  1. התנגשות Event Loop: המערכת מנסה להריץ את השאילתה האסינכרונית PersistentQueryProfilerService.get_slow_queries. מכיוון שפונקציית הראוטר (api_profiler_slow_queries) כנראה מוגדרת כפונקציה רגילה (סינכרונית) ולא אסינכרונית, מישהו יצר פונקציית מעטפת בשם _run_awaitable_blocking כדי לעקוף את זה. הפונקציה הזו מנסה להפעיל את הקורוטינה בתוך ה-Event Loop הקיים באמצעות run_until_complete, מה שזורק מיד את השגיאה Cannot run the event loop while another loop is running (כי השרת כבר מריץ לולאה ברקע).
  2. קריסת מערך הגיבוי (Fallback): לאחר שהניסיון הראשון נכשל, הפונקציה מנסה להפעיל את הקוד דרך Threadpool (_run_in_threadpool), וכשגם זה נכשל היא מנסה ליצור Thread חדש לגמרי (_run_in_fresh_thread). כל נסיונות המעקף האלו נכשלים בזה אחר זה עם אותה שגיאה בדיוק, מה שמעיד על ניהול הקשר (Context) שגוי.
  3. אובדן הקורוטינה: מכיוון שכל הנסיונות להריץ את הקוד קרסו, האובייקט האסינכרוני מעולם לא הופעל (Awaited). מנגנון איסוף הזבל (Garbage Collector) של פייתון מזהה את זה וזורק את האזהרה בסוף הלוג: RuntimeWarning: coroutine 'PersistentQueryProfilerService.get_slow_queries' was never awaited.

הפתרון השורשי:

בהתאם לעקרון שציינת – להימנע מפתרונות שהם "טלאי" – הפונקציה _run_awaitable_blocking היא טלאי ארכיטקטוני קלאסי (Anti-pattern). אסור להילחם ב-Event Loop של פייתון על ידי יצירת מעטפות שחוסמות את התהליך (Blocking).

הפתרון הנכון והשורשי הוא לזרום עם הארכיטקטורה האסינכרונית של השרת:
יש לשנות את הגדרת הראוטר ב-app.py (סביב שורה 5008) מ-def api_profiler_slow_queries ל-async def api_profiler_slow_queries, ופשוט לקרוא לשירות באופן טבעי עם await PersistentQueryProfilerService.get_slow_queries(). לאחר מכן, יש למחוק לחלוטין את מנגנון ה-_run_awaitable_blocking המועד לפורענות.

האם תרצה לשתף את אזור הקוד של api_profiler_slow_queries מתוך app.py כדי שנוכל להטמיע את השינוי?


אבל עברנו לגיבנט, זה לא מתנגש?


נקודה מעולה, וזה בדיוק החתיכה החסרה בפאזל!

המעבר ל-Gevent משנה לחלוטין את התמונה, וכן, יש כאן התנגשות ארכיטקטונית חריפה.

Gevent ו-Asyncio (הסביבה הטבעית של async/await בפייתון) פועלים תחת פרדיגמות שונות לחלוטין שממש "רבות" אחת עם השנייה על ניהול התהליכים ברקע:

  1. Gevent עובד על ידי יצירת Greenlets (מיקרו-תהליכים) וביצוע "Monkey-Patching" – הוא דורס ומשכתב ספריות ליבה של פייתון (כמו socket, threading, time) כדי להפוך קוד סינכרוני רגיל לאסינכרוני מאחורי הקלעים.
  2. Asyncio מצפה לנהל Event Loop משלו בעזרת התחביר של async/await, ומסתמך על ספריות הליבה המקוריות של פייתון.

למה קרסנו?
פונקציית המעקף שראינו, _run_awaitable_blocking, ניסתה לפתוח Thread (תהליכון) חדש כדי להריץ בתוכו את פקודות ה-asyncio מבלי לחסום את השרת. אבל בגלל ש-Gevent ביצע Monkey-Patching ל-threading, פייתון לא באמת פתח Thread אמיתי, אלא Greenlet של Gevent. ה-Asyncio זיהה שהוא מנסה להפעיל לולאה בתוך סביבה שכבר מנוהלת על ידי לולאה אחרת (של Gevent) – וזרק את השגיאה Cannot run the event loop while another loop is running.


הפתרונות השורשיים האפשריים (Root Cause Solutions)

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

הנה שתי הדרכים השורשיות לפתור את זה:

אופציה 1: יישור קו ל-Gevent (מומלץ אם רוב המערכת סינכרונית)
אם בחרתם ב-Gevent כבסיס הארכיטקטוני, אין שום סיבה להשתמש ב-async/await.

  • השינוי: צריך לשכתב את PersistentQueryProfilerService.get_slow_queries (ואת ספריית התקשורת למסד הנתונים שבה הוא משתמש, למשל לעבור מ-Motor ל-PyMongo הרגיל) כך שיהיה קוד סינכרוני לחלוטין.
  • למה זה עובד? Gevent יזהה את פעולות ה-I/O (כמו הקריאה למונגו) ויהפוך אותן לאסינכרוניות בעצמו באופן שקוף. תוכל למחוק לחלוטין את מנגנון ה-_run_awaitable_blocking.

אופציה 2: יישור קו ל-Asyncio (מומלץ אם האפליקציה כתובה ב-FastAPI/Quart)
אם רוב הקוד החדש שלכם (DB, קריאות API) כבר כתוב עם async/await, ה-Gevent רק מפריע.

  • השינוי: להסיר את ה-Gevent Worker מהשרת שלכם (למשל, ב-Gunicorn) ולעבור ל-ASGI Worker טבעי כמו uvicorn.workers.UvicornWorker.
  • למה זה עובד? זה יאפשר ל-Asyncio לרוץ בצורה טבעית ללא Monkey-Patching שישבור אותו.

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions