Расследование краша промышленной камеры при длительной работе — часть про утечку хендлов
· Го Комура · Разработка Windows, Расследование сбоев, Промышленная камера, Утечка хендлов, Проектирование логов
Когда Windows-приложение неожиданно падает после долгой работы, первым делом чаще всего хочется заподозрить утечку памяти. Но на практике нередко главным виновником оказывается утечка хендлов, которая проявляется лишь спустя недели в виде вторичного сбоя.
В этой статье разбирается случай, когда мы расследовали Windows-приложение, управляющее промышленной камерой, которое неожиданно падало примерно после месяца непрерывной работы. В ходе диагностики выяснилось, что причиной была утечка хендлов на путях отказа вокруг переподключения камеры.
В первой части разберём, что такое утечка хендлов, как мы диагностировали этот случай и какие логи стоит вести, чтобы предотвратить повторение. Во второй части, «Когда приложение управления промышленной камерой неожиданно падает через месяц (часть 2) — что такое Application Verifier и как построить основу для тестирования нештатных сценариев», речь пойдёт об основе для тестирования нештатных сценариев.
Собственные названия и часть полей логов скрыты, но сам подход в целом общий для Windows-приложений управления оборудованием.
Содержание
- Сначала — вывод (в двух словах)
- Что такое утечка хендлов
- 2.1. Что здесь понимается под «хендлом»
- 2.2. Почему это чаще проявляется именно при долгой работе
- 2.3. Отличие от утечки памяти
- Пример: приложение управления промышленной камерой неожиданно падает через месяц
- 3.1. Наблюдавшиеся симптомы
- 3.2. Показатели, на которые посмотрели в первую очередь
- 3.3. Место утечки, оказавшееся истинной причиной
- Как мы диагностировали проблему
- 4.1. Сжимаем время вместо ожидания воспроизведения масштаба месяца
- 4.2. Смотрим на наклон
Handle Count - 4.3. Проверяем соответствие
create/openиclose/dispose - 4.4. При утечке хендлов ищем не «место краша», а «место утечки»
- Логи, необходимые для предотвращения повторения
- 5.1. Минимальный набор, который стоит вести с самого начала
- 5.2. Логи, которые мы реально усилили
- 5.3. С какой детализацией собирать
- Как выбирать (кратко)
- Итог
- Справочные материалы
1. Сначала — вывод (в двух словах)
- В управляющем приложении, которое падает только после долгой работы, обязательно смотрите не только на
Private Bytes, но и наHandle Count - Утечки хендлов чаще прячутся не в штатном пути, а на путях
timeout/reconnect/ частичного отказа / раннего return - Строка, где реально происходит крах, чаще оказывается не местом утечки, а местом, где позже не удалось создать новый хендл
- В первую очередь нужны логи с контекстом
operation/session,handle countпроцесса, соответствиемopen/closeресурсов, ошибками Win32 / HRESULT / SDK - Вместо того чтобы ждать воспроизведения масштаба месяца, быстрее прогнать пути подключения, отключения, переподключения и отказа тысячи раз в короткой петле
- Application Verifier, который мы разберём во второй части, весьма эффективен, но перед этим основой служит умение отслеживать нарушения жизненного цикла по собственным логам
Иначе говоря, в подобных случаях первым делом стоит делать не «разглядывание факта, что упало после долгого периода», а приведение роста ресурсов и путей отказа в наблюдаемую форму.
К моменту обнаружения утечка хендлов чаще всего уже носит лицо вторичного сбоя. Поэтому если смотреть только на исключение в момент краша, легко уйти в совершенно неверном направлении.
2. Что такое утечка хендлов
2.1. Что здесь понимается под «хендлом»
Здесь под хендлом понимается идентификатор, с помощью которого процесс Windows обращается к ресурсам ОС. К этому относятся, например, такие объекты.
| Категория | Примеры |
|---|---|
| Объекты ядра | event, mutex, semaphore, thread, process, waitable timer |
| Ввод-вывод | open для file, pipe, socket, device |
| Часто встречающееся в управлении оборудованием | внутренние event SDK камеры, объекты ожидания, связанные с регистрацией callback-ов, хендлы, связанные с потоком захвата изображения |
Особенно проблемным в управляющих приложениях становится паттерн, при котором «ресурс, временно открытый ради конкретной операции, забывают закрыть на пути частичного отказа».
Типично это выглядит так:
- при каждом переподключении создаётся один event;
- регистрация callback-а или начало захвата изображения где-то по пути завершается отказом;
- на успешном пути ресурс закрывается, а на пути отказа — нет;
- в обычных коротких тестах проходит только успешный путь, поэтому проблема ускользает от внимания.
Такой тип утечки вполне обычно проникает и через код-ревью, и в реальной эксплуатации.
2.2. Почему это чаще проявляется именно при долгой работе
Утечка хендлов не обязательно ломает всё эффектно за один раз. Опаснее как раз утечка с небольшим наклоном, при которой за один отказ утекает всего один хендл.
flowchart LR
A[Обычная работа] --> B[Изредка timeout / reconnect]
B --> C[На пути отказа создаётся Event Handle]
C --> D[CloseHandle не вызывается]
D --> E[Handle Count немного растёт]
E --> F[Повторяется сотни раз]
F --> G[CreateEvent / открытие SDK завершается отказом]
G --> H[Крах / остановка в другом месте]
Если за один reconnect утекает всего один хендл, за несколько минут ничего не произойдёт. Но в приложении управления оборудованием, работающем 24/7, такие граничные условия, как тайм-ауты, повторная инициализация, восстановление после разрыва связи, происходят множество раз. В результате получается странная картина: проблема проявляется только спустя несколько недель.
Важно здесь то, что сама утечка хендлов не обязательно оказывается строкой краша. Чаще встречаются такие формы поломки.
- API, создающий новый event / file / thread, завершается отказом;
- SDK не может внутри себя создать нужный ресурс и возвращает лишь общий код отказа;
- обработка ошибок после отказа слаба, приложение натыкается на
null/ недействительный хендл и падает; - количество тайм-аутов растёт, и в итоге процесс убивает watchdog или вышестоящий контроллер.
То есть место краша — это «последняя жертва», а не обязательно «изначальный виновник».
2.3. Отличие от утечки памяти
При дефектах после долгой работы в первую очередь хочется заподозрить утечку памяти. Это, конечно, естественный порыв, но утечки хендлов иногда быстрее находить, если смотреть по другой оси.
| Аспект | Утечка памяти | Утечка хендлов |
|---|---|---|
| Что смотреть в первую очередь | Private Bytes, Commit, Working Set |
Handle Count |
| Типичные симптомы | Нехватка памяти, paging, замедление, OOM | Отказы Create* / Open* / внутренней инициализации SDK, вторичные сбои |
| Где чаще прячется | Кэши, удержанные ссылки, забытое освобождение | Асимметрия между create/open и close/dispose |
| Как проявляется | Память постепенно растёт | Handle count постепенно растёт и не возвращается |
Поэтому при диагностике проблем долгой работы смотреть только на память — это, по сути, «вести машину с одним закрытым глазом».
Как минимум Handle Count и Thread Count стоит отслеживать вместе — это заметно упрощает картину.
3. Пример: приложение управления промышленной камерой неожиданно падает через месяц
3.1. Наблюдавшиеся симптомы
Ситуация была простой.
- Windows-приложение, управляющее промышленной камерой, работает 24/7;
- в обычном режиме работает нормально;
- примерно через месяц однажды приложение внезапно падает;
- после перезапуска снова какое-то время работает нормально.
Первая трудность в том, что «до падения проходит долгое время». Ждать месяц ради каждого воспроизведения — довольно тяжело как метод расследования.
Ещё неприятнее было то, что место краша не было точно одинаковым каждый раз. Иногда сразу после начала переподключения, иногда при начале захвата изображения, иногда после отказа вызова SDK.
При такой картине поначалу можно подозревать что угодно из следующего:
- нестабильность на стороне SDK камеры;
- временные сбои из-за связи или отключения устройства;
- утечку памяти;
- race вокруг потоков;
- сбой инициализации, не отражённый в логах.
Иначе говоря, состояние было такое: слишком много «в чём-то подозрительных» кандидатов.
3.2. Показатели, на которые посмотрели в первую очередь
Поэтому первым делом посмотрели, как растут ресурсы процесса в целом. В этом случае наблюдаемые тенденции были примерно такими.
| Показатель | Наблюдаемая тенденция | Интерпретация |
|---|---|---|
Handle Count |
Постепенно растёт после reconnect и timeout, не возвращается | Подозреваем утечку хендлов |
Private Bytes |
Есть колебания, но наклон монотонного роста слабый | Главный виновник не обязательно куча |
Thread Count |
Практически на одном уровне | Утечка потоков маловероятна |
| Место краша | Каждый раз немного другое | Вероятен вторичный сбой |
На этом этапе фокус заметно сузился. Потому что естественнее было читать ситуацию не как «падает через месяц», а как «что-то постепенно понемногу утекает по пути, и в результате падает через месяц».
3.3. Место утечки, оказавшееся истинной причиной
В итоге причиной оказался пропущенный close event-хендла, созданного на пути отказа инициализации при переподключении камеры.
В упрощённом виде поток выглядит так.
sequenceDiagram
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: крах как вторичный сбой
В виде наброска кода утечка выглядит так.
handle = CreateEvent(...)
if (!RegisterCallback(handle))
{
return Error; // пропущен CloseHandle(handle)
}
if (!StartAcquisition())
{
return Error; // и здесь тоже пропущен close
}
...
CloseHandle(handle)
Причина, по которой это легко ускользает от коротких тестов, тоже довольно понятна.
- при обычном запуске и обычном завершении хендл закрывается;
- отказ случается только посреди переподключения;
- теста, который массово прогоняет именно этот путь отказа, нет;
- в продакшене утечка накапливается постепенно, за несколько недель.
То есть структура была такая: «невидимо, если смотреть только на штатный путь, но вполне обычно утекает на путях отказа».
Направление исправления не было чем-то эффектным.
- сблизить ответственность
create/openиclose/dispose; - перенести освобождение в
finally/ деструктор / объект сессии, чтобы оно срабатывало даже при частичном отказе; - явно определить владение вокруг регистрации callback-а и начала захвата;
- выражать «кто закрывает» не комментарием, а ответственностью в самом коде.
Это не столько особая техника, сколько наведение порядка, встраивающее время жизни ресурса прямо в код.
4. Как мы диагностировали проблему
4.1. Сжимаем время вместо ожидания воспроизведения масштаба месяца
В подобном расследовании ждать месяц при каждой попытке — плохая стратегия. Правильнее многократно прогонять подозрительные пути за короткое время.
В этом случае мы сжали воспроизведение, прогнав такую петлю.
flowchart LR
A[Запуск] --> B[Открытие камеры]
B --> C[Начало захвата]
C --> D[Имитация timeout / разрыва связи]
D --> E[Переподключение]
E --> F[Возобновление захвата]
F --> G{Повторить N раз}
G -- Да --> D
G -- Нет --> H[Проверить разницу в конце]
Суть в том, чтобы тратить время не на обычный период «съёмка идёт», а на граничные операции жизненного цикла.
Конкретно эффективны такие сценарии.
- массово прогонять
open -> start -> stop -> close; - намеренно вызывать тайм-ауты и гонять переподключения;
- вызывать отказ сразу после регистрации callback-а;
- вносить прерывания разрыва связи, прерывания переподключения, гонки при shutdown.
Идеально воспроизводить месяц реальной эксплуатации не требуется. Наоборот, тысячи раз наступить на подозреваемую границу жизненного цикла — куда ближе к причине.
4.2. Смотрим на наклон Handle Count
При расследовании утечки хендлов смотреть только на абсолютные значения бывает малопонятно. Важно, возвращается ли счётчик после операции, которая должна его вернуть, и сколько хендлов прибавляется за сколько операций.
Примерно такой порядок удобен для чтения ситуации.
- определить baseline после прогрева;
- фиксировать
Handle Countпосле reconnect / start-stop / close; - смотреть разницу за каждый цикл;
- смотреть и наклон, усреднённый по нескольким циклам.
Например, такой взгляд.
leakSlope =
(currentHandleCount - baselineHandleCount)
/ reconnectCount
Много или мало абсолютное значение в 2000, зависит от приложения. Но если на каждый reconnect приходится +1 и оно не возвращается, это весьма подозрительно.
Хитрость здесь — не смотреть на Handle Count изолированно, а фиксировать рядом как минимум следующее.
Handle CountPrivate BytesThread CountReconnectCount- в какой фазе мы сейчас находимся
С этим можно довольно быстро понять, «растёт ли память», «растут ли потоки» или «ресурсы не возвращаются при каждом переподключении».
4.3. Проверяем соответствие create/open и close/dispose
Даже поняв, что Handle Count процесса в целом подозрителен, этого одного недостаточно, чтобы дойти до места утечки.
Дальше нужны логи, показывающие жизненный цикл ресурса парами.
Как образ — вот такие structured-логи.
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.
Значения хендлов Windows впоследствии могут переиспользоваться, поэтому в логах удобнее сопровождать их как минимум следующим.
sessionIdresourceIdkindaction(Create/Open/Register/Close/Dispose/Unregister)osHandlephase
Так проще заметить однобокий поток, где Create есть, а Close — нет.
4.4. При утечке хендлов ищем не «место краша», а «место утечки»
Этот момент довольно важен.
Утечка хендлов часто выглядит так.
- строка краша: отказ
CreateEvent; - реальная утечка:
CloseHandleпропущен на пути отказа ещё несколько дней назад.
То есть API, на котором в итоге всё упало, — это выход ущерба, а не обязательно вход причины.
Поэтому порядок расследования такой:
- посмотреть, какой ресурс продолжает расти;
- посмотреть, на какой операционной границе он не возвращается;
- найти место, где нарушено соответствие
create/openиclose/dispose; - и лишь в конце прочитать место краша.
В таком порядке заметно проще не заблудиться.
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 и подобные |
Чтобы знать, в середине какой фазы всё сломалось |
| Среда выполнения | pid, tid, buildVersion, machineName |
Чтобы сопоставлять с дампами / символами / поставленными артефактами |
Мы не утверждаем, что этого достаточно. Но без как минимум этого легко получить логи, фиксирующие лишь сам факт «упало».
5.2. Логи, которые мы реально усилили
В этом случае мы усилили логи в следующих направлениях.
- Периодический heartbeat
- раз в 1–5 минут выводить
Handle Count/Private Bytes/Thread Count/ReconnectCount
- раз в 1–5 минут выводить
- Граничные логи в рамках сессии камеры
OpenStartCallbackRegisteredAcquisitionStartTimeoutDetectedReconnectStartReconnectDoneCloseStartCloseDone
- Логи жизненного цикла ресурса
Create/Open/RegisterиClose/Dispose/Unregisterдля event / thread / file / timer / токенов регистрации SDK
- Нормализация ошибок
- не ограничиваться одним сообщением исключения, а выводить одновременно
win32Error,HRESULT,sdkError,phase
- не ограничиваться одним сообщением исключения, а выводить одновременно
Важно не менять форму логов между успехом и отказом. Если при аномалии формат становится другим, агрегировать их позже становится тяжело.
5.3. С какой детализацией собирать
Здесь часто соблазняются вариантом «на всякий случай выводить всё на уровне INFO». Но если так сделать, при последующем чтении логов возникает стена текста. Это довольно тяжело.
По детализации реалистично примерно такое разделение.
- Периодический мониторинг
Handle Count,Private Bytes,Thread Count,ReconnectCount
- Границы операций
- start / done / fail сессии
- Границы ресурсов
create/open/registerиclose/dispose/unregister
- Детали при аномалии
- код ошибки, стек, триггер сбора дампа
Подробный лог каждого кадра обычно не нужен. Скорее для проблем долгой работы эффективнее логи, по которым видно, «какая ответственность открыла, а какая — закрыла».
6. Как выбирать (кратко)
- Падает только через несколько дней — недель
- в первую очередь добавьте heartbeat по
Handle Count/Private Bytes/Thread Count
- в первую очередь добавьте heartbeat по
- Есть retry / reconnect / shutdown
- сначала постройте harness, который массово прогоняет именно эти границы
- Активно используются native SDK / P/Invoke / Win32
- применить Application Verifier из второй части — весьма оправданно
- Рядом есть GUI
- помимо
Handle Count, стоит смотреть и наGDI Objects/USER Objects
- помимо
- Одно лишь исключение в момент краша ничего не объясняет
- быстрее сначала привести в порядок structured-логи operation / session / resource lifecycle
Последний пункт довольно важен. В расследовании сбоев исход часто решает не сама техника анализа, а то, приведено ли всё в наблюдаемую форму.
7. Итог
В приложении, которое падает только после долгой работы, смотрите не только на память, но и на Handle Count. Утечки хендлов чаще прячутся не в штатном пути, а на путях отказа нештатных сценариев, а место краша чаще оказывается выходом вторичного сбоя, а не местом самой утечки. Если сводить чтение симптомов к сути, всё сводится именно к этим трём пунктам.
Для предотвращения повторения — сблизьте ответственность create/open и close/dispose, ведите логи с контекстом на уровне сессии / операции и фиксируйте одновременно и ресурсы процесса, и жизненный цикл ресурсов. В тестах вместо ожидания воспроизведения масштаба месяца прогоняйте timeout / reconnect / shutdown в коротких петлях и делайте критерием приёмки не только «не ломается», но и «прослеживаемо, когда сломалось». В этом случае сработала именно такая комбинация. Во второй части мы используем Application Verifier, чтобы заранее выявлять труднопроявляемые формы поломки вроде нехватки памяти и аномалий хендлов.
В управляющих приложениях важно, чтобы работал штатный путь, но возможность понять «что произошло», когда что-то сломалось, при долгой эксплуатации значит очень многое.
Утечка хендлов — как раз тот тип дефекта, где эта разница окупается сполна. Если смотреть не только в момент возникновения проблемы, а через рост, границы и пары ответственности, отслеживать её становится заметно проще.
8. Справочные материалы
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Строим основу для тестирования нештатных сценариев Windows с помощью Application Verifier
Разбираем, что такое Application Verifier, вместе с построением основы для тестирования нештатных сценариев Windows с использованием Hand...
Реагирование на инциденты не заканчивается восстановлением — шаблон постмортема (предотвращения повторения) для небольших команд разработки
Считать инцидент закрытым сразу после исправления и извинений — гарантированный способ повторить его снова. Адаптируем blameless-постморт...
Почему TCP-ретрансмиссии останавливают связь с промышленной камерой, и как это диагностировать
Разбираем, как искать причину, когда связь с промышленной камерой останавливается на несколько секунд из-за TCP-ретрансмиссий: потери пак...
Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»
Разбираем, почему долго работающее Windows-приложение оказывается «остановленным к утру», начиная с различий между спящим режимом S3, гиб...
Если вам досталась система без исходного кода и без документации — практический план, как сопровождать её, не останавливая работу
Разбираем практический план начала эксплуатации и сопровождения бизнес-системы, у которой нет ни исходного кода, ни спецификаций. Охватыв...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Расследование ошибок и долгие сбои
Периодические сбои, диагностика связи, сбои после длительной работы и проверка путей отказа.
Связанные примеры проектов
В этих примерах используется сходный подход к анализу, расстановке приоритетов или переработке.
Как мы связали сбой после длительной работы с утечкой дескрипторов
Кейс о том, как усиление наблюдаемости и журналирования превратило редкий ежемесячный сбой в предметное расследование утечки дескрипторов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Расследование ошибок и причин
Диагностика сбоя, проявляющегося только после длительной работы, — тема, крайне хорошо ложащаяся на расследование сбоев и анализ первопричин.
Разработка приложений для Windows
Если нужно пересмотреть устройство Windows-приложения, включая проектирование логов и эксплуатационное наблюдение, это также связано с консультациями по разработке Windows-приложений.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что такое утечка хендлов?
- Ситуация, когда процесс Windows забывает закрыть хендл, через который он обращается к ресурсам ОС — event, mutex, file, socket и подобным, — из-за чего Handle Count непрерывно растёт. Особенно часто встречается паттерн, при котором ресурс, временно открытый ради конкретной операции, забывают закрыть на путях частичного отказа — тайм-аут, переподключение, ранний return, — а в обычных коротких тестах проходит только успешный путь, поэтому проблема легко ускользает от внимания.
- Как отличить утечку памяти от утечки хендлов?
- Смотреть нужно на разные показатели. При утечке памяти постепенно растут Private Bytes и Commit, а при утечке хендлов постепенно растёт и не возвращается обратно Handle Count. При диагностике проблем долгой работы, если смотреть только на память, легко оказаться в положении «водишь одним глазом», поэтому Handle Count и Thread Count стоит отслеживать вместе как минимум. Если рядом присутствует GUI, стоит смотреть ещё и на GDI Objects / USER Objects.
- Почему при утечке хендлов приложение падает только после долгой работы?
- Если утечка небольшая — по одному хендлу на каждый отказ — за считаные минуты ничего не произойдёт, но при работе 24/7 такие граничные условия, как тайм-ауты и переподключения, повторяются множество раз, и утечка накапливается за несколько недель. В итоге проблема проявляется как вторичный сбой в момент, когда не удаётся создать очередной event/file/thread. Важно и то, что место краша часто оказывается не местом утечки, а лишь последней жертвой.
- Как расследовать утечку хендлов?
- Не дожидаясь воспроизведения масштаба месяца, стоит сжать воспроизведение — прогнать в короткой петле тысячи раз подозрительные граничные операции жизненного цикла: open -> start -> stop -> close, тайм-ауты, переподключения. Определив baseline после прогрева, стоит смотреть разницу Handle Count за каждый цикл и общий наклон, искать через structured log с session Id, resourceId и action места, где нарушено соответствие create/open и close/dispose, и лишь в конце читать место краша — в таком порядке проще не заблудиться.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки