четверг, 8 января 2015 г.

22.5 ZeroMQ: надежные схемы "Запрос/Ответ". Хартбитинг в деталях.

  Запрос/Ответ. Надежные схемы "Запрос/Ответ". Хартбитинг в деталях.

Хартбитинг

 


Хартбитинг решает проблему распознавания "жив партнер или нет?" Эта проблема касается не только ZeroMQ. Протокол TCP имеет долгий таймаут (30 минут и больше), что делает невозможным определить - умер ли ли партнер, был ли дисконнект, или партнер просто уехал на выходные в Прагу пить водку.
Реализовать хартбитинг не так просто. Автор оригинала примеров для шаблона "Пират-параноик" пишет, что это заняло около пяти часов. Для реализации остальной части цепочки "запрос-ответ" потребовалось от силы десять минут. Особенно легко реализуются "ложные отказы", когда, к примеру, партнеры решают, что приключился дисконнект из-за неправильного хартбитинга.

В плане использования хартбитинга совместно с ZeroMQ разработчики обычно придерживаются трех подходов:

1. "А, и так сойдет!"




Наиболее общим подходом является вообще отказ от использования хартбитинга и упование на всевышнего. Много, если не большинство приложений ZeroMQ так и работают. ZeroMQ во многих случаях провоцирует такой подход, изолируя партнеров друг от друга. Какие проблемы следуют из такого подхода?
  • При использовании в приложении сокета ROUTER, который работает с партнерами, когда партнеры отключаются и снова подключаются,  в приложении будет происходить утечка памяти (ресурсы, которые использует приложение для каждого партнера), и приложение будет работать все медленнее и медленнее.
  • Когда используются приемники данных на основе сокетов типа SUB или DEALER, нет возможности определить - "хорошее" ли это молчание (когда данные не пришли), или это "плохое" молчание (когда партнер мертв). Когда же приемник знает, что партнер на другом конце мертв, он сможет, к примеру, попытаться передать данные другому партнеру.
  • При использовании протокола TCP, отсутствие обмена данными в течении длительного времени в некоторых сетях определяется как отказ, влекущий разрыв соединений. Отправка чего-либо (технически,  реализация "keep-alive" отличается от хартбитинга), позволит поддерживать соединение в живых.

2. Односторонний хартбитинг

Второй подход - это когда сообщения харбитинга отправляются от каждого узла его партнерам примерно раз в секунду. Когда один узел не слышит других в течении некоторого времени (обычно в течении нескольких секунд), он принимает решение о том, что партнер мертв. Звучит разумно, не правда ли? Как бы не так. Это работает в некоторых случаях, но в других случаются разные неприятности.
Для схемы "Издатель-Подписчик" это работает, и эта схема - единственная, где такой подход можно использовать. Сокеты SUB не могут отвечать сокетам PUB, но сокеты PUB могут могут рассылать своим подписчикам сообщения "Я жив!".
С целью оптимизации трафика можно отправлять хартбит только тогда, когда для отсылки нет реальных данных. Более того, постепенно можно отправлять хартбиты  реже и реже, если сетевая активность является проблемой (например, в мобильных сетях, когда излишняя сетевая активность разряжает батарею). Пока получатель все еще может обнаружить отказ (как остановку деятельности), это нормально.

Типичные проблемы подобного подхода:
  • Возможны ошибки в случае, когда мы отправили большие объемы данных и хартбиты окажутся задержанными до тех пор, пока данные не будут отосланы. Если хартбиты задерживаются,  вы можете принять неправильное решение о таймауте и дисконнекте из-за перегрузки сети. Таким образом, следует всегда рассматривать любые входящие данные как хартбит, вне зависимости от того, оптимизировал ли отправитель работу с хартбитами.
  • В шаблоне "Издатель-Подписчик" необработанные на приеме сообщения отбрасываются. А сокеты PUSH и DEALER будут помещать их в очередь. Таким образом, если мы шлем хартбиты мертвым партнерам и они возвращаются обратно, мы получим обратно все отправленные хартбиты, которых могут быть тысячи. Это слишком много.
  • Такой подход предполагает, что временны'е параметры хартбитинга одинаковы для всей сети. Но это предположение может оказаться неверным. Некоторые партнеры для более быстрого выявления отказов в сети могут реализовать более агрессивный хартбитинг. А некоторые, с целью сбережения питания и уменьшения сетевого трафика, реализуют более спокойный хартбитинг.

 3. Пинг-понг.


