Как стать автором
Обновить

Асинхронная обработка запросов в СУБД в памяти, или как справиться с миллионом транзакций в секунду на одном ядре

Время на прочтение10 мин
Количество просмотров19K
Всего голосов 47: ↑45 и ↓2+43
Комментарии39

Комментарии 39

Мне кажется, называть хеш-таблицу СУБД — это сильно большая натяжка. Очень сильно.

Мне кажется вы что-то не так прочитали.


СУБД — это серверное приложение, тогда как хеш-таблица является библиотекой, т.е. СУБД — это хеш-таблица плюс кое-что еще. И это кое-что еще включает в себя как минимум саму по себе серверную обвязку.
СУБД не имеет никакого отношения к серверам. Можно иметь sqlite, работающий с локальной базой, и всё равно иметь СУБД. Ключевое в СУБД — это предоставление механизмов для работы с базами данных. Произвольная выборка, правила запросов, etc.

То, что вы описываете, мало отличается от обычного жёсткого диска. Записали по ключу (номеру сектора), прочитали по ключу. Чтобы жёский диск превратился в СУБД, оно должно быть способно делать хоть что-то полезное. Например, WHERE. Или ORDER BY. Или LEFT INNER JOIN. Ну хотя бы WHERE LIMIT и SORT, как самый минимум.
Вам не кажется.
Видимо, не туда?
А как реализованы сами остановки, где пассажиры ждут пока их заберут?
… как наша система работает и справляется с миллионом транзакций в секунду на одном ядре.

Можно уточнить, эти транзакции (которых миллион на одном ядре) состоят из нескольких атомарных операций? Их можно в процессе выполнения откатить? Изолированы ли они как-то друг от друга (блокировки, вот это вот всё)?
https://habrahabr.ru/post/221667/
Там одна операция. Они самые простейшие. Изолированы друг от друга.
«Транзакции». :-)
> мы пользуемся сокращением TX
Наверное пошло от:
Tx Rx
T — Trancieve (отправление)
R — Recieve (получение)
Мда…
По сути документации:
This model makes all programmatic locks unnecessary: cooperative multitasking ensures that there will be no concurrency around a resource, no race conditions, and no memory consistency issues

и
box.begin() Begin the transaction. Disable implicit yields until the transaction ends. Signal that writes to the write-ahead log will be deferred until the transaction ends. In effect the fiber which executes box.begin() is starting an “active multi-request transaction”, blocking all other fibers.
box.commit() End the transaction, and make all its data-change operations permanent.
box.rollback() The requests in a transaction must be sent to the server as a single block. It is not enough to enclose them between begin and commit or rollback. To ensure they are sent as a single block: put them in a function, or put them all on one line, or use a delimiter so that multi-line requests are handled together.

И судя по тому, что в документации написано, что все попадает в один исполнительный поток на сервере, ждет! асинхронного подтверждения на запись на диск, а в случае проблемы откатывает все, что было изменено после операции вызвашей проблему.
До полноценной RDMS ей как до границы раком.
Безусловно, много там интересных вещей и ребята проделали большую работу, но это по факту Key-Value массив «на стероидах» в памяти, с обвязкой для репликации и самовосстановления из сохранных на диске данных.
Ждет подтверждения от диска наверное же при коммите, что вполне обычно в таких случаях. Коммит должен гарантировать ту D из ACID. Все так делают.

А вот на счет полноценности не стоит ругать. Их транзакции довольно просты и понятны, изолированы друг от друга и не оставляют много пространства для ошибок, в отличии от того ада, который вы увидите в базах данных с MVCC, типа InnoDB или PostgreSQL, где среднестатистический программист даже и не знает о разных уровнях изоляции и отсутствии 100% гарантий на consistency (по ACID).
С самим ACID то нет вопросов. Вопрос с несколькими ACID, даже если они меняют не перекрывающиеся данные :) Нашел на сайте описание детальное, а потом потерял.
All database operations in a transaction should use the same storage engine. It is not safe to access tuple sets that are defined with {engine='vinyl'} and also access tuple sets that are defined with {engine='memtx'}, in the same transaction.


