LINUX.ORG.RU

Почему InnoDB юзает свой buffer pool, а например LMDB полагается на mmap?

 


1

2

Предположим, мы не говорим о записи, только о чтении страниц.

В чем причина выбрать решение, при котором ты выделяешь самостоятельно 2G оперативы, поддерживаешь там LRU страниц, что-то ещё и таскаешь туда страницы при необходимости с диска через pread(), копируя данные лишний раз в юзерспейс. А не просто за-mmap-ить весь 100-гиговый файл и ходить туда как в память, а про LRU и всё такое пускай думает ядро?

Автор LMDB гнул пальцы, что его решение изящно из-за выбора второго варианта: типа код движка проще, думать про страницы не надо, меньше копирований и т.п.

Но при этом конечно мы не контролируем что делает ядро. Например, не каждый догадается вызвать madvice(MDV_RANDOM) после mmap, чтобы ядро перестало зря префетчить страницы. Или например с mmap мы начинаем мучать VM-подсистему, оно там что-то активно делает с TLB и никто не уверен что это не вредно.

В общем интересно вот это всё.

За выбором своего buffer pool в InnoDB стояло скорее всего требование уметь менять страницы, при этом не раздражая ядро (не вынуждая его записывать их обратно в файл), причем измененные страницы в innodb сначала должны попасть в double write buffer на диск, а не сразу на своё место. Но казалось бы, пока мы не начали менять страницы, почему не mmap. В общем интересно какие ужасы в этом плане в поведении ядра напугали чуваков в InnoDB, о которых я не знаю.

В IT раз за разом повторяется одна и та же история. Молодой программист обозревает какую-то область и заявляет: «Вы тут говно какое-то понаписали, навернули сложностей на ровном месте, всё глючит и тормозит, я сейчас покажу как надо». И выкатывает прототип (часто на новом модном языке X), код в сто раз меньше, в тысячу раз понятнее, программа работает вдвое быстрее и вообще всё замечательно.

Но есть нюанс. В альтернативе реализовано только 80% возможностей. А в процессе реализации следующих 80% появляются костыли, архитектурные сложности, пять слоёв абстракции, чтобы прикрыть несоответствие между старой архитектурой и новыми требованиями и …

Приходит следующий молодой программист и заявляет …

ugoday ★★★★★ ()

Но казалось бы, пока мы не начали менять страницы, почему не mmap. В общем интересно какие ужасы в этом плане в поведении ядра напугали чуваков в InnoDB, о которых я не знаю.

Ничего не понял. Разве в InnoDB база данных только на чтение? Или вопрос в том, почему они используют один механизм, а не два?

monk ★★★★★ ()

Автор LMDB гнул пальцы, что его решение изящно из-за выбора второго варианта: типа код движка проще, думать про страницы не надо, меньше копирований

В LMDB почти ничего нет. Перед созданием своей поделки я рассматривал эту штуку как вариант, но быстро пришел к пониманию, что кроме key-value с короткими запросами оно больше ни для чего не годится, потому что базируется на блокировке всей таблицы при выполнении операции, а потому, естественно, никакой семантики commit/rollback нет, а значит и сложных операций вообще.

Как правильно заметил ugoday (у меня научился, что ли?): MyISAM много лет назад был примерно такой же примитивной базой без транзакций и с полной блокировкой таблиц.

По поводу:

А не просто за-mmap-ить весь 100-гиговый файл и ходить туда как в память, а про LRU и всё такое пускай думает ядро?

Это прокатывает только если у тебя 100 Гб оперативы. Иначе система закэширует не то и не тогда. В любом более-менее сложном применении нужно обязательно вырубать системный кэш и велосипедить свой.

byko3y ★★★ ()

Проблемы будут, потому, что твоя программа не единственная и такой ммап будет конкурировать с обычными файловыми кешами, например. Запустит админ какой-нибудь grep -R test / и пойдёт система вытеснять все кеши, греп-то важней. А если ты руками всё делаешь, память выделяешь и тд, тут тебя уже не вытеснят, пока своппиться не начнёт система.

Legioner ★★★★★ ()
Ответ на: комментарий от byko3y

