Паттерны отказоустойчивых приложений. Роберт Ганмер

Это книга о технике обработки ошибок, которая делает работу с ними проще.

В первой части мы прочитаем введение и главы 1–4. Рассмотрим понятия отказоустойчивости, принципы отказоучтойчивого дизайна и архитектурные паттерны.

Введение

Мы не живём в идеальном мире. Во всех системах есть ошибки, которые приводят к отказам. Задача разработчиков — минимизировать количество этих ошибок и их негативное воздействие на систему и пользователей.

Шаблоны — это не панацея. Они решают проблемы внутри определённого контекста. После решения проблемы они могут оставить систему в новом контексте с новыми проблемами.

Глава 1. Введение в отказоусточивость

Неисправность, ошибка и отказ — три разных термина.

  • Неисправность (fault) — дефект в системе, причина ошибки (error).
  • Ошибка (error) — неправильное поведение системы, которое приводит к отказу (failure).
  • Отказ (failure) — поведение системы, которое не соответствует спецификациям.

Причинно-следственная цепочка выглядит так: fault → error → failure.

Ошибки и неисправности важны для отказоустойчивой системы, потому что могут быть замечены раньше, чем наступит отказ.

Отказы бывают нескольких видов.

  • Тихий отказ (fail-silent failure) — такой, при котором отказавшая часть либо не предоставляет результата работы, либо предоставляет правильный результат.
  • Аварийный отказ (crash failure) — при котором отказавшая часть прекращает работу после первого тихого отказа.
  • Отказ с остановкой (fail-stop failure) — такой аварийный отказ, который виден остальным частям системы.

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

Покрытие (coverage) — условная вероятность, что система восстановится после ошибки автоматически в заданный отрезок времени. Надёжные (reliable) и доступные (available) системы стремятся к покрытию не меньше, чем 0.95.

Надёжность (reliability) — вероятность, что система будет работать без сбоев в течение заданного промежутка времени. Надёжность описывают:

  • среднее время до отказа, MTTF (mean time to failure) — от старта до первого отказа;
  • среднее время до восстановления, MTTR (mean time to repair) — с момента отказа до полного восстановления;
  • среднее время между отказами, MTBF (mean time between failures) — сумма MTTF и MTTR.

Доступность (availability) — доля времени, в которое система способна выполнять свою функцию. Аптайм (uptime) — время, когда система доступна, даунтайм (downtime) — когда не доступна.

Отказоустойчивая система спроектирована так, чтобы эффективно справляться с нормальной рабочей нагрузкой и изящно (gracefully) справляться с перегрузками.

Глава 2. Отказоустойчивое мышление

Ключевой вопрос при разработке отказоустойчивых приложений — «Что может пойти не так?».

Отказоустойчивость (fault tolerance) — это способность системы нормально функционировать даже при наличии отказов. Также это способность ограничить вред от ошибки, возникшей в системе. Качество (quality) — насколько хорошо система может работать без отказов.

Стремление к отказоустойчивости может привести к технологическому и архитектурному оверхеду. Чрезмерное увеличение сложности для обнаружения и исправления ошибок с большой вероятностью приведёт к ещё большему количеству ошибок. Применяйте KISS.

Важные допущения, проверки и предположения:

  • Не полагайтесь слепо на данные из хранилища, они могут содержать ошибки — проверяйте все данные перед использованием.
  • Утечка памяти — частая и высоковероятная ошибка.
  • Проектируйте структуры данных так, чтобы их можно было проверять и валидировать, а также, чтобы невалидные можно было откорректировать.

Для разработки системы полезно использовать N-version programming. Это подход, при котором систему независимо проектируют несколько команд. Плюс в том, что команды, скорее всего, будут использовать разные алгоритмы, структуры и подходы. Это увеличит количество альтернатив, из которых можно выбрать содержащую минимальное количество неисправностей.

Тестирование и верификация — ключевые свойства отказоустойчивой системы. Они показывают, успешны ли предотвращение неисправностей и исправление ошибок. Тестирование внедрением ошибок (Fault Insertion Testing) — единственный способ определить покрытие (coverage).

Методология отказоустойчивого дизайна:

  • определить, что может пойти не так;
  • определить стратегии смягчения рисков, какие действия могут предотвратить появление потенциальных неисправностей;
  • создать модель системы, выделяя ключевые точки системы и способы применения избыточности (modes of redundancy);
  • внести ключевые архитектурные решения;
  • внедрить в архитектуру способы уменьшения рисков;
  • учесть необходимость в администрировании людьми всех систем, неважно, насколько они отказоустойчивы.

Глава 3. Введение в паттерны

Жизненный цикл отказа состоит из 4 фаз:

  • обнаружение ошибки (error detection);
  • восстановление (error recovery);
  • уменьшение ошибок (error mitigation);
  • обработка отказа (fault treatment).

Системы без внутреннего состояния (stateless) как правило содержат меньше ошибок, чем системы с внутренним состоянием (stateful). Если в системе есть операции, занимающие продолжительное время, её принято считать стейтфул-системой. Когда стейтфул-система теряет внутреннее состояние, она теряет способность продолжать функционировать.

Разработка отказоустойчивой системы — дорогая. Готовьтесь вложить больше ресурсов по сравнению с разработкой обычной системы.

Глава 4. Архитектурные паттерны

Архитектурные паттерны рассказывают, как проектировать систему с оглядкой на отказоустойчивость.

Карта соотношения архитектурных паттернов
Карта соотношения архитектурных паттернов

4.1 Блоки уменьшения риска (Units of mitigation)

При разработке вам хочется уменьшить риск полного отключения системы. Как сохранить систему работоспособной при наступлении отказа?

Монолит не подходит — если происходит ошибка, монолит не работает полностью. Интерфейсы между блоками уменьшения риска должны быть обозначены чётко и понятно. Граница между частями системы должна быть чёткой и делить систему на понятные части. Такое разделение — способ предотвратить распространение ошибки от одной части системы к другим.

Блоки уменьшения риска…

  • могут быть продублированы, чтобы обеспечить избыточность;
  • это рантайм-сущности, потому что выполняют работу во время выполнения программы;
  • могут представлять собой сгруппированные модули;
  • должны быть в состоянии проводить самоконтроль, когда есть подозрение на неправильную работу;
  • обязаны быть непреодолимым барьером для ошибок.

4.2. Корректировочные аудиты (Correcting audits)

Ошибки в данных могут и будут возникать.

Данные должны восприниматься неразрывно от их контекста. (1984 может быть валидным годом, но не может быть валидным количеством лет пользователя.) Ошибки в данных приводят к тому, что:

  • вычисления, опирающиеся на эти данные, будут неправильными;
  • может возникнуть ошибка в данных, связанных с ними;
  • может полностью отказать выполнение какой-либо операции.

Аудиты позволяют выявить некорректные данные.

  • Проверьте структурные свойства. Например, что связанные списки — действительно связанные.
  • Проверьте известные соотношения. Например, температуру в градусах Фаренгейта можно перепроверить значением в градуах Цельсия.
  • Проверьте, что данные не противоречат здравому смыслу. Вряд ли 1984 — валидное количество лет пользователя.
  • Проверьте данные прямым сравнением. Если есть отдельная копия тех же данных, проверьте их на соответствие.

Для каждой структуры данных предусмотрите, что может пойти с ней не так. Когда появляется ошибка в данных, хорошим тоном считается:

  • скорректировать их;
  • записать в логи, что произошло;
  • восстановить выполнение программы с того места, когда данные в последний раз были корректными.

Старайтесь обнаружить и исправить ошибки в данных как можно раньше; проверяйте связанные данные, записывайте все случаи в логи.

4.3. Избыточность (Redundancy)

Как уменьшить время между обнаружением ошибки и возврату к нормальной работе после восстановления?

Всё время, пока система не восстановила нормальную работу после ошибки, она недоступна. Уменьшение этого периода времени увеличивает доступность. Один из способов ускорить процесс восстановления — выполнять только самое необходимое для обработки ошибки. Всё остальное — отложить на период после восстановления.

Избыточность (redundancy) бывает нескольких типов:

  • пространственная;
  • временнáя;
  • информационная.

Избыточные элементы не обязательно имеют одинаковую функциональность, всё что нужно — чтобы избыточный элемент мог выполнить какую-нибудь часть работы дублируемого элемента. Разнообразие — хороший инструмент в борьбе с распространением ошибок в системе.

Избыточность — не бесплатна.

Есть несколько способов обеспечить пространственную избыточность:

  • метод “Active-Active” — полное дублирование функциональности дублируемого элемента; скорейшее восстановление, большие издержки;
  • метод “Active-Standby” — то же, но дублирующий не выполняет полезную функцию сразу; чуть меньшее время восстановления, чуть меньшие издержки;
  • “N+M” — есть М активных элементов, есть N избыточных, которые готовы заменить любой из M элементов при появлении отказа.
Соотношение стоимости и времени восстановления от типа избыточности
Соотношение стоимости и времени восстановления от типа избыточности

4.4. Блоки восстановления (Recovery blocks)

В программах бывают скрытые ошибки. Как удостовериться, что результат работы безошибочен?

Программа с блоками восстановления (recovery blocks) состоит из частей с главным блоком и побочными. Если результат работы главного блока не проходит приёмочный тест, полезную работу проводят побочные блоки до тех пор, пока результат не пройдёт тест. Если тест всё равно не проходит, то ошибка регистрируется в Обработчике ошибок (Error Handler).

Общепринятая схема для построения побочных блоков — делать каждый последующий более простым, чем предыдущий. Будьте готовы, что информации может потеряться по пути, так как каждый последующий блок выполняет меньше действий, чем предыдущие.

Избегайте создания слишком большого количества побочных блоков. Используйте Ограничение повторов (Limit retries), чтобы не допустить зацикливания системы.

4.5. Минимизировать человеческое вмешательство (Minimize Human Intervention)

Люди — частая причина множества ошибок. Как оградить людей от выполнения неправильных действий, которые приводят к ошибкам?

Кроме ошибок с железом и софтом существуют процедурные ошибки — которые появляются в результате действий персонала. Проектируйте систему так, чтобы сократить количество возможных процедурных ошибок. Людям быстро становится скучно, и они перестают уделять внимание рутинным и монотонным задачам.

Система должна давать чёткие и однозначные инструкции, что делать, если наступил отказ. Но при этом персонал не должен быть необходим для разрешения ошибки.

4.6. Максимизировать человеческое участие (Maximize Human Participation)

Должна ли система игнорировать людей в принципе?

Для многих типов систем (например, авионика) возможность оператора перебить или изменить обработку ошибки — жизненно-необходима. Такие системы могут входить в «безопасный режим» (safe mode) и перестать выполнять автоматические действия, ждя человеческого вмешательства.

Определите, для кого проектируется система. Создавайте способы для квалифицированных пользователей участвовать в обработке ошибок, если требуется.

4.7. Интерфейс тех. обслуживания (Maintenance Interface)

Должны ли сигналы приложения и сигналы тех. обслуживания быть смешаны?

Нет, они должны быть разделены. Сигналы обслуживания должны быть обработаны даже тогда, когда система перегружена. Кроме того, смешение сигналов может привести к дырам в безопасности.

4.8. Ответственный (Someone in charge)

Что угодно может пойти не так, даже во время обработки ошибки. Система может перестать выполнять не только основную функцию, но и перестать обрабатывать ошибки.

Когда система знает, что она должна выполнять в конкретный момент времени, она более крепкая. Часть системы, которая может определить, что что-то не работает, или работает не правильно, называется Наблюдатель отказов (Fault observer).

Для каждого отдельного действия, связанного с обработкой ошибок, должна быть одна чётко определённая сущность.

4.9. Эскалация (Escalation)

Что делать системе, если её попытки обработать ошибку не достигли желаемого результата?

Применять методы обработки со следующих уровней. Поднимать ошибку «наверх» в иерархии системы. Отдавать сигнал на «подъём» должен Ответственный (Someone in charge).

4.10. Наблюдатель отказов (Fault observer)

Система не падает после ошибки, а обрабатывает их автоматически. Как нам понять, какие ошибки и когда произошли?

Наблюдатель отказов уведомляет персонал о случившихся ошибках через Интерфейс тех. обслуживания. Наблюдатель отказов не обязательно должен быть внутренней частью системы, он может быть внешним сервисом.

Сообщайте обо всех ошибках Наблюдателю. Он позаботится о том, чтобы все заинтересованные стороны узнали об ошибках, которые произошли.

4.11. Обновление программы (Software update)

Система не должна останавливать свою работу даже для того, чтобы обновиться.

Внедряйте способность вносить изменения, патчи, обновления в архитектуру с первого релиза. Не надейтесь, что даже после этого обновление будет простой задачей в будущем.

Что дальше?

В следующей части проичтаем главы 5–8. Рассмотрим паттерны обнаружения ошибок, восстановления от них, снижения их вероятности и «замазывания брешей».

Ссылки