Then the only safe atomic functions for memtx databases would be functions which contain only one database request, or functions which contain a select request followed by a data-change request.
Автор некорректно хвалит опцию -O3, сравнивая её с -O0; разумеется, сравнение с гораздо более безопасной и менее спорной так же в некоторых других отношениях -O2 не дало бы такого выигрыша в производительности.
Справедливое замечание. Принимаю. Проверили конкретную программу с -O2 и даже с -O1 — примерно также как с -O3. Однако, все равно остаюсь фанатом -O3. На моей практике всегда, если падало с -O3, то после даже нескольких дней копаний, чертыханий и ругани про себя на кривую реализацию gcc, всегда находил конкретный баг. Хотя, честно скажу, руки постоянно тянулись вернуть все в -O2. Возможно, я не натыкался на реальные баги в gcc, связанные с -O3.
Я в своей практике общения с g++ сталкивался с двумя глобальными проблемами из-за -O3.

Первая — gcc 4.4.4 на некотором коде с некоторым набором параметров компилятора просто падал с core dump. Тогда оптимизация была просто понижена до -O0 (чтобы вообще не разбираться), благо не компилировался код вспомогательного приложения и разбираться, что не нравится компилятору особого смысла не было — и так все работало быстро.

Вторая проблема была с gcc 4.3.4. В приложении использовался Boost и его контейнеры из Boost.Container. Наиболее яркое и запомнившееся проявление проблемы было, когда был boost::container::vector с данными, если у него посмотреть size(), то там одно значение (неправильное или 0), если посчитать end()-begin() — другое значение (почти всегда правильное), если отладчиком посмотреть в структуру, где оно хранит данные, то в памяти значения всегда верные.
Ни в одной другой версии компилятора (новее) воспроизвести не удалось. В gcc 4.3.4 Нормально работало только с оптимизацией -O1. Я бы еще понял, если бы код был многопоточный, но код был однопоточным и по сути логически один и тот же код давал то правильные данные, то неправильные.

Если у Вас есть идея, как можно подебажить второй случай, то я с радостью ее выслушаю, так как мне было бы интересно попытаться докопаться до причины такого поведения компилятора.
Обычно в таких случаях я локализовывал проблему до минимальной программы, на которой она воспроизводится, т.е. откусывал от нее куски «методом половинного деления», пока не оставалась минимальная по размеру программа, в которой устойчиво воспроизводилась проблема. Далее я превращал все в ассемблер и разбирался, как на самом деле выглядит код. В вашем случае — во что конкретно развернулось end()-begin().

Другой способ поиска проблемы, если программа точно однопоточная (надо в этом убедиться, ибо потоки могут юзаться где-то в либах глубоко внутри), то надо искать в выходы за границы массива и аккуратно проверить, что арифметика указателей везде корректная + надо посмотреть на const_cast, static_cast и reinterpret_cast (а также C-style cast) — нет ли там опасного.
Нашел свободный день на исследование и обнаружил, что проблему в gcc 4.3.4 вызывает включение -fstrict-aliasing.
При этом, добавление -Wstrict-aliasing=1 (самый высокий уровень предупреждений) никаких релевантных предупреждений не дает (что при компиляции в gcc 4.3.4, что при использовании более новых версий). Точнее даже в коде, из которого собирается проблемный исполняемый файл и библиотеки предупреждений просто нет.
Более того, при использовании gcc версии 4.4.4 и новее никаких проблем с работой кода не наблюдается.

Так что ситуация понятнее не стала.

Интересно было бы еще запустить тесты на gcc 4.3.[5-6] и 4.4.[0-3], но я сходу не нашел старых дистрибутивов линуксов, где они есть по умолчанию, чтобы не пересобирать компиляторы самостоятельно.
«Обычно в таких случаях я локализовывал проблему до минимальной программы, на которой она воспроизводится, т.е. откусывал от нее куски «методом половинного деления», пока не оставалась минимальная по размеру программа, в которой устойчиво воспроизводилась проблема.» — до какой программы вы в итоге дошли?
В итоге в этом случае я дошел до реального break strict-aliasing rules.

Это действительно баг в коде boost::container::flat_map, но который на моем наборе компиляторов стрелял только в gcc 4.3.4.