Это прокатывает только если у тебя 100 Гб оперативы. Иначе система закэширует не то и не тогда. В любом более-менее сложном применении нужно обязательно вырубать системный кэш и велосипедить свой.

Вот это можно аргументировать? Чем LRU страниц внутри ядра не подходит?

igloev ()
Ответ на: комментарий от Legioner

Допустим, подобного не делается, потому что админы понимают, что сервер важный и продовый. Ссли страница не изменена, она будет в одном экземпляре. И файловый кеш и ваше приложение будет смотреть физически в один кусок памяти ведь.

igloev ()
Ответ на: комментарий от igloev

Чем LRU страниц внутри ядра не подходит?

Тебе выше ответили про grep. LRU даже близко не валяется рядом с адекватными механизмами кэширования базы, что обычно подразумевает приоритетное кэширование индексов и метаданных.

byko3y ★★★ ()
Ответ на: комментарий от igloev

Если страница не изменена, она будет в одном экземпляре

Я тебя удивлю, но СУБД тоже держат страницу в одном экземпляре. Если при выполнении запроса страница не кэширована, то она временно поднимается в кэш. Ну а данным с последних транзакций и вовсе ничего напрямую не соотстветвует на диске — они собираются в кэше из старых данных и write-ahead лога.

byko3y ★★★ ()
Ответ на: комментарий от byko3y

Мы говорим про mmap только в контексте чтения с диска.

Предположим, на сервере ничего тяжелого, кроме твоего бинарника, не работает, потому что так приказали админам. Далее, предположим, что коду нужны какие-то страницы (блоки B+Tree дерева), которые лежат на диске. Почему mmap в данном случае - плохо? Чем LRU в ядре будет тупее некого другого кеширования тех же страниц?

igloev ()

хотя вместо этого есть VirtualAlloc()

наверное причина в желании самостоятельно контролировать алгоритм, нет никакой гарантии, что на всех платформах ядро каждой конкретной ОС будет всегда использовать LRU

Harald ★★★★★ ()

Множество приложений смотрят количество свободной памяти и меняют свое поведение. mmap на всю систему - не очень приличное поведение по отношению к таким программам. Конечное если он cgroup или если программа всего одна - то это ок.

vertexua ★★★★★ ()
Ответ на: комментарий от igloev

Далее, предположим, что коду нужны какие-то страницы (блоки B+Tree дерева), которые лежат на диске. Почему mmap в данном случае - плохо? Чем LRU в ядре будет тупее некого другого кеширования тех же страниц?

А я разве спорю с тем, что в простых сценариях проблем нет — я так сразу и написал. То есть, чтение и запись ведется с минимальным размером в страницу/блок диска, а еще лучше — последовательно. Напоминаю, что SSD никак не решают проблему произвольного доступа, а просто показывают немного лучшие показатели, чем HDD.

byko3y ★★★ ()
Ответ на: комментарий от max_lapshin

Если ты пишешь сетевую программу, то она может в любой момент залипнуть на долгие миллионы микросекунд и не отвечать ничего

Ох уж мне эти посиксопроблемы. В многопотоке, в число которых, внезапно, входит ядерный ввод-вывод, подобные зависания не являются проблемой, поскольку выполнение просто переходит другому потоку. Так-то ввод-вывод из одного устройства HDD/SSD принципиально непараллелизуем.

byko3y ★★★ ()
Ответ на: комментарий от vertexua

Множество приложений смотрят количество свободной памяти и меняют свое поведение

GC-говно не является «множеством программ». FF/Хром так не делает, а электроны и вьебсайты нынче являются очень популярной платформой.

byko3y ★★★ ()
Ответ на: комментарий от igloev

Ну как и read(). Я не знаю чем там дисковая подсистема сейчас занята ведь

Он имел в виду какие-нибудь epoll/kqueue в качестве альтернативы mmap, что нынче популярно делать в мире посиксового наследия и зовется асинхронной обработкой запросов.

byko3y ★★★ ()
Ответ на: комментарий от igloev

read — понятный, явно очерченный системный вызов. Притом, ещё и прерываемый.

Его принято гонять в отдельном пуле тредов.

mmap принято использовать прям здесь с полной потерей гарантий на скорость ответа.

max_lapshin ★★★ ()
Ответ на: комментарий от max_lapshin

