2.20 Обработка ошибок и паттерны устойчивости
Обзор и мотивация
Каждая написанная вами программа будет отказывать. Диск заполняется, сеть падает, сервис выходит по тайм-ауту, вызывающий передаёт мусор, зависимость возвращает то, о чём документация не упоминала. Вопрос никогда не в том, случится ли отказ; он в том, встретит ли ваш код этот отказ с планом или с сюрпризом. Обработка ошибок это ремесло решения, строка за строкой и функция за функцией, что делает ваш код, когда мир не сотрудничает. Это наименее эффектная часть конструирования и та, что решает, больше любой функции, доверяют ли люди вашей системе.
Эта глава об устойчивости на уровне кода и компонентов: выборах внутри функции, модуля или API. Она дополняет главу 3.5, рассматривающую устойчивость на уровне системы (балансировка нагрузки, репликация, переключение между сервисами при отказе). Глава 3.5 держит всю платформу на ногах, когда регион гаснет; эта глава не даёт одному запросу испортить ваши данные или исчезнуть без следа. Они подкрепляют друг друга. Предохранитель в вашей архитектуре мало значит, если код за ним проглатывает исключения, а защитная функция не спасёт вас, если окружающей системе недостаёт избыточности. Эта глава также опирается на главу 2.9 (конструирование программного обеспечения), где обработка ошибок была одной дисциплиной среди многих; здесь она становится всей темой.
Для больших команд приз это согласованность. Когда сотни инженеров обрабатывают ошибки сотнями разных способов, каждый сервис становится головоломкой, а каждый инцидент раскопками. В корпоративной среде эта несогласованность повышает стоимость каждого аудита и каждой интеграции. В государственных и других высокорисковых системах ставки острее: корректность, безопасный отказ и ясный след аудита не функции, добавляемые потом, а свойства, которыми система должна обладать с первого коммита. Система пособий, молча неверно считающая, или система записей, теряющая отказ без журналирования, не просто содержит ошибки. Она ненадёжна так, что подрывает институт за ней.
Ключевые принципы
- Различайте ошибки, неисправности и отказы и обрабатывайте каждое на правильном слое.
- Выбирайте немедленный отказ или безопасный отказ намеренно, по контексту, никогда случайно.
- Делайте контракт обработки ошибок каждой функции и API явным и честным.
- Проверяйте на границах; доверяйте внутри них; защищайтесь без паранойи.
- Никогда молча не проглатывайте ошибку; выносите её, оборачивайте или обрабатывайте намеренно.
- Делайте повторы безопасными идемпотентностью, тайм-аутами, отступлением и дрожанием.
- Уделяйте пути ошибки то же проектное внимание, что и счастливому пути.
Рекомендации
Различайте ошибки, неисправности и отказы
Небрежный словарь порождает небрежную обработку, поэтому начните с ясных терминов. Неисправность (fault) это изъян системы: ошибка в коде, плохая конфигурация, зависимость, которая лежит. Ошибка (error) это неверное внутреннее состояние, которое порождает неисправность: null там, где должно быть значение, баланс, который больше не сходится. Отказ (failure) это то, что видит внешний наблюдатель: запрос возвращает неверный ответ или не возвращает ответа. Одна неисправность может вызвать много ошибок, а многие ошибки могут быть пойманы до того, как какая-либо станет видимым отказом. Весь смысл обработки ошибок в том, чтобы разорвать эту цепочку, поймать ошибку до того, как она станет отказом, который переживёт пользователь или аудитор.
Этот словарь также говорит, где действовать. Неисправности устраняются в ревью, тестировании и конфигурации. Ошибки обрабатываются во время выполнения паттернами этой главы. Отказы устраняются наблюдаемостью (глава 9.2) и системной устойчивостью главы 3.5. Когда ваша команда разделяет эти слова, разборы инцидентов становятся острее: вы можете точно сказать, где цепочку следовало разорвать и не разорвали, а не спорить о том, что такое «баг».
Выбирайте немедленный или безопасный отказ по контексту
Немедленный отказ (fail-fast) означает остановку в момент, когда что-то неверно, отказ продолжать на плохом состоянии, чтобы проблема проявилась громко и близко к причине. Безопасный отказ (fail-safe) означает деградацию в известное безвредное состояние и продолжение обслуживания того, что можно безопасно. Ни один не универсально верен, и навык в выборе по контексту. Во время разработки и на внутренних границах немедленный отказ ваш друг: программа, останавливающаяся при нарушенном инварианте, даёт короткую трассировку стека вместо долгой загадки. В эксплуатации, на краях системы, обращённой к пользователям, часто побеждает безопасный отказ: панель рекомендаций, ничего не возвращающая, лучше страницы оформления заказа, которая не загружается.
Решайте это намеренно для каждой границы и записывайте решение. Компонент управления полётом или медицинского устройства отказывает безопасно в определённое состояние, потому что продолжение на испорченных данных могло бы навредить кому-то. Проводка в главной книге отказывает немедленно, потому что проводка неверной записи хуже её отсутствия. Неверная пара опасна в обоих направлениях: безопасный отказ там, где нужен был немедленный, скрывает порчу, а немедленный там, где нужен был безопасный, превращает косметическую заминку в сбой.
Выберите механизм сигнализации ошибок и используйте его последовательно
Языки дают два широких способа сообщить, что что-то пошло не так. Обработка исключений выбрасывает объект вверх по стеку вызовов, пока какой-то обработчик не поймает его, отделяя путь ошибки от основной логики. Альтернатива явные значения ошибок: функция возвращает и результат, и ошибку, а вызывающий должен проверить оба. Многие современные языки формализуют второе с помощью типа Result, часто называемого Result или Either, который вынуждает вызывающего развернуть либо успех, либо отказ перед использованием значения. У каждого подхода есть цена. Исключения держат счастливый путь чистым, но могут скрывать поток управления и искушать разработчиков блоками «поймать всё», стирающими информацию. Явные результаты делают каждый отказ видимым в сигнатуре типа, но добавляют церемонии и могут быть проигнорированы, если язык не принуждает к проверке.
Правильный ответ скорее не в механизме, а в согласованности и честности. Выберите идиому, которой отдают предпочтение ваш язык и экосистема, и применяйте её единообразно по сервисам, чтобы читатель всегда знал, как путешествует отказ. Резервируйте исключения для действительно исключительных условий, а не для обычного потока управления вроде «пользователь не найден», что лучше моделировать как обычный результат. Что бы вы ни выбрали, никогда не позволяйте отказу стать невидимым: непроверенное значение ошибки так же опасно, как пустой блок catch. В большой кодовой базе письменное соглашение плюс линтер, помечающий проигнорированные ошибки, побеждают предпочтение любого отдельного человека.
Сделайте контракт обработки ошибок явным
У каждой функции и каждого API есть контракт обработки ошибок, записал его кто-нибудь или нет. Он отвечает: что здесь может пойти не так, как вы об этом узнаете и что гарантируется о состоянии, когда это случится? Сделайте этот контракт явным. Документируйте, какие ошибки функция может вернуть или выбросить, различайте восстановимые ошибки (вызывающий может разумно повторить или откатиться) и невосстановимые (вызывающий не может этого исправить и должен пробросить или прервать) и указывайте, оставляет ли функция состояние неизменным при отказе. Это последнее свойство, иногда называемое строгой гарантией исключений, означает, что неудавшийся вызов как будто никогда не происходил, а это именно то, что позволяет вызывающему безопасно повторять.
Для публичного или межкомандного API этот контракт часть интерфейса, так же реальная, как типы параметров. Проектируйте небольшую стабильную таксономию ошибок: ограниченный набор категорий вроде ошибки валидации, не найдено, конфликт, не авторизован, зависимость недоступна и внутренняя ошибка. Тогда вызывающие могут ветвиться по категории, не разбирая строки. Ясная таксономия делает обработку ошибок компонуемой между многими сервисами и делает отказы проверяемыми, потому что каждый отказ отображается на известный названный вид.
Проверяйте на границах и защищайтесь без паранойи
Воспринимайте данные, пересекающие границу доверия (сетевой запрос, файл, пользовательский ввод, сообщение от другого сервиса), как враждебные, пока они не проверены, и проверяйте их на границе, один раз, тщательно. Это защитное программирование, применённое с суждением. Внутри модуля, чьи входные данные вы уже проверили, избыточные проверки на каждой строке скрывают логику и подавляют те самые отказы, которые вы хотели бы видеть. Дисциплина такова: защищайтесь жёстко на краях, доверяйте внутри них. Проверяйте структуру, диапазоны и инварианты там, где данные входят, преобразуйте их в типы, делающие недопустимые состояния непредставимыми, и позвольте внутреннему коду предполагать, что он работает с чистыми данными.
У паранойи есть реальная цена. Код, заваленный проверками на null и защитными ветками, труднее читать и, что хуже, часто превращает ясный отказ в молчаливое пожимание плечами, возвращая значение по умолчанию там, где следовало поднять тревогу. Защитность, маскирующая ошибки, не безопасность; это отсрочка.
Делайте повторы безопасными, ограниченными и вежливыми
Многие неисправности преходящи: мгновенный сбой сети, перезапускающийся сервис, краткое состязание за блокировку. В любой распределённой системе (глава 3.3) эти частичные отказы норма, а не исключение. Повтор естественная реакция, но наивный цикл повторов это заряженное ружьё. Во-первых, сделайте повторяемую операцию идемпотентной, то есть выполнение её дважды имеет тот же эффект, что и однажды. Без идемпотентности повтор после тайм-аута может списать деньги с карты дважды или создать две записи, потому что вы не можете сказать, отказала ли первая попытка или потерялось лишь её подтверждение. Используйте ключи идемпотентности для записей, чтобы получатель мог распознать и устранить повтор.
Во-вторых, ставьте тайм-аут на каждый удалённый вызов, чтобы зависшая зависимость не могла подвесить вас. В-третьих, разносите повторы экспоненциальным отступлением, удваивая ожидание после каждой попытки, и добавляйте дрожание (небольшую случайную задержку), чтобы тысяча клиентов, восстанавливающихся одновременно, не синхронизировалась в лавину, снова валящую восстанавливающийся сервис. В-четвёртых, ограничивайте число повторов и общее время, затем изящно сдавайтесь. Повторы без пределов, отступления, дрожания и идемпотентности один из самых частых способов, которым мелкая заминка становится самонанесённым сбоем.
Добавляйте предохранители, переборки и плавную деградацию в код
Когда зависимость действительно лежит, повторы лишь тратят усилия и углубляют яму. Предохранитель (circuit breaker) наблюдает долю отказов вызовов к зависимости и, как только отказы пересекают порог, «размыкается», чтобы отказывать немедленно в течение периода остывания, а не ждать обречённые вызовы. После остывания он пропускает пробный вызов и снова замыкается, если зависимость восстановилась. Это защищает и ваших вызывающих (быстрые предсказуемые отказы вместо накопившихся тайм-аутов), и бьющуюся зависимость (передышка для восстановления). Шаблон переборок, названный по водонепроницаемым отсекам корабля, изолирует ресурсы так, чтобы одна насыщенная зависимость не могла съесть все потоки или соединения и потопить весь процесс; вы даёте каждой зависимости собственный ограниченный пул.
Эти шаблоны сочетаются с плавной деградацией на уровне кода: когда несущественная зависимость недоступна, возвращайте урезанный, но полезный результат, а не ошибку. Показывайте кэшированные данные с пометкой об устаревании, скрывайте панель персонализации, ставьте запись в очередь на потом. Это локальное дополнение к системной устойчивости главы 3.5: архитектура даёт избыточность между машинами, а ваш код даёт разумное поведение, когда части не хватает.
Оборачивайте ошибки контекстом и никогда не проглатывайте их
Ошибка, читающаяся как «соединение отклонено» на десять слоёв выше места, где она произошла, почти бесполезна. По мере распространения ошибки оборачивайте её контекстом: что вы пытались сделать, какая сущность или запрос, какая зависимость, сохраняя исходную причину, чтобы корень не потерялся. Хорошие языки и библиотеки поддерживают такое цепочечное связывание ошибок напрямую. Цель в том, чтобы одна строка журнала говорила дежурному инженеру, что отказало, во время какой операции и для какого ввода. Это сырьё для наблюдаемости главы 9.2 и отладки главы 2.15.
Смертный грех это проглатывание ошибки: пустой блок catch, проигнорированное возвращаемое значение, catch, журналирующий на уровне debug и продолжающий, будто ничего не случилось. Проглоченная ошибка не исчезает; она позже вновь появляется как испорченные данные или необъяснимый дефект, теперь оторванный от причины. Каждая ошибка должна встретить одну из трёх судеб: быть обработанной (восстановление или деградация), быть обёрнутой и проброшенной или, на вершине стека, быть записанной в журнал с полным контекстом и привести к отказу. Если вы поймали ошибку и не сделали ничего из этого, вы выбрали скрыть будущий инцидент от своего будущего «я».
Компромиссы: плюсы и минусы
| Подход | Плюсы | Минусы |
|---|---|---|
| Исключения | Чистый счастливый путь; трудно игнорировать непроверяемые | Скрытый поток управления; соблазн стирания через «поймать всё» |
| Явные значения ошибок / типы Result | Отказ виден в сигнатуре; принуждает к обработке | Больше церемонии; можно проигнорировать без принуждения |
| Немедленный отказ | Громко выносит ошибки близко к причине | Плохой пользовательский опыт на краю |
| Безопасный отказ | Продолжает обслуживание; защищает пользователей и данные | Может маскировать порчу там, где нужен был немедленный |
| Повторы с отступлением | Автоматически переживают преходящие неисправности | Усиливают нагрузку и двойные записи без идемпотентности |
| Предохранитель | Быстрые отказы; даёт зависимостям восстановиться | Дополнительное состояние и настройка; может маскировать постоянную проблему |
| Защитная проверка на границах | Рано, один раз, громко ловит плохие данные | При избытке загромождает логику и скрывает реальные отказы |
Основное напряжение между видимостью и шумом. Обрабатывайте ошибки слишком тихо, и вы скроете проблемы, пока они не станут дорогими; слишком громко и везде, и вы утопите сигнал в церемонии и замаскируете важные отказы. Разрешайте по месту и замыслу. Будьте громкими и строгими на границах, где входят плохие данные и отказы зависимостей. Будьте тихими и доверяющими внутри, где входные данные уже чисты. Решайте немедленный или безопасный отказ по границе и записывайте это. Цель в коде, где у каждого отказа ровно один ясный владелец и одна ясная судьба, и ничто не проваливается молча в щели.
Вопросы для обсуждения с командой
Есть ли у нас единая общая таксономия ошибок и соглашение об обработке по сервисам или каждая команда импровизирует? В большой команде это разница между отказами, которые компонуются, и отказами, которые сбивают с толку. Когда один сервис возвращает HTTP 500 при проблеме валидации, другой выбрасывает типизированное исключение, а третий возвращает null, каждая интеграция становится переговорами, а каждый инцидент упражнением в переводе. Принесите примеры одного и того же логического отказа, скажем «запись не найдена», в трёх ваших сервисах и посмотрите, насколько по-разному они его сигнализируют. Ответ должен стать письменным стандартом: ограниченный набор категорий ошибок, единый способ их сигнализации и линтер или контрольный список ревью, их обеспечивающий. Согласованность здесь окупается в каждой будущей интеграции, аудите и смене дежурства.
Для каждой критической границы выбрали ли мы немедленный или безопасный отказ намеренно и соответствует ли код этому выбору? Большинство команд никогда не принимали это решение явно, а значит, его принял за них тот, кто первым написал код, и непоследовательно. Противоположные соображения реальны: безопасный отказ держит пользователей обслуженными, но может позволить порче распространиться, а немедленный защищает данные, но может превратить мелкий сбой зависимости в видимый отказ. Принесите историю инцидентов и спросите для нескольких худших, отказал ли код так, как вы выбрали бы, если бы вас спросили заранее. Свидетельство, которое вам нужно, это карта ваших границ с намеренной меткой на каждой, особенно везде, где замешаны деньги, безопасность или записи граждан. Где метка и код расходятся, вы нашли своё следующее исправление.
Когда мы в последний раз намеренно задействовали путь ошибки и вёл ли он себя как задумано? Путь ошибки обычно наименее протестированный код, которым вы владеете, однако именно в нём доверие выигрывается или проигрывается, а «мы отказываем безопасно» утверждение, которое нельзя подтвердить, если вы никогда не видели, как это происходит. Цикл повторов без идемпотентности, предохранитель с неверным порогом, проглоченное исключение в редко достигаемой ветке: всё это прячется, пока настоящий инцидент не найдёт их за вас. Принесите результаты намеренного внедрения отказа (убитая зависимость, вызванный тайм-аут, искажённая нагрузка) в реалистичную среду. Последующее действие состоит в том, чтобы сделать внедрение отказов рутиной, так что восстановление, деградация и поведение безопасного отказа проверяются непрерывно, а не на надежде. Любой путь ошибки, который вы никогда не вызывали, это обещание, которое вы не проверили.
Какие из наших операций записи идемпотентны и где повтор после потерянного подтверждения продублировал бы реальный эффект вроде платежа или записи? Повтор самый частый рефлекс устойчивости и, при небрежном исполнении, самый частый способ, которым преходящий сбой превращается в продублированные деньги или данные. В большой команде логика повторов часто живёт одновременно в общих клиентах, промежуточном слое и отдельных сервисах, так что одна запись может повторяться на нескольких слоях, и никто не владеет совокупным поведением. Противоположное тяготение в том, что ключи идемпотентности, устранение дубликатов и сохранённые исходы запросов добавляют хранилище и код, а команды под давлением поставки пропускают их для записей, которые ошибочно считают безопасными. Принесите опись ваших внешне видимых записей, у каждой пометку, несёт ли она ключ идемпотентности и как получатель распознаёт и устраняет повтор. В корпоративной и государственной среде отметьте первыми те, что двигают деньги или изменяют запись гражданина, потому что двойной платёж или продублированное пособие замечание аудита, а иногда юридический риск, а не просто дефект.
Происходят ли наши тайм-ауты, предохранители и переборки из одной общей протестированной библиотеки или каждая команда делает их вручную? Эти шаблоны легко описать и легко сделать тонко неверно: отсутствующий тайм-аут, порог предохранителя, который никогда не срабатывает, пул соединений, рассчитанный так, что одна медленная зависимость морит голодом весь процесс. Когда каждая команда реализует их заново, вы накапливаете много слегка сломанных копий и ни одного места, где можно исправить изъян один раз, найдя его. Противоположное соображение в том, что общая библиотека навязывает общий интерфейс и ритм обновлений, а команды с необычными средами выполнения или потребностями задержки могут возражать или обходить её. Принесите обзор того, сколько различных реализаций повторов и предохранителей реально работает в эксплуатации и у каких сервисов по-прежнему нет тайм-аута на исходящих вызовах вовсе. Для большой корпорации или агентства проверенная общая библиотека также даёт рецензентам безопасности и аудиторам один компонент для сертификации вместо десятков, что снижает стоимость каждой проверки.
Если бы инцидент случился прошлой ночью, мог бы любой дежурный инженер проследить его по одной строке журнала и мог бы аудитор позже увидеть каждый отказ, записанный системой? Обёрнутая, категоризированная, хорошо журналируемая ошибка это разница между десятиминутной диагностикой и полуночными раскопками, а проглоченная будущий инцидент, который вы скрыли от самих себя. В большой команде отказы пересекают много переходов между сервисами, поэтому ценность приходит от согласованного контекста и корреляционных идентификаторов, переживающих эти переходы, а не от усердия любой одной команды. Противоположное напряжение это стоимость и шум: журналируйте всё, и вы утопите сигнал и заплатите за хранение; слишком мало, и вы не сможете восстановить, что произошло. Принесите реальный недавний отказ и пройдите его след от начала до конца, отмечая каждый переход, где контекст терялся или ошибка ловилась и отбрасывалась. В регулируемых и государственных системах воспринимайте это как свойство соответствия, потому что непроверяемый отказ или решение, которое нельзя объяснить годы спустя, юридический риск, а не просто операционный пробел.
Взгляд по секторам
Стартап. С горсткой инженеров и без запаса времени тратьте бюджет на обработку ошибок там, где отказ стоит вам клиента или ваших данных: ставьте тайм-аут на каждый исходящий вызов, делайте записи, двигающие деньги, идемпотентными и добавьте правило линтера против проигнорированных ошибок. Пропустите сложный фреймворк; тип Result для основных функций и плавная деградация для некритичных зависимостей покупают большую часть безопасности за несколько дней работы. Отказывайте немедленно в разработке, чтобы ошибки проявлялись громко, и сопротивляйтесь самостоятельному написанию предохранителя, пока у вас нет зависимости, которая его оправдывает.
Малый бизнес. Без специалиста по устойчивости в штате и при ограниченном бюджете опирайтесь на то, что уже дают ваш язык, фреймворк и облачный провайдер, а не стройте шаблоны с нуля: управляемые очереди, повторы на стороне провайдера и тайм-ауты библиотек покрывают больше, чем ожидает большинство команд. Формулируйте решение как «купить или построить» и покупайте везде, где зрелая зависимость берёт на себя повторы, отступление и идемпотентность. Сосредоточьте скудное внимание на одной-двух границах, где неверная или потерянная транзакция действительно навредит, и убедитесь, что они отказывают безопасно и оставляют след.
Крупная организация. Между многими командами приз согласованность: единая таксономия ошибок, общая библиотека тайм-аутов, повторов, предохранителей и переборок и линтер и контрольный список ревью, обеспечивающие их в конвейере. Направляйте каждую ошибку в единую платформу наблюдаемости с корреляционными идентификаторами, чтобы отказ можно было проследить через переходы между сервисами, и стандартизируйте решения «немедленный или безопасный отказ» по границам, чтобы аудиты находили документированный обоснованный шаблон, а не россыпь местных привычек. Управляйте общей библиотекой как настоящим продуктом, потому что изъян, исправленный там один раз, исправлен везде.
Государственный сектор. Корректность, безопасный отказ и долговечный след аудита обязательства, а не предпочтения. Отказывайте немедленно при любом нарушенном инварианте, касающемся денег или права, проверяйте каждый ввод, обращённый к гражданам, на границе и записывайте каждый отказ в неизменяемый журнал с достаточным контекстом, чтобы решение можно было объяснить и пересмотреть годы спустя. Закупки и долгие сроки жизни систем означают, что контракты ошибок должны быть задокументированы, чтобы гражданские служащие могли сопровождать код задолго после ухода первоначальных авторов, а любой компонент поставщика должен раскрывать своё поведение при отказах, а не прятать за непрозрачным интерфейсом.
Примеры
Стартап. Стартап из четырёх человек выпускает приложение, вызывающее стороннего платёжного провайдера и почтовый сервис. Рано они добавляют наивный цикл повторов и тут же дважды списывают деньги с клиента, когда тайм-аут маскирует успешное списание. Исправление учит уроку: они добавляют ключи идемпотентности ко всем записям, ставят тайм-аут на каждый исходящий вызов и переходят на экспоненциальное отступление с дрожанием. Они принимают тип Result для основных функций сервиса, чтобы отказ показывался в сигнатуре, а правило линтера помечает любую проигнорированную ошибку. Когда отправка почты отказывает, оформление заказа деградирует плавно, ставя сообщение в очередь, а не блокируя продажу. Дисциплина стоит несколько дней и избавляет от класса инцидентов, обошедшихся бы гораздо дороже возвратами и доверием.
Крупная организация. Международная логистическая компания ведёт сотни сервисов и стандартизирует обработку ошибок во всех. Каждый сервис отображает отказы на общую таксономию (валидация, не найдено, конфликт, зависимость недоступна, внутренняя), так что вызывающие ветвятся по категории, а не разбирают сообщения. Общая библиотека предоставляет предохранители, ограниченные повторы с отступлением и дрожанием и пулы соединений с переборками, поэтому никто не делает эти шаблоны вручную неверно. Каждая ошибка журналируется с корреляционным контекстом, питающим платформу наблюдаемости главы 9.2, так что дежурный инженер может проследить отказ через переходы между сервисами по одной строке. Поскольку стандарт единообразен и обеспечивается в конвейере, инженеры уверенно переходят между незнакомыми сервисами, а аудиторы видят, что каждый отказ записан, категоризирован и прослеживаем.
Государственный сектор. Национальное агентство пособий строит систему определения права и выплат, где неверный ответ может отказать человеку в арендных деньгах или переплатить из казны. Корректность и безопасный отказ не обсуждаются, поэтому код отказывает немедленно при любом нарушенном финансовом инварианте: расчёт, который не сходится, отказывается проводиться, а не проводит неверную цифру. Каждый ввод, обращённый к гражданам, проверяется на границе, а недопустимые состояния сделаны непредставимыми в типах предметной области. Каждый отказ записывается в неизменяемый журнал аудита с полным контекстом, удовлетворяя юридическому требованию, чтобы решения можно было объяснить и пересмотреть годы спустя. Где несущественная зависимость вроде предпросмотра документов лежит, система плавно деградирует, чтобы сотрудник всё равно мог обработать заявление. Новые гражданские служащие наследуют код с документированными контрактами ошибок и могут безопасно его сопровождать задолго после ухода первоначальных авторов.
Бизнес-обоснование: мотивы, ROI и TCO
Отдача от дисциплинированной обработки ошибок проявляется как меньше инцидентов, более короткие инциденты и более дешёвые. Большинство сбоев в эксплуатации не экзотичны; они восходят к проглоченному исключению, отсутствующему тайм-ауту, лавине повторов или границе, доверившей данным, которые следовало проверить. Каждый из них предотвратим описанными здесь шаблонами, а каждый предотвращённый инцидент экономит не только прямую стоимость простоя, но и нарастающие расходы на аварийное реагирование, отток клиентов и расследование. Поскольку обёрнутую, хорошо журналируемую ошибку можно диагностировать за минуты, а не часы, среднее время восстановления падает, а вместе с ним и доля неудачных изменений, когда инженеры перестают бояться пути ошибки.
Стоимость внедрения скромна и в основном разовая. Вы записываете таксономию ошибок, предоставляете общую библиотеку для повторов и предохранителей, чтобы команды не изобретали их плохо, добавляете правила линтера против проигнорированных ошибок и вырабатываете привычку внедрять отказы. Стоимость пренебрежения нарастает молча: проглоченные ошибки оседают в испорченные данные, дорогие для расплетения, а несогласованная обработка умножает стоимость каждой интеграции и каждого аудита. В регулируемой и государственной среде непроверяемый отказ риск соответствия и юридический риск, а не просто инженерная проблема. Чтобы обосновать руководству, свяжите дисциплину обработки ошибок с метриками, за которыми оно уже следит: частота инцидентов, среднее время восстановления, доля неудачных изменений и замечания аудита.
Антипаттерны и ловушки
- Молчаливое проглатывание: пустые блоки catch и проигнорированные возвращаемые значения, превращающие отказ в отложенную оторванную загадку.
- Стирание через «поймать всё»: широкий
catch, журналирующий общее сообщение и отбрасывающий исходную ошибку и её контекст. - Повтор без идемпотентности: повторное выполнение неидемпотентных записей после тайм-аута, двойное списание или дублирование записей.
- Лавины повторов: нет отступления, нет дрожания и нет предела, поэтому клиенты синхронизируются и вновь валят восстанавливающуюся зависимость.
- Нет тайм-аутов: неограниченные удалённые вызовы, позволяющие одной зависшей зависимости исчерпать потоки и заморозить весь процесс.
- Исключения как поток управления: выбрасывание и ловля для обычных исходов вроде «не найдено», скрывающие логику и замедляющие код.
- Защитная паранойя: проверки на каждой строке, хоронящие логику и превращающие реальные отказы в молчаливые значения по умолчанию.
- Ошибки в виде строк: вызывающие разбирают текст сообщения об ошибке, потому что нет стабильной категоризированной таксономии, по которой ветвиться.
- Безопасный отказ там, где нужен был немедленный: продолжение на испорченном состоянии в системе, где неверный ответ хуже отсутствия ответа.
Модель зрелости
- Уровень 1, Начальный: Обработка ошибок стихийна и реактивна, решается по разработчику. Пустые блоки catch и проигнорированные возвраты обычны, повторы наивны, тайм-аутов нет, а отказы проявляются как испорченные данные или загадочные дефекты без последовательного журналирования.
- Уровень 2, Развитие: Команды принимают базовые практики, но непоследовательно. Ошибки журналируются с некоторым контекстом, очевидное проглатывание не поощряется на ревью, тайм-ауты и простые повторы есть, но соглашения различаются между сервисами, идемпотентность неровная, а путь ошибки редко тестируется.
- Уровень 3, Стандартизация: Общая таксономия ошибок и соглашение об обработке задокументированы и соблюдаются по организации. Проверка границ, идемпотентные повторы с отступлением и дрожанием, предохранители, переборки и оборачивание ошибок стандартны, предоставляются общими библиотеками, а каждая ошибка питает единый конвейер наблюдаемости.
- Уровень 4, Управление: Поведение при обработке ошибок измеряется относительно базовых значений и контролируется данными. Доли повторов, срабатывания предохранителей, число тайм-аутов, находки проглоченных ошибок статическим анализом, среднее время восстановления и доля неудачных изменений отслеживаются по сервисам; пороги предохранителей и тайм-ауты настраиваются по наблюдаемым данным задержки и отказов, а не наугад; внедрение отказов идёт по расписанию; а команды пересматривают эти метрики, чтобы ловить регрессии и подкреплять свидетельствами каждый выбор «немедленный или безопасный отказ».
- Уровень 5, Оркестрация: Устойчивость интегрирована с планированием поставки и рисков и непрерывно улучшается. Таксономия, общие библиотеки и стандарты эволюционируют из каждого инцидента, эксперименты хаоса и внедрения отказов рутинны, а организация адаптирует тайм-ауты, пороги предохранителей, стратегии деградации и решения по границам по мере смещения трафика, зависимостей и картины рисков.
Идеи для обсуждения
- Где в вашей кодовой базе ошибка сейчас проглатывается и как бы вы узнали, что вы неправы, считая, что это не так?
- Какие из ваших операций записи идемпотентны и какие выполнились бы дважды, если бы повтор сработал после потерянного подтверждения?
- Должно ли «пользователь не найден» быть исключением, значением ошибки или обычным результатом и отвечает ли ваша команда на это последовательно?
- Каково ваше реальное правило о том, где происходит валидация, и можете ли вы указать границу, доверяющую данным, которым не должна?
- Как вы решаете порог и остывание предохранителя и как узнали бы, что текущие настройки неверны?
- Если бы аудитор попросил показать каждый отказ, пережитый вашей системой в прошлом месяце, смогли бы вы его предъявить, категоризированный и с контекстом?
Главные выводы
- Различайте неисправности, ошибки и отказы и разрывайте цепочку, прежде чем внутренняя ошибка станет видимым отказом.
- Выбирайте немедленный или безопасный отказ намеренно по границе и делайте контракт обработки ошибок каждой функции явным.
- Проверяйте жёстко на границах доверия и доверяйте внутри них; защитность, маскирующая отказы, это отсрочка, а не безопасность.
- Делайте повторы безопасными идемпотентностью, тайм-аутами, экспоненциальным отступлением и дрожанием и добавляйте предохранители и плавную деградацию в код.
- Оборачивайте ошибки контекстом, направляйте их в наблюдаемость и никогда не проглатывайте; каждая ошибка должна быть обработана, проброшена или записана в журнал и вынесена.
Источники и дополнительное чтение
- Michael T. Nygard, Release It! Design and Deploy Production-Ready Software
- Andrew Hunt and David Thomas, The Pragmatic Programmer
- Steve McConnell, Code Complete: A Practical Handbook of Software Construction
- Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
- Marc Brooker, “Timeouts, Retries, and Backoff with Jitter,” Amazon Builders’ Library
- Martin Fowler, “CircuitBreaker,” martinfowler.com
- Nassim Nicholas Taleb, Antifragile: Things That Gain from Disorder