https://github.com/boostorg/container/blob/develop/include/boost/container/flat_map.hpp#L61-L70
Функция force хотя бы работает, а вот force_copy сильно не всегда. Почему они написаны именно так я не знаю. В рассылке мне ответили только то, что да, это ломает strict-aliasing, но зачем так сделали — осталось без ответа.

Так что минус один баг в компиляторе, +1 баг в Бусте.
Но чтобы не было все так хорошо с компиляторами (и в моем списке осталось ровно 2 бага), вспомню еще один случай на компиляторе из RHEL7.0, когда при включении LTO он просто падал с segmentation fault на некотором коде :)
Ну т.е. проблема локализована, вопрос исчерпан?
По этой проблеме да. Я ошибся, обвинив компилятор основываясь в основном на факте, что ошибка проявлялась только на одной версии gcc.

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

Segmentation fault процесса компилятора я все же никак не могу списать на ошибку в своем коде, а не в компиляторе и -O3 :)
Согласен :)
1. Подскажите, почему для тестов взят unordered_map, вместо обычного map? В индексах СУБД часто используются деревья, поэтому пример с map-ом был бы более репрезентативным.
2. Подскажите, почему был выбран именно такой тест? Какое поведение СУБД он моделирует?
 for (int i = 0; i < SIZE;++i)
     c += m[i*i] += i;

3. Не до конца понятно, как технически реализуется групповое чтение запросов из сокета. Ведь в традиционной схеме сервер устанавливает с клиентом соединение, для чего на стороне сервера создается индивидуальный для конкретного клиента сокет, из которого сервер читает запрос и куда сервер пишет ответ. В такой схеме не получится организовать групповое чтение. Можете подсказать, какая техника работы с сокетом в данном случае предлагается.
Предположу, что какой нибудь epoll.
epoll возвращает список сокетов, в которых готовы данные для чтения. Вопрос в том, каким образом одним системным вызовом можно выполнить чтение из нескольких готовых сокетов? Традиционный recv позволяет прочитать данные только из одного сокета.
См. выше
Клиент внутри себя параллельно (паралллельно, потому что у клиента много потоков внутри) формирует запросы и далее запихивает их в один сокет одним пакетом. Они приезжают на сервер. Сервер считывает пакет за один сискол.
Забыл на 1 и 2 ответить. Ответ на 3 ниже.

1. Потому что хэш быстрее дерева на точечных лукапах и тест у нас точечного лукапа (на range лукапах, разумеется, надо брать дерево, ибо хэш их не умеет). В IMDB Tarantool есть хэш индекс (https://tarantool.org/doc/singlehtml.html). В RDBMS Postgres тоже есть хэш индекс (https://www.postgresql.org/docs/9.1/static/indexes-types.html).

2. Внешние сложения — чтобы оптимизатор прошел цикл, без свертки всего сразу. Умножение внутри — чтобы обеспечить какой-никакой рандом. Реальный рандом сам бы вносил в вычисления большую погрешность, т.к. выполняется не быстро.
1. А зачем использовать в виде индекса дерево (O(log(N)), если и хэш (O(1)) подходит? В Tarantool специально для этого есть разные виды индексов — TREE и HASH.

2. Это обман оптимизатора. Чтобы он по честному прошел весь цикл. Важно, что там внутри цикла есть как минимум одно обращение по индексу. Моделируем SIZE обращений по индексу.

3. https://habrahabr.ru/company/mailru/blog/317274/#comment_9959174
а что за история с замедлением макбука? в эпле тоже заговор, да? или вы винду на нем катаете?
НЛО прилетело и опубликовало эту надпись здесь
Я этот термин специально не ввожу, чтобы тролли как мухи не налетели. Это онлайн обработка. Автобус не ждет пока пакет (салон) наполнится.
Хорошая статья, терминология тут особо не важна
Важно то, что она очень доступно доводит читателя до нужных мыслей
Миллион? На одном ядре? Это очень круто!

Не могли бы Вы дать конфигурацию сервера, который позволит повторить такой результат?

С большим удовольствием поставлю Tarantool на самый мощный сервер DO для эксперимента.
Сообщите, пожалуйста, какой сервер использовали Вы, для сравнения результатов.
В статье все написано. Если кликните на эту ссылку, то все увидите. Это Amazon t2.micro.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий