Привет, меня зовут Александр. Я более трех лет работаю backend разработчиком над микросервисами компании. Микросервисная архитектура стала очень популярной в больших проектах, ее используют большинство команд на разном стеке технологий. Сегодня разберемся как наша команда создает сервисы с процентом надежности и устойчивости стремительно приближающимся к 99,99.

Давайте проведем некий вводный курс, как понять что сервис надежный и устойчивый. Сразу создать сервис в высоким процентом надежности трудно, а до запуска проекта и прогона тестов почти невозможно. Требуется статистика по сбоям, ошибкам, доступности сервисов и системы в целом. Зато как только выкатили релиз и пришли клиенты, все становится куда яснее..
Что же такое устойчивость сервиса или системы?
Наша команда определила что под устойчивостью мы будем понимать вероятность того, что система находится в работоспособном состоянии, т.е. выполняет свои функции даже при сбое отдельных ее компонентов и восстановится после неисправности в определенный интервал времени, пусть за 3 минуты (желательно чтобы время восстановления было как можно меньше).
А надежность?
Тут все проще.. Надежность - это вероятность того, что наша система правильно и полноценно работает в течении определенного периода времени. То есть работает исправно, не сбоит, не глючит и не зависает, все ее компоненты занимаются по своему предназначению.
Под периодом времени подразумевается любой интервал, но чем продолжительнее тем точнее вероятность.
Выкатили первый релиз, проработали пол года, составили статистику сбоев, нашли ошибки, их причины, решения по устранению. И только теперь приступаем к определению устойчивости и надежности системы.
Определение процента устойчивости и надежности сервиса вычисляется по формуле: 100 - (100 * Тобщ_время_сбоев / Тпериода_работы)
Пример:
Наша система работала 6 месяцев - с января по июнь (260640 мин)
Общее время сбоев по устойчивости: 98 мин
Общее время сбоев по надежности: 76 мин
U = 100 - (100 * 98 / 260640) = 99,9624%
N = 100 - (100 * 76 / 260640) = 99,9708%
Желательно достигать значений близко 99.99%.
Этапы определения устойчивости и надежности:
Сбор всех инцидентов за время работы(берем любой интервал времени, который подходит);
Группируем все сбои по времени, например, до 2 минут, до 15, до 60 и т.д;
Анализируем группы сбоев:
если много коротких сбоев, до двух, пяти минут, а также схожие причины, недоступность отдельного сервиса, выпадающий exeption, ошибки валидации, и т.п., то значит надежность системы или отдельного сервиса низкая, и необходимо приступить к ее повышению;
если наоборот, сбоев мало, и они занимают много времени (например за пол года было 2 сбоя по 10 минут), да еще и причина сбоя совершенна непонятна, то система или сервис является не устойчивой и требуется в первую очередь не искать и исправлять причины, а разработать алгоритм по быстрому восстановлению, как пример, повышать навыки администратора по работе в стрессовых и неизвестных ситуациях, по скоростному восстановлению;
если присутствуют сбои с разными интервалами времени, и разными причинами в разных сервисах, а также в сторонних сервисах (например в API стороннего сервиса, банка, торговой площадки, партнера и т.д.), то придется напрягаться всей команде разом и повышать как устойчивость, так и надежность одновременно. Такие ситуации сплош и рядом встречаются при первых нескольких релизах (при первом точно), когда еще неизвестно поведение системы, клиентов, сторонних сервисов или же когда за устойчивость и надежность хватаются спустя годы активной работы системы.
Обсуждаем причины сбоев и их возможности исправлений;
Приступаем к исправлению, тестированию и обновлению.
Приступаем к действиям: Простые способы к повышению устойчивости
Стоит сразу признать, проблема устойчивости - это необъятная проблема и может произойти на любом уровне. Так как мы не обезопасены от отказов железа, внешнего ПО, стихийных бедствий и т.п.
Продумывание верной архитектуры.

