חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle

· עודכן בתאריך: · · פיתוח Windows, חקירת תקלות, מצלמה תעשייתית, דליפת handle, תכנון לוגים

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

המקרה שמוצג כאן הוא חקירה של אפליקציית Windows ששולטת במצלמה תעשייתית, וקורסת בפתאומיות אחרי כחודש של הפעלה רציפה. לאחר בידוד, התברר שהסיבה הייתה דליפת handle שהתרחשה במסלול הכשל סביב חיבור מחדש (reconnect) של המצלמה.

בפרק הראשון נסדר מהי דליפת handle, איך בודדנו את המקרה הזה, ואילו לוגים כדאי להשאיר כדי למנוע הישנות. בפרק השני נדבר על תשתית בדיקות למקרי קצה, תחת הכותרת תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier.

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

תוכן עניינים

  1. קודם כול, המסקנה (במשפט אחד)
  2. מהי דליפת handle
    • 2.1. ה”handle” שמדובר בו כאן
    • 2.2. למה זה נוטה להתגלות רק בהפעלה ממושכת
    • 2.3. ההבדל מדליפת זיכרון
  3. מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת בפתאומיות אחרי חודש
    • 3.1. התסמינים שהופיעו
    • 3.2. המדדים הראשונים שנבדקו
    • 3.3. מקום הדליפה שהיה הגורם האמיתי
  4. איך בודד המקרה
    • 4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים
    • 4.2. הסתכלות לפי השיפוע של Handle Count
    • 4.3. הסתכלות על ההתאמה בין create/open לבין close/dispose
    • 4.4. בדליפת handle מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”
  5. הלוגים הנדרשים כדי למנוע הישנות
    • 5.1. הסט המינימלי שכדאי להשאיר קודם
    • 5.2. הלוגים שחוזקו בפועל
    • 5.3. באיזו רזולוציה לתעד
  6. חלוקה גסה בין המצבים
  7. סיכום
  8. מקורות

מפת הידע של המאמר

המאמר הזה מסודר סביב מקרה שבו אפליקציית בקרה למצלמה תעשייתית קרסה פתאום אחרי כחודש של הפעלה רציפה, ומרכז את דרך האיתור של דליפת handle ואת תכנון הלוגים. דליפת ה-handle קרתה כי ב-failure path של timeout או reconnect, ה-handle של CreateEvent לא עבר CloseHandle ודלף, מה שהתבטא כ-Handle Count שלא חוזר, ובאפליקציה שבה יש גם GUI זה הגיע קודם לתקרה של מספר אובייקטי ה-GDI והתבטא כתקלה משנית. הפתרון הוא לקרב את האחריות על הבעלות ועל השחרור באמצעות טיפוס בסגנון RAII או בלוק finally, כדי למנוע מלכתחילה שכחה של קריאה ל-CloseHandle. בפירוק קריסה שאחרי הפעלה ממושכת, הבסיס הוא לא להמתין לשחזור בסדר גודל של חודשים אלא לדרוך הרבה פעמים על ה-failure path בלולאה קצרה, ולבדוק יחד את Handle Count, ‏Private Bytes ו-Thread Count עם לוגים מובנים; ה-Application Verifier של הפרק השני ממוקם ככלי שנשען מעל הבסיס הזה.

מפת הידע של חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית (פרק דליפת ה-handle)תרשים שמראה איך בפירוק קריסה שאחרי הפעלה ממושכת בודקים יחד את Handle Count עם Private Bytes ו-Thread Count; איך אי-שחרור ב-failure path עובר דרך דליפת handle ומתגלה כתקלה משנית; ואת מקומם של RAII ו-CloseHandle במניעה, ושל לוגים מובנים ו-Application Verifier כשכבות מעליהם.עלול לגרום לנבדק באמצעותנבדק באמצעותמחייבמענה מומלץ לצריך לקדום למונעמשתמש במצמצםעלול לגרום למענה מומלץ למונעעלול לגרום למענה מומלץ למענה מומלץ למענה מומלץ למענה מומלץ למענה מומלץ לדליפת handleHandle Count‏failure path (מסלול כשל באמצע הפעולה)דליפת זיכרוןPrivate BytesCreateEventCloseHandleיומן מובנה (structured log)Application VerifierRAII(Resource Acquisition Is Initialization)תקלה משנית (secondary failure)אובייקט GDIבידוד קריסה אחרי הפעלה ממושכתThread Count

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 18, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

1. קודם כול, המסקנה (במשפט אחד)

  • באפליקציית בקרה שקורסת רק אחרי הפעלה ממושכת, יש להסתכל תמיד לא רק על Private Bytes אלא גם על Handle Count
  • דליפת handle נוטה להסתתר לא במסלול הרגיל, אלא במסלולים של timeout /‏ reconnect / כשל באמצע הפעולה / early return
  • השורה שבה בפועל קורסת האפליקציה היא לרוב לא המקום שבו התרחשה הדליפה, אלא המקום שבו כבר לא ניתן היה ליצור handle חדש
  • הלוגים הראשונים שנדרשים הם: הקשר של operation/session, ה-handle count של התהליך, ההתאמה בין open ל-close של המשאב, ושגיאות Win32 /‏ HRESULT / SDK
  • עדיף להריץ בלולאה קצרה אלפי פעמים חיבור, ניתוק, חיבור מחדש ומסלולי כשל, מאשר לחכות לשחזור בסדר גודל של חודשים
  • Application Verifier, שיוזכר בפרק השני, יעיל מאוד, אבל לפני כן הבסיס הוא לוודא שאפשר לעקוב אחר קריסת מחזור החיים באמצעות הלוגים העצמיים שלכם

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

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

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

איור 1: לפני שמסתכלים על “עובדת הקריסה”, כדאי להפוך את קצב הגידול ומסלולי הכשל לניתנים לתצפית.

2. מהי דליפת handle

2.1. ה”handle” שמדובר בו כאן

ה-handle שמדובר בו כאן הוא מזהה שדרכו תהליך Windows מפנה למשאב של מערכת ההפעלה. דוגמאות למשאבים כאלה:

קטגוריה דוגמאות
אובייקטים של הקרנל (kernel objects) event,‏ mutex,‏ semaphore,‏ thread,‏ process,‏ waitable timer
קלט/פלט open עבור file,‏ pipe,‏ socket, device
דברים נפוצים בבקרת התקנים event פנימי של ה-SDK של המצלמה, אובייקט המתנה שמקושר לרישום callback, handle-ים הקשורים לת’רד הצילום

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

התהליך האופייני נראה כך:

  • בכל reconnect נוצר event אחד
  • רישום ה-callback או תחילת הצילום נכשלים באמצע
  • ב-success path מתבצע close, אבל ב-failure path לא
  • בבדיקות קצרות שגרתיות עוברים בעיקר במסלול ההצלחה, ולכן זה מתפספס

דפוס מהסוג הזה מסתתר בקלות רבה, גם בסקירת קוד וגם בהפעלה בפועל.

תבנית אופיינית של דליפה רק במסלול כשלתרשים המראה שבכל reconnect נוצר event, ואם רישום ה-callback או תחילת הצילום מצליחים מתבצע close ב-success path, אך במסלול כשל באמצע לא מתבצע close והדליפה מתפספסת בבדיקות קצרות שעוברות בעיקר במסלול ההצלחה.הצלחהכשל באמצעבכל reconnect נוצר eventהרישום וההתחלה הצליחו?close ב-success pathאין close ב-failure pathבדיקות קצרות מפספסות, כי הן עוברות במסלול ההצלחה

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

2.2. למה זה נוטה להתגלות רק בהפעלה ממושכת

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

השרשרת מהפעלה רגילה ועד לקריסה בעקבות דליפה קטנהתרשים המראה שבהפעלה רגילה מתרחשים מדי פעם timeout או reconnect שיוצרים Event Handle במסלול כשל בלי שנקרא CloseHandle, כך ש-Handle Count גדל מעט בכל פעם, וזה חוזר על עצמו מאות פעמים עד ש-CreateEvent או פתיחת ה-SDK נכשלים וגורמים לקריסה או עצירה במקום אחר.הפעלה רגילהמדי פעם timeout / reconnectיצירת Event Handle במסלול הכשלCloseHandle לא נקראHandle Count גדל מעטחוזר על עצמו מאות פעמיםCreateEvent / פתיחת ה-SDK נכשליםקריסה / עצירה במקום אחר

איור 3: דליפה קטנה של יחידה אחת בכל פעם, מצטברת מאות פעמים בתנאי הגבול של הפעלת 24/7, ואז מתגלה.

אם בכל reconnect דולף רק handle אחד, לא קורה שום דבר תוך כמה דקות. אבל באפליקציית בקרת התקנים שרצה 24/7, תנאי גבול כמו timeout, אתחול מחדש והתאוששות מניתוק מתרחשים שוב ושוב. כתוצאה מכך, נוצר מראה מוזר: התקלה מתגלה רק אחרי שבועות.

החשוב כאן הוא שדליפת ה-handle עצמה לא בהכרח הופכת לשורת הקריסה. בדרך כלל האפליקציה נשברת כך:

  • קריאה ל-API שיוצר event / file / thread חדש נכשלת
  • ה-SDK לא מצליח ליצור פנימית משאב נדרש, ומחזיר רק קוד כשל כללי
  • טיפול השגיאות אחרי הכשל דל, והאפליקציה נתקלת ב-null או ב-handle לא תקין ונופלת
  • מספר ה-timeout גדל, ובסופו של דבר watchdog או בקרה מסדר גבוה יותר הורגים את התהליך

כלומר, נקודת הקריסה היא ה”קרבן האחרון”, ולאו דווקא “החשוד הראשון”.

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

איור 4: הדליפה עצמה לא בהכרח הופכת לשורת הקריסה. בדרך כלל השבירה היא תקלה משנית.

כאן עולה שאלה תמימה: למה זה קורס בגלל בסך הכול כמה אלפי handle-ים?

אם מסתכלים רק על המספרים, הגבול העליון רחוק מאוד. הגבול העליון התיאורטי ל-handle-ים של אובייקטי הקרנל הוא 2^24 (כ-16.77 מיליון) לתהליך. אבל מכיוון שה-handle-ים ממוקמים ב-page pool, המספר שבפועל ניתן ליצור נקבע לפי הזיכרון הזמין, וב-Windows בגרסת 32 סיביות המספר קטן בהרבה מהערך התיאורטי.

