При разработке крупной системы рано или поздно встает вопрос её надежности. Надежности во всех смыслах этого понятия. Некоторые программы изначально создаются с большой степенью надежности, в других начинают реализовывать что-то после того, как клиенты теряют важные данные или терпение и уходят, а в большинстве программ вообще нет средств обеспечения надежности работы.
В этой статье я опишу, что такое надежность программ, зачем и кому она нужна, и как её можно реализовать, например, на C++ для Windows.


Итак, что же такое “надежность” и каким приложениям она нужна?
Большинство людей воспринимает надежность, как синоним “времени бесперебойной работы до первого глюка” — есть такая метрика. Для разных систем требования к этой метрике разные, например, если игра работает 5-6 часов в среднем без падений, то обычно этого уже более чем достаточно для того, чтобы признать ее качественной. Если же медицинский прибор перезагружается раз в неделю — это уже может быть критичным. Ну, а для ракеты, летящей к Юпитеру, даже 1 глюк за 2 года критичен.
Все мы в принципе умеем достигать требуемого уровня качества программы в виде этой метрики — для игр это сделать проще, для ракет сложнее, но все это делают и никакого таинства тут нет.


Но в погоне за метрикой многие забывают один ключевой момент — сколько бы часов ваша программа не наработала бесперебойно, в итоге она всё же заглючит. Упадет или зависнет, или испортит данные, или начнет делать что-то неожиданное и т.п. Нет исключений из этого правила. Глючат и спутники, и марсоходы, хотя и пишутся по супер дорогим и строгим стандартам НАСА.
И настоящая система “надежности” включается именно в этот момент — в момент возникновения нештатной ситуации. Все, что было до этого — это работа в штатном режиме, который вы можете контролировать, и которым вы можете управлять. А все, что просходит после глюка — это область чистого искусства, когда вы должны предугадать и среагировать на что-то, о чем вы не имеете никакого понятия (ведь если бы имели — это было бы уже исправлено, так?).
Если вы достигнете хорошей метрики “часов до первого глюка”, но не создадите никакой системы восстановления после того, как глюк случился — клиенты вас проклянут!



Итак, спрошу опять — что же такое “надежность”?
“Надежность” — это умение программы самовостанавливаться и восстанавливать утерянные данные после исключительных ситуаций.
Всё просто. Как бы долго программа не работала без падений — это не стоит и копейки, если в случае падения уничтожается всё, что было создано за несколько часов или, хуже — дней.
Вспомните злополучный пакет MS Office. Тот же MS Word или Outlook. Сложно найти более сложную и глючную программу, но кого это волнует? Word падает у меня на машине несколько раз за день, но каждый раз он автоматически рестартует и восстанавливает то, что у меня было в документе перед падением! Теперь даже Ctrl+S жать постоянно не надо! Это и есть истинная надежность, даже если Word не может проработать и пары часов без падения. И то же самое касается всех программ в пакете MS Office — Microsoft вложился в повышение надежности Office и вложился именно в “правильную надежность”, а не в “число часов наработки на отказ”. К тому же у них многие проблемы создаются сторонними плагинами или даже сторонними приложениями типа антивирусов, так что они и наработку на отказ-то контролировать до конца не могут.


Я уже писал, что может быть повреждено в программе в случае падения — это данные и алгоритмы работы.
С данными все понятно, а как могут измениться алгоритмы работы, если программы уже нет? Если программа состоит из одного выполняемого модуля, то тут проблема только одна — она исчезнет и пользователь будет разочарован. Представьте, что MS Word не показывал бы окно с галочкой “Перезапустить”, а просто бы исчезал — это был бы негативный опыт.
Алгоритмы работы же могут измениться (и изменятся, будьте уверены!), если у вас программа содержит несколько исполняемых модулей или хотя бы потоков. Если один из потоков завис или вылетел, но приложение еще не закрылось (например, работает Windows error reporting), то остальные потоки продолжают работать и на 100% уверенно могу сказать, что никто и никогда не тестировал их в этом режиме. Они будут предполагать, что зависший поток работает и полагаться на него и тем самым работа всех алгоритмов изменится — что-то еще зависнет, где-то сработают неожиданные таймауты, а где-то могут и испортиться пользовательские данные.
Если же у вас несколько запущенных параллельно процессов общаются друг с другом (классический вариант — это когда для улучшения безопасности, один процесс работает в user аккаунте, а второй в system или admin), то в случае падения одного из них ситуация запутывается еще сильнее. Невозможно правильно реализовать и протестировать все комбинации, когда в любой из моментов одно из запушенных приложений вдруг исчезает.


Какие же есть способы восстановления после сбоя?
С проблемой повреждения данных можно бороться по-разному. Если поставить себе задачу, что больше 5 минут работы не должно быть потеряно, то сохранение раз в 5 минут состояния программы во временный файл будет достаточно. В случае падения можно из него восстановить данные.
Если вообще нельзя терять данные, то можно сохранять полное состояние по-прежнему раз в 5 минут, а в промежутках сохранять небольшое инкрементальное состояние, так что по одному большому файлу и набору инкрементальных можно будет восстановить все данные.
Если же вам нельзя сохранять постоянно состояние или сохранение занимает слишком много времени (а 1 секунда для некоторых приложений, например, для игр — это уже много), то поможет простой трюк:
Поставьте Unhandled exception filter (SetUnhandledExceptionFilter) и ловите момент возникновения ошибки. Не пытайтесь восстановиться или проигнорировать ошибку — уже что-то пошло не так и вы не знаете что именно, так что дайте программе упасть. Но перед этим вызовите сохранения ее состояния.
Тут в вас должны проснуться все ваши теоретические знания и закричать “Но ведь так нельзя! Состояние программы неопределено в этот момент!”. Да, неопределено, теория права. Но практика показывает, что в 99.9% случаев, ошибки происходят в алгоритмах, а не содержатся в данных. Так что сохранение данных в случае падения обычно завершается успешно.
Например, все, кто играет в игры, сталкивается с падениями, когда ты вдруг осознаешь, что не сохранялся час или два. И жуть как не хочется играть после этого. А ведь если бы программист игры просто вызвал SetUnhandledExceptionFilter и вызвал бы save для игры в случае падения да еще и игру бы перезапустил автоматически — 99% игроков бы на всех форумах тащились от этой фичи даже если игра бы падала раз в час.
Хочу обратить внимание, что SetUnhandledExceptionFilter практически не работает под Vista и Win7. Там всегда падение перехватывает Error Reporting. Но в WinAPI добавился новый API в этих системах — Application Recovery and Restart Functions. Метод RegisterApplicationRecoveryCallback оттуда — это практически полный аналог SetUnhandledExceptionFilter и всегда вызывается. Используйте и SetUnhandledExceptionFilter и RegisterApplicationRecoveryCallback для надежного восстановления на всех версиях Windows.


Второй важной задачей при сбое является автоматический перезапуск приложения.
Да, не всегда это нужно. Если приложение содержит интерфейс и постоянно взаимодействует с пользователем, то можно спросить о рестарте у него. Как это часто делают игры или тот же пакет MS Office. Они показывают окно с ошибкой и галочкой “Перезапустить”.
Если же ваше приложение работает в фоне, то вы даже и не захотите, чтобы пользователь увидел о проблеме. Если вы сможете незаметно для него перезапуститься и при этом не потерять никаких данных — это идеальный вариант.
Обычно невозможно перезапуститься из калбэка, который вызывается в случае падения (котогрый вы зарегистрировали через SetUnhandledExceptionFilter или RegisterApplicationRecoveryCallback). Если вы запускаете новую копию приложения в этот момент, то столкнетесь с ситуацией, что у вас некоторое время будет запущено 2 одинаковых приложения. А значит начнутся проблемы с доступом к файлам, именованных хэндлам и т.п. И помните — остальные потоки, кроме зависшего, могут продолжать работать, так что обеспечить надежную работу в этом режиме будет сложно.
Так что лучший способ перезапуска изнутри recovery callback — это запуск специального приложения, которое будет просто ждать завершения вашего приложения и потом запускать его опять. Написать и отладить такое специальное приложение — это дело нескольких часов и все проблемы с перезапуском решены. Если будете его писать, то советую ждать завершения приложения не по имени, а по Process ID, иначе будут проблемы с похожими именами и множественными залогиненными пользователями. Также предусмотрите возможность задания командной строки для запускаемого приложения.


Итак, главная проблема в обеспечении надежности приложения — это возможность автоматического восстановления. Сохраняйте состояние и автоматически перезапускайтесь. Не пытайтесь восстановиться без перезапуска, если произошла критическая ошибка — гораздо надежнее перезапуститься и загрузить сохраненное состояние.
Вы можете надстроить еще кучу небольших улучшений на эту систему, вроде автоматического создания бэкап файлов при каждом открытии файла или отсылке важных данных в Cloud и т.п — этим вы позволите пользователю восстановиться даже сработал этот 0.1% и ваша система сохранения состояния дала сбой.
Не так даже важно, в 99% случаев вы сможете восстановиться или в 99.9%: Главное — чтобы у вас такая система самовосстановления была.

От Redactor