На этапе проектирования перед нами всегда стоит задача разбиения системы на отдельные сервисы. Подобная архитектура позволяет писать и поддерживать код без дополнительных усилий, ну почти, если сравнить с монолитом.
Следующей задачей при создании архитектуры системы - это возможность обеспечения каждого сервиса личной базой данных, т.е. придерживаемся паттерна DPS (Database per service). Это, в большинстве случаев, исключает возможность быстрого разрастания и наполнения базы и избавляет систему от узкого места. Каждый сервис хранит у себя в базе только те данные, которые необходимы только ему. В данном случае, если БД станет недоступна, то это отразится только на одном сервисе, сразу будет видно в каком сервисе искать причину, а после исправления, нагрузка на только что выжившую базу будет намного меньше. Ведь запросы в нее идут от одного сервиса, а не из всей системы.
2. Использование паттернов отказоустойчивости.
Раз проблем устойчивости не избежать, их нужно оградить от попытки влияния на нашу систему. Чтобы при наступлении проблемы последствия были минимальными или быстро восстанавливаемые. Умные люди придумали паттерны отказоустойчивости, которые могут оградить систему от масштабного сбоя: Circuit Breaker, Rate Limiter, Bulkhead и т.д. Использовать не обязательно все паттерны, тем более что многие моменты, реализованные в системе, частично выполняют их функции, но пренебрегать ими не стоит.
3. Готовность сотрудников.
Данный способ предусматривает подготовку сотрудников в целом и увеличение их готовности заменить больного или временно занятого другими задачами сотрудника, возможность быстрого выявления причин.
Необходимо создать некий алгоритм действий сотрудников при сбое, кто берет на себя логи, кто идет в помощь админу и готовит новые сборки, кто устраняет. Это может быть реализовано по зонам ответственности или по задачам.
Если есть дежурные, которые контролируют систему в целом, значит они должны знать всю систему, уметь читать логи и видеть всю систему. То есть мониторинг должен быть так отлажены, что будет явно указывать причину сбоя и его решение.
4. Откаты, релизы, бэкапы и восстановление.
Следующий способ увеличения устойчивости - это сохранность данных, релизов к которым за наименьшее время можно вернуться в случае сбоев без отказов системы. То есть релизы должны быть взаимозаменяемые. К данному способу можно отнести также и опыт администратора, который должен быстро отреагировать, понять что восстановить, какой сервис перезапустить, но мы выделили для этого отдельный способ.
5. Гибкое управление ресурсами
Это не совсем способ, а быстрее подход. Системы должна быть гибкая к управлению ресурсами. Это позволит в процессе функционирования при возрастании нагрузки на какой-либо сервис увеличить его ресурсы на время устранения причины и не допустить сбоя системы.
6. Статический анализ кода, тестирование.
Существуют различные анализаторы кода, которые не позволят пропустить в релиз, так называемый плохой код, который может использоваться неэффективно, или потреблять лишнюю память, иметь уязвимости.
Ну и конечно же тесты. Перед каждым релизом всегда необходимо отдать выполненную таску тестировщикам, которые прогонят все виды тестов, начиная от каждого модуля до всей системы в целом, и сверху еще проведут тестирование под различными нагрузками и предупредят дежурного или админа о критических нагрузках которые готовы выдержать. Тесты это неотъемлимая часть каждого релиза.
А теперь повышаем надежность
Тут расскажу более подробнее о способах, которые использую при разработке. За надежность все же больше отвечает разработчик, а за устойчивость администратор, но не стоит считать это за правило - это чисто мое видение как разработчика.
Разбор сбоев по ошибкам и выявленным причинам
Первое и самое простое чем стоит заняться при повышении надежности - это искать и исправлять причины сбоев. То есть определить сервис, найти ошибку, строку, понять причину этой ошибки. Подумать что можно исправить, переписать, заменить, добавить чтобы данная ошибка больше не возникала.Минусы данного способа определяются длительным времени и требовании свободного разработчика(которого никогда нет, или не хватает опыта), естественно большим повышением надежности тут, конечно, не пахнет. А то и вообще придется переписать половину сервиса и наплодить новых ошибок, которые вызовут сбой. Данный способ нельзя применить до запуска.
Добавление заглушек для сторонних сервисов

Следующий способ можно применить еще при разработки сервиса. При недоступности сторонних сервисов, выдавать заглушки(моковые данные) до тех пор пока сторонний сервис не поднимут или исправят. Как только сервис заработал, взяли отложенные задачи из очереди и получили данные которые требовались.
Минусы:
Не всегда можно продолжить работу без полученных данных стороннего сервиса;
Нет смысла использовать в синхронных системах без отдельных фоновых обработчиков;
Трудно реализовать в сервисах с высокой нагрузкой или наоборот при работе с сторонними сервисами, не готовыми к такому потоку.
Изолировать сервисы друг от друга Способ очень похож на предыдущий, только тут требуется настроить зависимости между сервисами так, система оставалась работоспособной при отказе любого сервиса. Естественно есть сервисы без которых просто невозможна работа системы, да, они называются основными. Они могут занимать лишь часть от общего количества сервиса, и должны иметь такие зависимости, что при отключении всех дополнительных сервисов система остается работоспособна и спокойно принимает клиентов. Как это делается? Берем запрос, который приходит от клиента, смотрим что он получает, что он вызывает и к кому обращается. Потом идет туда куда он обращается и смотрим что он там получает и куда обращается и так по цепочке по всем связям. Тут важно продумать все связи и запросы между всеми сервисами, как поведется система, если сервис 1.1 перестанет отвечать? Обратится в другой сервис? Вернет пустые данные, получит их из кэша или полезет в БД? А может просто выручит флаг, который перенаправит запрос по другому пути? Или сработает система мониторинга и пошлет уведомление дежурному который временно переключит рубильник и временно отключит дополнительную тройную проверку очень важных данных? Как это проверяется? Легко.. Запускается система на тестовой среде, гасится сервис от которого изолировали другие сервисы, если все работает, вы молодцы. Работать не обязательно будет так же как и при полноценной системе, например, не будет приходить фото пользователя, или изображение карусели, так как нарылся сервис, который отвечает за выгрузку файлов, или временно не будет отображаться история заказов, или не будет работать уведомления, но будет работать система заказов. Главное чтобы работал основной функционал вашего приложения и клиент может даже не заметить что у вас был сбой.
Кеширование данных

Повышает надежность почти до 100%, главное чтобы данных было не овер много и не требовалось постоянно обновлять их в кеше. С помощью кеширования повышаем надежность почти на максимум и параллельно снижаем нагрузку на сервер.
Создавать упрощенную логику, но качественную.
Нажали кнопку "Оплатить", а запрос в банк не отвечает, упал по таймауту или прислал код 500, не стоит пугать клиента конкретным ответом, скажите что платеж в ожидании, проводится на стороне банка. Сохраните нервы клиенту, поддержке и повысите надежность, а затем повторите запрос.Разделение кластера на несколько малых изолированных кластеров
При взаимодействии с клиентами по всему миру, стране или с различными сторонними сервисами при обновлении или незначительном сбое могут пострадать сразу все участники.
Например, есть сервис N который обращается в несколько сторонних сервисов M,M2 и M3 и получает от них данные, которые возвращаются пользователям. Сторонний сервис M стал недоступен (разработчики системы с которым общается сервис M не изолировали сервисы друг от друга, поэтому один сервис нагнул систему). Мы молодцы, изолировали сервисы друг от друга, которые работают со сторонними сервисами M, M2 и M3 соответственно, и добавили кэширование, поэтому все запросы для стороннего сервис M копятся в очереди. Думаем над созданием кэширования в сервисах M, M2 и M3, для защиты перегрузки на память, в случае сбоя сразу нескольких сервисов. Сейчас стоит ограничение. А в любое время мониторинг может замерцать замерцает красным, память закончиться и наш сервис N упасть, хоть и стоит ограничение. А в это время клиенты достают данные не только из стороннего сервиса не только через сервис M, но и M2 и M3, но наш сервис упал N, клиенты остались ни с чем, а мы с плохой оценкой и низким рейтингом.
Что нужно сделать? Для каждого стороннего сервиса создать отдельный сервис, который объединит шлюз. Тогда при сбое стороннего сервиса M, сервисы M2 и M3 продолжили работу, а клиентам из сервиса M отправить уведомление что наш партнер сейчас испытывает трудности и просит обратиться вас через сервис M2 или M3 или подождать. А есть есть возможность, то система самостоятельно должна перенаправлять запросы клиентов в рабочий сервис с наименьшей нагрузкой.
Плохо.

Лучше.

Еще лучше.

Не стоит зацикливаться на устойчивости, не стоит зацикливаться на надежности! Естественно на 100% повысить ни одно, ни другое скорее всего не получится, ну если только напишите идеальный код (чего не бывает), а если напишите, то обязательно крякнет жесткий или мать. Старайтесь поддерживаться данным правилам или советам, рекомендую.
Все что описано, это все что удавалось применять при разработки сервисов за последние два года. Некоторые способы использовались лишь несколько раз, некоторые регулярно, некоторые разработчиками, некоторые админами, но все они реально позволяют избавиться от часто-повторяющихся, непредсказуемых и убыточных сбоев.