Джино: хостинг и веб-сервисы

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | dklab_multiplexor: постоянное JavaScript-соединение с сервером в условиях сотен тысяч онлайн-клиентов 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


2009-09-12
Обсудить на форуме

Принять участие в разработке библиотеки/утилиты можно на GitHub.

Внимание! Данный инструмент устарел с выходом его идейного продолжателя — Dklab Realplexor. Пользуйтесь лучше Realplexor-ом.


Dklab_multiplexor — это инструмент, который позволяет держать одновременно сотни тысяч долгоживущих открытых HTTP-соединений с сервером. Например, если на вашем сайте находится одновременно несколько сот тысяч посетителей, каждый из них может быть связан с сервером постоянным соединением, установленным из JavaScript. Это, например, полезно при организации онлайн-чатов или мгновенных уведомлений.

Лирическое отступление 
Dklab_multiplexor не претендует на полную универсальность или исключительность. Это лишь простейший инструмент, который наконец-то дошли руки опубликовать. Ближайшие аналоги:
  • APE: более функциональный, но и значительно более сложный;
  • NginxHttpPushModule: модуль для nginx, обеспечивающий похожую функциональность и обладающий простым интерфейсом (я, правда, сам не проверял, насколько он работоспособен, но, говорят, там не хватает нескольких полезных функций).
Попробуйте их посмотреть.

Зачем это нужно?

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

Чайник 

Данный функционал, например, поддерживается социальными сетями Мой Круг, Facebook, а также популярной почтовой службой Google Mail (в последней он справедливо называется "чатом").

Данную задачу можно решить двумя способами.

  1. Неправильный способ. Раз в 10 секунд делать из JavaScript запрос на сервер для проверки, не появилось ли новых сообщений. Этот метод не работает, если на сайте одновременно находится очень большое количество пользователей, т.к. нагрузка на сервер растет слишком быстро. Кроме того, потребление трафика пользователем также оказывается крайне высоким.
  2. Правильный способ. Устанавливать постоянное и длительное соединение с сервером, ожидая поступления данных через него. Если сообщений нет, соединение просто держится открытым на протяжение нескольких минут. Если соединение по каким-либо причинам закрылось, оно вновь открывается. В итоге и трафика потребляется мало, и нагрузка на сервер оказывается невелика. Так работает GMail, Мой Круг и т. д., и именно на этом принципе построен dklab_multiplexor.

Краткая инструкция по применению

Мультиплексор — событийно-ориентированный демон, написанный на Perl с применением библиотеки libevent. Рабочий расход памяти — порядка 10М на 1000 одновременных соединений. Начните с просмотра умолчательного файла конфигурации dklab_multiplexor.conf.

Запустим Мультиплексор

Запустите Мультиплексор и оставьте его работать:

Листинг 1: Запуск мультиплексора
# cd /path/to/dklab_multiplexor
# perl dklab_multiplexor.pl >/var/log/multiplexor.log 2>&1 &

Попробуйте telnet-ом установить соединение с портом 8088 (см. WAIT_ADDR). Если telnet "подвиснет", значит, все работает правильно:

Листинг 2: Проверка, что Мультиплексор запустился
# telnet localhost 8088

Если Мультиплексор не запустился, скорее всего, в вашей системе отсутствует библиотеки libevent и Event::Lib. Установить их в RHEL-системе можно командами:

Листинг 3: Установка необходимых библиотек в RHEL, CentOS и т. д.
# yum install libevent-devel
# perl -MCPAN -e "install Event::Lib"

Прикинемся браузером клиента

Попробуем теперь проэмулировать браузер Клиента с идентификатором 1z2y3z. Запустите команду:

Листинг 4: Эмуляция браузера Клиента
# wget -O- http://localhost:8088/?identifier=1z2y3z

Команда wget -O- открывает HTTP-соединение по указанному URL и распечатывает ответ сервера. Вы увидите, что wget как бы "подвис". Это нормально: "браузер" ждет, когда кто-то отправит в Мультиплексор блок данных для пользователя с идентификатором 1z2y3z.