read — понятный, явно очерченный системный вызов. Притом, ещё и прерываемый
mmap принято использовать прям здесь с полной потерей гарантий на скорость ответа

Вообще не вижу разницы между блокирующим read и mmap. Мягкий page fault проходит с ожиданием завершения ядерного потока ввода-вывода — ровно это же делает блокирующий read.

byko3y ★★★ ()
Ответ на: комментарий от byko3y

А я разве спорю с тем, что в простых сценариях проблем нет

Я просто пытаюсь понять, что за сценарии считаются непростыми. Про запуск грепа всей файловой системы на продакшене - допустим такого не происходит никогда ни на каком сервере, где работает продовый MySQL.

Из интересных аргументов против mmap(), которые находил:

  1. TLB poisoning, потому что вся эта куча страничек начинает жрать мозги VM-подсистеме

  2. Что творит ядро ты до конца всё равно не контролируешь, даже вызовы, скажем madvice(MDV_RANDOM) разрешают ядру на них забивать. Например ядро решило, что тебе надо префетчнуть ещё 16 страниц вперёд и оно начало искать память в VM не под 1 страницу, которую ты пытаешься читать, а под 16 и это занимает больше времени, чем ты думал.

Из интересных аргументов за mmap видел такой:

  1. при копировании страницы из памяти ядра в юзерспейс внутри вызовов семейства read(), используется неоптимальный невекторный код (без всяких MMX, AVX…), потому что ядро не желает сохранять стейты этих сложных вещей в контексте, чтобы не раздувать контекст, а когда копируешь что-то из mmap-ленной памяти куда-то, то весь копирующий код юзерспейсный и может быть любой степени наркоманскости и векторности.
igloev ()

LMDB рассчитана на read-intensive applications и в этих сценариях опережает (нередко в разы) все прочие движки. Исключения конечно есть, но это редкие/специфические случаи.

В случае с b-tree LRU в ядре работает неплохо, но если этот самый LRU действительно реализован и памяти более-менее хватает. Сейчас в Linux LRU для Unified Page Cache сделан неплохо. В результате в сценариях чтения LMDB выигрывает. Если же памяти не хватает, то и нет толку от собственного пула в условном InnoDB.

С другой стороны, в InnoDB (и массе других движков) через пул/кэш страниц решается масса других задач (блокировка/разделение данных между транзакциями и т.п.). А в LMDB все транзакции строго последовательны (в Tarantool и Redis примерно аналогично), и при полном ACID отсутствует часть проблем и накладных расходов.

Кстати, есть какой-то продвинутый васяно-форк LMDB, погуглите.

anonymous ()
Ответ на: комментарий от igloev

При чем тут вообще read vs mmap? Да, интерфейсы отличаются, но в конечном счете и mmap можно использовать без буферизации.

Ценой системного вызова по сравнению с самой стоимостью ввода-вывода можно пренебречь, поскольку смена контекста — это около тысячи циклов на фоне 4 килобайт инфы в блоке, копирование которых само по себе займет несколько сотен циклов. Для самых быстрых применяющихся практически SSD дисков (5 гигабайт в секунду) столько же циклов нужно для того, чтобы произвести последовательное чтение 4 килобайт — только здесь стоимость системного вызова начинает отсвечивать.

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

при копировании страницы из памяти ядра в юзерспейс внутри вызовов семейства read(), используется неоптимальный невекторный код (без всяких MMX, AVX…), потому что ядро не желает сохранять стейты этих сложных вещей в контексте, чтобы не раздувать контекст

Оно не использует векторные функции для передачи данных между режимами, но внутри ядра вполне может копировать память из одного буфера в другой через вектора — почему нет?

byko3y ★★★ ()
Ответ на: комментарий от anonymous

erthink, мы тебя узнали.

LMDB рассчитана на read-intensive applications и в этих сценариях опережает (нередко в разы) все прочие движки

Только ты забыл упомянуть, что это самое read должно представлять из себя достаточно простую оперцию, да и к тому же короткую — иначе заблокирует даже редкие операции записи. Если у тебя внезапно имеется потребность делать сложные и потому долгие операции выборки между несколькими сущностями, то внезапно все преимущества LMDB как корова языком слизала.

