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

UnixForum





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

Система обмена сообщениями ZeroMQ

Глава 24 из книги "Архитектура приложений с открытым исходным кодом", том 2.

Оригинал: "ZeroMQ".
Автор: Martin Sústrik, перевод: Н.Ромоданов

24.8. Модель распараллеливания

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

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

Учитывая эти проблемы, мы решили перейти на другую модель. Цель состояла в том, чтобы полностью избежать блокировок и позволить каждому потоку работать на полной скорости. Взаимодействие между потоками было реализовано с помощью асинхронных сообщений (события), которые передаются между потоками. Это, как знают инсайдеры, является классической моделью актера (actor model).

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

Рис.24.7: Несколько рабочих потоков

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

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

Получается, что завершение работы полностью асинхронной системы является в чистом виде устрашающе сложной задачей. При попытке завершить работу тысячи движущихся частей, некоторые из которых работают, некоторые находятся в состоянии ожидания, некоторые - в процессе инициализации, некоторые из них уже завершили свою работу самостоятельно, возможны возникновения всех видов состояний гонки, утечки ресурсов и тому подобное. Подсистема завершения работы является, безусловно, самой сложной частью ØMQ. Быстрый просмотр трекера ошибок показывает, что около 30 - 50% обнаруженных ошибок связаны в той или иной форме с этапом завершения работы системы.

Усвоенный урок: Когда стремитесь к экстремальной производительности и масштабируемости, то рассмотрите модель актера; это чуть ли не единственная вариант в подобных случаях. Однако, если вы не пользуетесь специализированной системой, например, Erlang или самой ØMQ, вам придется написать и вручную отладить инфраструктуру большого объема. Кроме того, с самого начала подумайте о процедуре завершения работы системы. Это будет самая сложная часть кода, и если у вас нет четкого представления о том, как ее реализовать, вам, вероятно, следует в первую очередь пересмотреть использование модели актера.

24.9. Неблокирующие алгоритмы

В последнее время в моде стали неблокирующие алгоритмы. Это простые механизмы межпотокового взаимодействия, в которых не используются предоставляемые ядром примитивы синхронизации, такие как взаимоисключаемые события или семафоры; они предпочитают выполнять синхронизацию с использованием атомарных операций процессора, таких как атомное сравнение и своп (CAS). Следует иметь в виду, что они не в буквальном смысле работают без блокировок — в действительности блокировки происходят за кулисами на аппаратном уровне.

ØMQ использует неблокирующую очередь в конвейерных объектах для передачи сообщений между потоками пользователя и рабочими потоками ØMQ. Есть два интересных аспекта, касающихся того, как ØMQ использует неблокирующую очередь.

Во-первых, в каждой очередь есть ровно один поток, который осуществляется запись, и ровно один поток, который осуществляет чтение. Если необходима связь типа «1-N», то создается несколько очередей (рис.24.8). Благодаря такой организации очереди не надо беспокоиться о о синхронизации записи (есть только один поток, осуществляющий запись) или чтения (есть только один поток, осуществляющий чтение), причем очередь может быть реализована очень эффективным способом.

Рис.24.8: Очереди

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

Способ ускорения — опять же - потоковая обработка. Представьте, что у вас есть 10 сообщений, которые должны быть записаны в очередь. Это может произойти, например, когда вы получили сетевой пакет, содержащий 10 небольших сообщений. Получение пакета является атомарным событием, вы не можете получить половину пакета. В результате этого атомарного события в неблокирующую очередь потребуется записать 10 сообщений. Нет никакого смысла выполнять атомарную операцию для каждого сообщения. Вместо этого, вы можете накопить сообщения в виде порции «предзаписи» в той части очереди, которая доступна исключительно для записывающего потока, а затем сбросить ее в очередь с помощью одной атомарной операции.

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

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

Рис.24.9: Неблокирующая очередь

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


Далее: 24.10. Интерфейс API