Designing and refactoring
3 May 2011

Построение отказоустойчивой (fault tolerant) системы

В разработке банковского ПО данному аспекту системы уделяется наибольшее внимание. Часто, описывая отказоустойчивую систему, используют слова: Fault Tolerance, Resilience, Reliability, Stability, DR (disaster recovery). Данная характеристика — суть способность системы продолжать корректно работать при падении одной или нескольких подсистем, от которых она зависит. Я кратко опишу какие подходы могут применяться в данной области и приведу пару примеров.

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

С чего начать?


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

Так же можно посмотреть в сторону такой вещи как fault tree analysis. Это когда вы строите диаграмму ваших компонент в виде дерева зависимостей, и снизу вверх начинаете проставлять кумулятивную вероятность падения ваших компонентов. На этой диаграмме будет хорошо видны самые уязвимые места, которые потенциально могут принести самый большой ущерб.

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

Способы реализации


Есть два принципиально разных подхода, хотя они вполне могут независимо или полузависимо сочетаться для вашей системы. Один подход — это DR (disaster recovery), когда полная копия всей вашей системы может быть поднята в другом датацентре. Данный метод выручает практически в любой ситуации, однако, он может имеет очень длительный промежуток простоя. Я знаю одну систему, в которой это занимает в районе часа. Но тут тоже надо быть очень аккуратным, не только конфигурация системы, но и железо в DR должны совпадать с продакшеном. Например, когда мы тестировали DR, в одной из систем с которыми я работал, то у нас при сильной нагрузке оказалось, что один из компонентов заточен на работу в однопоточной среде и его пропускная способность пропорциональна частоте процессора, а на DR у нас была мега железка с огромным количеством ядер, но с небольшой тактовой частотой, так что этот компонент стал затыкаться. Вобщем, тест был весьма неудачен и «более клевое» железо на текущей конфигурации нашей системы нам явно не подходило.

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

low level fault tolerant services

Принцип тут следующий — ваша система должна состоять из более менее независимых систем каждая из которых должна быть отказоустойчивой. В идеале не изобретать велосипед, а использовать уже готовые решения. Это на самом деле наилучший вариант, когда вся ваша система будет опираться на так называемые low level fault tolerant services.

Single point of failure

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

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

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

Active-Active

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

Active-Passive

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

Так же важно понимать, что если вы не используете готовое решение, то его acttive-passive реализация скорее всего потребует больше усилий, нежели active-active solution, так как тут нужен надежный способ проверки, что активный компонент функционирует, и нужно уметь восстанавливать состояние на момент падения или постоянно его синхронизировать. Еще данное решение всегда будет иметь некую задержку в работе при падении компонента. Зато пассивный компонент скорее всего будет потреблять много меньше ресурсов и вы можете получить отказоустойчивое решение на более слабом железе.

Балансировка нагрузки (Load Balancing)

Обычно этот термин всплывает при построении высоконагруженных систем. Однако для отказоустойчивых систем данный принцип тоже можно применить. Идея в том, что у вас есть несколько идентичных компонентов, между которыми более менее равномерно распределяется вся нагрузка. В отличие от вышеописанной active-active стратегии, здесь каждую задачу выполняет только один компонент. Данный механизм идеально подходит для stateless компонент, иначе для отказоустойчивости вам постоянно придется синхронизировать состояние. Например в случае веб-серверов — делать репликацию сессии. В данном решении очень важно иметь как минимум N+1 redundancy, т.е. если для пиковых нагрузок вам необходимо N работающих на всю катушку компонент, то в вашей системе должно присутствовать N+1 таких компонент, так как иначе, если у вас упадет один из элементов, то на все остальные нагрузка увеличиться и вся ваша система рухнет как домино.

Самый распространенный пример load balancing — это, наверное, балансирование нагрузки веб-серверов. Тут бывают как программные решения (Nginx), так и специальные железки. В backend системах load-balancing часто используют для реализации балансирования реализуют при помощи JMS очереди, вешая на не неё несколько идентичных слушателей. В данном случае очередь гарантирует, что сообщение попадет только в один из компонентов, и пока он его не обработает, следующее сообщение он взять не сможет. Кстати в такой же системе, если в качестве очереди взять topic, то можно реализовать active-active систему.

