Паттерны отказоустойчивых приложений. Часть 2

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

В этой части рассмотрим паттерны обнаружения ошибок, восстановления от них, снижения их вероятности и «замазывания брешей».

Глава 5. Паттерны обнаружения ошибок

Ошибки и неисправности должны быть обнаружены. Есть два распространённых механизма для обнаружения ошибок. Первый — проверить, что возвращает функция, есть ли там коды ошибок. Второй — использовать встроенные в язык исключения и конструкции try‑catch. Когда ошибки обнаружены, они должны быть изолированы, чтобы не распространиться по системе.

5.12. Соотношение отказов (Fault correlation)

Какой отказ проявляется?

Определите уникальные признаки ошибки, чтобы понять категорию отказа. Как только ошибка определена, вокруг неё необходимо построить Барьер содержащий ошибку (Error containment barrier), чтобы предотвратить распространение.

5.13. Барьер, содержащий ошибку (Error containment barrier)

Что система должна сделать в первую очередь при обнаружении ошибки?

Последствия ошибки не всегда могут быть предсказаны заранее. Как и не все потенциальные ошибки могут быть предсказаны. Ошибки перемещаются от компонента к компоненту системы, если для этого нет ограничений. В программах барьер для распространения — это Блок уменьшения риска (Unit of mitigation).

5.14. Полное сравнение (Complete parameter checking)

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

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

(Это, кстати, напоминает контрактное программирование.)

5.15. Мониторинг системы (System Monitor)

Как одна часть системы может понять, что другая часть жива и функционирует?

Если при наступлении отказа часть системы может уведомить остальные части о своём состоянии, восстановление может начаться быстрее. Можно положиться на Сообщения подтверждения (Acknowledgement messages) — это сообщения, которые компоненты системы передают друг другу в ответ на какие‑то события. Другой способ — создать часть системы, которая будет смотреть за состоянием компонентов сама.

Когда наблюдаемый компонент перестаёт функционировать, об этом следует сообщить Наблюдателю отказов. При внедрении Мониторинга системы (System monitoring) необходимо определить, задержку после которой сообщение об отказе будет отправлено Наблюдателю отказов.

5.16. Пульс (Heartbeat)

Как Мониторинг системы может быть уверен, что конкретная наблюдаемая задача всё ещё работает?

Иногда компонент, за которым наблюдают, понятия не имеет, что за ним наблюдают. В таких случаях Мониторинг системы (System monitoring) должен запросить отчёт о состоянии, Пульс (Heartbeat). Проверьте, чтобы у Пульса не было нежелаемых побочных эффектов.

5.17. Подтверждение (Acknowledgement)

При общении двух компонентов какой самый простой способ для одного из них определить, что другой жив и функционирует?

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

5.18. Сторожевой пёс (Watchdog)

Как определить, что компонент жив и функционирует, если добавить Подтверждение нельзя?

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

5.19. Реалистичный порог (Realistic Threshold)

Как много времени должно пройти, прежде чем Мониторинг системы отправит сообщение об отказе?

Здесь нам интересны два отрезка: задержка обнаружения (detection latency) и задержка сообщений (messaging latency). Первая — сколько времени Мониторинг системы должен ждать ответа от компонента. Вторая — время между запросами, которое определяет статус компонента. Неадекватно подобранные значения снизят производительность.

Выставляйте задержку сообщений (messaging latency), исходя из худшего возможного времени коммуникации + время на обработку Пульса. Выставляйте задержку обнаружения (detection latency), исходя из критичности компонента.

5.20. Существующие метрики (Existing Metrics)

Как система может измерить интенсивность перегрузки, не увеличивая перегрузку?

Используйте встроенные механизмы индикации перегрузки системы ¯\_(ツ)_/¯

5.21. Голосование (Voting)

Получилось несколько вариантов результатов. Какой использовать?

Разработайте стратегию голосования, чтобы выбрать ответ. Пропишите веса для каждого из ответов. Можно сделать предположение, что активный элемент — тот, чьему результату можно доверять с большей вероятностью. Если ответы слишком большие, чтобы проверить их полностью, можно использовать Чексумму (Checksum).