בקיצור, מקרים שבהם מגיעים לגבול התיאורטי הם דווקא המיעוט. מה שבפועל “מכה קודם” הוא בדרך כלל אחד מאלה:

מה נתקל בגבול קודם סדר גודל היכן זה משפיע
אובייקטי GDI תיאורטית 65,536 לכל session, ובנוסף יש גבול ברירת מחדל לתהליך שניתן לשנות בערך GDIProcessHandleQuota ברישום, בטווח של 256 עד 65,536 אפליקציות שיש בהן גם GUI. בקלות רבה נתקלים בגבול הזה כבר בסדר גודל של אלפים
טבלת הניהול הפנימית של ה-SDK תלוי ביצרן טבלת ה-handle-ים או המערך באורך קבוע שה-SDK של המצלמה מחזיק פנימית מתמלאים קודם
משאבי קרנל כמו ה-page pool משותפים לכל המכונה כשגם משאבים אחרים חוץ מ-handle-ים נצרכים באותו זמן
מרחב הכתובות הווירטואלי של תהליך 32 סיביות 2GB / 3GB לא ה-handle עצמו, אלא ה-buffer שמוקצה יחד איתו הוא מה שמשפיע

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

מה נתקל בגבול לפני הגבול התיאורטיתרשים המראה שהמקרים שבהם מגיעים לגבול התיאורטי של handle-י הקרנל הם מיעוט, ובפועל נתקלים קודם בגבול אובייקטי GDI, בטבלת הניהול הפנימית של ה-SDK, או במרחב הכתובות של תהליך 32 סיביות, ולכן יש להסתכל אם המשאב חוזר ולא על הגבול עצמו.הגבול התיאורטי של הקרנל כ-16.77 מיליוןהגעה עד לשם היא המיעוטמה שבפועל נתקל קודםהגבול של GDIטבלת הניהול הפנימית של ה-SDKמרחב הכתובות של 32 סיביותשיפוט לפי האם המשאב חוזר

איור 5: “יש עוד מרחב עד הגבול” לא מרגיע. ברגע שנוצר שיפוע, יש להתייחס לזה כאל חריגה.

2.3. ההבדל מדליפת זיכרון

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

היבט דליפת זיכרון דליפת handle
המדד הראשון שבודקים Private Bytes,‏ Commit,‏ Working Set Handle Count
תסמין אופייני לחץ זיכרון, paging, האטה, OOM כשל ב-Create* /‏ Open* / אתחול פנימי של ה-SDK, תקלה משנית
היכן זה נוטה להסתתר cache, החזקת הפניה, שכחת שחרור אי-סימטריה בין create/open לבין close/dispose
איך זה נראה הזיכרון גדל בהדרגה ה-handle count גדל בהדרגה ולא חוזר

לכן, בבידוד תקלה בהפעלה ממושכת, הסתכלות רק על הזיכרון נוטה להיות כמו נהיגה בעין אחת. לפחות Handle Count ו-Thread Count כדאי להסתכל עליהם יחד, וזה מקל מאוד על הסידור.

מדדים שכדאי להסתכל עליהם יחד בבידוד הפעלה ממושכתתרשים המראה שמכיוון שדליפת זיכרון ודליפת handle נבדקות במדדים שונים, הסתכלות רק על מדדי זיכרון כמו Private Bytes אינה מספיקה, ויש להסתכל יחד גם על Handle Count וגם על Thread Count כדי להימנע ממצב של נהיגה בעין אחת.בידוד תקלה בהפעלה ממושכתמדדי זיכרון (כמו Private Bytes)Handle CountThread Countגדל ולא חוזר — סימן לדליפת handle

איור 6: הסתכלות רק על הזיכרון היא נהיגה בעין אחת. יש לעקוב באותו מסך גם אחרי מספר ה-handle-ים והתהליכונים.

3. מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת בפתאומיות אחרי חודש

3.1. התסמינים שהופיעו

התופעה הייתה פשוטה:

  • אפליקציית Windows ששולטת במצלמה תעשייתית פועלת 24/7
  • בזמן רגיל היא פועלת כרגיל
  • אחרי כחודש, יום אחד היא קורסת בפתאומיות
  • אחרי הפעלה מחדש, שוב פועלת כרגיל למשך זמן מה

מה שמקשה בהתחלה הוא “שלוקח הרבה זמן עד שזה קורס”. לחכות חודש לכל שחזור בודד זה קשה מאוד לחקירה.

מה שהקשה עוד יותר הוא שמקום הקריסה לא היה בדיוק זהה בכל פעם. לפעמים זה קרה מיד אחרי תחילת reconnect, לפעמים בתחילת הצילום, ולפעמים אחרי כשל בקריאה ל-SDK.

עם הופעה כזו, בהתחלה אפשר לחשוד בכל אחד מאלה:

  • חוסר יציבות בצד ה-SDK של המצלמה
  • תקלה זמנית שמקורה בתקשורת או בניתוק ההתקן
  • דליפת זיכרון
  • race סביב תהליכונים
  • כשל אתחול שלא נכנס ללוג

כלומר, המצב היה שיש יותר מדי “דברים שנראים חשודים איכשהו”.

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

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

3.2. המדדים הראשונים שנבדקו

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

