Комментарии 34
Судя по https://github.com/tursodatabase/limbo/blob/main/COMPAT.md - совместимость и с SQL и SQLite неполная. Можно ли использовать инструмент для чего-то кроме экспериментов - непонятно.
Отдельный вопрос, в каком режиме тестировался SQLite - у него вроде была возможность не дожидаться фиксации данных на диске, что может ускорить обработку с точки зрения потребителя (если в тексте есть - я невнимательно читал значит).
заменив синхронные инструкции байткода их асинхронной альтернативой
Вот только в таких действиях есть один нюанс, который часто забывают - нельзя просто взять и превратить всю запись в асинхронную. Порядок записи в некоторых случаях является критичным для целостности данных в случае внезапного отключения, и нельзя просто взять и заменить весь синхронный I/O на асинхронный, иначе легко можно получить корраптнутые данные.
Ну и опять же повышение конкурентности во много раз тоже весьма гипотетическая - надо дожидаться завершения предыдущей транзакции до начала следующей, поэтому по сути выиграть можно только в ситуации когда делается массовое сохранение множества измененных блоков, а это не самый частый случай в обычной ситуации.
А с чего бы порядок выполнения операций записи изменился просто от перехода с синхронного i/o на асинхронный?
А кто гарантирует порядок в случае асинхронного I/o?
Код, который этот i/o, ну, вызывает?
Правильно! Только нужно копать глубже, сможет ли ваш асинхронный код на уровне языка программирования обеспечить упорядоченную запись. А как там на уровне ОС, контроллеров дисков? И если мы говорим о БД, то о всём этом нужно задумываться.
Можно же выполнять операции асинхронно относительно вызывающего кода, но ставить их в очередь, которая обеспечивает именно последовательное выполнение (в том же порядке, как операции были помещены в очередь).
Ага, и вот вы насабимитили в очередь и ждете перед последовательного завершения всех операций. По сути, вы скатились в обычный pread/pwrite, только теперь у вас добавилась очередь запросв, тред опроса завершения I/O, механизмы ожидания завершения I/O. Зато асинхронно.
Можно, но как гарантировать что прям до диска всё дойдёт в нужном порядке, тот же posix aio это очередь которая обрабатывается несколькими нитями, кто первый встал того и тапки.
Я к тому, что в случае БД - системного ПО, всё гораздо сложнее.
Ну тривиальный пример - вы пишете в два места большого файла. Они лежат в разных блоках прошивка решила что блок в который вы отправляете запись надо покомпактить. Запись в него откладывается пока компакт не будет сделан. А во второй блок можно писать сразу. Так что вторая операция завершится раньше первой.
Ну или если не вдаваться в совсем сложные материи, то у вас может быть raid0 или linear multidevice том на нескольки устройствах, вы отправляете 128 записей асинхронно, первый диск медленней чем второй, те операции которые легли на второй диск закончатся раньше чем те что на первый.
И из-за всей этой кухни разработчики блочных драйверов в линуксе совсем не просто так ввели флаги preflush и fua в команды, и разработчики файловых систем не просто так упарываются с журналами и redirect write, наверное они что-то знают, вам не кажется?
А что тут меняется от появления именно асинхронного i/o?
Если две задачи пишут в файл параллельно - то не важно блокирующая эта запись из разных системных потоков или асинхронная из одного. Если это одна задача, которая ждёт окончания одной операции прежде чем приступить к другой - то в чём, блин, проблема от асинхронности?
то в чём, блин, проблема от асинхронности?
В куче добавляющихся костылей, когда вам приходится реализовывать pread/pwrite прыжками вокруг асинхронных API, тредов и очередей
Если это одна задача, которая ждёт окончания одной операции прежде чем приступить к другой - то в чём, блин, проблема от асинхронности?
Если одна задача (поток) ждёт окончания предыдущей операции прежде чем приступить к другой, то это синхронные операции. И тут конечно нет никаких проблем от асинхронности. т.к. её тут нет.
В том-то и дело, что задача в асинхронном коде не обязана иметь выделенный под неё поток. И именно это отсутствие привязки к потоку и отличает синхронную операцию от асинхронной.
В том-то и дело, что задача в асинхронном коде не обязана иметь выделенный под неё поток
И тогда у вас останется три варианта :
сигнальная модель (поток инициировавший I/O останавливается, завершение операции порождает сигнал, который обрабатывается диспетчером, который возобновляет выполнение потока ожидающего завершения этого I/O)
вашим циклом в котором вы опрашиваете статус операции, возможно с ожиданием на событии (примитиве синхронизации)
порождение I/O события в eventloop программы (при этом придется реализовать state machine таска, чтобы продолжить его с того места где он прервался)
Увы, ничего другого пока не придумали. Отдельный тред позволяет просто автоматизировать второй вариант, не более.
порождение I/O события в eventloop программы (при этом придется реализовать state machine таска, чтобы продолжить его с того места где он прервался)
Никакого конечного автомата делать самому не требуется когда в языке есть сопрограммы. А они давно уже есть и в Rust, и в C++.
Так что для асинхронности давно уже стандартом является вариант с event loop, в котором никакой отдельный поток под задачу не выделяется, все задачи используют общий пул потоков.
Это конечно замечательно, что в расте и c++ переизобрели N:M шедулер. Но он давно реализован в операционных системах (где кстати весь I/O давно асинхронный, а синхронные вызывы в большинстве своем являются парой "асинхронный с последующим ожиданием").
А сопрограммы как выполняются?
Нет, это просто перекладывание задачи на ОС.
Нет, это в вымышленном мире.
10^7мкс это 10 секунды. На "select * from users limit 100"? Что-то не верится. Обычно такие запросы как раз выполняются за считанные микросекунды.
Что-то не так с описанием эксперимента.
А и да – а как же ACID?
"К чёрту ACID" читается между строк. Как я понял, на D(urability) они в открытую "положили", не стесняясь.
C ACID никак, но не потому что асинхронность, а потому что транзакции недоделаны.
Однако, мне вот теперь интересно, сколько прироста производительности было вызвано асинхронностью, а сколько - недоделанными транзакциями.
SQLite использует синхронный ввод/вывод. Традиционные системные вызовы POSIX read()
и write()
блокируют поток, пока операция не будет завершена. Для небольших приложений это нормально, но когда на сервере работают сотни баз данных, такой механизм становится узким местом. В этом случае крайне важна максимальная эффективность использования ресурсов.
Мы же говорим о базе данных, там используются блокирующие вызовы не потому, что так проще, а потому что нужна гарантия записи данных на диск это же один из столпов ACID, не важно какой там механизм транзакции будет по итогу, простой с двухфазной блокировки или WAL. И в целом функция write если посмотреть man не гарантирует записи на диск, а скорее копирование куда то в кеши файловой системы:
A successful return from write() does not make any guarantee that data has been committed to disk. On some filesystems, including NFS, it does not even guarantee that space has successfully been reserved for the data. In this case, some errors might be delayed until a future write(), fsync(2), or even close(2). The only way to be sure is to call fsync(2) after you are done writing all your data.
в теории можно поменять на вызовы io_uring, но все ровно как только приходит commit нужно будет вызывать дорогую операцию синка/флаша и ждать когда все данные гарантирвоанно не сохранятся на диск. Мне кажется тут выигрышь будет для совсем синтетических тестов.
А аналога sync/syncfs в io_uring нет?
Но даже если и так - всё равно можно получить ускорение, если выделить отдельный поток под вызовы sync. Пусть лучше 1 поток ждёт чем все потоки сразу.
Да даже если и нет вызова, как бы сделать механизм который скажет, что все асинхронные вызовы завершились и данные на диске, не так сложно, но это на мой взгляд не сильно решает проблему, вызов write под капотом тоже вполне себе может быть асинхронный в большинстве своем, второй вопрос сколько во второй поток можно скидывать страниц если у вас большая транзакция и гигабайты данных нужно изменить, все гигабайты данных держать в асинхронной очереди, ок закинули мы их в эту очередь и так же ждем пока они сохранятся на диск.
Тут все таки проблема что нам для того что бы завершить транзакцию нужно точно получить сигнал от системы что данные легли на диск, либо получить ошибку, наверное можно с помощью асинхронных вызовов в каких то случаях увеличить производительность вопрос в цене реализации и дальнейшей поддержки и багафикса, сколько работал с sqlite каких то проблем со скоростью в нем не было, а мы к нему прикручивали и шифрование и полностью загоняли OpenStreetMap.
А вот, к слову, асинхронный рендеринг карты действительно дал хороший прирост производительности, там не было единой точки синхронизации и происходило чтение фичь из sqlite и отправка их в асинхронную очередь, а на другой стороне пул потоков их выбирали и рендерили в буфер.
Да ничем это не поможет, когда вам нужна гарантированная упорядоченная запись (например когда вы пишете в журнал транзакций). Если у ваша задача требует упорядоченности операций, вы упретесь производительность однопоточного I/O.
А весь этот I/O uring - попытка избавиться от копирования userspace <-> kernelspace и ослабить ограничения имеющегося асинхронного I/O. Можете запустить fio direct=1 с ioengine libaio, io_uring и обычным синхронным I/O - в один поток они неотличимы, в несколько потоков libaio и io_uring одинаковы.
SQLite можно ускорить и сильно проще: https://sqlite.org/forum/forumpost/19870fae957d8c1a?t=h
Мне кажется, что в статье не раскрыты несколько тем:
1) Реальная БД имеют некую степень локальности запросов ~10-15% от объема БД. Много операций осуществляется еще на уровне кэша, и даже с WAL не каждая операция сразу пишется в файл базы (fsync).
2) Асинхронный вывод же использует какой то механизм callback и сигналов, чтобы сообщить результат IO операции. Это вроде как означает, что в ACID транзакциях надо дождаться завершения всех операций. В таком случае даст ли асинхронность ускорение?
Иными словами, придётся переписать львиную долю SQLite. Исследователи же пошли другим путём: переписали SQLite на Rust, используя
io_uring
.
как будто бы было желание переписать на Rust, но не хватало цели кроме переписывания как такового и io_uring - как будто бы такой повод дает
Эксперимент по ускорению SQLite