Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Кластеризация согласно консенсусу

Оригинал: Clustering by Consensus
Автор: Dustin J. Mitchell
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: январь 2017 г.

Тестирование

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

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

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

Модульное Unit-тестирование

Модульные тесты для кластера Cluster простые и короткие:

class Tests(utils.ComponentTestCase):
    def test_propose_active(self):
        """A PROPOSE received while active spawns a commander."""
        self.activate_leader()
        self.node.fake_message(Propose(slot=10, proposal=PROPOSAL1))
        self.assertCommanderStarted(Ballot(0, 'F999'), 10, PROPOSAL1)

Этот метод проверяет отдельное поведение (создание роли commander) отдельно взятого блока (класс Leader). Метод следует хорошо известному образцу "задать начальные значения, выполнить действия, проверить утверждение" ("arrange, act, assert"): настраивается активный лидер, ему отправляется сообщение и проверяется результат.

Внедрение зависимостей

Для того, чтобы справиться с созданием новых ролей, мы используем технику, называемую "внедрение зависимостей". Каждый класс роли, добавляющей в сеть другие роли, берет в качестве аргументов конструктора список объектов класса, которые обычно необходимы при создании реальных классов. Например, конструктор класса Leader выглядит следующим образом:

class Leader(Role):
    def __init__(self, node, peers, commander_cls=Commander, scout_cls=Scout):
        super(Leader, self).__init__(node)
        self.ballot_num = Ballot(0, node.address)
        self.active = False
        self.proposals = {}
        self.commander_cls = commander_cls
        self.scout_cls = scout_cls
        self.scouting = False
        self.peers = peers

Метод spawn_scout (и аналогично, spawn_commander) создает новый объект роли с помощью self.scout_cls:

class Leader(Role):
    def spawn_scout(self):
        assert not self.scouting
        self.scouting = True
        self.scout_cls(self.node, self.ballot_num, self.peers).start()

Магия этого метода заключается в том, что при тестировании класс Leader может создавать фиктивные классы и, таким образом, его тестирование можно выполнять отдельно от классов Scout и Commander.

Корректность интерфейса

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

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

Что касается проверки конкретных данных, например, формат значений accepted_proposals, то как реальные, так и тестовые данные можно проверять с помощью одной и той же функции, в данном случае — с помощью verifyPromiseAccepted. В тестах на принимающей стороне этот метод используется для проверки каждого возвращаемого сообщения Promise, а в тестах для роли scout этот метод используется для проверки каждого фиктивного (прим.пер.: используемого при тестировании) сообщения Promise.

Интеграционное тестирование

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

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

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

Fuzz-тестирование или фаззин

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

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

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

Прим.пер.: Fuzz-тестирование или фаззинг — это методика тестирования, при которой на вход программы подаются невалидные, непредусмотренные или случайные данные. Эта методика является тестированием методом "грубой силы". Преимуществом такого подхода является его простота. Очевидный недостаток, что, поскольку при этом обычно не учитывается внутренняя структура и поведение приложения (т. е. приложение рассматривается как "черный ящик"), для достаточно хорошего покрытия кода при таком тестировании необходимо будет перебрать очень большое количество вариантов входных данных.

Силовая борьба

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

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

Если весь кластер не согласен, какой узел станет активным лидером, то возникает проблема: разные роли replica отправляют сообщения Propose разным лидерам, что приводит к сражению ролей scout. Поэтому важно, чтобы выборы лидера происходили быстро, и чтобы все члены кластера узнали о результате как можно быстрее.

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

Перейти к следующей части статьи.