מדד המגמה שנצפתה הפרשנות
Handle Count גדל מעט בכל פעם אחרי reconnect או timeout, ולא חוזר חשד לדליפת handle
Private Bytes יש עליות וירידות, אבל השיפוע החד-כיווני חלש ייתכן שהאשם אינו ה-heap
Thread Count כמעט קבוע סיכוי נמוך לדליפת thread
מקום הקריסה קצת שונה בכל פעם סביר להניח שזו תקלה משנית

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

ההשערה שהצטמצמה מתצפית המדדים הראשוניתתרשים המראה שהתצפית שלפיה רק Handle Count גדל ולא חוזר, שיפוע Private Bytes חלש, Thread Count כמעט קבוע, ומקום הקריסה שונה בכל פעם, הובילה להצטמצמות ההשערה לכיוון של דליפה הדרגתית שמובילה לקריסה אחרי חודש.Handle Count גדל ולא חוזרנקודת המבט מצטמצמתשיפוע Private Bytes חלשThread Count כמעט קבועדליפה הדרגתית שקורסת אחרי חודש

איור 8: כשמסדרים יחד את הצורה של ארבעת המדדים, מתגלה לא “קורס אחרי חודש” אלא “דולף כל הזמן”.

3.3. מקום הדליפה שהיה הגורם האמיתי

בסופו של דבר, הגורם היה שכחת close של event handle שנוצר במסלול כשל האתחול בזמן reconnect של המצלמה.

אם מפשטים את התהליך, זה נראה כך:

התהליך שבו הדליפה במסלול הכשל של ה-reconnect מובילה לקריסהתרשים רצף המראה שהאפליקציה יוצרת Event, נכשלת ברישום ה-callback, יוצאת במסלול הכשל בלי לקרוא CloseHandle, וש-Handle Count גדל בהדרגה עם כל reconnect עד שקריאה חדשה ל-CreateEvent או ל-Open נכשלת וגורמת לקריסה כתקלה משנית.ה-SDK של המצלמהWindowsאפליקציית הבקרהה-SDK של המצלמהWindowsאפליקציית הבקרהיציאה (return) במסלול הכשלCloseHandle לא נקראloop[reconnect שוב ושוב]CreateEventרישום callbackכשל באמצע / timeoutHandle Count גדל מעט בכל פעםCreateEvent / Open הבאכשלקריסה כתקלה משנית

איור 9: הגורם האמיתי היה שכחת close של event handle במסלול הכשל של ה-reconnect. אחרי הצטברות, הקריסה קורית במקום אחר.

כדי לתת מושג לגבי הקוד, זו הצורה של הדליפה:

handle = CreateEvent(...)

if (!RegisterCallback(handle))
{
    return Error;   // CloseHandle(handle) חסר כאן
}

if (!StartAcquisition())
{
    return Error;   // גם כאן חסר close
}

...
CloseHandle(handle)

גם הסיבה לכך שקל לפספס את זה בבדיקות קצרות די ברורה:

  • הפעלה תקינה -> סיום תקין: מתבצע close
  • הכשל קורה רק באמצע reconnect
  • אין בדיקה שעוברת בכמות גדולה במסלול הכשל הזה
  • בסביבת הייצור זה מצטבר בהדרגה במשך שבועות

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

כיוון התיקון לא מרשים במיוחד:

  • לקרב את האחריות של create/open לזו של close/dispose
  • להעביר את השחרור ל-finally / destructor / אובייקט ה-session, כך שהשחרור יתבצע תמיד גם בכשל באמצע
  • להבהיר את הבעלות (ownership) לפני ואחרי רישום ה-callback או תחילת הצילום
  • לבטא את “מי סוגר” באחריות הקוד עצמו, ולא בהערות (comments)
עיקרי כיוון התיקוןתרשים המראה שכיוון התיקון הוא לקרב את האחריות של create לזו של close, להעביר את השחרור ל-finally, לדסטרוקטור או לאובייקט ה-session כדי שהוא יתבצע תמיד גם בכשל באמצע, ולבטא את מי שסוגר באחריות הקוד ולא בהערות.כיוון התיקוןקירוב האחריות של create ו-closeהעברת השחרור ל-finally או לדסטרוקטורביטוי הבעלות באחריות הקודלא להסתמך על מוסכמה שמבוססת רק על הערות

איור 10: זה לא תיקון מרשים. משתילים את מחזור החיים של המשאב במבנה הקוד עצמו.

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

ב-‏C++, מכינים טיפוס RAII קטן אחד שמחזיק את ה-handle, כך שלא משאירים HANDLE גולמי בתוך הפונקציה.

// C++17 / Windows
#include <windows.h>
#include <utility>

class UniqueHandle
{
public:
    UniqueHandle() noexcept = default;
    explicit UniqueHandle(HANDLE h) noexcept : h_(h) {}

    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;

    UniqueHandle(UniqueHandle&& other) noexcept
        : h_(std::exchange(other.h_, nullptr)) {}

    UniqueHandle& operator=(UniqueHandle&& other) noexcept
    {
        if (this != &other)
        {
            reset(std::exchange(other.h_, nullptr));
        }
        return *this;
    }

    ~UniqueHandle() { reset(); }

    HANDLE get() const noexcept { return h_; }
    explicit operator bool() const noexcept { return h_ != nullptr; }

    void reset(HANDLE h = nullptr) noexcept
    {
        if (h_ != nullptr)
        {
            ::CloseHandle(h_);
        }
        h_ = h;
    }

private:
    HANDLE h_ = nullptr;
};

בעזרת זה, אין צורך להוסיף CloseHandle באופן ידני בכל מסלול כשל.

// שדה ב-CameraSession: UniqueHandle frameReady_;
bool CameraSession::Reconnect()
{
    UniqueHandle frameReady{ ::CreateEventW(nullptr, TRUE, FALSE, nullptr) };
    if (!frameReady)
    {
        return false;   // היצירה עצמה נכשלה. אין מה לסגור
    }

    if (!RegisterCallback(frameReady.get()))
    {
        return false;   // גם אם עושים return כאן, הדסטרוקטור יסגור
    }

    if (!StartAcquisition())
    {
        // כשל אחרי שהרישום כבר הושלם: קודם מבטלים את הרישום ורק אז סוגרים.
        // אם יוצאים בלי לבטל את הרישום, הדסטרוקטור כן יקרא ל-CloseHandle,
        // אבל ה-SDK עדיין מחזיק את ה-handle שהועבר אליו. בפריים הבא הוא
        // עשוי לאותת למספר שכבר שוחרר, ואם אותו מספר מוקצה מחדש למשאב
        // אחר, זה יתגלה כ"אירוע לא קשור שקם מעצמו"
        UnregisterCallback();
        return false;
    }

    // רק כשמצליחים, מעבירים את הבעלות לצד ה-session
    frameReady_ = std::move(frameReady);
    return true;
}

ב-‏C#, במקרים רבים לא מספיק using בודד, ולכן מציבים דגל של “האם הועברה הבעלות”, וזורקים את המשאב ב-finally רק כשהיא לא הועברה. אם כותבים סתם using var, המשאב יושמד גם כשההצלחה מתרחשת.

// C# / .NET 8
// שדה ב-CameraSession: private ManualResetEvent? _frameReady;
public bool Reconnect()
{
    var frameReady = new ManualResetEvent(false);
    var handedOver = false;
    var registered = false;

    try
    {
        if (!RegisterCallback(frameReady))
        {
            return false;
        }

        registered = true;

        if (!StartAcquisition())
        {
            return false;
        }

        _frameReady?.Dispose();
        _frameReady = frameReady;
        handedOver = true;
        return true;
    }
    finally
    {
        if (!handedOver)
        {
            // לפני שזורקים, קודם מבטלים את ההפניה שהצד החיצוני מחזיק בה.
            // ה-SDK מחזיק את ה-handle שהועבר אליו בעת הרישום, ולכן אם הופכים
            // את הסדר, יגישו קריאה ל-handle שכבר שוחרר
            if (registered)
            {
                UnregisterCallback();
            }

            frameReady.Dispose();
        }
    }
}

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

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

איור 11: לא כותבים “אם נכשל, לסגור” בכל מקום מחדש. יעד הבעלות קובע מי אחראי לשחרור.

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

4. איך בודד המקרה

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

מונח בעברית המשמעות במאמר הזה
baseline ערך בסיס הערך שמתייצב אחרי שה-warm-up מסתיים. משם מסתכלים על ההפרש
leakSlope שיפוע הדליפה כמה גדל בכל מחזור. מדד עצמאי שמייצג את קצב הגידול
structured log לוג מובנה לוג שבו הפריטים קבועים בסגנון key=value, במקום פרוזה. אפשר לרכז אותו אחר כך באופן אוטומטי
heartbeat דיווח תקופתי לוג שמוציא בקצב קבוע אישור חיים וערכי משאבים
harness מסגרת בדיקה תוכנית הרצה קטנה שמריצה שוב ושוב רק את הפעולה שרוצים לבדוק, במקום האפליקציה המלאה
phase שלב סימון כמו OpenStart או ReconnectStart, שמראה באיזה שלב של העיבוד נמצאים כרגע

4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים

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

במקרה הזה, כיווצנו את השחזור בעזרת לולאה כזו:

לולאת השחזור המכווץתרשים המראה שלולאת השחזור מתחילה בהפעלה, פתיחת מצלמה והתחלת צילום, ואז מדמה timeout או ניתוק, מבצעת reconnect וחידוש צילום, וחוזרת על כך N פעמים לפני בדיקת ההפרש בסיום.כןלאהפעלהopen של המצלמהתחילת צילוםדימוי timeout / ניתוקreconnectחידוש הצילוםלחזור N פעמים?בדיקת ההפרש בסיום

איור 12: בלי לחכות לשחזור בסדר גודל של חודשים, מריצים אלפי פעמים בלולאה קצרה רק את הגבולות של open, ניתוק ו-reconnect.

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

התרחישים שבאמת עוזרים הם כאלה:

  • להריץ open -> start -> stop -> close בכמות גדולה
  • לגרום ל-timeout בכוונה כדי להריץ reconnect
  • לגרום לכשל מיד אחרי רישום ה-callback
  • להכניס הפרעת ניתוק, הפרעת reconnect ותחרות (race) בזמן shutdown

אין צורך לשחזר בצורה מושלמת חודש שלם של הפעלה בפועל. דווקא לעבור אלפי פעמים על גבול מחזור החיים החשוד מקרב הרבה יותר לגורם.

4.2. הסתכלות לפי השיפוע של Handle Count

