- PVSM.RU - https://www.pvsm.ru -
Tarantool — это не просто база данных. Tarantool — это app-сервер с базой данных на борту, поэтому для реализации кое-каких вещей, на которые люди тратят большое количество времени, с Tarantool нужно очень немного ресурсов.
На написание данной статьи меня натолкнула эта статья [1].
Очень много людей в IT-мире занимается одним и тем же. Расскажу о своем опыте решения этих же проблем.
Несколько лет назад мы копали ровно ту же задачу: пользователь хочет в веб-приложении в реальном времени (с задержками, определяемыми только сетью) получать уведомления о тех или иных событиях.
Рассмотрим требования в указанной статье:
Поскольку данных недостаточно, сделаем дополнительно несколько допущений/прикидок. Если это, например, некий чат, где весь миллион пользователей что-то постоянно пишет, то предположим, что один пользователь создает одно сообщение за 10 секунд. Тогда получим, что миллион пользователей генерирует 100 тыс. сообщений в секунду.
И тут появляется первый важный аспект: что передаем?
Это традиционная дилемма всех очередей, агрегаторов, серверов событий: что является единицей хранения? Поэтому введем понятия:
С событием могут быть связаны данные:
Например: пользователь 345
оформил заказ на перевозку груза X
из точки А
в точку Б
, при оформлении задействовал банковскую карту Z
и т. п.
Информацию о том, кто (источник события) оформил заказ (факт события), будем называть событием, а информацию, откуда транспортируется груз, какой груз, прочие сведения о пользователе, заказе и грузе, — данными. То есть событие — это структура, описывающая в себе факт события, а данные — это все остальное.
Линия разделения «событие — данные» условная, но все же.
Пример события:
{
"type": "order",
"user": 345,
"status": "created",
"orderid": 12345
}
Пользователь 345
создал заказ 12345
. Заказ на момент отправки события имел статус created
(это уже избыточные данные).
Теперь сформирую некое эмпирическое правило правильного архитектурного выбора:
При разработке очередей, агрегаторов, серверов событий серверы должны манипулировать именно событиями, а не данными.
Однако еще раз повторюсь, линия разделения условная: событие из примера содержит часть данных (поле status
).
Итак, вернемся к задаче: мы строим именно сервер событий, поэтому можем прикинуть трафик. Например, среднее событие будет представлять собой JSON-хеш от 4 до 10 элементов — текст размером 60—160 байт. То есть поток событий, обеспечивающий работу миллиона пользователей (100 тыс. событий в секунду), по средним прикидкам, составит от 6 до 16 мегабайт в секунду. Для того чтобы прокачать этот трафик через один узел сети, достаточно сети с пропускной способностью — 200 мегабит в секунду.
Теперь прикинем, сколько ресурсов надо на то, чтобы доставить эти события миллиону пользователей. У каждого, конечно, своя архитектура, но можно говорить о некоторых общих принципах. Скорее всего, если одно сообщение надо доставить миллиону пользователей, то это ошибка в архитектуре (хотя и такое бывает). Нам же надо задаться какой-то средней величиной. Будем считать, что одно событие доставляется в среднем десяти пользователям: в чате у вас в друзьях редко будет более 10 друзей онлайн, если говорить об исполнении заказов — редко будет более 10 исполнителей и т. п.
Таким образом, чтобы доставить события в нашей задаче до пользователей, нужно где-то 2—3 гигабит трафика. Поэтому имеем второй ключевой аспект: данную задачу можно решить, используя всего один современный сервер с сетевой картой на 10 гигабит и RAM ~10 гигабайт (если выбрать интервал кеширования 10 минут). Убедившись, что данную систему можно строить на одном сервере, попытаемся построить реальную систему масштабируемо и на нескольких.
Один из самых быстрых способов хранения закешированных данных на диске — WAL-лог: данные поступают в кеш RAM и дописываются в WAL-лог. Поскольку данные в WAL-лог только пишутся, пишутся в режиме append
, то таким способом можно утилизировать практически 100 % пропускной способности записи диска. Опущу тут рассмотрение недостатков WAL, упомяну лишь то, что WAL-логи очень хорошо приспособлены к репликации.
В БД Tarantool [2] реализован WAL-лог, он не только позволяет реплицировать данные на другой хост, но и предоставляет двунаправленную асинхронную мастер-мастер репликацию.
Бенчмарки Тарантула на средненьком ноутбуке (2012 года выпуска) на размере сообщения 220 байт показывают производительность 160—180 тыс. записей в секунду, что в полтора-два раза больше, чем нам требуется для данной задачи.
Способов доставки данных может быть множество (мы поговорим подробнее о них позже). Сейчас мы рассмотрим пока не транспортную, а алгоритмическую часть способа доставки.
Для того чтобы доставка работала в условиях реального мира, выдвигаем к ней следующие требования:
Опираясь на эти требования, методом проб и ошибок мы пришли к следующей схеме клиента:
Соответственно, под подобный алгоритм работы клиента подходит в качестве транспорта как обычный long-polling (в каждом запросе передается номер последнего принятого сообщения), так и websocket (номер последнего принятого сообщения передается только при реконнектах).
Методом проб и ошибок мы пришли к тому, что все события мы характеризуем уникальным ключом события. Уникальный ключ события представляет собой в общем виде строковый идентификатор события. Поскольку зачастую этот строковый идентификатор формируется из нескольких разных идентификаторов, то мы пришли к тому, что идентификатором события является некий массив строковых идентификаторов.
Например: пользователь 123
пишет сообщение в чат 345
, упоминая пользователя 567
. Генерируется событие с ключом [ 'chat', 345 ]
, которое доставляется всем онлайн-пользователям, находящимся в чате 345
, и еще одно событие ['user', 567]
, которое получает пользователь 567
.
В развернутом виде эти события могут выглядеть, например, так:
{
"key": [ "chat", 345 ],
"data": {
"type": "new_message",
"msgid": 9876
}
}
и
{
"key": [ "user", 567 ],
"data": {
"type": "notice",
"chatid": 345,
"msgid": 9876
}
}
Мы подошли к схеме формирования ключей сообщений.
Не имеет большого смысла (и даже иногда вредно) иметь множество ключей сообщений под примерно одинаковые вещи.
Имеет смысл выделять новый ключ только для качественно иной сущности.
Пример 1: имеет смысл использовать один ключ для задач:
Пример 2: имеет смысл использовать разные ключи для задач:
То есть ключ сообщения должен примерно определять круг получателей. Воспринимайте ключ примерно как адрес на конверте.
Определившись со схемой данных и событий, подходим к реализации проекта в железе.
Сообщения будем хранить в плоской таблице вида:
Для этой таблицы нам понадобится два индекса:
Схему получившейся у меня БД можно посмотреть здесь [3].
БД Tarantool [2] помогает нам легко писать pub/sub-приложения при помощи встроенной библиотеки fiber
.
Каждый клиентский запрос обрабатывается в отдельном fiber
(легковесный аналог процесса). При помощи этой парадигмы легко обслуживать десятки тысяч соединений одним процессором, причем:
Алгоритм подписки (subscribe) на один ключ примерно следующий:
fiber
в список fiber
’ов, подписанных на данный ключ.Алгоритм записи (push) примерно следующий:
fiber
.Весь серверный код уместился менее чем в 500 строк кода на LUA, при этом код включает в себя еще и масштабирование системы на несколько CPU/серверов.
В данный момент эта система, функционируя на трех Тарантулах (расположенных на одном виртуальном (OpenVZ нода) сервере), утилизирует на 10 % два выделенных ей ядра CPU и обслуживает где-то 50 тыс. пользователей.
По расчетам, на этом одном «железном» сервере можно спокойно крутить где-то 500 тыс. пользователей. Возможно, потребуется выделить еще ядро-два CPU.
Проблемы с числом сокетов на хост, описанные в упомянутой выше статье, решаются идентично.
На (каждом) мастер-инстансе работает демон (fiber
) очистки, удаляющий устаревшие сообщения. Алгоритм демона примитивен:
Начали мы делать эту систему еще во времена Tarantool 1.5, который еще не умел делать двунаправленную асинхронную репликацию. Поэтому архитектурно система представляет собой:
Мастер и реплики — полностью идентичные инстансы, просто push делаем (пока) строго в один сервер.
То есть в данный момент масштабирование производится добавлением реплик, а максимальная производительность ограничена производительностью одного мастера (для серверного современного юнита это где-то 400—500 тыс. сообщений в секунду).
Поскольку на Tarantool 1.6 появилась двунаправленная мастер-мастер репликация, то возникла возможность масштабироваться и в сторону ее использования. План примерно такой (пока не реализовано):
В остальном алгоритм не меняется. Таким образом можно отмасштабироваться без серьезного изменения архитектуры до 10—30 мастер-серверов (то есть 4—20 млн исходящих сообщений в секунду).
Она включает в себя простой асинхронный http-сервер (для случая long polling), ну, или асинхронный сервер посложнее (для случая websocket + lp). На Perl + AnyEvent данный сервер-прослойка займет где-то 200 строк кода.
Мы не используем клиентскую авторизацию в подсистеме сервера событий (клиент в данном контексте — пользователь сайта), поскольку не видим необходимости.
Но в принципе, добавив к каждому сообщению пару «ключ — значение», информирующую, «кому можно увидеть эти данные», и сравнив ее, например, с информацией из кук запроса, эту авторизацию несложно сделать.
Поскольку мы реализуем сервер, манипулирующий событиями, а не данными, пока нам не требовалось возиться с авторизацией.
Мы стали работать в этом направлении еще во времена, когда только начинались разговоры о веб-сокетах и появлялись draft’ы стандартов. Поэтому очень долго основным транспортом был (и во многом остается) обычный http long polling. Эксперименты с веб-сокетами в условиях мобильных сетей показали, что:
Поэтому с точки зрения интерактивных сайтов, работающих на мобильных сетях, говорить о применении веб-сокетов уже можно (можно охватить около 70 % устройств), но пока все-таки рановато (30 % неохваченных — это много).
Если в составе веб-проекта имеются очередь и сервер событий, то многие вещи, которые вызывают сложности в других архитектурах, делаются просто.
Вариант с чатом, описанный выше, очевиден. Еще очень красивое применение сервера событий — работа с длительными/затратными алгоритмами (генерация отчетов, сбор статистики и даже кодирование видео).
Например, мы хотим перекодировать видео пользователей. Понятно, что процесс этот длительный, выполнять его в обработчике запросов HTTP нельзя. Пользователь, загрузив видео, хочет понимать, что происходит. Ставим задачу конвертации видео в очередь, пользовательский JS запоминает номер задачи и начинает «ловить» события, связанные с ней. Процесс конвертации отправляет события по мере выполнения задачи. Браузер на основании событий может показывать аккуратный и, главное, актуальный прогресс-бар.
Автор: linuxover
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/267325
Ссылки в тексте:
[1] эта статья: https://habrahabr.ru/company/tensor/blog/341068/
[2] Tarantool: http://tarantool.org
[3] можно посмотреть здесь: https://github.com/dr-co/lp/blob/master/lua/lp/migrations.lua#L14
[4] проблема 10К: https://ru.wikipedia.org/wiki/C10k
[5] Проект сервера сообщений: https://github.com/dr-co/lp/blob/master/README.rus.md
[6] Еще идентичный проект на Tarantool: https://habrahabr.ru/company/mailru/blog/232981/
[7] Данная статья в Git: https://github.com/unera/lp-article
[8] Источник: https://habrahabr.ru/post/341498/
Нажмите здесь для печати.