Лирическое отступление 
По умолчанию время ожидание WAIT_TIMEOUT равно 300 секундам. Если за это время ответ не приходит, соединение принудительно закрывается, и JavaScript-код Клиента, который вы напишете, должен установить новое соединение.

Итак, в реальной ситуации JavaScript-код Клиента устанавливает HTTP-соединение с Мультиплексором, используя XMLHttpRequest. При выполнении GET-запроса JavaScript указывает идентификатор пользователя, чтобы получать только сообщения для этого пользователя. Клиент ждет либо прихода данных о новых сообщениях от Мультиплексора (тогда он его отображает), либо же завершения соединения через 300 секунд. В обоих случаях Клиент сразу же устанавливает новое соединение с Мультиплексором и ждет от него нового ответа, и так до бесконечности. Таким образом, за счет медленных соединений поддерживается низкая загрузка сервера, а также обеспечивается мгновенность передачи нового сообщения от Сервера к Клиенту.

Передадим Мультиплексору данные для Клиента

Откройте вторую консоль на сервере, где у вас "висит" только что запущенный wget. Давайте передадим Мультиплексору на порт 10010 (см. IN_ADDR) строчку "Hello!" и укажем, что она предназначена для клиента с идентификатором 1z2y3z. Наберите в консоли команду:

Листинг 5: Передача данных для Клиентов xxx, yyy и 1z2y3z
# telnet localhost 10010
HTTP/1.1 200 OK
X-Multiplexor: identifier=xxx,yyy,1z2y3z

Hello!

После этого нажмите Ctrl+], потом q и Enter, чтобы окончить передачу данных. Постарайтесь уложиться в 20 секунд (см. IN_TIMEOUT), т.к. иначе Мультиплексор сам закроет соединение, не дождавшись данных.

Лирическое отступление 
Обратите внимание: в одном запросе можно указывать сразу несколько ID Клиентов, разделяя их запятыми (без пробелов!). В нашем случае мы для иллюстрации посылаем данные сразу трем Клиентам: xxx, yyy и, конечно же, 1z2y3z.

Мультиплексор реализует буферизацию данных. Таким образом, если на момент передачи сообщения некоторый Клиент не был подключен к Мультиплексору (например, он как раз переходит на другую страницу сайта), Мультиплексор сохранит сообщение (максимум на 20 секунд, см. OFFLINE_TIMEOUT) и передаст его Клиенту, как только он подключится.

Фактически, мы сейчас "руками" проэмулировали то, что должен делать скрипт на сайте при приходе нового сообщения для пользователя 1z2y3z. Соответствующий PHP-код может выглядеть так:

Листинг 6: Отправка данных Мультиплексору
$f = fsockopen("localhost", "10010");
fwrite($f, 
  "HTTP/1.1 200 OK\n" .
  "X-Multiplexor: identifier=xxx,yyy,1z2y3z\n" .
  "\n" .
  "Hello!\n"
);
fclose($f);

Чайник 

Вместо X-Multiplexor можно использовать любой другой заголовок.
Мультиплексор ищет строчку identifier=* в любом месте передаваемых данных.

Ура, заработало!

Если вы все сделали правильно, то wget -O- в соседней консоли "отвис", а на экране появилась строчка "Hello!". Можно видеть, что Клиенту пришли в точности те данные, которые были отправлены Мультиплексору, "байт в байт". С точным протоколом передачи информации между сервером и JavaScript-частью вы должны определиться сами при разработки скриптов вашего сайта.

Команда ONLINE: получение списка online-клиентов

Если послать Мультиплексору не текст HTTP-ответа для браузера, а строчку "ONLINE\n", то он ответит в соединение списком всех ID клиентов, находящихся в online-режиме. Элементы списка разделяются запятыми, а сам список оканчивается точкой "." на отдельной строке. (Точка нужна, чтобы удостовериться: данные были прочитаны полностью и не оборвались.) Результат может выглядеть, например, так:

Листинг 7: Возможный результат команды ONLINE
xxx,yyy,1z2y3z
.

Пример PHP-кода:

Листинг 8: Запрос ID всех клиентов, находящихся online
$f = fsockopen("localhost", "10010");
fwrite($f, "ONLINE\n");
stream_socket_shutdown($f, STREAM_SHUT_WR);
$ids = stream_get_contents($f);
fclose($f);
if (substr($ids, -1) == ".") {
  // Checked that ALL data is received ("." at the end).
  print_r(explode(",", trim(substr($ids, 0, -1))));
}





Подробная инструкция и технические детали

Итак, мультиплексор соединений dklab_multiplexor (далее "Мультиплексор") — это демон, позволяющий держать значительное количество открытых ждущих TCP-соединений (несколько десятков тысяч) и передавать через них сообщение в сторону "Сервер -> Клиент". Протокол, стоящий выше TCP-уровня, при этом не специфицируется и может быть любым (например HTTP).

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

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

+-------------------+                    ------------------ 
| Сервер обработки  |                   |                  |   <===WAIT=== Клиент A
| и базы данных     |  ======IN=======> |   Мультиплексор  |   <===WAIT=== Клиент B
| (e.g Apache + PHP |                   |                  |   <===WAIT=== Клиент C
+-------------------+                    ------------------ 
(указаны направления установления TCP-соединений).

Каждой линии выделен отдельный TCP-порт (указываются в конфигурации). Например, линия WAIT может иметь порт 8088, а линия IN — порт 10010.

  • Линия WAIT — медленная. Это означает, что длительность соединения по ней составляет минуты, и поэтому число одновременных соединений может достигать сотен тысяч. Клиент устанавливает соединение с портом WAIT и посылает в него данные, идентифицирующие в дальнейшем Клиента, после чего ожидает ответа от Мультиплексора. Ответ, как правило, представляет собой команду клиенту, определяемую протоколом вышестоящего уровня. Команды никак не интерпретируются Мультиплексором; он передает их как обычный поток байтов, не анализируя.
  • Линия IN — быстрая. Это значит, что Сервер устанавливает соединение по ней, передает команды, снабженные идентификатором Клиента, и сразу же отключается. Каждая команда помещается во внутреннюю очередь, связанную с Клиентом, которому предназначена команда. Затем команды из очереди переправляются соответствующим Клиентам через линию WAIT. Мультиплексор никак не интерпретирует передаваемые данные: для него это просто массив байтов.

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

Чайник 

Заметим, что передача данных от Клиента к Серверу должна идти "мимо" Мультиплексора. Она обычно обрабатывается сторонними средствами (т.к. там не нужна поддержка большого числа соединений, а сами эти соединения "быстрые"). Например, в случае реализации чат-клиента с использованием Мультиплексора входящие сообщения Клиент читает из WAIT-линии Мультиплексора, а исходящие сообщения посылает либо напрямую на Сервер, либо на привязанный к нему балансер, либо на какой-то другой прокси-сервер, не затрагивая при этом Мультиплексор. Итак, прием входящих сообщений — это не задача Мультиплексора.

Идентификация Клиента

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

Протокол запроса линии WAIT

Клиент присоединяется к линии WAIT (например, при помощи XMLHttpRequest) и передает в нее массив байтов. Внутри этого массива должен содержаться идентификатор Клиента в формате, определяемом следующим регулярным выражением:

\bidentifier=([a-zA-Z0-9_]+)\W

Мультиплексор анализирует байты на наличие данной последовательности. Это позволяет использовать Мультиплексор для практически любого вышестоящего протокола: например, в случае HTTP-протокола идентификатор можно прислать в теле GET-запроса: /1z2y3z?identifier=123456

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

Идентификатор должен встретиться в первых WAIT_MAXLEN байтах запроса, в противном случае соединение принудительно разрывается.

Протокол ответа линии WAIT

Формат данных никак не специфицируется. Данные извлекаются из очереди и передаются Клиенту в том виде, в котором они поступили от Сервера, без изменений и анализа. (Например, в случае HTTP-протокола это может быть обычный HTTP-ответ.) Если очередь пуста, Мультиплексор ожидает ее пополнения и, как только данные пришли, отправляет их Клиенту. Если данные не приходят в течение WAIT_TIMEOUT секунд, Мультиплексор принудительно закрывает соединение, не отправляя никаких данных Клиенту.

После отправки ответа Мультиплексор закрывает соединение с Клиентом.

Протокол линии IN

Линия IN используется для передачи как данных, так и команд в Мультиплексор.

Линия IN: передача данных для Клиента

Протокол практически идентичен протоколу запроса линии WAIT: Сервер передает данные для Клиента, снабжая их идентификатором Клиента прямо в теле данных. Весь обмен может занимать не более IN_TIMEOUT секунд, а размер запроса не может превышать IN_MAXLEN байтов.

Имеется одно важное отличие протокола линии IN от протокола WAIT: в линии IN можно указывать сразу несколько ID Клиентов через запятую.

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

Линия IN: ONLINE: запрос списка online-Клиентов

Чтобы получить список online-Клиентов, нужно послать в линию IN строчку: "ONLINE\n".

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

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

Автоматическая очистка очереди и неактивные онлайн-Клиенты

Вначале немного терминологии.

  • Клиент называется активным, если он имеет текущее открытое соединение с Мультиплексором.
  • Клиент называется online, если он либо активный, либо отключился "совсем недавно" (см. ниже).
  • Клиент называется offline, если он не имеет текущего соединения с Мультиплексором, а также отключился "давно".

Сразу же после подключения Клиент объявляется online-Клиентом. Если Клиент не переподключился в течение OFFLINE_TIMEOUT секунд после своего последнего отсоединения, он объявляется offline-Клиентом. Для offline-клиентов очередь команд очищается. Кроме того, попытка добавить новую команду для offline-клиента вызывает ее игнорирование.

Данное поведение предотвращает переполнение очереди команд, а также позволяет отслеживать внутри Мультиплексора, какие Клиенты находятся в логическом состоянии online, а какие — в offline, независимо от того, имеют они текущее подключение к Мультиплексору или нет.

Конфигурация и расположение

Основной файл Мультиплексора располагается в dklab_multiplexor.pl. При его запуске происходит считывание двух конфигурационных файлов:

  1. ./dklab_multiplexor.conf
  2. указанный в аргументах командной строки (например, /etc/dklab_multiplexor.conf)

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

Потребление ресурсов

Мультиплексор может слушать как один, так и несколько IP-адресов и портов в системе. Адреса и порты задаются в директива WAIT_ADDR. Указав несколько IP-адресов, можно принимать огромное число одновременных входящих соединений (сотни тысяч и миллионы) в рамках одной машины. Расход памяти Мультипдексора составляет примерно 10М на тысячу открытых соединений.

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

К сожалению, библиотека Perl Event::Lib не может работать в многопоточном режиме, поэтому в случае наличия нескольких процессоров в системе Мультиплексор будет использовать только один из них. В будущих версиях Event::Lib это обещают исправить, так что можно будет доработать Мультиплексор для полного использования ресурсов процессора.

Чтобы снять на число открытых сокетов (и файлов), установленное по умолчанию в некоторых ОС равным 1024, сразу после запуска Мультиплексор выполняет команду:

ulimit -n 1048576

Сопряжение с балансером

Демон Мультиплексора по умолчанию слушает порт 8088. Чтобы получить к нему доступ, на балансере можно использовать правило: переадресовывать GET-запросы на /multiplexor* на 127.0.0.1:8080. Таким образом, к Мультиплексору можно обращаться, например, по такому адресу:

http://example.com/multiplexor?identifier=abcd

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

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

http://example.com/multiplexor?identifier=abcd

Браузер "подвиснет" в ожидании события. Затем на сервере быстро (пока не закончился тайм-аут) запустить:

telnet localhost 10010

(это порт линии сервера) и в страшной спешке впечатать туда:

HTTP 200 OK
Content-type: text/html
X-multilexor: identifier=abcd

A message from the server!

Затем нажать Ctrl+] и q (чтобы завершить соединение). Браузер должен "отвиснуть" и отобразить текст сообщения.

Тут главное — сделать все быстро, чтобы не был превышен тайм-аут по линии IN (по умолчанию 20 секунд). Значения тайм-аутов см. в конфигурации Мультиплексора. Если в целях тестирования нужно запустить еще одну копию Мультиплексора на другом порту, это можно сделать, поменяв конфигурационный файл.







Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com