לפני זה, נכתוב איפה בכלל מסתכלים על Handle Count. בלי זה, כל הסעיף הזה נשאר תיאורטי בלבד.

שיטה פעולה מתאים למקרה
Task Manager פותחים את הלשונית “פרטים”, לוחצים ימני על כותרת העמודה ← “בחירת עמודות” ← מסמנים “handles” רוצים לראות מהר כמה יש כרגע
Process Explorer בוחרים את התהליך, פותחים Properties, ורואים את Handle Count בלשונית Process Performance. סידור חלונית ה-Handles התחתונה לפי Type מראה גם פירוט לפי סוג רוצים לדעת אילו handle-ים גדלים
handle.exe handle -s -p CameraApp נותן ריכוז טקסטואלי לפי סוג רוצים לתעד תצפית קבועה בלוג
PowerShell Get-Process -Name CameraApp \| Select-Object Name, Id, HandleCount רוצים לאסוף באופן תקופתי בסקריפט
typeperf typeperf "\Process(CameraApp)\Handle Count" -si 60 -sc 1440 -o handles.csv רוצים לתעד CSV לאורך זמן רב
האפליקציה עצמה הטמעת GetProcessHandleCount או Process.HandleCount בתוך לוג ה-heartbeat רוצים לאסוף רק לוגים ממכונת הייצור

פירוט לפי סוג ואיך לעקוב אחרי גידול של event-ים ללא שם מסוכם כשלבים בכתובת Process Explorer /‏ Handle /‏ VMMap בפועל.

בחקירה של הפעלה ממושכת, השיטה המרכזית היא זו האחרונה בטבלה — “האפליקציה עצמה מוציאה”. אדם שמסתכל על Task Manager לא יכול להמשיך בכך 24/7.

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

בתור שיטת הסתכלות, הסדר הבא בדרך כלל ברור:

  1. קביעת baseline אחרי ה-warm-up
  2. תיעוד Handle Count אחרי reconnect / start-stop / close
  3. בדיקת ההפרש בכל מחזור
  4. בדיקת השיפוע המצטבר על פני כמה מחזורים

לדוגמה, כך אפשר להסתכל:

leakSlope =
    (currentHandleCount - baselineHandleCount)
    / reconnectCount

האם 2000 בערך מוחלט זה הרבה או מעט תלוי באפליקציה. אבל אם בכל reconnect עולה ב-1 ולא חוזר, זה חשוד מאוד.

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

  • מיד אחרי ההפעלה יש עלייה — את זה לא קוראים
  • אחרי שה-warm-up נגמר, הערך אמור לעלות ולרדת בהתאם לפעולות, אבל לנוע בתוך טווח קבוע
  • אחרי מחזור אחד של open -> start -> stop -> close, נורמלי שהערך יחזור כמעט לאותו ערך שהיה לפני המחזור
  • אם אחרי 100 מחזורים ההפרש מה-baseline הוא בסדר גודל של יחידות בודדות, זה בריא ברובו
  • לעומת זאת, אם זה עולה בקו ישר ביחס ישר למספר המחזורים, אז זה דולף בדיוק בקצב השיפוע הזה בכל מחזור

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

איך קוראים את השיפוע של Handle Countתרשים המראה שאחרי warm-up קובעים baseline, בודקים את ההפרש בכל מחזור, ואם הערך חוזר אחרי המחזור זה בריא ברובו, ואם הוא עולה בקו ישר ביחס למספר המחזורים אז יש דליפה קבועה בכל מחזור.חוזרעולה בקו ישרקביעת baseline אחרי warm-upבדיקת ההפרש בכל מחזורהערך חוזר אחרי המחזור?בריא ברובודליפה קבועה בכל מחזור

איור 13: לא שופטים לפי הערך המוחלט, אלא לפי הצורה — “חוזר או לא חוזר”.

הטיפ כאן הוא לא להסתכל רק על Handle Count, אלא לתעד לפחות גם את הבאים:

  • Handle Count
  • Private Bytes
  • Thread Count
  • ReconnectCount
  • ה-phase הנוכחי

כך אפשר להבין הרבה יותר מהר האם “הזיכרון גדל”, “התהליכונים גדלים”, או “המשאב לא חוזר בכל reconnect”.

4.3. הסתכלות על ההתאמה בין create/open לבין close/dispose

גם אם מתגלה ש-Handle Count של התהליך כולו חשוד, זה לבד לא מגיע עד מקום הדליפה. מה שצריך אחר כך הוא לוג שמראה את מחזור חיי המשאב כזוגות.

בתור דוגמה, זה נראה כמו structured log כזה:

CameraSession session=421 cameraId=CAM01 phase=ReconnectStart reason=FrameTimeout handleCount=1824 privateBytesMB=418

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Create osHandle=0x00000ABC handleCount=1825

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Close osHandle=0x00000ABC handleCount=1824

החשוב כאן הוא לא להסתמך רק על osHandle. מכיוון שערך ה-handle ב-Windows עשוי לעבור שימוש חוזר בהמשך, קל יותר לעקוב אם מוסיפים בלוג לפחות את הבאים:

  • sessionId
  • resourceId
  • kind
  • action(Create/Open/Register/Close/Dispose/Unregister)
  • osHandle
  • phase

כך קל יותר למצוא זרימה “עם ריאה אחת” — יש Create, אבל אין Close.