Аккуратнее с результатами, которые могут быть разными, но в то же время правильными. Для таких ответов надо проверять не «правильность», а корректность.

5.22. Рутинное тех. обслуживание (Routine Maintenance)

Как сделать, чтобы ошибки, которые можно предотвратить, не случались?

Проводите рутинное, превентивное обслуживание системы. Корректировочные аудиты (Correcting audits) сохранят данные чистыми и без ошибок. Периодично повторяемые, они становятся Рутинными аудитами (Routine audtis).

5.23. Рутинные упражнения (Routine Exercises)

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

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

5.24. Рутинные аудиты (Routine audtis)

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

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

5.25. Чексумма (Checksum)

Как определить, что полученное значение некорректно?

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

5.27. Дырявое ведро (Leaky bucket counter)

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

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

Глава 6. Паттерны восстановления от ошибок

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

6.28. Карантин (Quarantine)

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

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

6.29. Интенсивное восстановление (Concentrate recovery)

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

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

6.30. Обработчик ошибок (Error handler)

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

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

Выделяйте код обработки ошибок в специальные блоки. Это проще в поддержке и добавлении новых обработчиков.

6.31. Перезагрузка (Restart)

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

Перезагрузите приложение ¯\_(ツ)_/¯

Холодная перезагрузка (cold restart) — при которой все системы начинают функционировать «с нуля», как будто бы систему только что включили. Подогретая перезагрузка (warm restart) — может пропустить некоторые шаги.

6.32. Откат назад (Rollback)

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

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

6.33. Откат вперёд (Roll‑forward)

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

Если система событийно‑ориентирована, значит, она реагирует на внешние стимулы при работе. В этом случае можно восстановиться в точке после ошибки, где ожидается появление следующего стимула. Считайте все операции до ошибки не выполненными.

6.34. Возврат к опорной точке (Return to Reference Point)

Откуда возобновить работу, если для возникшей ошибки нет соответствующих точек Отката назад и вперёд?

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

6.35. Ограничение повторов (Limit retries)

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

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

6.36. Фейловер (Failover)

Активный элемент содержит ошибку. Как система может продолжить исправно функционировать?

В идеале избыточный элемент должен мгновенно заменить активный, в котором появилась ошибка. Этим должен заниматься Ответственный (Someone in charge). Стратегию нельзя использовать, если избыточные элементы уже делят общую рабочую нагрузку.

6.37. Чекпоинт (Checkpoint)

Незавершённая работа может быть потеряна при восстановлении.

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

6.38. Что сохранять (What to save)

Что должно содержаться в Чекпоинте?

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

6.39. Удалённое хранилище (Remote storage)

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

Храните их в центрально‑доступном хранилище.

6.41. Сброс данных (Data reset)

Что делать, если в данных невоспроизводимая и некорректируемая ошибка?

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

Глава 7. Паттерны снижения ошибок

Эти паттерны рассказывают, как уменьшить негативные эффекты ошибок без изменения приложения или состояния системы.

7.43. Отложенная работа (Deferred work)

Какую работу система может отложить на потом?

Делайте рутинные задачи откладываемыми.

7.44. Переоценка решений о перегрузке (Reassess Overload Decision)

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

Создайте канал обратной связи, который позволит заново определиться с решениями относительно Соотношения отказов (Fault correlation).

7.46. Очередь за ресурсами (Resource queue)

Что делать с запросами к ресурсам, которые не могут быть обработаны прямо сейчас?

Сохраните запросы в очереди. Определите её конечную длину.

7.47. Расширяемый автоматический контроль (Expansive Automatic Controls)

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

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

7.48. Защитный автоматический контроль (Protective Automatic Controls)

Что система может сделать, чтобы не закопаться навсегда в ответах на всё возрастающее количество запросов?

Заранее определите лимиты на обрабатываемые запросы, чтобы защитить способность системы выполнять её основную работу.

7.53. Замедление (Slow down)

Что делать, если появляется такое количество запросов, которое система не может даже потенциально обработать эффективно?