Третий подход заключается в использовании диалога типа "пинг-понг". Один партнер шлет команду ПИНГ другому, который отвечает командой ПОНГ. Ни  одна из команд не несет никакой полезной нагрузки. Пинги и понги не коррелируют. Так как роли "клиента" и "сервера" для некоторых сетей являются произвольными, обычно мы определяем, что любой из партнеров может отправить пинг и получить понг в ответ. Тем не менее, так как таймауты зависят от сетевых топологий, которые обычно известны динамически подключаемым клиентам, то обычно именно клиенты и пингуют сервер.
Такой подход работает для всех брокеров, использующих сокет ROUTER. Подобную оптимизацию мы применили во второй модели, и даже улучшили его: мы интерпретируем прием любых входные данных как понг, а пинг отправляем только тогда, когда нет реальных данных для отправки.

Хартбитинг для схемы "Пират-параноик"


Для "Пирата-параноика" мы использовали второй подход. Определенно, что это не самый простой вариант. Автор оригинала примеров утверждает, что сейчас бы, возможно, вместо этого выбрал бы вариант пинг-понг. Тем не менее, принципы схожи. Хартбит - сообщения асинхронно проходят в обоих направлениях, и любой из партнеров может принять решение, что его корреспондент "мертв" и перестать общаться с ним.
Вот так рабочий обрабатывает хартбитинг от очереди (прокси):
  • Мы вычисляем liveness ("живучесть"), то есть величину, показывающую, сколько хартбитов мы можем пропустить перед тем, как примем решение, что очередь мертва. Ее значение начинается с трех, и при каждом пропуске хартбита мы уменьшаем это значение на единицу.
  • В цикле zmq_poll мы ожидаем одну секунду, которая представляет собой наш хартбит - интервал.
  • Если в течении этого времени от очереди пришло любое сообщение, мы снова устанавливаем значение liveness := 3.
  • Если в течении этого времени не было сообщений, мы уменьшаем значение liveness на 1.
  • Если значение liveness стало равным нулю, считаем, что очередь умерла.
  • Если очередь мертва, мы уничтожаем сокет, создаем новый и выполняем реконнект.
  • Чтобы избежать открытия и закрытия слишком большого числа сокетов, перед реконнектом мы ждем в течении некоторого интервала времени, и удваиваем этот интервал до тех пор, пока он не достигнет 32 секунд.
А вот так формируется хартбитинг к очереди:
  • Мы вычисляем, когда следует послать следующий хартбит, это одна переменная. так как у нас всего один партнер (очередь).
  • В цикле zmq_poll, всякий раз при превышении интервала, мы шлем очереди хартбит.
Ядро кода, реализующего хартбитинг для рабочего:



 
const
  c_HEARTBEAT_LIVENESS = 3; // Нормально, если 3-5
  c_HEARTBEAT_INTERVAL = 1000; // msecs
  c_INTERVAL_INIT = 1000; // Начальный интервал реконнекта
  c_INTERVAL_MAX = 32000; // После экспоненциальной отсрочки

  ...

  // Если индикатор живучести liveness обнуляется, считаем, что связь
  // с очередью прервалась

   liveness := c_HEARTBEAT_LIVENESS;
   interval := c_INTERVAL_INIT;

  // Отсылает хартбиты через равные промежутки времени
  heartbeat_at := zclock_time() + c_HEARTBEAT_INTERVAL;

  ...

  while true do begin
    zPollItemInit(items, worker, 0, ZMQ_POLLIN, 0);
    rc := zmq_poll(@items, 1, c_HEARTBEAT_INTERVAL * ZMQ_POLL_MSEC);
    ...

    if (items.revents and ZMQ_POLLIN) <> 0 then begin
    // Принимаем любое сообщение из очереди
      liveness = c_HEARTBEAT_LIVENESS;
      interval = c_INTERVAL_INIT;
    end
    else begin
      Dec(liveness) ;
      if liveness = 0 then begin
        zclock_sleep (interval);
        if interval < c_INTERVAL_MAX then
          interval := interval * 2;
        zsocket_destroy (ctx, worker);
        ...
        liveness := c_HEARTBEAT_LIVENESS;
       end
    end
    // Если пришло время - отправка хартбита очереди
    if zclock_time () > heartbeat_at then begin
      heartbeat_at := zclock_time () + c_HEARTBEAT_INTERVAL;
      // Отправка хартбит - сообщения очереди
    end
  end