לוג שעוקב אחרי מחזור חיי המשאב כזוגותתרשים המראה שרישום Create ו-Close כזוגות וקישורם באמצעות sessionId ו-resourceId מאפשר למצוא Create שלא קיבל Close, ושה-osHandle עובר שימוש חוזר ולכן אי אפשר לעקוב לפיו בלבד.רישום Create ו-Close כזוגותקישור לפי sessionId ו-resourceIdגילוי Create שלא קיבל CloseosHandle עובר שימוש חוזר, אי אפשר להסתמך עליו לבדו

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

4.4. בדליפת handle מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”

הנקודה הזו חשובה מאוד.

דליפת handle לרוב נראית כך:

  • השורה שקורסת: כשל ב-CreateEvent
  • הדליפה האמיתית: כמה ימים לפני כן, CloseHandle היה חסר במסלול הכשל

כלומר, ה-API שקרס לבסוף הוא יציאת הנזק, ולא בהכרח כניסת הגורם.

לכן, סדר החקירה הוא כזה:

  1. לבדוק אילו משאבים ממשיכים לגדול
  2. לבדוק באיזה גבול פעולה הם לא חוזרים
  3. לחפש איפה ההתאמה בין create/open לבין close/dispose התקלקלה
  4. בסוף לקרוא את נקודת הקריסה

בסדר הזה, קל בהרבה שלא ללכת לאיבוד.

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

איור 15: נקודת הקריסה היא רק היציאה. יש להתחיל מ”מקום הדליפה” שהוא הכניסה.

5. הלוגים הנדרשים כדי למנוע הישנות

5.1. הסט המינימלי שכדאי להשאיר קודם

מה שעזר בחקירה הזו לא היה סתם הגדלת כמות הלוגים. אלא סידור והוספה של “מידע שמאפשר להגיע לגורם בהמשך”.

כמינימום, כדאי להשאיר את הבאים:

קטגוריה פריטים מינימליים רצויים הסיבה
הקשר הפעולה cameraId,‏ sessionId,‏ operationId,‏ reconnectCount,‏ phase כדי לקשר איזו פעולה ובאיזה מספר סיבוב זה קרה
משאבי התהליך handleCount,‏ privateBytes,‏ workingSet,‏ threadCount כדי לבודד קודם מה בעצם גדל
מחזור חיי המשאב action,‏ resourceId,‏ kind,‏ osHandle,‏ owner כדי לעקוב אחרי ההתאמה בין create/open לבין close/dispose
תוצאת קריאות חיצוניות win32Error,‏ HRESULT,‏ sdkError,‏ timeoutMs כדי להשוות אחר כך בין סוגי הכשל
מעברי מצב OpenStart,‏ OpenDone,‏ ReconnectStart,‏ ReconnectDone,‏ ShutdownStart וכדומה כדי לדעת באיזה שלב באמצע ה-phase המצב התקלקל
סביבת ההרצה pid,‏ tid,‏ buildVersion,‏ machineName כדי להתאים בין dump, symbol וחומר ההפצה

לא נטען שזה מספיק. אבל לפחות בלי זה, קל מאוד שהלוג יישאר רק עם העובדה “זה קרס”.

5.2. הלוגים שחוזקו בפועל

במקרה הזה, חיזקנו את הלוגים בכיוונים הבאים:

  1. heartbeat תקופתי
    • הוצאת Handle Count /‏ Private Bytes /‏ Thread Count /‏ ReconnectCount כל 1–5 דקות
  2. לוג גבולות לפי session של המצלמה
    • OpenStart
    • CallbackRegistered
    • AcquisitionStart
    • TimeoutDetected
    • ReconnectStart
    • ReconnectDone
    • CloseStart
    • CloseDone
  3. לוג מחזור חיי המשאב
    • Create/Open/Register ו-Close/Dispose/Unregister עבור event / thread / file / timer / SDK registration token
  4. נרמול השגיאות
    • לא להסתפק בהודעת החריגה לבדה, אלא להוציא יחד win32Error,‏ HRESULT,‏ sdkError,‏ phase

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

ארבע מערכות הלוג שחוזקותרשים המראה ששילוב של heartbeat תקופתי, לוג גבולות ה-session, לוג מחזור חיי המשאב ונרמול השגיאות, בלי לשנות את הפורמט בין הצלחה לכשל, יוצר לוג שמאפשר להגיע לגורם בהמשך.heartbeat תקופתי (ערכי המשאבים)לוג שמוביל לגורםלוג גבולות ה-sessionלוג מחזור חיי המשאבנרמול השגיאותלא לשנות את הפורמט בין הצלחה לכשל

איור 16: לא מגדילים את כמות הלוג, אלא מסדרים ארבע מערכות שאפשר להצליב אחר כך.

5.3. באיזו רזולוציה לתעד

מה שעושים בטעות לרוב זה “בינתיים להוציא הכול כ-INFO”. אבל אם עושים את זה, בקריאה מאוחר יותר נוצרת “חומת לוגים”. זה די מתיש.

בתור רזולוציה, החלוקה הבאה בערך ריאלית:

  • ניטור תקופתי
    • Handle Count,‏ Private Bytes,‏ Thread Count,‏ ReconnectCount
  • גבולות פעולה
    • התחלה, סיום וכישלון של ה-session
  • גבולות משאב
    • create/open/register מול close/dispose/unregister
  • פירוט בזמן חריגה
    • error code, stack, טריגר לאיסוף dump

לוג מפורט לכל פריים בדרך כלל לא נחוץ. מה שיעיל יותר לתקלה בהפעלה ממושכת הוא לוג שממנו אפשר לקרוא “איזו אחריות פתחה, ואיזו אחריות סגרה”.

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

איור 17: במקום פירוט לכל פריים, מסדרים רזולוציה שממנה אפשר לקרוא “מי פתח ומי סגר”.

6. חלוקה גסה בין המצבים

  • קורס רק אחרי ימים עד שבועות
    • קודם כול, להוסיף heartbeat של Handle Count /‏ Private Bytes /‏ Thread Count
  • יש retry / reconnect / shutdown
    • קודם להכין harness שמריץ בכמות גדולה רק את הגבולות האלה
  • שימוש נרחב ב-SDK ילידי /‏ P/Invoke /‏ Win32
    • שווה מאוד להפעיל את Application Verifier מהפרק השני
  • יש גם GUI
    • כדאי להסתכל, מלבד Handle Count, גם על GDI Objects /‏ USER Objects
  • החריגה ברגע הקריסה לבדה לא אומרת כלום
    • מהיר יותר לסדר קודם structured log של operation /‏ session / מחזור חיי המשאב

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

7. סיכום

באפליקציה שקורסת רק אחרי הפעלה ממושכת, יש להסתכל לא רק על הזיכרון אלא גם על Handle Count. דליפת handle נוטה להסתתר לא במסלול הרגיל אלא ב-failure path של המסלול החריג, ונקודת הקריסה היא לרוב יציאה של תקלה משנית ולא נקודת הדליפה עצמה. כשמסתכלים על התסמינים, זה בעצם מצטמצם לשלוש הנקודות האלה.

כדי למנוע הישנות, מקרבים את האחריות של create/open לזו של close/dispose, משאירים לוג עם הקשר לפי session /‏ operation, ומתעדים גם את משאבי התהליך וגם את מחזור חיי המשאב. בבדיקות, בלי לחכות לשחזור בסדר גודל של חודשים, מריצים timeout /‏ reconnect /‏ shutdown בלולאה קצרה, וקובעים כתנאי הצלחה לא רק “לא להישבר” אלא גם “אפשר לעקוב כשזה נשבר”. זה השילוב שעזר במקרה הזה. בפרק השני נשתמש ב-Application Verifier כדי לחשוף מראש שברים קשים להתגלות, כמו מחסור בזיכרון או תקלת handle.

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

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

פרק שני: תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier

8. מקורות

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

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מהי דליפת handle?
מצב שבו התהליך ב-Windows שוכח לסגור handle שמפנה למשאב מערכת הפעלה כמו event,‏ mutex,‏ file או socket, וה-Handle Count ממשיך לגדול. הדפוס הנפוץ במיוחד הוא שכחת סגירה של משאב שנפתח באופן זמני עבור פעולה מסוימת, במסלול כשל באמצע הפעולה כמו timeout,‏ reconnect או early return — ובגלל שבבדיקות קצרות שגרתיות עוברים בעיקר במסלול ההצלחה, קל לפספס את זה.
איך מבחינים בין דליפת זיכרון לדליפת handle?
המדדים שמסתכלים עליהם שונים. בדליפת זיכרון, Private Bytes או Commit גדלים בהדרגה, ואילו בדליפת handle, ה-Handle Count גדל בהדרגה ולא חוזר. בבידוד תקלה בהפעלה ממושכת, הסתכלות רק על הזיכרון נוטה להיות כמו נהיגה בעין אחת, ולכן הבסיס הוא להסתכל גם על Handle Count וגם על Thread Count. אם קיים גם GUI, כדאי להסתכל גם על GDI Objects /‏ USER Objects.
אם יש דליפת handle, למה זה קורס רק אחרי הפעלה ממושכת?
דליפה עם שיפוע קטן — למשל handle אחד בודד שדולף בכל כשל — לא גורמת לשום דבר תוך כמה דקות, אבל בהפעלה של 24/7 תנאי גבול כמו timeout או reconnect מתרחשים שוב ושוב, ומצטברים במשך שבועות. בסופו של דבר, ברגע שקריאה ל-API שיוצר event/file/thread חדש נכשלת, זה מתגלה כתקלה משנית. חשוב גם שנקודת הקריסה לרוב אינה המקום שבו התרחשה הדליפה, אלא הקרבן האחרון בשרשרת.
איך חוקרים דליפת handle?
בלי לחכות לשחזור בסדר גודל של חודשים, מכווצים את זמן השחזור על ידי הרצה של אלפי איטרציות בלולאה קצרה על גבולות מחזור חיים חשודים, כמו open -> start -> stop -> close או timeout ו-reconnect. קובעים baseline אחרי warm-up, בודקים את ההפרש והשיפוע של Handle Count בכל מחזור, ומחפשים איפה ההתאמה בין create/open ל-close/dispose התקלקלה בעזרת structured log שמכיל sessionId,‏ resourceId ו-action. קריאת נקודת הקריסה בסוף — ולא בהתחלה — מקטינה את הסיכוי להתבלבל בדרך.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג