Tarantool — это не просто база данных. Tarantool — это app-сервер с базой данных на борту, поэтому для реализации кое-каких вещей, на которые люди тратят большое количество времени, с Tarantool нужно очень немного ресурсов.
На написание данной статьи меня натолкнула эта статья.
Очень много людей в IT-мире занимается одним и тем же. Расскажу о своем опыте решения этих же проблем.
Несколько лет назад мы копали ровно ту же задачу: пользователь хочет в веб-приложении в реальном времени (с задержками, определяемыми только сетью) получать уведомления о тех или иных событиях.
Рассмотрим требования в указанной статье:
- на странице «слушаем» в среднем 10 событий;
- миллион пользователей;
- требуется персистентность (терять события мы не хотим).
Поскольку данных недостаточно, сделаем дополнительно несколько допущений/прикидок. Если это, например, некий чат, где весь миллион пользователей что-то постоянно пишет, то предположим, что один пользователь создает одно сообщение за 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 реализован 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: имеет смысл использовать разные ключи для задач:
- послать всем пользователям чата уведомление о новом сообщении;
- послать пользователю X чата уведомление о том, что его упомянули.
То есть ключ сообщения должен примерно определять круг получателей. Воспринимайте ключ примерно как адрес на конверте.
Реализация
Определившись со схемой данных и событий, подходим к реализации проекта в железе.
Сообщения будем хранить в плоской таблице вида:
- номер сообщения (постоянно возрастающая последовательность);
- время сохранения события;
- ключ сообщения;
- данные сообщения.
Для этой таблицы нам понадобится два индекса:
- индекс по номеру сообщения (Primary Key). Этот индекс будет использоваться в алгоритмах чистки БД от старых сообщений;
- индекс для выборки сообщений по ключу (составной: ключ: номер)
Схему получившейся у меня БД можно посмотреть здесь.
БД Tarantool помогает нам легко писать pub/sub-приложения при помощи встроенной библиотеки fiber
.
Каждый клиентский запрос обрабатывается в отдельном fiber
(легковесный аналог процесса). При помощи этой парадигмы легко обслуживать десятки тысяч соединений одним процессором, причем:
- код не разбит на «лапшу» из callback’ов (как Node.js);
- легко решается проблема 10К.
Алгоритм подписки (subscribe) на один ключ примерно следующий:
- Смотрим, есть ли данные по ключу (новые события), если есть — сразу их возвращаем.
- Записываем текущий
fiber
в списокfiber
’ов, подписанных на данный ключ. - Засыпаем на некоторый таймаут (для мобильных сетей, чтобы они не рвали websocket’ы, экспериментально установлено хорошее значение — 25 секунд).
- Отвечаем пользователю (в том числе и пустым ответом, об этом ниже).
Алгоритм записи (push) примерно следующий:
- Записываем новое событие в БД.
- Если есть подписанные на записанные ключи клиенты — будим их
fiber
.
Весь серверный код уместился менее чем в 500 строк кода на LUA, при этом код включает в себя еще и масштабирование системы на несколько CPU/серверов.
В данный момент эта система, функционируя на трех Тарантулах (расположенных на одном виртуальном (OpenVZ нода) сервере), утилизирует на 10 % два выделенных ей ядра CPU и обслуживает где-то 50 тыс. пользователей.
По расчетам, на этом одном «железном» сервере можно спокойно крутить где-то 500 тыс. пользователей. Возможно, потребуется выделить еще ядро-два CPU.
Проблемы с числом сокетов на хост, описанные в упомянутой выше статье, решаются идентично.
Чистка старых сообщений
На (каждом) мастер-инстансе работает демон (fiber
) очистки, удаляющий устаревшие сообщения. Алгоритм демона примитивен:
- Выбрать самое старое (минимальный номер) сообщение.
- Посмотреть время его создания.
- Если время жизни не исчерпалось — подождать необходимый интервал.
- Удалить старое сообщение.
- Перейти к п. 1.
Масштабирование
Начали мы делать эту систему еще во времена Tarantool 1.5, который еще не умел делать двунаправленную асинхронную репликацию. Поэтому архитектурно система представляет собой:
- мастер-сервер (в него можно делать push сообщений);
- реплики (к ним могут коннектиться клиенты).
Мастер и реплики — полностью идентичные инстансы, просто push делаем (пока) строго в один сервер.
То есть в данный момент масштабирование производится добавлением реплик, а максимальная производительность ограничена производительностью одного мастера (для серверного современного юнита это где-то 400—500 тыс. сообщений в секунду).
Развитие
Поскольку на Tarantool 1.6 появилась двунаправленная мастер-мастер репликация, то возникла возможность масштабироваться и в сторону ее использования. План примерно такой (пока не реализовано):
- преобразуем номер сообщения в массив: номер мастер-сервера, номер сообщения;
- клиент между реконнектами «помнит» не одно значение, а этот массив значений;
- PROFIT!
В остальном алгоритм не меняется. Таким образом можно отмасштабироваться без серьезного изменения архитектуры до 10—30 мастер-серверов (то есть 4—20 млн исходящих сообщений в секунду).
Недостатки (куда без них)
- LUA (главный недостаток). Язык простой, но ограничения (1 гигабайт RAM на инстанс) заставляют масштабироваться несколько раньше, чем мы могли бы достигнуть пределов роста в рамках одной «железки».
- К сожалению, транспортную http-часть мы пока не выложили в открытый доступ (причина — зависимости от сугубо внутренних модулей вроде чтения конфигов и т. п.).
Она включает в себя простой асинхронный http-сервер (для случая long polling), ну, или асинхронный сервер посложнее (для случая websocket + lp). На Perl + AnyEvent данный сервер-прослойка займет где-то 200 строк кода.
Клиентская авторизация
Мы не используем клиентскую авторизацию в подсистеме сервера событий (клиент в данном контексте — пользователь сайта), поскольку не видим необходимости.
Но в принципе, добавив к каждому сообщению пару «ключ — значение», информирующую, «кому можно увидеть эти данные», и сравнив ее, например, с информацией из кук запроса, эту авторизацию несложно сделать.
Поскольку мы реализуем сервер, манипулирующий событиями, а не данными, пока нам не требовалось возиться с авторизацией.
О транспорте
Мы стали работать в этом направлении еще во времена, когда только начинались разговоры о веб-сокетах и появлялись draft’ы стандартов. Поэтому очень долго основным транспортом был (и во многом остается) обычный http long polling. Эксперименты с веб-сокетами в условиях мобильных сетей показали, что:
- Веб-сокет требует прокачки через себя события (пусть и пустого) один-два раза в минуту, иначе мобильные сети принудительно закрывают соединение.
- Вследствие этого нивелируется разница между веб-сокетом и long polling + keep alive.
- Есть еще довольно много мобильных устройств, браузеры которых не дружат с веб-сокетами.
Поэтому с точки зрения интерактивных сайтов, работающих на мобильных сетях, говорить о применении веб-сокетов уже можно (можно охватить около 70 % устройств), но пока все-таки рановато (30 % неохваченных — это много).
Применения
Если в составе веб-проекта имеются очередь и сервер событий, то многие вещи, которые вызывают сложности в других архитектурах, делаются просто.
Вариант с чатом, описанный выше, очевиден. Еще очень красивое применение сервера событий — работа с длительными/затратными алгоритмами (генерация отчетов, сбор статистики и даже кодирование видео).
Например, мы хотим перекодировать видео пользователей. Понятно, что процесс этот длительный, выполнять его в обработчике запросов HTTP нельзя. Пользователь, загрузив видео, хочет понимать, что происходит. Ставим задачу конвертации видео в очередь, пользовательский JS запоминает номер задачи и начинает «ловить» события, связанные с ней. Процесс конвертации отправляет события по мере выполнения задачи. Браузер на основании событий может показывать аккуратный и, главное, актуальный прогресс-бар.
Ссылки
Комментарии (40)
alterpub
02.11.2017 16:08Дмитрий, а возможно использовать Tarantool вместо Influxdb как tsdb?
linuxover Автор
02.11.2017 16:09насколько я знаю в Mail.ru используют тарантул и по такому назначению, но сам не копался в таком направлении :)
blind_oracle
02.11.2017 16:19Из двух спиц и буханки хлеба можно сделать троллейбус — но зачем?
linuxover Автор
02.11.2017 16:29мне показалось что реализация сервера событий на RabbitMQ — это как раз черезчур избыточное и тяжеловесное решение.
Потому я статью и написал
diamond_nsk
02.11.2017 16:36{
«type»: «order»,
«user»: 345,
«status»: «created»,
«orderid»: 12345
}
…
Однако еще раз повторюсь, линия разделения условная: событие из примера содержит часть данных (поле status).
Ну так-то все элементы вышеуказанной структуры события можно считать избыточными, а не только status, т.к. элемента
«orderid»: 12345
достаточно, чтобы понять, что событием является заказ, и по номеру заказа при необходимости можно получить оставшиеся данные (клиента, статус). А вот если на принимающей стороне надо как-то особо реагировать, например, именно на новые заказы («created»), то этот элемент («status») уже не является избыточным.linuxover Автор
02.11.2017 16:38ага: это как с нормализацией данных в БД. Если все нормализовывать, то хайлоад вряд ли получится.
а если все денормализовывать, то вряд ли получится что-то приличное.
поэтому общий подход такой: стараемся нормализовывать, но там где скорость важна — денормализуем.
так и с событиями: понятно что убрав ВСЕ данные из системы событий — получаем нулевую ценность этой самой системы. Но стремясь сохранять минимум данных в этой системе — получаем максимум профита.
как-то так :)Akuma
02.11.2017 22:44это как с нормализацией данных в БД. Если все нормализовывать, то хайлоад вряд ли получится. а если все денормализовывать, то вряд ли получится что-то приличное.
Надо запомнить это выражение и говорить его всем, кто пинает за денорамализацию данных :)
marsdenden
02.11.2017 21:43клиент получает первый вариант сообщения — он уже знает, как ему среагировать,
клиент получает второй вариант сообщения — ему придется делать дополнительный запрос, чтобы понять как среагировать
Реагировать придется в любом случае, но сэкономить дополнительные пару десятков байт и получить лишние запросы? А зачем?
bgnx
02.11.2017 17:12Поскольку данные в WAL-лог только пишутся, пишутся в режиме append, то таким способом можно утилизировать практически 100 % пропускной способности записи диска.
Что-то не верится в такую скорость работы базы данных. На сколько я знаю утилизировать 100% пропускной способности записи на диск можно только через буфер а это значит что мы жертвуем надежностью и после внезапного выключения сервера мы потеряем n записей которые мы якобы записали на диск отправив клиенту 200 ok.
Бенчмарки Тарантула на средненьком ноутбуке (2012 года выпуска) на размере сообщения 220 байт показывают производительность 160—180 тыс. записей в секунду, что в полтора-два раза больше, чем нам требуется для данной задачи.
А можно больше технических подробностей? Выполняется ли fsync после каждой записи в 220 байт? Если нет, то какой размер буфера (чтобы понимать сколько данных потеряются при выключении сервера) и какая тогда будет реальная производительность базы данных при fsync после каждой записи ?
linuxover Автор
02.11.2017 17:33> Что-то не верится в такую скорость работы базы данных.
вот мой бенчмарк: gist.github.com/unera/2ca7d3122c58757cf5dde9a4f61af5dc
попробуйте сколько на Вашей системе получится. Только удаляйте файлы 000* перед каждым запуском, а то много мусора получится в текущей директорииbgnx
02.11.2017 19:35У меня получилось где-то
2017-11-02 18:42:39.349 [2913] main/101/bench.lua I> RPS: 188622.15003245 2017-11-02 18:42:39.849 [2913] main/101/bench.lua I> RPS: 194763.78046304
Как-то сильно сомневаюсь что тарантул делает честный fsync после каждых 220 байт. Я тут решил провести эксперимент на ноде
const fs = require('fs'); const N = 1000000; const BufferLength = 1024*100 const fd = fs.openSync(__dirname+'/db.db', 'a'); const data = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; console.log('---start'); const time = Date.now(); let buffer = ''; for(let i = 0; i < N; i++){ buffer += data if(buffer.length > BufferLength){ fs.writeSync(fd, buffer, null, 'utf8'); buffer = ''; //fs.fsyncSync(fd) } } if(buffer.length){ fs.writeSync(fd, buffer, null, 'utf8'); } const timeEnd = Date.now()-time; console.log('time', timeEnd);
При буфере в 100кб происходит запись 1млн 220байтных строк у меня где-то за 840мс. При уменьшении буфера до 1кб — за 3.5 секунд. При нулевом буфере (вызов записи после каждых 220 байт) — 13-15с. Но это только систный вызов записи на диск. А ос имеет еще свой кеш или буфер и для того чтобы мы могли быть уверенны что записали 220 байт на диск нужно вызвать fsync и ос тогда сбросит этот буфер на диск. И если я откомментирую строчку
fs.fsyncSync(fd)
то производительность падает на пару порядков — запись 1000 строк по 220байт с нулевым буфером у меня занимает целых 6 секунд!linuxover Автор
02.11.2017 20:04> Как-то сильно сомневаюсь что тарантул делает честный fsync после каждых 220 байт. Я тут решил провести эксперимент на ноде
тарантул группирует fsync'и (я ж выше писал), но возвращает управление после update/insert/replace только после fsync. То есть когда Вы делаете update он может задержать реальную запись на диск с тем чтобы скомпоновать ее с параллельными записями.
в итоге получается практически максимальная производительность диска.bgnx
02.11.2017 20:38Спасибо, теперь понятно. То есть все равно получается один fsync на множество записей, но поскольку каждый запрос на запись ждет других запросов пока не заполнится буфер, то потеря этих данных не страшна для клиента который пишет что-то в базу как часть транзакции так как управление не возвращается
blind_oracle
02.11.2017 19:38Что-то не верится в такую скорость работы базы данных.
Там же запись последовательная, а не рандомная как в обычной РСУБД, так что почему бы и нет?
Любая современная NoSQL база со схожим подходом к хранению данных показывает примерно аналогичные результаты.Varim
02.11.2017 21:12Обычная РСУБД тоже сначала в лог пишет, а потом, когда нибудь, буферы страниц сбрасывает.
blind_oracle
02.11.2017 21:32Ну, вот это «когда-нибудь» и есть *много-много рандомных IOPS*. Просто отложенные, но сути это ведь не меняет.
linuxover Автор
02.11.2017 21:40дык валидный WAL лог означает необязательность валидного сброса страниц на диск: данные уже не потеряются. Просто чуть дольше будет взлетать, если CHECKPOINT не завершен
blind_oracle
02.11.2017 21:42Это понятно, но в NoSQL с WAL как Тарантул и прочие редисы сброс WAL вообще не предусмотрен, только периодический снапшоттинг. А в SQL лог сбрасывается в основной tablespace очень даже активно во время нормальной работы, что и вызывает упирание в IOPS.
linuxover Автор
02.11.2017 21:51у тарантула появился дискстор, было бы интересно если бы его прикрутить на бакенд inmemory: насколько ухудшится ситуация.
я еще не бенчмаркал пока в этом направлении. WAL + снапшотинг очень долго взлетают на запуске.Varim
02.11.2017 21:55у тарантула появился дискстор, было бы интересно если бы его прикрутить на бакенд inmemory: насколько ухудшится ситуация.
Что это значит? Тарантул когда то не сохранял на диск? работал только как RAM кэш что ли? Сейчас сохранения данных тарантула не нативная тулза?linuxover Автор
02.11.2017 22:00не не, он всегда сохранял, просто сохранял (и сохраняет) в схему WAL + снапшоты
а теперь есть еще один вид сохранялки — дисковое хранилище (я еще не разбирался с механизмом, но вроде нечто вроде левел-DB), вроде и недостатки WAL нивелированы и скорость не слишком сильно просажена (но повторюсь — я еще не трогал руками сам)
и вот если там разница не очень существенная (скажем не более 30%) то может имеет смысл скрестить ужа с ежом и собрать профиты. вот я о чем :)
linuxover Автор
02.11.2017 17:18Что-то не верится в такую скорость работы базы данных. На сколько я знаю утилизировать 100% пропускной способности записи на диск можно только через буфер а это значит что мы жертвуем надежностью и после внезапного выключения сервера мы потеряем n записей которые мы якобы записали на диск отправив клиенту 200 ok.
но это так и есть.
недостатком WAL является "долгий взлет" БД после рестарта. Для устранения этого недостатка применяют снапшоттинг, который тоже имеет кучу недостатков. Но вот в работе WAL-лог пожалуй самый быстрый механизм сохранения данных на диске.
поэтому WAL логи применяются во всех БД, ну и журналирование в файловых системах по сути тот же WAL-лог :)
А можно больше технических подробностей? Выполняется ли fsync после каждой записи в 220 байт?
да режим fsync включен. Бенчмарк делался таким способом: создано 200 файберов, которые выполняют записи в БД. Сам тарантул их там группирует в пакеты и делает один fsync на несколько
bgnx
02.11.2017 19:50Сам тарантул их там группирует в пакеты и делает один fsync на несколько
Группировка пакетов и вызов одного fsync на несколько пакетов это и есть буфер и если внезапно выключится сервер то потеряется не последняя запись (в случае вызова fsync после каждой записи) а несколько
danikin
02.11.2017 20:06Вы слишком плохого мнения о Tarantool и неверно представляете себе как работает Тарантул :) Пакеты группируются из параллельных запросов, которые летят из разных файберов. Ответ клиенту приходит только после записи ВСЕГО пакета на диск и выполнения fsync. Учите матчасть: habrahabr.ru/company/mailru/blog/317274
bgnx
02.11.2017 20:54Спасибо, разобрался. А какой размер этого буфера или пакета? Его можно настраивать? Я правильно понимаю, что поскольку каждый запрос на запись ждет других запросов пока не заполнится буфер, то чем меньше размер буфера то будет быстрее каждый отдельный запрос но будет больше fsync-ов, а значит более медленная скорость записи на диск? А чем больше размер буфера то меньше fsynс-ов и быстрее сброс на диск но тем дольше будет ждать каждый отдельный запрос (пока не заполнится буфер) и тем самым будет занимать оперативку (а миллион ожидающих tcp-коннектов то займет не один гигабайт памяти)?
danikin
03.11.2017 09:15Не за что! :-) Размер настраивается автоматически. Пока идет текущий fsync, все следующее копится в буфере. fsync прошел — тут же начинается запись буфера на диск и последующий fsync, а тем временем уже копится новый буфер. Все параллельно как в сказке моего детства: "— Одну ягодку беру, на другую смотрю, третью замечаю, а четвёртая
мерещится. " — skazki.org.ru/tales/dudochka-i-kuvshinchik
Резонный вопрос — почему нет настроек? Рассмотрим два варианта настроек:
1. Что если записывать буфер на диск не сразу после завершения предыдущего fsync, а еще подкопить его некоторый интервал времени? Это очевидно, ухудшит latency — клиенты будут ждать дольше ответ. Но никак не улучшит потолок throughput — просто потому, что все что не записалось сейчас, запишется в следующем fsync, который автоматом возьмет на себя больше записей, которые в этот раз не успели записаться.
2. Что если записывать буфер на диск не полностью. Это, очевидно, ухудшит потолок throughput, т.к. сделает fsync более частым, но почти никак не повлияет на latency, т.к. основное время будет уходить не на сискол write, даже если их много, а на сискол fsync.
P.S.
Ну и давайте уже к практике переходить. Обычно Тарантул обеспечивает такие скорости и с таким запасом, что подобного рода настройки излишне. С другой стороны, если Тарантул тормозит, то и настройки такие не помогут — обычно проблема или в зациклившийся луашке или в десятках индексах на таблицу :-)
Varim
02.11.2017 21:09
Но это как то странно, разве буфер не должен работать так: накопили N транзакций в RAM, записали эти N транзакций на диск, как только записали — всем этим N клиентам отправили «200 ok»?мы жертвуем надежностью и после внезапного выключения сервера мы потеряем n записей которые мы якобы записали на диск отправив клиенту 200 ok.
но это так и есть.linuxover Автор
02.11.2017 21:12«но это так и есть» относилось к сомнениям в производительности :) сорри надо было разделить квоту.
да буффер копит N транзакций и пишет N и после этого всему N отвечает условный (тут не http) 200 Ok.
и получается что если например ошибка записи на диск то эти N могут получить ошибку записи. тоже все вместе: в базе будет то что валидно на момент последней записи.
Sovetnikov
02.11.2017 20:04Т.е. тест с миллионом пользователей вы не делали?
В коде очень комментариев не хватает.
Интересно если ещё кто-то предложит своё решение :)linuxover Автор
02.11.2017 20:06делали стресс-тестирование, пришли к тому что одна железка примерно пол миллиона будет держать: там больше начинаются проблемы с недостатком всяких сокетов, нежели проблемы с нагрузкой.
в реале работает (я ж написал в статье) 50 тыс пользователей и грузят они два ядра на 10-15% :)
linuxover Автор
02.11.2017 22:13> В коде очень комментариев не хватает.
зато там есть юниттесты, которыми код довольно неплохо покрыт. их можно как примеры использовать. Ну и дока на русском.
Yarique
03.11.2017 14:45Tarantool opensource + ко всему, оказывается. Надо будет на новогодних глянуть код поподробнее
alexeyknyshev
04.11.2017 23:28LUA (главный недостаток). Язык простой, но ограничения (1 гигабайт RAM на инстанс) заставляют масштабироваться несколько раньше
Ну всё таки не Lua, a LuaJIT, а раз уж говорим про последний, то проблема решается хранением данных не в памяти lj VM: ffi, lua C API или собственно сам storage engine тарантулаdanikin
04.11.2017 23:30Вот именно. И как бонус — эти данные не теряются при рестарте, если wal включен.
linuxover Автор
05.11.2017 22:26а тут двояко: если lua поставить вместо luajit, будет бенчмаркать крайне фигово.
если luajit то у него ограничения про гигабайт
> то проблема решается хранением данных не в памяти lj VM: ffi, lua C API или собственно сам storage engine тарантула
тут проблема в том что на каждый файбер требуется и lua память, соответственно число файберов ограничено не только производительностью но и luajit'ом
nekufa
Отличная статья, спасибо, Дмитрий!