QNX RTP Logo QNX Realtime Platform: Русский Портал QNX
Thursday, 20 Nov 2008 10:40
Меню

Проект OpenNET - все о Unix
Разработка структуры системы для среды, основанной на обмене сообщениями, часть II Print E-mail
Роб Кёртен, перевод Андрей Чиликин, Дмитрий Алексе

Краткое изложение:

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

Содержание статьи

"Управление потоком сообщений"

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

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

Считыватели карт

Драйвер устройства чтения карты довольно прост – он ждет пока через аппарат считывания будет проведена карта и считывает ее идентификатор. И хотя сам драйвер достаточно прост, в его основе лежит дизайн, который радикально отличается от дизайна драйвера запорного механизма двери. Вспомним, что запорный механизм является приемником данных ("data sink") – управляющая программа посылает ему команду на выполнение определенного действия. В случае считывателей карт такая структура неприменима (мы не хотим постоянно опрашивать считыватель карт о наличии поступивших данных; мы хотим, чтобы он сам посылал нам данные, когда кто-нибудь воспользуется своей картой).

Драйвер считывателя карт должен быть источником данных ("data source") – он должен передавать данные другому приложению по мере их поступления.

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

Проблема масштабирования возникает из-за того, что управляющая программа постоянно опрашивает состояния считывателей карт. Это примерно то же самое, как если бы ваши дети постоянно спрашивали вас во время поездки: "Мы уже приехали?" Вместо этого гораздо лучше было бы (хотя это и маловероятно в реальной жизни) если бы они спросили (один раз!): "Скажите нам, когда мы приедем на место.". Если управляющая програма занимается постоянным опросом устройств, мы тратим впустую процессорное время – т.е. увеличиваем накладные расходы: мы теряем время и на запрос и на получение ответа. К тому же мы не только нагружаем процессор, но и нагружаем сеть, что может привести к перегрузке сети в больших распределенных системах. Если в нашей системе лишь несколько считывателей и опрос ведется раз в секунду – это не такая уж большая проблема. Однако, если мы опрашиваем считаватели чаще, а их число не так уж и мало – следует задуматься над вопросом масштабируемости.

Было бы идеально, если бы считыватель карт передавал нам данные по мере их поступления.

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

Между тем, кто-то может воспользоваться другим считывателем карт в субботу утром. Простое добавление программных потоков – по одному на каждый считыватель, тоже нельзя назвать хорошо масштабируемым решением. Хотя программные потоки обходятся достаточно дешево, но они все равно требуют ресурсов – это как минимум 8 Кб оперативной памяти (4 Кб для данных и 4 Кб для исполняемого кода, в зависимости от типа вашего процессора), внутренние данные ядра и накладные расходы на переключение контекста.

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

Исходя из этого, казалось бы все просто.

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

Однако здесь есть один тонкий и весьма критичный момент.

Если драйвер считывателя посылает сообщение управляющей программе, и, одновременно, управляющая программа посылает сообщение драйверу у нас возникает проблема. Состоит она в том, что две программы, одновременно пославшие друг другу сообщения, взаимно блокируются в ожидании ответа от оппонента. И хотя может показаться, что такая проблема не должна возникать часто, на практике это означает что она даст о себе знать во время демонстрации заказчику. Зачем управляющей программе вообще может потребоваться отсылать сообщение драйверу считывателя? Для примера, на некоторых считывателях есть цветной индикатор (зеленый/красный), показывающий разрешен или нет пользователю доступ, следовательно управляющая программа должна послать сообщение чтобы установить индикатор в определенное состояние. (Очевидно, что в этом примере есть несколько способов избежать взаимной блокировки; но в случае разработки структуры реальной системы, вам НИКОГДА не следует закладывать возможности создания такой ситуации!)

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

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

К сожалению, такая структура проекта не позволит нам использовать "простое решение", которое мы обсуждали выше, а именно возможность отправки сообщения считывателем карт управляющей программе. Стандартное решение в таком случае – это неблокирующее сообщение, так называемый "импульс" ("pulse").

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

Давайте применим такую структуру к нашему примеру со считывателем карт.

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

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

Какая структура более предпочтительна? Какое решение будет выгоднее и с какими ограничениями мы можем столкнуться?

Ответ на это кроется в это объеме передаваемых данных.

Так как с помощью импульсов можно передавать только короткие сообщения (всего 40 бит – 8 бит для передачи "кода команды" и 32 бита для данных) объем передаваемых "импульсом" данных весьма ограничен. В нашем примере этого вполне достаточно для передачи состояния индикаторов. Мы можем использовать 8 бит кода для команды "установить цвет индикатора" и 32 бита данных для передачи цвета: зеленый (доступ разрешен), красный (доступ запрещен) и возможно желтый (ожидание обработки запроса). Если бы нам нужно было передать больше данных, мы уперлись бы в 40-битное ограничение. Например, если бы считыватель был оснащен кроме цветового индикатора еще и клавиатурой с матричным дисплеем, то их можно было бы использовать для запроса пароля. Тогда управляющей программе необходимо будет принять введенные пользователем данные.

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

Масштабируемость

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

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

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

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

    Второй вопрос также из разряда использования ресурсов – если вы применяете интенсивный обмен сообщениями, со временем вы просто «забьете» полосу пропускания вашего канала передачи данных. Так что если даже процессор загружен всего на 10%, но при этом полоса пропускания используется на 90%, вы достигните предела возможностей вашего канала передачи данных в первую очередь. Все это подводит нас к третьему, основному, вопросу о масштабируемости. Если вы используете довольно большое количество ресурсов на одной рабочей станции, то традиционным решением масштабируемости будет разделение нагрузки по нескольким сетевым узлам. Давайте «промасштабируем» наш пример с системой безопасности на университетский городок. Нет сомнений что применение одного компьютера для обслуживания сотен (или тысяч) запорных механизмов дверей, считывателей карт и других устройств, не будет являться правильным решением. Такая система скорее всего моментально развалится под напором поступающих данных при проведении учебной пожарной тревоги, когда по всей территории городка одновременно все начнут пользоваться своими карточками чтобы вернуться на место после отбоя тревоги.

    Вместо этого мы лучше создадим отдельные контролируемые "зоны".

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

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

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

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

    Распределенная обработка данных

    Давайте теперь представим, что данные в системе должны изменяться очень быстро.

    Как бы мы справились с такой проблемой? Стандартным решением в этом случае было бы применение так называемой "распределенной обработки данных".

    Это значит, что наша система должна состоять из нескольких серверов и каждый из них будет обрабатывать определенное подмножество запросов. Наиболее просто это можно проиллюстрировать представив что каждая карта доступа имеет 32 разрядный серийный номер (идентификационный код карты). Если нам нужно разбить нашу задачу с сервером, скажем, на 16 подзадач, тогда мы можем сделать это довольно просто. Мы можем использовать четыре младших бита серийного номера в качестве «адреса» сервера, который обрабатывает запрос о этой карте. (При этом мы принимаем, что 32 разрядные серийные номера имеют равномерное распределение адресов в младших четырех битах.) А что делать если нужно разбить задачу на 32 подзадачи? Просто использовать пять младших бит серийного номера, и так далее.

    Резюме

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

    [Вернуться к списку]
    ©   2000-2003 Команда проекта QNX.ORG.RU // QNX.ORG.RU Team
    Авторы проекта: Дмитрий Алексеев [dmi] и Дмитрий Васильев. Техническое сопровождение проекта: Игорь Сорокин [isorokin]. Информационное сопровождение: Дмитрий Алексеев [dmi]
    QNX - зарегистрированная торговая марка QNX Software Systems, Ltd., Canada. Остальные упоминаемые на сайте торговые марки и логотипы являются исключительно собственностью их уважаемых владельцев. Ничьи права не затронуты. Материалы сайта не могут быть скопированы и где-либо использованы в той или иной форме без письменного разрешения разработчиков сайта.
    Powered by Mambo Open Source