Очередь (брокер) делает то же самое, но управляется временем смерти для каждого рабочего.

Несколько советов по реализации собственного хартбитинга:

  • В качестве ядра главной задачи приложения используем zmq_poll или реактор.
  • Начинаем с реализации хартбитинга между партнерами, тестируем методом имитации отказов, а затем реализуем оставшуюся часть функционала потока сообщений. Добавить хартбитинг позднее будет гораздо сложнее.
  • Используем простые приемы трассировки(например, вывод в консоль). Для облегчения трассировки потока сообщений между партнерами используем метод dump, вроде того, который реализован в пакете zmsg; количество сообщений увеличиваем постепенно, чтобы можно было заметить, что что-то не так.
  • В реальном приложении хартбитинг должен быть настраиваемым, и обычно оговариваться с партнером. Некоторым партнерам нужен агрессивный хартбитинг с интервалом минимум в 10 миллисекунд. Другим партнерам, находящимся на значительном удалении, будет достаточно и 30-то секундного интервала хартбитинга.
  • Если для разных партнеров нужен разный интервал хартбитинга, то таймаут поллинга должен быть минимальным (самый малый промежуток времени) из них. Не используем бесконечный таймаут.
  • Для реализации хартбитинга используем тот же самый сокет, который используется для сообщений, таким образом хартбитинг заодно выполнит задачу keep-alive для предотвращения остановки слабоактивного коннекта (некоторые файрволлы обрывают коннекты, которые молчат, по их мнению, слишком долго).

Соглашения и протоколы


Несложно заметить, что наша реализация "Пирата - параноика" из-за хартбитинга несовместима с "Простым пиратом". Но что же есть "совместимость"? Для обеспечения совместимости, нужно придерживаться какого-нибудь соглашения, которое позволило бы разным командам писать код, который гарантированно будет работать вместе. Такое соглашение будем называть "протокол".
Довольно приятно экспериментировать, не ограничивая себя спецификациями, однако, такое невозможно при разработке реальных приложений. Что произойдет, если мы напишем рабочего на другом языке программирования? Сможем ли мы прочитать код код, чтобы понять, как он работает? Что будет, если мы по каким-либо причинам изменим протокол? Даже простой протокол станет сложным, если он успешен и развивается.
Отсутствие соглашений - верный признак одноразового приложения. И так, напишем соглашения по данному протоколу. Как это делается?
Базовые соглашения по публичным протоколам ZeroMQ описаны здесь: rfc.zeromq.org.
Чтобы создать новую спецификацию, регистрируем ее, если нужно, в wiki и следуем инструкциям. Это достаточно просто, хотя написать технический текст - не то же самое, что выпить чашку чая.
Для подготовки нового Протокола Пиратских Шаблонов (Pirate Pattern Protocol - PPP)  автору понадобилось около пятнадцати минут. Не такая уж и большая спецификация, но её вполне достаточно, чтобы использовать в качестве аргумента для заявления: "ваша очередь несовместима с PPP, будьте добры - исправьте!".

Превращение PPP в реальный протокол потребует куда большей работы:
  • В сообщении READY должна присутствовать версия протокола, чтобы можно было различать между собой версии PPP.
  • В данный момент времени сообщения READY и HEARTBEAT не вполне отличимы от обычных запросов и ответов. Чтобы они отличались, сообщения должны иметь структуру, которая  содержит "тип сообщения".

Далее мы рассмотрим Сервис - Ориентированную Надежную Очередь: шаблон Мажордом.

(Продолжение).




Комментариев нет :

Отправить комментарий