Используйте Эскалацию (Escalation), чтобы использовать заранее определённые лимиты на потребление ресурсов. Каждый следующий шаг более суров и экономичен, чем предыдущие. Цель — замедлить всё настолько, чтобы система была в состоянии справиться с хоть какой‑либо нагрузкой.

7.56. Меченые данные (Marked data)

Что сделать, чтобы предотвратить распространение ошибки, когда система находит данные с ошибкой?

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

Глава 8. Паттерны «замазывания брешей»

После того, как ошибка обработана, необходимо предотвратить её повторное появление.

8.60. Воспроизводимая ошибка

Необходимо скорректировать настоящую ошибку, а не потратить время впустую.

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

8.61. Заплатки (Small patches)

Какой шаблон Обновления программы (Software update) наименее вероятно принесёт с собой новые ошибки?

Используйте маленькие обновления частей. Обновляйте и заменяйте только то, что необходимо.

8.62. Анализ причин (Root cause Analysis)

Что именно чинить? Как именно чинить?

Спрашивайте себя «Почему это произошло» до тех пор, пока не докопаетесь до настоящей причины отказа.

Процесс проектирования отказоустойчивой системы

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

Шаг 1. Определите, что может пойти не так

Чётко‑определённые спецификации помогут выявить и определить, какие ситуации считать отказами.

Шаг 2. Определите, как уменьшить риски

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

Шаг 3. Определите необходимую избыточность

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

Шаг 4. Определите ключевые архитектурные решения

Продумайте, какие именно паттерны могут быть и будут использованы в конкретной реализации.

Шаг 5. Определите возможности уменьшения рисков

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

Шаг 6. Взаимодействие системы с людьми

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

Ссылки по теме

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

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

В первой части мы прочитаем введение и главы 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. Рассмотрим паттерны обнаружения ошибок, восстановления от них, снижения их вероятности и «замазывания брешей».

Ссылки

Что я понял, благодаря неуспехам. Реджинальд Брейдвайт

Эта книга больше не о программировании, а скорее об отношении к работе и индустрии в целом. Что делать, чего не делать, почему так, и к чему приводят неправильные решения.

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

Глава 1. Что я понял, благодаря неуспехам

Разработка проекта может провалиться, если хотя бы одна из составляющих хромает:

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

Люди

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

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

Внешние навязанные ограничения (государственные, например) как правило вредят.

Действие

Получать обратную связь надо быстро — fail fast! Чем раньше узнаете, что у вас проблемы, тем проще найти решение. Работает как в отношении кода, так и продукта в целом.

Детали

Детали и спорные решения в проекте должны быть всегда видны всем. Перестать следить за деталями — начать хоронить проект.

Расписание

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

Сила

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

История

Гипотеза, основанная на действиях из прошлого, может не сработать в будущем.

«Если бы мы больше времени уделили планированию, то спланировали бы лучше; так было в прошлом проекте» — вовсе не факт.

Уметь заканчивать

Уходить из проекта тоже иногда полезно. Игра в героя, который тащит на себе всё, может плохо сказаться как на здоровье «героя», так и на здоровье проекта. Иногда вам не обязательно заканчивать то, что начали вы. Иногда вам не обязательно заканчивать то, что начали другие.

Глава 2. Дизайн софта

Строить лучше

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

Лучшая архитектура

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

Глава 3. Теория, которая подтверждает наблюдения

Теория D, теория P, и почему они важны

Если мы верим в теорию D (deterministic — что процесс можно описать полностью, и если что‑то не сходится, то у нас просто не хватает данных), то мы верим, что проект можно спланировать заранее и полностью.

Если мы верим в теорию P (probabilistic — что предсказать полностью ничего нельзя, а только какие‑то части и только с какой‑то точностью), то мы верим, что планировать следует только какие‑то части проекта, и когда что‑то идёт не так, нам надо добавить в план новую информацию, чтобы скорректировать его.

Вера определяет поведение

Адепты теории D верят, что:

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

Адепты теории P верят, что:

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

Глава 5. Проект‑менеджмент как рынок с информацией

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

Проект похож на рынок, где какая‑то информация ценится там, где её не хватает. Задача менеджеров в том, чтобы уметь отделять ценную информацию (которая поможет предсказать исход проекта или его части) от неценной; уметь «покупать» её у разработчиков и «продавать» её руководству или инвесторам.

Глава 6. Кирпичи

Софт делается не из кирпичей

Аналогия с кирпичами опасна. Кирпичи — слишком простые. Если понятно, как работать с одним кирпичом, понятно, как работать с остальными. В софте не так. Мало того, что там не всегда понятно, как обращаться с «кирпичами», там ещё и непонятно, сколько их надо, чтобы собрать проект.

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

Разработку трудно параллелить

«Кирпичи» из которых состоит софт, связаны друг с другом в каком‑либо виде. Чтобы разрабатывать их параллельно, требуется изначально продумать, как они должны взаимодействовать — что не всегда получается хорошо.

Если добавлять людей в проект, чтобы распараллелить разработку на поздних стадиях (когда какая‑то часть уже написана), то продуктивность даже упадёт.

Вторая беда с аналогией про кирпичи в том, что она позволяет думать, будто можно оценить прогресс по количеству использованных кирпичей. Но софт — трансфинитивен (простите, я не смог перевести иначе).

Важную роль также играет и «куда положить какой кирпич» и «как его соединить с другими». Одна ошибка может откатить отметку прогресса сильно назад.

Третье отличие от кирпичей: с развитием проекта меняются клиентские требования, поэтому прогресс придётся пересчитывать постоянно.

Глава 7. Цикл «попытка‑провал»

Весь смысл этого цикла в том, чтобы создать план, но заранее понимать, что план не выдержит столкновения с реальностью. Поэтому лучше напороться на проблемы раньше (fail fast!).

Обратная связь

Есть ошибка, при которой софт разрабатывается инкрементами вместо итераций.

Инкремент — часть софта, которая сама по себе закончена, но не несёт ценности для бизнеса. Итерация — законченная часть, которая несёт ценность бизнесу.

Чтобы обратная связь работала, это должна быть обратная связь от настоящих пользователей. Единственный способ получать такую обратную связь и получать её быстро — итеративная разработка.

Глава 8. Облысение софта

Технический долг приводит к нерабочему продукту.

Глава 9. Мышиная ловушка

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

Глава 10. Утиное программирование

Это менеджерский антипаттерн; мнение, что всё, что не связано с программистами, языками программирования или инструментами — программированием не является.

Глава 11. Не получается найти хороших продажников

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

Эту книгу дополнят

Некоторые другие книги о программировании:

И вообще:

Потерянная абстракция

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

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

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

Вопрос: что не так с этим условием в конце?

class Reader {
  constructor(user, shelf) {
    this.user = user
    this.shelf = shelf
  }

  getBooksFromShelf = () =>
    this.shelf.getBooks()
}

const reader = new Reader(
  someUserData, 
  someBookShelfData)

// вот тут проблема ↓
if (reader.getBooksFromShelf().contains(book.id)) {
  // ...
}

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

Смысл вместо реализации

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

Но как только мы…

// заменим условие на метод-предикат canRead,
// в который вынесем всю техническую реализацию...
if (reader.canRead(book)) {
  // ...
}

class Reader {
  // ...
  canRead = book => 
    this.getBooksFromShelf().contains(book.id)
}

…код станет гораздо понятнее.

Говорящий метод

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

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

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

В книге “97 Things Every Programmer Should Know” есть глава “Code in the Language of Domain”. Она как раз описывает подобные случаи.

Ссылочки, ссылочки, ссылочки

Ютуб, книга, два конспекта и статья:

От Редакса к хукам

В Реакте в версии 16.7.0 появились Hooks (дальше по тексту — хуки). Это API, которое позволяет использовать локальный стейт без использования классов. И среди них есть один, который, как мне кажется, может заменить собой Редакс.