Ну типа я сразу написал, что LRU кэш оси прокатывает только в простейших случаях.

byko3y ★★★ ()
Ответ на: комментарий от byko3y

При чем тут вообще read vs mmap?

потому-что read дополнительно породит трафик по памяти и засрет кеш процессора. всегда.

Да, интерфейсы отличаются, но в конечном счете и mmap можно использовать без буферизации.

гуру, а пояснишь разницу между mmap с буферизацией и без? в каком месте там буферизация?

anonymous ()
Ответ на: комментарий от byko3y

Только ты забыл упомянуть, что это самое read должно представлять из себя достаточно простую оперцию, да и к тому же короткую — иначе заблокирует даже редкие операции записи. Если у тебя внезапно имеется потребность делать сложные и потому долгие операции выборки между несколькими сущностями, то внезапно все преимущества LMDB как корова языком слизала.

А как в твоей любимой/собственной БД чтение блокирует запись меньше чем в LMDB?

Ну типа я сразу написал, что LRU кэш оси прокатывает только в простейших случаях.

гуру, а в каких случаях линуксовый LRU хоть немного хуже наколенного кеширования b-tree?

anonymous ()

Предположим, мы не говорим о записи, только о чтении страниц.

Тут всё-таки стоит упомянуть, что обновление данные в mmap требует либо отображения на чтение-запись (что может быть противопоказано для быстрого кеша в клиенте), либо требует наличия unified page cache в ядре (чтобы менять данные через запись в дескриптор и сразу видеть в памяти). И вот этот unified page cache отсутствует в OpenBSD, а во многих системах оказывает дополнительно/разное влияние на поведение LRU.

В чем причина выбрать решение, при котором ты выделяешь самостоятельно 2G оперативы, поддерживаешь там LRU страниц, что-то ещё и таскаешь туда страницы при необходимости с диска через pread(), копируя данные лишний раз в юзерспейс. А не просто за-mmap-ить весь 100-гиговый файл и ходить туда как в память, а про LRU и всё такое пускай думает ядро?

Ну причина в том, что дефолтовый ядерный LRU может тебя чем-то не устраивать. Например, ты можешь считать что эти 2G (или 200) тебе обязаны дать и потом не забирать (через другие процессы или файловый кеш).

Или что у тебя есть 1G (условно 50%), которые точно выгодно кешировать по-сильнее (дольше не выталкивать из кеша, или вообще не выталкивать), т.е. если требуется чтобы на поведение LRU влияло локальное значение/содержание данных.

Автор LMDB гнул пальцы, что его решение изящно из-за выбора второго варианта: типа код движка проще, думать про страницы не надо, меньше копирований и т.п.

В среднем он прав, особенно если памяти хватает и/или нет точного понимания о собственной стратегии кеширования (для b-tree почти всегда так).

Но при этом конечно мы не контролируем что делает ядро. Например, не каждый догадается вызвать madvice(MDV_RANDOM) после mmap, чтобы ядро перестало зря префетчить страницы. Или например с mmap мы начинаем мучать VM-подсистему, оно там что-то активно делает с TLB и никто не уверен что это не вредно.

При использовании mmap в не-тривиальных сценариях все-таки нужно пользовать madvice. В LMDB можно сказать MDB_NORDAHEAD, а libmdbx по-умолчанию самостоятельно дергает madvice в зависимости от объема доступной памяти.

За выбором своего buffer pool в InnoDB стояло скорее всего требование уметь менять страницы, при этом не раздражая ядро (не вынуждая его записывать их обратно в файл), причем измененные страницы в innodb сначала должны попасть в double write buffer на диск, а не сразу на своё место. Но казалось бы, пока мы не начали менять страницы, почему не mmap. В общем интересно какие ужасы в этом плане в поведении ядра напугали чуваков в InnoDB, о которых я не знаю.

Для InnoDB сценарий «только чтения» странный и гораздо более редкий в сравнении с LMDB, которая изначально сделана для LDAP-справочников (много чтений/поиска и редкие изменения). Поэтому в InnoDB гораздо меньше смысла делать что-либо не задумываясь об изменении данных.

Во-вторых, разработчики чуть менее чем всех БД считают что смогут обеспечить стратегии/тактики кеширования лучше чем LRU, опираясь на знания о потребности в данных и планах запросов. Иногда это получается, но знать как именно работает собственный кеш часто просто удобнее, чем зависеть от тонкостей поведения разных ядер и платформ.

В-третьих, ядерный LRU на page cache работает с учетом потребности всех процессов. Т.е. если кто-то делает бакап системы без O_DIRECT, то ваши данные смоют из кеша.

anonymous ()
Ответ на: комментарий от byko3y

При чем тут вообще read vs mmap? Да, интерфейсы отличаются, но в конечном счете и mmap можно использовать без буферизации.

Немного не понял это. mmap без буферизации только и можно, а на read надо городить свои буферы. Ну ладно, не важно, это отход от темы…

То, что ты упоминаешь — это мелочи конкретной реализации

Не понял, можно конкретнее. Что - мелочи?

Оно не использует векторные функции

Как я понял, смысл в том, что если ядро начнёт юзать MMX/AVX для собственных нужд, ему нужно будет сохранить перед этим состояние этих регистров, а потом восстановить, чтобы юзер не заметил, что ядро в них намусорило. И вот чтобы не заниматься их сохранением/восстановлением (а то их слишком много развелось и context switch с ними дороже), ядро их и не юзает внутри себя. Возможно я какой-то бред прочитал, но вот так.

igloev ()
Ответ на: комментарий от anonymous

потому-что read дополнительно породит трафик по памяти и засрет кеш процессора. всегда

Ты сравниваешь это со сценарием «прочитать один байт», но если ты делаешь настоящий ввод-вывод, то разницы нету — кэш будет засран в любом случае.

гуру, а пояснишь разницу между mmap с буферизацией и без? в каком месте там буферизация?

Для улучшения работы последовательного доступа ось может на упреждение считывать следующие старницы, таким образом минимизируя кол-во мягких page fault, поскольку последние приводят к переключению контекста. Ну хорошо, не буферизация, а read-ahead — сорта говна.

byko3y ★★★ ()
Ответ на: комментарий от anonymous

А как в твоей любимой/собственной БД чтение блокирует запись меньше чем в LMDB?

На самом деле я ошибся и чтение не блокирует запись в LMDB.

гуру, а в каких случаях линуксовый LRU хоть немного хуже наколенного кеширования b-tree?

Я не писал, что тупое кэширование деревьев требует хитроумного кэша. Но база данных состоит далеко не из одних деревьев.

А во-вторых, конкретный алгоритм вытеснения страниц линя плохо обрабатывает именно сценарии частй записи в большое число отмапленных файликов.

byko3y ★★★ ()
Ответ на: комментарий от anonymous

В среднем он прав, особенно если памяти хватает и/или нет точного понимания о собственной стратегии кеширования (для b-tree почти всегда так)

Особенно когда у тебя БД не содержит ничего, кроме единственного (небинарного) дерева. Да, это правда.

Во-вторых, разработчики чуть менее чем всех БД считают что смогут обеспечить стратегии/тактики кеширования лучше чем LRU, опираясь на знания о потребности в данных и планах запросов

Тот же MyISAM вообще сам не кэширует содержимое записей, например, он только кэширует деревья индексов, поскольку индексы более важны. «Честное» LRU на уровне ОС делать дорого, потому разрабы ядер срезают углы.

byko3y ★★★ ()
Ответ на: комментарий от igloev

Про запуск грепа всей файловой системы на продакшене - допустим такого не происходит никогда ни на каком сервере, где работает продовый MySQL.

угу, банально ошибиться сессией ведь нельзя — некоторые вон rm не там запустили и без базы остались. Греп был примером того как простыми средствами сделать плохо, но это не значит, что в проде будет именно греп.

anonymous ()
Ответ на: комментарий от igloev

Не понял, можно конкретнее. Что - мелочи?

Тысячу циклов на чтение блока, в том числе циклы на обновление TLB — это копейки по сравнению с самим вводом-выводом.

Немного не понял это. mmap без буферизации только и можно, а на read надо городить свои буферы. Ну ладно, не важно, это отход от темы

Пояснил: Почему InnoDB юзает свой buffer pool, а например LMDB полагается на mmap? (комментарий)

