Сбои в программных системах неизбежны. То, как обрабатываются эти сбои, может существенно повлиять на производительность системы, надежность и прибыль бизнеса. В этом посте я хочу обсудить положительные стороны неудачи. Почему вам следует стремиться к неудаче, почему неудача — это хорошо и почему избегание неудач может снизить надежность вашего приложения. Мы начнем с обсуждения отказоустойчивости и отказоустойчивости; это приведет нас ко второму обсуждению неудач в целом.
Кстати, если вам нравится содержание этой и других публикаций из этой серии, загляните в мой
Отказоустойчивые системы предназначены для немедленного прекращения работы при возникновении непредвиденной ситуации. Этот немедленный сбой помогает обнаружить ошибки на ранней стадии, что упрощает отладку.
Безотказный подход гарантирует немедленное обнаружение ошибок. Например, в мире языков программирования Java воплощает этот подход, мгновенно создавая NullPointerException
при обнаружении null
значения, останавливая систему и проясняя ошибку. Такая немедленная реакция помогает разработчикам быстро выявлять и устранять проблемы, не допуская их усугубления.
Заблаговременно обнаруживая и останавливая ошибки, отказоустойчивые системы снижают риск каскадных сбоев, когда одна ошибка приводит к другим. Это упрощает сдерживание и решение проблем до того, как они распространятся по системе, сохраняя общую стабильность.
Легко писать модульные и интеграционные тесты для отказоустойчивых систем. Это преимущество становится еще более выраженным, когда нам нужно понять, почему тест не прошел. Отказоустойчивые системы обычно указывают непосредственно на проблему в трассировке стека ошибок.
Однако отказоустойчивые системы несут в себе свои риски, особенно в производственных средах:
В отказоустойчивых системах используется другой подход, направленный на восстановление и продолжение работы даже в непредвиденных условиях. Это делает их особенно подходящими для неопределенных или нестабильных сред.
Микросервисы являются ярким примером отказоустойчивых систем, обеспечивающих отказоустойчивость благодаря своей архитектуре. Автоматические выключатели, как физические, так и программные, отключают неисправные функции, чтобы предотвратить каскадные сбои, помогая системе продолжать работу.
Отказоустойчивые системы гарантируют, что системы смогут выдержать даже суровые производственные условия, снижая риск катастрофического сбоя. Это делает их особенно подходящими для критически важных приложений, например, в аппаратных устройствах или аэрокосмических системах, где плавное восстановление после ошибок имеет решающее значение.
Однако у отказоустойчивых систем есть и недостатки:
Трудно определить, какой подход лучше, поскольку оба имеют свои преимущества. Отказоустойчивые системы обеспечивают немедленную отладку, меньший риск каскадных сбоев, а также более быстрое обнаружение и устранение ошибок. Это помогает выявить и устранить проблемы на ранней стадии, предотвращая их распространение.
Отказоустойчивые системы корректно обрабатывают ошибки, что делает их более подходящими для критически важных систем и нестабильных сред, где катастрофические сбои могут иметь разрушительные последствия.
Чтобы использовать сильные стороны каждого подхода, эффективной может быть сбалансированная стратегия:
Сбалансированный подход также требует четкого и последовательного внедрения процессов кодирования, проверки, инструментов и тестирования, гарантируя его плавную интеграцию. Fail-fast может хорошо интегрироваться с оркестровкой и наблюдаемостью. По сути, это переносит аспект отказоустойчивости на другой уровень OPS, а не на уровень разработчика.
Здесь все становится интереснее. Речь не идет о выборе между отказоустойчивостью и отказоустойчивостью. Речь идет о выборе правильного слоя для них. Например, если ошибка обрабатывается на глубоком уровне с использованием отказоустойчивого подхода, она не будет замечена. Это может быть нормально, но если эта ошибка окажет негативное влияние (производительность, мусорные данные, повреждение, безопасность и т. д.), тогда у нас возникнет проблема позже, и мы не будем иметь ни малейшего понятия.
Правильное решение — обрабатывать все ошибки на одном уровне. В современных системах верхний уровень — это уровень OPS, и это имеет наибольший смысл. Он может сообщить об ошибке инженерам, которые наиболее квалифицированы для ее устранения. Но они также могут обеспечить немедленное смягчение последствий, например перезапуск службы, выделение дополнительных ресурсов или возврат версии.
Недавно я был на лекции, где спикеры перечисляли свою обновлённую облачную архитектуру. Они решили использовать ярлык для доступа к микросервисам, используя структуру, позволяющую повторить попытку в случае сбоя. К сожалению, неудача ведет себя не так, как нам хотелось бы. Вы не можете полностью устранить его только с помощью тестирования. Повторная попытка не является безопасной. На самом деле это может означать катастрофу.
Они протестировали свою систему, и «она работает» даже в производстве. Но давайте предположим, что катастрофическая ситуация все же произойдет, их механизм повторной попытки может работать как атака типа «отказ в обслуживании» на их собственные серверы. Количество причин, по которым подобные специальные архитектуры могут потерпеть неудачу, просто ошеломляет.
Это особенно важно, когда мы переосмысливаем неудачи.
Сбои в программных системах связаны не только со сбоями. Аварию можно рассматривать как простой и немедленный сбой, но есть и более сложные проблемы, которые следует учитывать. На самом деле, сбои в эпоху контейнеров — это, пожалуй, самые лучшие сбои. Система перезагружается плавно, практически без перерыва.
Повреждение данных гораздо более серьезное и коварное явление, чем сбой. Это влечет за собой долгосрочные последствия. Поврежденные данные могут привести к проблемам с безопасностью и надежностью, которые сложно устранить, требуя обширной переработки и потенциально не подлежащих восстановлению данных.
Облачные вычисления привели к появлению методов защитного программирования, таких как автоматические выключатели и повторные попытки, в которых особое внимание уделяется всестороннему тестированию и ведению журналов для обнаружения и корректного устранения сбоев. В каком-то смысле эта среда отправила нас назад с точки зрения качества.
Безотказная система на уровне данных могла бы предотвратить это. Устранение ошибки выходит за рамки простого исправления. Это требует понимания основной причины и предотвращения повторения, включая комплексное ведение журнала, тестирование и улучшение процессов. Это гарантирует полное устранение ошибки и снижает вероятность ее повторения.
Если это ошибка в производстве, вам, вероятно, следует вернуться, если вы не можете немедленно отменить производство. Это всегда должно быть возможно, а если нет, то вам следует над этим поработать.
Прежде чем приступить к исправлению, необходимо полностью осознать неисправности. В своих компаниях я часто пропускал этот шаг из-за давления, а в маленьком стартапе это простительно. В более крупных компаниях нам необходимо понять основную причину. Культура разбора ошибок и производственных проблем имеет важное значение. Исправление также должно включать в себя смягчение последствий процесса, предотвращающее попадание подобных проблем в рабочую среду.
Отказоустойчивые системы гораздо легче отлаживать. Они имеют более простую архитектуру, и в них легче выявить проблему в конкретной области. Крайне важно генерировать исключения даже для незначительных нарушений (например, проверок). Это предотвращает каскадные типы ошибок, которые преобладают в свободных системах.
Это должно быть дополнительно усилено модульными тестами, которые проверяют определяемые нами ограничения и выбрасывают правильные исключения. В коде следует избегать повторных попыток, поскольку они чрезвычайно затрудняют отладку, и их правильное место — на уровне OPS. Чтобы облегчить эту задачу, тайм-ауты по умолчанию должны быть короткими.
Неудача — это не то, чего мы можем избежать, предсказать или полностью протестировать. Единственное, что мы можем сделать, — это смягчить удар в случае сбоя. Часто такое «смягчение» достигается за счет использования длительных тестов, призванных максимально воспроизвести экстремальные условия с целью обнаружения слабых мест наших приложений. Этого редко бывает достаточно. Надежным системам часто приходится пересматривать эти тесты на основе реальных производственных сбоев.
Отличным примером отказоустойчивости может быть кэш ответов REST, который позволяет нам продолжать работу, даже когда служба не работает. К сожалению, это может привести к сложным нишевым проблемам, таким как отравление кеша или ситуации, в которой забаненный пользователь все еще имел доступ из-за кеша.
Отказоустойчивость лучше всего применять только в производстве/постановке и на уровне OPS. Это уменьшает количество изменений между производством и разработкой. Мы хотим, чтобы они были как можно более похожими, но это все равно изменение, которое может негативно повлиять на производство. Однако преимущества огромны, поскольку возможность наблюдения позволяет получить четкое представление о сбоях системы.
Обсуждение здесь немного окрашено моим недавним опытом создания наблюдаемых облачных архитектур. Однако тот же принцип применим к любому типу программного обеспечения, будь то встроенное или облачное. В таких случаях мы часто предпочитаем реализовать отказоустойчивость в коде, в этом случае я бы предложил реализовать ее последовательно и осознанно на определенном уровне.
Существует также особый случай библиотек/фреймворков, которые часто обеспечивают непоследовательное и плохо документированное поведение в таких ситуациях. Я сам грешу на такую непоследовательность в некоторых своих работах. Это легко сделать ошибку.
Это мой последний пост по теории отладки, который является частью моей книги/курса по отладке. Мы часто думаем об отладке как о действии, которое мы предпринимаем в случае сбоя. Это не так. Отладка начинается в тот момент, когда мы пишем первую строку кода. Мы принимаем решения, которые повлияют на процесс отладки во время написания кода, часто мы просто не осознаем этих решений, пока не получим ошибку.
Я надеюсь, что этот пост и серия помогут вам написать код, подготовленный к неизвестному. Отладка по своей природе связана с неожиданностями. Тесты не могут помочь. Но, как я показал в своих предыдущих постах, мы можем предпринять множество простых практик, которые облегчат подготовку. Это не разовый процесс, это повторяющийся процесс, требующий переоценки принятых решений по мере того, как мы сталкиваемся с неудачей.