חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle
· עודכן בתאריך: · Go Komura · פיתוח Windows, חקירת תקלות, מצלמה תעשייתית, דליפת handle, תכנון לוגים
כשאפליקציית Windows קורסת בפתאומיות אחרי הפעלה ממושכת, נוטים מאוד לחשוד קודם בדליפת זיכרון. עם זאת, לא מעטים המקרים שבהם האשם האמיתי הוא דליפת handle, שרק אחרי שבועות מתגלה סוף סוף כתקלה משנית.
המקרה שמוצג כאן הוא חקירה של אפליקציית Windows ששולטת במצלמה תעשייתית, וקורסת בפתאומיות אחרי כחודש של הפעלה רציפה. לאחר בידוד, התברר שהסיבה הייתה דליפת handle שהתרחשה במסלול הכשל סביב חיבור מחדש (reconnect) של המצלמה.
בפרק הראשון נסדר מהי דליפת handle, איך בודדנו את המקרה הזה, ואילו לוגים כדאי להשאיר כדי למנוע הישנות. בפרק השני נדבר על תשתית בדיקות למקרי קצה, תחת הכותרת תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier.
שמות פרטיים וחלק מפריטי הלוג הוסתרו, אבל אופן החשיבה עצמו משותף במידה רבה לכלל אפליקציות בקרת ההתקנים ב-Windows.
תוכן עניינים
- קודם כול, המסקנה (במשפט אחד)
- מהי דליפת handle
- 2.1. ה”handle” שמדובר בו כאן
- 2.2. למה זה נוטה להתגלות רק בהפעלה ממושכת
- 2.3. ההבדל מדליפת זיכרון
- מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת בפתאומיות אחרי חודש
- 3.1. התסמינים שהופיעו
- 3.2. המדדים הראשונים שנבדקו
- 3.3. מקום הדליפה שהיה הגורם האמיתי
- איך בודד המקרה
- 4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים
- 4.2. הסתכלות לפי השיפוע של
Handle Count - 4.3. הסתכלות על ההתאמה בין
create/openלביןclose/dispose - 4.4. בדליפת handle מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”
- הלוגים הנדרשים כדי למנוע הישנות
- 5.1. הסט המינימלי שכדאי להשאיר קודם
- 5.2. הלוגים שחוזקו בפועל
- 5.3. באיזו רזולוציה לתעד
- חלוקה גסה בין המצבים
- סיכום
- מקורות
מפת הידע של המאמר
המאמר הזה מסודר סביב מקרה שבו אפליקציית בקרה למצלמה תעשייתית קרסה פתאום אחרי כחודש של הפעלה רציפה, ומרכז את דרך האיתור של דליפת 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 של הפרק השני ממוקם ככלי שנשען מעל הבסיס הזה.
flowchart LR
accTitle: מפת הידע של חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית (פרק דליפת ה-handle)
accDescr: תרשים שמראה איך בפירוק קריסה שאחרי הפעלה ממושכת בודקים יחד את Handle Count עם Private Bytes ו-Thread Count; איך אי-שחרור ב-failure path עובר דרך דליפת handle ומתגלה כתקלה משנית; ואת מקומם של RAII ו-CloseHandle במניעה, ושל לוגים מובנים ו-Application Verifier כשכבות מעליהם.
handle_leak["דליפת handle"]
handle_count["Handle Count"]
failure_path["failure path (מסלול כשל באמצע הפעולה)"]
memory_leak["דליפת זיכרון"]
private_bytes["Private Bytes"]
createevent_api["CreateEvent"]
closehandle["CloseHandle"]
structured_logging["יומן מובנה (structured log)"]
application_verifier["Application Verifier"]
raii["RAII(Resource Acquisition Is Initialization)"]
secondary_failure["תקלה משנית (secondary failure)"]
gdi_object["אובייקט GDI"]
long_run_crash_investigation["בידוד קריסה אחרי הפעלה ממושכת"]
thread_count["Thread Count"]
failure_path -->|"עלול לגרום ל"| handle_leak
handle_leak -->|"נבדק באמצעות"| handle_count
memory_leak -->|"נבדק באמצעות"| private_bytes
createevent_api -->|"מחייב"| closehandle
structured_logging -->|"מענה מומלץ ל"| handle_leak
structured_logging -->|"צריך לקדום ל"| application_verifier
raii -->|"מונע"| handle_leak
raii -.->|"משתמש ב"| closehandle
application_verifier -.->|"מצמצם"| handle_leak
handle_leak -->|"עלול לגרום ל"| secondary_failure
structured_logging -->|"מענה מומלץ ל"| secondary_failure
closehandle -->|"מונע"| handle_leak
gdi_object -.->|"עלול לגרום ל"| secondary_failure
handle_count -->|"מענה מומלץ ל"| long_run_crash_investigation
structured_logging -->|"מענה מומלץ ל"| long_run_crash_investigation
failure_path -->|"מענה מומלץ ל"| long_run_crash_investigation
private_bytes -->|"מענה מומלץ ל"| long_run_crash_investigation
thread_count -->|"מענה מומלץ ל"| long_run_crash_investigation
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 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) ברגע הקריסה, קל ללכת בכיוון מוטעה למדי.
flowchart TB
accTitle: המבנה של מה שכדאי לעשות קודם
accDescr: תרשים המראה שהסתכלות רק על החריגה ברגע הקריסה נוטה להוביל לכיוון מוטעה, בעוד שהפיכת קצב גידול המשאבים ומסלולי הכשל לניתנים לתצפית מובילה לזהות האמיתית של הדליפה שמתחפשת לתקלה משנית.
crash["הסתכלות רק על החריגה ברגע הקריסה"] -.-> wrong["הליכה לכיוון מוטעה"]
obs["תצפית על קצב הגידול ומסלולי הכשל"] --> right["הגעה לזהות האמיתית של הדליפה"]
איור 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 לא
- בבדיקות קצרות שגרתיות עוברים בעיקר במסלול ההצלחה, ולכן זה מתפספס
דפוס מהסוג הזה מסתתר בקלות רבה, גם בסקירת קוד וגם בהפעלה בפועל.
flowchart TB
accTitle: תבנית אופיינית של דליפה רק במסלול כשל
accDescr: תרשים המראה שבכל reconnect נוצר event, ואם רישום ה-callback או תחילת הצילום מצליחים מתבצע close ב-success path, אך במסלול כשל באמצע לא מתבצע close והדליפה מתפספסת בבדיקות קצרות שעוברות בעיקר במסלול ההצלחה.
ev["בכל reconnect נוצר event"] --> q{"הרישום וההתחלה הצליחו?"}
q -->|"הצלחה"| close["close ב-success path"]
q -->|"כשל באמצע"| leak["אין close ב-failure path"]
leak -.-> unseen["בדיקות קצרות מפספסות, כי הן עוברות במסלול ההצלחה"]
איור 2: שכחת סגירת משאב שנפתח באופן זמני במסלול של כשל באמצע. צורה נפוצה במיוחד באפליקציות בקרה.
2.2. למה זה נוטה להתגלות רק בהפעלה ממושכת
דליפת handle לא בהכרח שוברת הכול בבת אחת בפעם הראשונה. מה שבאמת מטריד הוא דליפה עם שיפוע קטן, למשל דליפה של יחידה אחת בלבד בכל כשל בודד.
flowchart LR
accTitle: השרשרת מהפעלה רגילה ועד לקריסה בעקבות דליפה קטנה
accDescr: תרשים המראה שבהפעלה רגילה מתרחשים מדי פעם timeout או reconnect שיוצרים Event Handle במסלול כשל בלי שנקרא CloseHandle, כך ש-Handle Count גדל מעט בכל פעם, וזה חוזר על עצמו מאות פעמים עד ש-CreateEvent או פתיחת ה-SDK נכשלים וגורמים לקריסה או עצירה במקום אחר.
A["הפעלה רגילה"] --> B["מדי פעם timeout / reconnect"]
B --> C["יצירת Event Handle במסלול הכשל"]
C --> D["CloseHandle לא נקרא"]
D --> E["Handle Count גדל מעט"]
E --> F["חוזר על עצמו מאות פעמים"]
F --> G["CreateEvent / פתיחת ה-SDK נכשלים"]
G --> H["קריסה / עצירה במקום אחר"]
איור 3: דליפה קטנה של יחידה אחת בכל פעם, מצטברת מאות פעמים בתנאי הגבול של הפעלת 24/7, ואז מתגלה.
אם בכל reconnect דולף רק handle אחד, לא קורה שום דבר תוך כמה דקות. אבל באפליקציית בקרת התקנים שרצה 24/7, תנאי גבול כמו timeout, אתחול מחדש והתאוששות מניתוק מתרחשים שוב ושוב. כתוצאה מכך, נוצר מראה מוזר: התקלה מתגלה רק אחרי שבועות.
החשוב כאן הוא שדליפת ה-handle עצמה לא בהכרח הופכת לשורת הקריסה. בדרך כלל האפליקציה נשברת כך:
- קריאה ל-API שיוצר event / file / thread חדש נכשלת
- ה-SDK לא מצליח ליצור פנימית משאב נדרש, ומחזיר רק קוד כשל כללי
- טיפול השגיאות אחרי הכשל דל, והאפליקציה נתקלת ב-
nullאו ב-handle לא תקין ונופלת - מספר ה-timeout גדל, ובסופו של דבר watchdog או בקרה מסדר גבוה יותר הורגים את התהליך
כלומר, נקודת הקריסה היא ה”קרבן האחרון”, ולאו דווקא “החשוד הראשון”.
flowchart TB
accTitle: נקודת הקריסה היא הקרבן האחרון
accDescr: תרשים המראה שדליפה מתמשכת של handle כלשהו במקום כלשהו מובילה בסופו של דבר לכשל של API שיוצר משאב חדש, שמתגלה כקריסה או עצירה במקום אחר עם טיפול שגיאות דל, ולכן השורה שקורסת היא רק הקרבן האחרון ולא החשוד הראשון.
leak2["דליפה מתמשכת של handle במקום כלשהו"] --> fail["כשל ב-API שיוצר משאב חדש"]
fail --> vict["קריסה או עצירה במקום אחר"]
vict -.-> note["השורה שקורסת היא רק הקרבן האחרון"]
איור 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 שמוקצה יחד איתו הוא מה שמשפיע |
כלומר, הקריאה “עדיין יש מרחב עד הגבול, אז זה בסדר” לא תקפה. יש להסתכל לא על השאלה אם מגיעים לגבול, אלא על האם מה שאמור לחזור אכן חוזר. ברגע שנוצר שיפוע, בטוח יותר להניח כבר שמדובר בחריגה.
flowchart TB
accTitle: מה נתקל בגבול לפני הגבול התיאורטי
accDescr: תרשים המראה שהמקרים שבהם מגיעים לגבול התיאורטי של handle-י הקרנל הם מיעוט, ובפועל נתקלים קודם בגבול אובייקטי GDI, בטבלת הניהול הפנימית של ה-SDK, או במרחב הכתובות של תהליך 32 סיביות, ולכן יש להסתכל אם המשאב חוזר ולא על הגבול עצמו.
limit["הגבול התיאורטי של הקרנל כ-16.77 מיליון"] -.-> rare["הגעה עד לשם היא המיעוט"]
rare -.-> first["מה שבפועל נתקל קודם"]
first --> g1["הגבול של GDI"]
first --> g2["טבלת הניהול הפנימית של ה-SDK"]
first --> g3["מרחב הכתובות של 32 סיביות"]
g1 --> see["שיפוט לפי האם המשאב חוזר"]
g2 --> see
g3 --> see
איור 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 כדאי להסתכל עליהם יחד, וזה מקל מאוד על הסידור.
flowchart TB
accTitle: מדדים שכדאי להסתכל עליהם יחד בבידוד הפעלה ממושכת
accDescr: תרשים המראה שמכיוון שדליפת זיכרון ודליפת handle נבדקות במדדים שונים, הסתכלות רק על מדדי זיכרון כמו Private Bytes אינה מספיקה, ויש להסתכל יחד גם על Handle Count וגם על Thread Count כדי להימנע ממצב של נהיגה בעין אחת.
watch["בידוד תקלה בהפעלה ממושכת"] --> m1["מדדי זיכרון (כמו Private Bytes)"]
watch --> m2["Handle Count"]
watch --> m3["Thread Count"]
m2 -.-> hint["גדל ולא חוזר — סימן לדליפת handle"]
איור 6: הסתכלות רק על הזיכרון היא נהיגה בעין אחת. יש לעקוב באותו מסך גם אחרי מספר ה-handle-ים והתהליכונים.
3. מקרה: אפליקציית בקרת מצלמה תעשייתית שקורסת בפתאומיות אחרי חודש
3.1. התסמינים שהופיעו
התופעה הייתה פשוטה:
- אפליקציית Windows ששולטת במצלמה תעשייתית פועלת 24/7
- בזמן רגיל היא פועלת כרגיל
- אחרי כחודש, יום אחד היא קורסת בפתאומיות
- אחרי הפעלה מחדש, שוב פועלת כרגיל למשך זמן מה
מה שמקשה בהתחלה הוא “שלוקח הרבה זמן עד שזה קורס”. לחכות חודש לכל שחזור בודד זה קשה מאוד לחקירה.
מה שהקשה עוד יותר הוא שמקום הקריסה לא היה בדיוק זהה בכל פעם. לפעמים זה קרה מיד אחרי תחילת reconnect, לפעמים בתחילת הצילום, ולפעמים אחרי כשל בקריאה ל-SDK.
עם הופעה כזו, בהתחלה אפשר לחשוד בכל אחד מאלה:
- חוסר יציבות בצד ה-SDK של המצלמה
- תקלה זמנית שמקורה בתקשורת או בניתוק ההתקן
- דליפת זיכרון
- race סביב תהליכונים
- כשל אתחול שלא נכנס ללוג
כלומר, המצב היה שיש יותר מדי “דברים שנראים חשודים איכשהו”.
flowchart TB
accTitle: שני הדברים שהקשו על החקירה במקרה הזה
accDescr: תרשים המראה שקריסה פתאומית אחרי כחודש של הפעלה רציפה גורמת לכך שכל שחזור לוקח חודש, ומקום הקריסה לא זהה בדיוק בכל פעם, ושני הדברים יחד יצרו מצב שבו יש יותר מדי מועמדים חשודים כמו SDK, תקשורת וזיכרון.
sym["קריסה בפתאומיות אחרי כחודש"] --> hard1["כל שחזור לוקח חודש"]
sym --> hard2["מקום הקריסה קצת שונה בכל פעם"]
hard2 -.-> many["יותר מדי מועמדים חשודים"]
איור 7: כש”לוקח הרבה זמן עד שזה קורס” ו”מקום הקריסה נודד” מצטרפים יחד, אי אפשר להתקדם בניחוש.
3.2. המדדים הראשונים שנבדקו
לכן, הדבר הראשון שנעשה היה הסתכלות על קצב הגידול של המשאבים בתהליך כולו. במקרה הזה, תוצאות התצפית הראו בערך את המגמה הבאה:
| מדד | המגמה שנצפתה | הפרשנות |
|---|---|---|
Handle Count |
גדל מעט בכל פעם אחרי reconnect או timeout, ולא חוזר | חשד לדליפת handle |
Private Bytes |
יש עליות וירידות, אבל השיפוע החד-כיווני חלש | ייתכן שהאשם אינו ה-heap |
Thread Count |
כמעט קבוע | סיכוי נמוך לדליפת thread |
| מקום הקריסה | קצת שונה בכל פעם | סביר להניח שזו תקלה משנית |
בשלב הזה, נקודת המבט כבר הצטמצמה משמעותית. מכיוון שהיה טבעי יותר להסתכל על זה לא כ”קורס אחרי חודש”, אלא כ”דולף משהו בהדרגה במהלך הדרך, ובעקבות זה קורס אחרי חודש”.
flowchart TB
accTitle: ההשערה שהצטמצמה מתצפית המדדים הראשונית
accDescr: תרשים המראה שהתצפית שלפיה רק Handle Count גדל ולא חוזר, שיפוע Private Bytes חלש, Thread Count כמעט קבוע, ומקום הקריסה שונה בכל פעם, הובילה להצטמצמות ההשערה לכיוון של דליפה הדרגתית שמובילה לקריסה אחרי חודש.
o1["Handle Count גדל ולא חוזר"] --> narrow["נקודת המבט מצטמצמת"]
o2["שיפוע Private Bytes חלש"] --> narrow
o3["Thread Count כמעט קבוע"] --> narrow
narrow --> view["דליפה הדרגתית שקורסת אחרי חודש"]
איור 8: כשמסדרים יחד את הצורה של ארבעת המדדים, מתגלה לא “קורס אחרי חודש” אלא “דולף כל הזמן”.
3.3. מקום הדליפה שהיה הגורם האמיתי
בסופו של דבר, הגורם היה שכחת close של event handle שנוצר במסלול כשל האתחול בזמן reconnect של המצלמה.
אם מפשטים את התהליך, זה נראה כך:
sequenceDiagram
accTitle: התהליך שבו הדליפה במסלול הכשל של ה-reconnect מובילה לקריסה
accDescr: תרשים רצף המראה שהאפליקציה יוצרת Event, נכשלת ברישום ה-callback, יוצאת במסלול הכשל בלי לקרוא CloseHandle, וש-Handle Count גדל בהדרגה עם כל reconnect עד שקריאה חדשה ל-CreateEvent או ל-Open נכשלת וגורמת לקריסה כתקלה משנית.
participant App as אפליקציית הבקרה
participant OS as Windows
participant SDK as ה-SDK של המצלמה
App->>OS: CreateEvent
App->>SDK: רישום callback
SDK-->>App: כשל באמצע / timeout
Note over App: יציאה (return) במסלול הכשל
Note over App: CloseHandle לא נקרא
loop reconnect שוב ושוב
App->>OS: Handle Count גדל מעט בכל פעם
end
App->>OS: CreateEvent / Open הבא
OS-->>App: כשל
App-->>App: קריסה כתקלה משנית
איור 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)
flowchart TB
accTitle: עיקרי כיוון התיקון
accDescr: תרשים המראה שכיוון התיקון הוא לקרב את האחריות של create לזו של close, להעביר את השחרור ל-finally, לדסטרוקטור או לאובייקט ה-session כדי שהוא יתבצע תמיד גם בכשל באמצע, ולבטא את מי שסוגר באחריות הקוד ולא בהערות.
pol["כיוון התיקון"] --> p1["קירוב האחריות של create ו-close"]
pol --> p2["העברת השחרור ל-finally או לדסטרוקטור"]
pol --> p3["ביטוי הבעלות באחריות הקוד"]
p3 -.-> nc["לא להסתמך על מוסכמה שמבוססת רק על הערות"]
איור 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.
flowchart TB
accTitle: מי אחראי לשחרור בהתאם להעברת הבעלות
accDescr: תרשים המראה שגם אם יוצאים מהעיבוד באמצע מכל נקודה, אם הבעלות הועברה לצד ה-session הוא אחראי לשחרור מכאן ואילך, ואם לא, הטיפוס או ה-finally תמיד יזרקו, ושלפני הזריקה מבטלים תחילה את הרישום אצל ה-SDK.
exit["יציאה מהעיבוד מכל נקודה באמצע"] --> q2{"הבעלות הועברה?"}
q2 -->|"הועברה"| keep["ה-session אחראי לשחרור מכאן"]
q2 -->|"לא הועברה"| drop["הטיפוס או ה-finally תמיד יזרקו"]
drop -.-> unreg["לפני הזריקה מבטלים את הרישום"]
איור 11: לא כותבים “אם נכשל, לסגור” בכל מקום מחדש. יעד הבעלות קובע מי אחראי לשחרור.
זה לא באמת טכניקה מיוחדת, אלא סידור שמטמיע את מחזור חיי המשאב בתוך הקוד עצמו.
4. איך בודד המקרה
מהפרק הזה מופיעים מונחים באנגלית הקשורים לחקירה כפי שהם. נשים כאן קודם הערת תרגום קצרה.
| מונח | בעברית | המשמעות במאמר הזה |
|---|---|---|
| baseline | ערך בסיס | הערך שמתייצב אחרי שה-warm-up מסתיים. משם מסתכלים על ההפרש |
| leakSlope | שיפוע הדליפה | כמה גדל בכל מחזור. מדד עצמאי שמייצג את קצב הגידול |
| structured log | לוג מובנה | לוג שבו הפריטים קבועים בסגנון key=value, במקום פרוזה. אפשר לרכז אותו אחר כך באופן אוטומטי |
| heartbeat | דיווח תקופתי | לוג שמוציא בקצב קבוע אישור חיים וערכי משאבים |
| harness | מסגרת בדיקה | תוכנית הרצה קטנה שמריצה שוב ושוב רק את הפעולה שרוצים לבדוק, במקום האפליקציה המלאה |
| phase | שלב | סימון כמו OpenStart או ReconnectStart, שמראה באיזה שלב של העיבוד נמצאים כרגע |
4.1. לקצר את הזמן בלי לחכות לשחזור בסדר גודל של חודשים
בחקירה כזו, לחכות חודש בכל פעם זה גישה גרועה. מה שצריך לעשות הוא להעביר את המסלול החשוד שוב ושוב, בזמן קצר.
במקרה הזה, כיווצנו את השחזור בעזרת לולאה כזו:
flowchart LR
accTitle: לולאת השחזור המכווץ
accDescr: תרשים המראה שלולאת השחזור מתחילה בהפעלה, פתיחת מצלמה והתחלת צילום, ואז מדמה timeout או ניתוק, מבצעת reconnect וחידוש צילום, וחוזרת על כך N פעמים לפני בדיקת ההפרש בסיום.
A["הפעלה"] --> B["open של המצלמה"]
B --> C["תחילת צילום"]
C --> D["דימוי timeout / ניתוק"]
D --> E["reconnect"]
E --> F["חידוש הצילום"]
F --> G{"לחזור N פעמים?"}
G -- "כן" --> D
G -- "לא" --> H["בדיקת ההפרש בסיום"]
איור 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, לפעמים קשה להבין רק מהערך המוחלט. החשוב הוא האם המספר חוזר אחרי הפעולה שאמורה להחזיר אותו, וכמה גדל בכל כמה פעולות.
בתור שיטת הסתכלות, הסדר הבא בדרך כלל ברור:
- קביעת
baselineאחרי ה-warm-up - תיעוד
Handle Countאחרי reconnect / start-stop / close - בדיקת ההפרש בכל מחזור
- בדיקת השיפוע המצטבר על פני כמה מחזורים
לדוגמה, כך אפשר להסתכל:
leakSlope =
(currentHandleCount - baselineHandleCount)
/ reconnectCount
האם 2000 בערך מוחלט זה הרבה או מעט תלוי באפליקציה. אבל אם בכל reconnect עולה ב-1 ולא חוזר, זה חשוד מאוד.
נכתוב גם קנה מידה לאיך המסלול התקין אמור להיראות. המספר עצמו תלוי באפליקציה, ולכן שופטים לפי הצורה.
- מיד אחרי ההפעלה יש עלייה — את זה לא קוראים
- אחרי שה-warm-up נגמר, הערך אמור לעלות ולרדת בהתאם לפעולות, אבל לנוע בתוך טווח קבוע
- אחרי מחזור אחד של
open -> start -> stop -> close, נורמלי שהערך יחזור כמעט לאותו ערך שהיה לפני המחזור - אם אחרי 100 מחזורים ההפרש מה-baseline הוא בסדר גודל של יחידות בודדות, זה בריא ברובו
- לעומת זאת, אם זה עולה בקו ישר ביחס ישר למספר המחזורים, אז זה דולף בדיוק בקצב השיפוע הזה בכל מחזור
לא מסתכלים על “הרבה או מעט”, אלא על האם זה חוזר או לא. אם מתבלבלים בנקודה הזו, מבזבזים זמן בחשד לאפליקציה תקינה.
flowchart TB
accTitle: איך קוראים את השיפוע של Handle Count
accDescr: תרשים המראה שאחרי warm-up קובעים baseline, בודקים את ההפרש בכל מחזור, ואם הערך חוזר אחרי המחזור זה בריא ברובו, ואם הוא עולה בקו ישר ביחס למספר המחזורים אז יש דליפה קבועה בכל מחזור.
base["קביעת baseline אחרי warm-up"] --> cyc["בדיקת ההפרש בכל מחזור"]
cyc --> q3{"הערך חוזר אחרי המחזור?"}
q3 -->|"חוזר"| ok["בריא ברובו"]
q3 -->|"עולה בקו ישר"| ng["דליפה קבועה בכל מחזור"]
איור 13: לא שופטים לפי הערך המוחלט, אלא לפי הצורה — “חוזר או לא חוזר”.
הטיפ כאן הוא לא להסתכל רק על Handle Count, אלא לתעד לפחות גם את הבאים:
Handle CountPrivate BytesThread CountReconnectCount- ה-
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 עשוי לעבור שימוש חוזר בהמשך, קל יותר לעקוב אם מוסיפים בלוג לפחות את הבאים:
sessionIdresourceIdkindaction(Create/Open/Register/Close/Dispose/Unregister)osHandlephase
כך קל יותר למצוא זרימה “עם ריאה אחת” — יש Create, אבל אין Close.
flowchart TB
accTitle: לוג שעוקב אחרי מחזור חיי המשאב כזוגות
accDescr: תרשים המראה שרישום Create ו-Close כזוגות וקישורם באמצעות sessionId ו-resourceId מאפשר למצוא Create שלא קיבל Close, ושה-osHandle עובר שימוש חוזר ולכן אי אפשר לעקוב לפיו בלבד.
lg["רישום Create ו-Close כזוגות"] --> ids["קישור לפי sessionId ו-resourceId"]
ids --> find["גילוי Create שלא קיבל Close"]
lg -.-> reuse["osHandle עובר שימוש חוזר, אי אפשר להסתמך עליו לבדו"]
איור 14: כדי לרדת ממספר כולל בתהליך אל מקום הדליפה, נדרש לוג שמזווג את מחזור חיי המשאב.
4.4. בדליפת handle מחפשים את “מקום הדליפה” ולא את “מקום הקריסה”
הנקודה הזו חשובה מאוד.
דליפת handle לרוב נראית כך:
- השורה שקורסת: כשל ב-
CreateEvent - הדליפה האמיתית: כמה ימים לפני כן,
CloseHandleהיה חסר במסלול הכשל
כלומר, ה-API שקרס לבסוף הוא יציאת הנזק, ולא בהכרח כניסת הגורם.
לכן, סדר החקירה הוא כזה:
- לבדוק אילו משאבים ממשיכים לגדול
- לבדוק באיזה גבול פעולה הם לא חוזרים
- לחפש איפה ההתאמה בין
create/openלביןclose/disposeהתקלקלה - בסוף לקרוא את נקודת הקריסה
בסדר הזה, קל בהרבה שלא ללכת לאיבוד.
flowchart TB
accTitle: סדר החקירה שמוביל למקום הדליפה
accDescr: תרשים המראה שהסדר שמפחית את הסיכוי ללכת לאיבוד הוא לבדוק אילו משאבים ממשיכים לגדול, לבדוק באיזה גבול פעולה הם לא חוזרים, לחפש איפה ההתאמה בין create ל-close התקלקלה, ורק בסוף לקרוא את נקודת הקריסה.
s1["בדיקת המשאבים שממשיכים לגדול"] --> s2["בדיקת הגבול שבו הם לא חוזרים"]
s2 --> s3["חיפוש איפה ההתאמה בין create ל-close התקלקלה"]
s3 --> s4["בסוף, קריאת נקודת הקריסה"]
איור 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. הלוגים שחוזקו בפועל
במקרה הזה, חיזקנו את הלוגים בכיוונים הבאים:
- heartbeat תקופתי
- הוצאת
Handle Count/Private Bytes/Thread Count/ReconnectCountכל 1–5 דקות
- הוצאת
- לוג גבולות לפי session של המצלמה
OpenStartCallbackRegisteredAcquisitionStartTimeoutDetectedReconnectStartReconnectDoneCloseStartCloseDone
- לוג מחזור חיי המשאב
Create/Open/Registerו-Close/Dispose/Unregisterעבור event / thread / file / timer / SDK registration token
- נרמול השגיאות
- לא להסתפק בהודעת החריגה לבדה, אלא להוציא יחד
win32Error,HRESULT,sdkError,phase
- לא להסתפק בהודעת החריגה לבדה, אלא להוציא יחד
החשוב הוא לא לשנות את סוג הלוג בין הצלחה לכשל. אם רק מקרי חריגה מקבלים פורמט אחר, אחר כך קשה לרכז אותם.
flowchart TB
accTitle: ארבע מערכות הלוג שחוזקו
accDescr: תרשים המראה ששילוב של heartbeat תקופתי, לוג גבולות ה-session, לוג מחזור חיי המשאב ונרמול השגיאות, בלי לשנות את הפורמט בין הצלחה לכשל, יוצר לוג שמאפשר להגיע לגורם בהמשך.
hb["heartbeat תקופתי (ערכי המשאבים)"] --> trace["לוג שמוביל לגורם"]
bd["לוג גבולות ה-session"] --> trace
rl["לוג מחזור חיי המשאב"] --> trace
er["נרמול השגיאות"] --> trace
trace -.-> same["לא לשנות את הפורמט בין הצלחה לכשל"]
איור 16: לא מגדילים את כמות הלוג, אלא מסדרים ארבע מערכות שאפשר להצליב אחר כך.
5.3. באיזו רזולוציה לתעד
מה שעושים בטעות לרוב זה “בינתיים להוציא הכול כ-INFO”. אבל אם עושים את זה, בקריאה מאוחר יותר נוצרת “חומת לוגים”. זה די מתיש.
בתור רזולוציה, החלוקה הבאה בערך ריאלית:
- ניטור תקופתי
Handle Count,Private Bytes,Thread Count,ReconnectCount
- גבולות פעולה
- התחלה, סיום וכישלון של ה-session
- גבולות משאב
create/open/registerמולclose/dispose/unregister
- פירוט בזמן חריגה
- error code, stack, טריגר לאיסוף dump
לוג מפורט לכל פריים בדרך כלל לא נחוץ. מה שיעיל יותר לתקלה בהפעלה ממושכת הוא לוג שממנו אפשר לקרוא “איזו אחריות פתחה, ואיזו אחריות סגרה”.
flowchart TB
accTitle: חלוקת רזולוציית הלוג
accDescr: תרשים המראה שניטור תקופתי מתעד ספירות, גבולות פעולה וגבולות משאב מתעדים את הזוגות של פתיחה וסגירה, ורק בזמן חריגה מתעד פירוט מלא, במקום להוציא הכול כ-INFO וליצור חומת לוגים בלתי קריאה.
lv["רזולוציית הלוג"] --> l1["ניטור תקופתי: ספירות"]
lv --> l2["גבולות פעולה וגבולות משאב"]
lv --> l3["פירוט מלא רק בזמן חריגה"]
all["הוצאת הכול כ-INFO"] -.-> wall["חומת לוגים בלתי קריאה"]
איור 17: במקום פירוט לכל פריים, מסדרים רזולוציה שממנה אפשר לקרוא “מי פתח ומי סגר”.
6. חלוקה גסה בין המצבים
- קורס רק אחרי ימים עד שבועות
- קודם כול, להוסיף heartbeat של
Handle Count/Private Bytes/Thread Count
- קודם כול, להוסיף heartbeat של
- יש 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. מקורות
- GetProcessHandleCount 関数 (processthreadsapi.h)
- Process.HandleCount プロパティ (System.Diagnostics)
- Kernel Objects - Win32 apps
- GDI Objects - Win32 apps
- typeperf - Windows Commands
- Process Explorer / Handle / VMMap בפועל — לעקוב אחרי תקיעה, דליפה ו”קובץ בשימוש” מהמצב הנוכחי הזה
- פרק שני: תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
מהו Application Verifier: מסודר כאן יחד עם בניית תשתית בדיקות למקרי קצה ב-Windows באמצעות Handles, Heaps, Low Resource Simulation ו-!h...
הסיבה לכך ששידור חוזר ב-TCP עוצר תקשורת עם מצלמה תעשייתית, ואיך מבודדים אותה
איך מבודדים עצירות של כמה שניות בתקשורת עם מצלמה תעשייתית שנגרמות משידור חוזר ב-TCP: אובדן מנות, RTO, חותמות הזמן של RFC1323 ונקודות הבדי...
תכנון שמירת יומנים ו-dump בקריסת יישום Windows
המאמר מסדר איך לשלב יומן רגיל, סמן קריסה סופי, WER LocalDumps ותהליך ניטור, כדי שגם כשיישום Windows קורס מחריגה בלתי צפויה או מבאג בתוכני...
מבוא לאיסוף crash dump ב-Windows - WER, ProcDump, WinDbg
המאמר מסדר את אופן הבחירה והשימוש ב-WER LocalDumps, ProcDump, MiniDumpWriteDump ו-WinDbg, כולל נקודות תפעוליות, כדי לחקור קריסות באפליק...
המלכודות של יישום תקשורת טורית - עד לתכנון חיבור מחדש ויומן
המאמר מסדר, מנקודת מבט מעשית, את המלכודות שכדאי להימנע מהן ביישום תקשורת טורית לחיבור ציוד ובקרת מכשירי מדידה - מבניית מסגרות, timeout, ...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
חקרי מקרה קשורים
חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.
כיצד קישרנו קריסה אחרי ריצה ארוכה לדליפת handles
חקר מקרה על האופן שבו נקודות תצפית ורישום טובות יותר הפכו קריסה חודשית לחקירה ממוקדת של דליפת handles.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
חקירת תקלות ואיתור גורמים
בידוד תקלה שקורסת רק אחרי הפעלה ממושכת מתאים היטב מאוד לנושא של חקירת תקלות וניתוח סיבות.
פיתוח יישומי Windows
כאשר רוצים לבחון מחדש את בניית האפליקציה בצד Windows, כולל תכנון לוגים וניטור תפעולי, זה מוביל גם לייעוץ על פיתוח אפליקציות Windows.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מהי דליפת 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. קריאת נקודת הקריסה בסוף — ולא בהתחלה — מקטינה את הסיכוי להתבלבל בדרך.