Защитное программирование (Defencive coding)

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

Infinite Loop. Следите за тем, чтобы при возникновения ошибки во время обработки сообщения, вы не выходили в бесконечный цикл, который постоянно пытается выполнить данную операцию, особенно если сообщение приходит вам извне, и вы не можете гарантировать, что оно корректно. Я уже ни один раз видел, как из-за этой ошибки подвисает вся система. Что же в этом случае делать? Например, можно отправлять сообщение об ошибке в систему мониторинга (см. ниже) и переходить к обработке следующего таска.

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

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

State Management. Попытайтесь сделать так, чтобы ваша система время от времени сохраняла свое состояние (checkpoint), чтобы в случае больших проблем вы всегда могли откатиться в последние консистентное состояние. Например, в FIX протоколе, распространенном в финансовых приложениях, есть понятие sequence. Каждое сообщение имеет свой порядковый номер, сервер запоминает номер последнего отосланного сообщения, а клиент запоминает номер последнего принятого сообщения. На рестарте они обмениваются данными номерами, и если между ними есть промежуток, то сервер высылает все пропущенные сообщения.

InfrastructureError. Если в вашем приложении произошла ошибка, из которой вы не можете восстановиться, например OutOfMemoryError в java, то можно попробовать остановить приложение и запустить его заново. Это можно автоматизировать с помощью такой тулзы как Tanuki Java Service Wrapper, о данной функции которого я подробно писал тут.

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

Изоляция инфраструктуры

Совместное использование железа вашей системы с какими бы то еще ни было приложениями очень опасно. Еще его называют принципом студенческой кухни (student kitchen), если кто-то что-то там делает, то это не может не повлиять на других людей, пользующихся теми же вещами. Самое неприятное, что данные случае очень сложно определить, если анализ проводить не сразу, а спустя определенное время. Примеров данной проблемы можно привести множество. Например, третье приложение начало активно использовать оперативную память — таким образов ваши процессы начали свопиться и производительно вашей системы жутко деградирует. Или третья система начала активно использовать жесткий диск, а вы пишите логи синхронно, в этом случае ваше приложение опять сильно замедлиться. Переполнение жесткого диска третьими системами и забивка канала связи — тоже возможные проблемы, правда они могут возникнуть и в случае когда железо используется только вашей системой, просто она состоит из большого количества процессов. Например, очень распространенный случай того как приложения написанные на java влияют друг на друга — это параллельный сборщик мусора. Когда в одном из компонентов он срабатывает, то по-умолчанию он использует все доступные CPU ресурсы на полную катушку, так что если у вас на этой же машине крутится приложение требующее быстрого отклика, то данная проблема не надумана. Хотя количество доступных потоков для сборки мусора всегда можно ограничить специальным флагов при запуске JVM.

Мониторинг


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

Для мониторинга существует множество готовых решений (Triton, Nagious), как платных так и open-source. Стандартные функции — это мониторинг диска, оперативной памяти, процессоров и трафика. Так же бывают различные плагины, которые позволяют мониторить лог файлы и при появлении там ошибок отсылать сообщение в систему мониторинга.

Не смотря на наличие готовых решений, банки почему-то разрабатывают свои собственные решения. Однако они уже больше заточены на получение сообщений посланных программно из внутренних приложений. Например, если по какой-то причине некая back-end система не смогла обработать ордер, то в описанную систему мониторинга можно отослать сообщения со всеми деталями ордера, чтобы support смог ввести детали ордера вручную.

Еще одна разновидность мониторинга — это health monitor, когда ваше приложение шлет специальные heartbeat, и если в течении определенного интервала времени от приложения ничего не было слышно, то в системе трекинга всплывает сообщение о неисправности.

Problem Management


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

В заключении


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

+61
26.5k 164
Comments 10