Как я понял, смысл в том, что если ядро начнёт юзать MMX/AVX для собственных нужд, ему нужно будет сохранить перед этим состояние этих регистров, а потом восстановить, чтобы юзер не заметил, что ядро в них намусорило

Да, нужно делать kernel_fpu_begin и kernel_fpu_end, которые не бесплатны:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/...

Я подчеркиваю, что эти kernel_fpu_begin просто отключает вытеснение потока (preempt_disable), потому никакого состояния FPU не сохраняется для ядерного кода. Судя по всему, преимущества векторных операций не перевешивают накладных расходов.

byko3y ★★★ ()
Ответ на: комментарий от byko3y

Хочу рассказать историю, может прокомментируете.

Жил-был сервер, на нём было 64G RAM и работало штук 16 процессов, каждый из которых заммапил 5-гиговый файл в память. Да, на всех памяти не хватало. Каждый демон иногда читал «какие-то» 100 байт из файла (неявно, посредством доступа по указателю; mmap же). Не рандомные 100 байт: некоторые части файла никогда не читались, а какие-то читались чаще - что юзер запросит; а у юзера деятельность не «рандомная» и некие паттерны всё-таки есть. В целом, на все 16 5-гиговых файла можно смотреть как на один 5*16 гиговый и идеальной системой кеширования можно считать общую LRU блоков на все 16 файлов. Ну или 16 отдельных LRU, разницы мало.

В общем ожидается, что одно ядро на все эти 16 файлов смотрит сквозь какое-то одно LRU. На практике хрен его знает как там внутри ядра оно реально работало всё это время, но диск был нагружен слабо - выглядит, что работает оно как я себе представляю: одно LRU на все файлы (да хоть 16 отдельных LRU per process). Но иногда возникали странные повисания внутри ядра на полсекунды с вот таким стеком: http://0x0.st/iDJF.png

igloev ()
Ответ на: комментарий от igloev

https://cdn.kernel.org/pub/linux/kernel/v5.x/ChangeLog-5.2

commit dffcac2cb88e4ec5906235d64a83d802580b119e
Author: Shakeel Butt <shakeelb@google.com>
Date:   Thu Jul 4 15:14:42 2019 -0700

    mm/vmscan.c: prevent useless kswapd loops
    
    In production we have noticed hard lockups on large machines running
    large jobs due to kswaps hoarding lru lock within isolate_lru_pages when
    sc->reclaim_idx is 0 which is a small zone.  The lru was couple hundred
    GiBs and the condition (page_zonenum(page) > sc->reclaim_idx) in
    isolate_lru_pages() was basically skipping GiBs of pages while holding
    the LRU spinlock with interrupt disabled.
    
    On further inspection, it seems like there are two issues:
    
    (1) If kswapd on the return from balance_pgdat() could not sleep (i.e.
        node is still unbalanced), the classzone_idx is unintentionally set
        to 0 and the whole reclaim cycle of kswapd will try to reclaim only
        the lowest and smallest zone while traversing the whole memory.
    
    (2) Fundamentally isolate_lru_pages() is really bad when the
        allocation has woken kswapd for a smaller zone on a very large machine
        running very large jobs.  It can hoard the LRU spinlock while skipping
        over 100s of GiBs of pages.
    
    This patch only fixes (1).  (2) needs a more fundamental solution.  To
    fix (1), in the kswapd context, if pgdat->kswapd_classzone_idx is
    invalid use the classzone_idx of the previous kswapd loop otherwise use
    the one the waker has requested.

Есть интересный вариант с раскидыванием баз по разным cgroup-ам, чтобы LRU у каждого был свой.

byko3y ★★★ ()

В общем случае БД может хранить данные на нескольких устройствах. Что произойдет, если одно из устройств выйдет из строя в момент активного чтения/записи? В случае вызовов pread/pwrite вернется код ошибки (EBADFD или что-то вроде того), в случае доступа к mmap’ed файлу произойдет аварийная остановка программы по SIGBUS. Теперь представьте себе большой кластер из БД, в котором регулярно «вылетают» диски. Скорее всего, оператор будет постоянно занят ручным восстановлением слетевших/corrupted БД. Это еще один аргумент относительно mmap/pread - надежность.

anonymous ()