В этой статье я предполагаю, что вы знаете разницу между функциональными компонентами и классами, в курсе о локальном стейте и жизненном цикле компонентов и о том, как работает Редакс. Без этого вникнуть будет трудно, но всё в одну статью я бы не уместил, так что вот ¯\_(ツ)_/¯

Что за хуки?

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

Например, здесь мы используем хук useState, чтобы создать и использовать переменную counter:

import React, {useState} from 'react'
const SimpleComponent = () => {
  const [counter, setCounter] = useState(0)
  // в первый раз значение counter будет равно тому, что мы передаём в useState
  // затем — тому, что мы установим через setCounter
  return <div>{counter}</div>
}

useState возвращает кортеж из значения и функции, которая будет это значение обновлять.

Из‑за того, что в одном компоненте можно использовать несколько хуков по несколько раз, у них есть ограничения и правила. Подробнее об этом написано в документации и рассказано в докладе на Реакт‑конфе.

useReducer

useReducer — это хук, который по принципу работы схож с редьюсерами из Редакса.

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  // initialState — начальное состояние
  // reducer — функция, которая принимает state и action 
  //           и обрабатывает изменение состояния
  //
  // state — текущее состояние
  // dispatch — функция, которая будет дёргать экшены, 
  //            чтобы обновить состояние

  return <div>Hello world</div>
}

По принципу работы это и есть Редакс. Проблема только в том, что переменные state и dispatch находятся внутри области видимости функции App, а значит использовать этот редьюсер в других компонентах у нас не получится.

…Если только мы не используем контекст.

Context API

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

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

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

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

import {createContext} from 'react'
// создаём контекст
const StoreContext = createContext()

const App = () => (
  // через провайдер в свойстве value указываем значение,
  // которое нам надо хранить и как-то использовать в других компонентах
  <StoreContext.Provider value={{meaningOfLife: 42}}>
    <OtherComponent />
  </StoreContext.Provider>
)

const OtherComponent = () => {
  <StoreContext.Consumer>
    // через консьюмер получаем доступ к значению
    {({meaningOfLife}) => (
      <div>{meaningOfLife}</div>
    )}
  </StoreContext.Consumer>
}

Вызывать консьюмер можно где угодно, и это позволяет делить состояние между компонентами. И тут возникает мысль, нельзя ли заменить Редакс на смесь хуков и контекста. Ну и эт, вроде, можно.

Пример

Я написал простенькое приложение с использованием Редакса и с использованием контекста и хуков. Это счётчик, значение которого можно менять кнопками, либо меняя значение в инпуте, а также обнулять его нажатием на кнопку из другого компонента.

По структуре оно будет состоять из корневого компонента App, компонента формы Form и ещё одного компонента с кнопкой внизу Display. Схематично можно изобразить так:

Компоненты Form и Display зависят от хранилища, в котором содержится состояние приложения. Все события кнопок и инпута вызывают экшены, которые будут обновлять хранилище.

С использованием Редакса

Вначале создадим хранилище и редьюсер, который будет заниматься его обновлением:

// reducers.js
import {combineReducers} from 'redux'

const app = (state, action) => {
  switch(action.type) {
    case 'PLUS':
      return {...state, counter: state.counter + 1}

    case 'MINUS':
      return {...state, counter: state.counter - 1}

    case 'MAGIC':
      return {...state, counter: Math.floor(Math.random() * 100)}

    case 'CHANGE':
      return {...state, counter: +action.value}

    case 'RESET':
      return {...initialState}

    default:
      return state
  }
}

export default combineReducers({ app })

// index.js
import {createStore} from 'redux'
import rootReducer from './reducers'
import App from './App'

// создаём хранилище
const store = createStore(rootReducer)

// используем через провайдер
render(
  <Provider store={store}><App /></Provider>,
  document.getElementById('app'))

Дальше создадим экшены, которые будут вызываться событиями кнопок и инпута:

// actions.js
export const plus = () => ({ type: 'PLUS' })
export const minus = () => ({ type: 'MINUS' })
export const magic = () => ({ type: 'MAGIC' })
export const reset = () => ({ type: 'RESET' })
export const change = e => ({ 
  value: e.target.value,
  type: 'CHANGE', 
})

Чтобы привязать какой‑то компонент к хранилищу, используем connect:

import {connect} from 'react-redux'
import {reset} from './actions'

// app — часть хранилища;
// reset — экшен;
// всё это мы привязали через connect перед экспортом ниже
const Display = ({app, reset}) => {
  const {counter} = app

  return <footer>
    <p>Another component knows that counter equals to {counter} as well!</p>
    <p>
      It even can 
      <button onClick={reset}>reset the coutner</button>
    </p>
  </footer>
}

// мапим свойства из хранилища и экшены
// на пропсы компонента
export default connect(
  state => ({ app: state.app }),
  {reset}
)(Display)

В результате приложение будет работать так.

Контекст + хуки

Теперь напишем то же самое без использования Редакса. Используем useReducer:

// store.js
export const initialState = {counter: 0}

// редьюсер точно такой же, как в прошлый раз 
export const reducer = (state, action) => {
  switch(action.type) {
    // ...
  }
}

// index.js
import {reducer, initialState} from './store'

const App = () => {
  // создаём в корневом компоненте хранилище
  // и функцию для его обновления
  const [state, dispatch] = useReducer(reducer, initialState)
  return <div></div>
}

Чтобы пробросить значения из хранилища компонентам, воспользуемся контекстом:

// context.js
import {createContext} from 'react'
export const StoreContext = createContext()

// index.js
import {reducer, initialState} from './store'
// импортируем созданный контекст
import {StoreContext} from './context'

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  // используем провайдер, чтобы передать в контекст 
  // хранилище и функцию для обновления
  return (
    <StoreContext.Provider value={{dispatch, state}}>
      <Form />
      <Display />
    </StoreContext.Provider>
  )
}

export default App

Чтобы привязать какой‑то компонент, используем консьюмер:

import React from 'react'
// импортируем контекст 
import {StoreContext} from './context'
// экшены точно такие же, как в прошлый раз
import {reset} from './actions'

const Display = () => (
  // получаем доступ к тому, что хранится в контексте
  <StoreContext.Consumer>
    // в нашем случае — state и dispatch
    {({state, dispatch}) => (
      <footer>
        // используем state, чтобы вывести значение счётчика
        <p>{state.counter}</p>
        // используем dispatch, чтобы дёрнуть экшен
        <button onClick={() => dispatch(reset())}>reset</button>
      </footer>
    )}
  </StoreContext.Consumer>
)

export default Display

А ещё можно сделать код чище, заменив консьюмер на useContext:

import React, {useContext} from 'react'
import {StoreContext} from './context'
import {reset} from './actions'

const Display = () => {
  // вызываем useContext, передавая аргументом нужный контекст
  const {state, dispatch} = useContext(StoreContext)

  return (
    // убираем консьюмер
    <footer>
      <p>{state.counter}</p>
      <button onClick={() => dispatch(reset())}>reset</button>
    </footer>
  )
}

export default Display

И работает оно точно так же.

А чо по весу и перформансу?

Я не удивился, когда бандл ужался на 12 кБ: с Редаксом — 166, без него — 154. Это логично, меньше зависимостей — меньше вес.

А вот прирост в скорости обработки экшенов и отрисовки меня слегка удивил. Я проводил измерения с помощью console.time и performance.measure. Средние значения за 100 итераций вышли такими:

 console.timeperformance.measure
Redux12 мс13 мс
Context + hooks9 мс8 мс

Минусы

Вызов экшенов стал чуть более многословным из‑за прямого использования dispatch. И если работать с контекстом без useContext, придётся использовать паттерн render‑prop, из‑за чего может подняться щит‑сторм ¯\_(ツ)_/¯

Но если серьёзно, хуки — пока что в стадии RFC, и возможно многое поменяется. Поэтому переписывать свои приложения на них не советует даже Дэн Абрамов. То есть это экспериментальная фигня.

Хотя выглядит всё равно заманчиво :–)

Ссылки

Документация

Доклады

Статьи со сравнениями

Измерения перформанса

Исходники и примеры

Раньше ↓