Доступ к файлам через отображение-в-память (mmap
) — это способность некоторых операционных систем отобразить содержимое какого-либо файла в адресное пространство программы. Сама программа получает доступ к содержимому файла через указатели, как если бы сам файл был бы целиком загружен в оперативную память. Операционная система прозрачно загружает части файла в оперативную память, и автоматически выгружает их, когда памяти не хватает.
MMAP захватила умы программистов СУБД на многие десятилетия, как альтернатива буферу данных. И вот здесь следует отметить, что в mmap
имеются серьёзные проблемы с корректностью и скоростью работы с данными в современных СУБД. В реальности, некоторые известные СУБД сперва использовали mmap
для работы с "больше-чем-вмещается-в-память" базами данных, но вскоре обнаружили эти скрытые ограничения, которые принудили их к самостоятельному управлению файловым вводом/выводом, после заметных трат на инженерные исследования. В этом смысле mmap
и СУБД подобны сочетанию кофе и острой пищи: неудачное сочетание, которое не очевидно, пока сам не попробуешь.
Покуда разработчики по прежнему пытаются использовать mmap
в новых СУБД, мы написали эту статью, чтобы предупредить остальных, что mmap это не подходящая замена привычному буферу. Далее мы обсудим основные недостатки mmap
, а также покажем явные ограничения производительности, которые мы обнаружили в наших экспериментах. На основании этих находок мы поделимся рецептами, когда разработчикам СУБД следует избегать mmap
для реализации файлового ввода/вывода.
1. Введение
Важной особенностью дисковых СУБД является их возможность поддерживать БД, которые не помещаются в доступную физическую память. Такая функциональность позволяет пользователю выполнять запросы к БД, как будто она находится целиком в оперативной памяти, даже если она туда не помещается. СУБД реализуют эту иллюзию с помощью чтения данных из внешнего хранилища данных (HDD или SSD) по требованию. Если памяти не хватает, СУБД отбрасывает ранее загруженные данные, чтобы освободить место для новых.
Традиционно СУБД реализуют управление данными во внешнем хранилище с помощью буфера, который взаимодействует с хранилищем с помощью системных вызовов read и write. Такие способы управления файловым вводом/выводом копируют данные в буфер и из буфера в пространстве пользователя, и через них СУБД реализует полный контроль над тем, как и когда она управляет данными.
Ещё СУБД может уступить ответственность за управление данными Операционной Системе, которая сама умеет управлять файловым отображением-в-память и кэшем. В POSIX вызов mmap
приводит к отображению файла во внешнем хранилище в виртуальное адресное пространство вызывающего процесса (т.е. СУБД), после чего ОС загружает данные из файла лишь в тот момент, когда СУБД обращается к ним. С точки зрения СУБД, база данных выглядит как целиком загруженная в память. Но ОС в тени выполняет все необходимые операции ввода/вывода, заменяя собой буфер СУБД.
Внешне mmap
походит на привлекательную реализацию файлового ввода/вывода в СУБД. Самым заметным преимуществом кажется лёгкость доступа и поддержки. СУБД больше не нужно отслеживать, какие данные находятся в памяти, а также не нужно отслеживать, к каким данным нужен доступ, и какие данные не сохранены на диск. Вместо этого СУБД имеет простой доступ ко всем данным, находящимся на диске через указатели, как если бы они находились в памяти; оставляя всё низкоуровневое управление ОС. Если доступная память заполняется, то ОС освободит место для новых данных, прозрачно выгружая (в идеале ненужные) данные из кэша данных.
С точки зрения производительности кажется, что у mmap
значительно меньше оверхеда, чем у традиционного буфера. В частности, mmap
не чувствителен к стоимости явных системных вызовов (т.е. read
/write
), а ещё избегает ненужного копирования в userspace, потому что СУБД может работать с данными напрямую в кэше ОС.
Начиная с ранних 1980х, эти предполагаемые плюсы побудили разработчиков СУБД пренебречь реализацией буферов и целиком положиться на ОС для управления файловым вводом/выводом [36]. Фактически разработчики некоторых широко известных СУБД (см. раздел 2.3) целиком положились на этот метод, а некоторые даже рекламировали mmap как ключевой метод хорошей производительности [20].
К сожалению, у подхода mmap
есть "тёмная сторона" со множеством сложных проблем, которые делают его нежелательным методом реализации файлового ввода/вывода в СУБД. Как мы далее опишем в этой статье, эти проблемы затрагивают как безопасность данных, так и производительность. Нам кажется, что методы борьбы с этими проблемами превышают простоту работы с mmap
. Поэтому мы убеждены, что mmap
привносит чересчур много сложностей, при отсутствии заметных преимуществ в производительности. Поэтому мы настоятельно советуем разработчикам СУБД не заменять mmap
ом традиционный буфер.
Остаток этой статьи устроен следующим образом: Мы начнём с короткого описания истории (раздел 2), затем перейдём к обсуждению основных проблем (раздел 3), и нашим экспериментам (раздел 4). Затем мы обсудим другие статьи (раздел 5), и наконец, подытожим нашими рекомендациями и советами о том, где вам возможно стоит рассмотреть mmap
как способ работы в вашей СУБД (раздел 6).
2. История
В этом разделе находится актуальное описание mmap
. Начнём с высокоуровневого обзора файлового ввода/вывода через отображение-в-память и POSIX mmap API. Далее поговорим о существующих проектах на базе mmap
.
2.1 Обзор MMAP
Пошаговая иллюстрация доступа к файлу через mmap
.
Диаграмма иллюстрирует пошаговый доступ к файлу ("cidr.db") через mmap
. (1) приложение вызывает mmap и получает указатель на отображение содержимого файла в память. (2) ОС резервирует часть виртуального адресного пространства приложения, но не загружает в него ничего из содержимого файла. (3) Приложение обращается к содержимому файла по указателю. (4) ОС пытается предоставить запрошенную страницу. (5) Поскольку никаких действительных отображений в этот виртуальный адрес не существует, ОС инициирует ошибку доступа к памяти, что приводит к загрузке нужной части файла из внешнего хранилища в физическую память. (6) ОС добавляет ссылку на загруженную физическую страницу в таблицу, которая отображает виртуальный адрес приложения в физический. (7) Ядро ЦПУ, которое попыталось получить доступ к отображённому адресу, кэширует полученные данные в локальном буфере трансляций (TLB), чтобы ускорить доступ в будущем.
По мере доступа приложения к другим данным, ОС также загружает их в память, при этом вытесняя по необходимости ранее загруженные страницы. При вытеснении, ОС удаляет соответствие из таблицы отображений, и из каждого локального буфера трансляций (TLB) каждого ядра. Очистка локального TLB ядра, инициировавшего доступ довольно проста, но ОС также нужно убедиться, что TLB других ядер также не осталось никаких "висячих" ссылок. Поскольку существующие ЦПУ не обеспечивают когерентности всех TLB, ОС приходится инициировать для этого дорогостоящее межпроцессорное прерывание, также известное как "сбой TLB" (TLB shootdown) [11]. Наши эксперименты показывают (см. раздел 4), что сбой TLB существенно влияет на производительность.
2.2 POSIX API
Рассмотрим наиболее важные системные вызовы POSIX для управления файловым вводом/выводом через mmap
, и опишем, как СУБД могут использовать их вместо традиционного буфера.
mmap. Как уже описано, этот вызов заставляет ОС отобразить файл в адресное пространство СУБД. После этого СУБД может читать или писать содержимое файла с помощью обычных операций доступа к памяти. ОС кэширует данные в памяти, а также, с помощью флага
MAP_SHARED
, в какой-то момент записывает изменения обратно во внешний файл. Альтернативно, с помощью флагаMAP_PRIVATE
создаётся copy-on-write отображение, доступное только вызывающему приложению (т.е. изменения не записываются во внешний файл).
madvise. С помощью этого вызова СУБД подсказывает ОС о том, как она собирается работать с данными — либо в масштабах всего файла, либо при доступе к его определённым областям. Рассмотрим три популярных флага:
MADV_NORMAL
,MADV_RANDOM
иMADV_SEQUENTIAL
. Когда ошибка доступа к данным возникает в Linux с действующим по умолчанию хинтомMADV_NORMAL
, ОС кэширует запрошенную страницу, а также 16 следующих и 15 предыдущих страниц. При размере страницы в 4 КБ,MADV_NORMAL
приводит к тому, что ОС считывает 128 КБ из внешнего хранилища, даже если приложению нужна всего одна страница. В зависимости от нагрузки такой префетчинг может улучшить, либо же ухудшить производительность СУБД. Например, хинтMADV_RANDOM
, который приводит к чтению одной нужной страницы, лучше работает для "больше-чем-вмещается-в-память" БД, аMADV_SEQUENTIAL
лучше подходит для БД, где файл читается последовательно от начала до конца.
mlock. Этот вызов позволяет СУБД закрепить данные в памяти так, чтобы ОС никогда их не вытеснила. Однако согласно стандарту POSIX (и его реализации в Linux), ОС может выгрузить "грязные" данные во внешний файл в любой момент, даже если они закреплены. Поэтому СУБД не может использовать
mlock
как гарантию, что изменённые данные не будут записаны во внешнее хранилище, что приводит к серьёзным последствиям для безопасности транзакций.
msync. Наконец, этот вызов явно записывает данные во внешнее хранилище. Без
msync
у СУБД не было бы никакого другого метода гарантировать, что изменённые данные сохранены во внешнем файле.
2.3 С MMAP что-то пошло не так
СУБД | Работа с MMAP | Детали |
---|---|---|
MonetDB | 2002— | [12, 21] |
MongoDB | 2009—2019 | [14, 3] |
LevelDB | 2011— | [5] |
LMDB | 2011— | [20] |
SQLite | 2013— | [7] |
SingleStore | 2013—2015 | [32] |
QuestDB | 2014— | [34] |
RavenDB | 2014— | [4] |
InfluxDB | 2015—2020 | [8, 1] |
WiredTiger | 2020— | [17] |
[Современные СУБД, основанные на mmap]
Управляемый ОС файловый буфер привлекает разработчиков СУБД на протяжении многих десятилетий [36], начиная с QuickStore [40] и Dalí [22], как наиболее ранних примеров mmap
-систем из 1990х. В настоящее время многие СУБД продолжают использовать mmap
для файлового ввода/вывода, как показано в таблице 1. Например, MonetDB хранит отдельные колонки как отображённые-в-память файлы [12, 21]. SQLite также предоставляет возможность использовать mmap
вместо системных вызовов read
/write
[7]. СУБД LMDB целиком работает на mmap
, и разработчики даже приводят это как главную причину хорошей производительности [20]. Так же упомянем другие системы с хранилищем на основе mmap
, такие, как QuestDB [34] и RavenDB [4].
Несмотря на эти очевидные "истории успеха", многие другие СУБД потерпели неудачу в попытке заменить традиционный буфер ввода/вывода на mmap
. Далее мы вспомним несколько примечательных историй о том, как mmap
в вашей СУБД может привести к ужасным последствиям.
MongoDB пожалуй, наиболее известная СУБД, которая использовала mmap
для файлового ввода/вывода. Насколько мы поняли подход разработчиков, на ранней стадии они решили реализовать основную систему хранения (MMAPv1) с помощью mmap
. Однако позже всплыло множество недостатков такого подхода, таких как слишком сложная схема копирования для поддержания целостности, а также невозможность реализовать какое-либо сжатие данных во внешнем хранилище. Позже выяснилось, что поскольку ОС управляет отображением файла в память, данные в памяти должны соответствовать физическому представлению во внешнем хранилище, что привело к излишнему расходованию места и уменьшению скорости ввода/вывода. После реализации хранилища WiredTiger в 2015-м, MongoDB перестали поддерживать MMAPv1, и окончательно избавились от него в 2019 [3]. В 2020-м MongoDB снова попытались использовать mmap
как одну из опций работы в WiredTiger, но теперь она используется в ограниченном контексте для уменьшения излишних накладных расходов при пересечении границ между пользовательским режимом и ядром ОС [17].
InfluxDB — СУБД для хранения временных рядов, использовала mmap
в ранних релизах [8]. Однако после того, как разработчики столкнулись с всплесками ввода/вывода при записи в БД, когда её размер превысил несколько гигабайт, очень похожими на издержки от вытеснения страниц (раздел 3.4), от него отказались. Кроме этих всплесков были также иные проблемы при запуске в среде контейнеров без явного хранилища (т.е. в облаке), что окончательно побудило их отказаться от использования mmap
для ввода/вывода в новом движке хранилища [1].
SingleStore избавились от файлового ввода/вывода, основанного на mmap
после того, как столкнулись с низкой производительностью обычных последовательных запросов [32]. Вызов mmap из СУБД занимал по 10-20мс для каждого запроса, что составило примерно половину всего времени выполнения запроса. После дальнейшего расследования разработчики нашли источник проблемы в конфликте из-за общей блокировки записи mmap
. И после переключения на системные вызовы read
, время выполнения запросов стало зависеть только от ЦПУ.
Некоторые другие системы избавились от mmap
ещё раньше. Например, Facebook создала RocksDB как форк от LevelDB от Google отчасти из-за проблем с производительностью при чтении данных, вызванных тем, что последняя использовала mmap
[5]. Создатели TileDB обнаружили, что использование системных вызовов mmap
при чтении данных с SSD обходится дороже, чем использование read
[27], о чём мы ниже поговорим на примере наших собственных анализов (раздел 4). Разработчики Scylla, распределённой СУБД NoSQL, попробовали несколько вариантов файлового ввода/вывода и отвергли mmap
из-за потери детального контроля как в стратегии вытеснения данных, так и в планировании операций ввода/вывода [23]. СУБД для хранения временных рядов VictoriaMetrics столкнулись с проблемами блокировки ввода/вывода через mmap
при доступе к "холодным" данным [37]. Наконец, RDF-3X отказались от своего оригинального движка на основе mmap
из-за несовместимости между реализациями ввода/вывода через отображённые файлы между POSIX и Windows [26].
3. Основные проблемы MMAP
На первый взгляд, mmap
кажется отличным решением: СУБД больше не нужно управлять собственным буфером, всю ответственность за это теперь несёт ОС. Удалив компоненты, которые напрямую связаны с файловым вводом/выводом, разработчики СУБД могут сосредоточиться на других аспектах системы. Однако прозрачная подкачка данных в действительности создает для СУБД несколько серьезных проблем, о которых мы поговорим ниже.
3.1 Проблема №1: Безопасность транзакций
Проблемы, связанные с обеспечением безопасности транзакций измененных данных в СУБД на основе mmap
, хорошо известны [22, 18]. Ключевым недостатком является то, что из-за прозрачной подкачки ОС может сбросить изменённые данные во внешнее хранилище в любой момент, независимо от того, была или нет зафиксирована транзакция записи. СУБД не может предотвратить это, а также не получает никаких уведомлений об этом.
СУБД, работающая через mmap
, должна реализовать сложный протокол, чтобы убедиться, что прозрачная подкачка не привела к нарушению безопасности транзакций. Методы работы с обновлениями можно разделить на три категории: copy-on-write на уровне ОС, copy-on-write на пользовательском уровне, и теневой буфер. Для упрощения модели будем считать, что СУБД хранит базу данных в единственном файле.
Copy-on-write на уровне ОС
Основной идеей этого подхода является создание двух копий базы данных с помощью mmap
, при этом изначально они указывают на одни и те же физические данные. Первая является основной копией, а вторая служит закрытым хранилищем, где накапливаются изменения транзакций. Важно, СУБД создаёт закрытое хранилище с помощью флага MAP_PRIVATE
, что позволяет ОС задействовать для этих данных механизм copy-on-write. Из всех СУБД такой подход был использован только в движке MMAPv1 от MongoDB.
СУБД изменяет данные в закрытой копии. При этом ОС прозрачно копирует данные в новое место и применяет изменения. Основная копия при этом не видит изменений, и ОС не сохраняет их в файле базы данных. Поэтому, чтобы обеспечить надежность, СУБД должна использовать журнал предварительной записи (WAL) для записи изменений. Когда транзакция фиксируется, СУБД сбрасывает соответствующие записи WAL во вторичное хранилище и использует отдельный фоновый поток для применения зафиксированных изменений к основной копии.
Поддержание раздельных копий обновлённых данных приводит к возникновению двух основных проблем. Во-первых, СУБД должна убедиться, что последние обновления из зафиксированных транзакций были распространены на основную копию, прежде чем разрешить запуск конфликтующих транзакций, что требует дополнительно отслеживать ожидаемые изменения. Во-вторых, закрытая копия в процессе изменений начинает расти, и в какой-то момент может случиться так, что у СУБД оказываются две полные копии базы данных в памяти. Для решения этой проблемы СУБД может периодически сжимать закрытую копию с помощью системного вызова mremap
. Но перед этим СУБД должна ещё раз убедиться, что все ожидаемые изменения распространены на основную копию. Более того, во избежание потерь данных во время работы mremap
, СУБД должна заблокировать изменения до тех пор, пока ОС не закончит сжатие закрытой копии.
Copy-on-write на пользовательском уровне
В отличие от системного механизма copy-on-write, данный подход предполагает ручное копирование изменённых данных из существующего mmap
в отдельный буфер в пространстве пользователя. Что-то подобное используют SQLite, MonetDB и RavenDB.
Для изменения данных, СУБД применяет изменения только к копиям, а также создаёт соответствующие записи в WAL. Для фиксации изменений СУБД может записать WAL во внешнее хранилище, после чего скопировать изменения обратно в mmap
. Поскольку копирование целых страниц ради небольших изменений довольно накладно, некоторые СУБД поддерживают применение записей из WAL напрямую в mmap
.
Теневой буфер
LMDB является наиболее известным сторонником этого подхода, который основан на дизайне теневой подкачки System R [13]. С теневым буфером СУБД поддерживает раздельные основную и теневую копии базы данных, обе из них на основе mmap
. Для изменения данных СУБД сперва копирует изменяемые данные из основной копии в теневую, и затем производит там нужные изменения. Фиксация изменений включает сброс изменённых данных во внешнее хранилище с помощью msync
, после чего теневая и основная копии базы данных меняются местами.
На первый взгляд такой подход кажется несложным в реализации, однако СУБД должна убедиться, что транзакции не конфликтуют и не видят частичных изменений. LMDB решает эту задачу, разрешая в каждый момент только одну запись.
3.2 Проблема №2: зависание ввода/вывода
При использовании традиционной буферизации, СУБД может использовать асинхронный ввод/вывод (например, libaio, io_uring), чтобы избежать блокировки потоков во время выполнения запросов. Например, при сканировании конечного узла в B+tree, СУБД могла бы асинхронно выдавать запросы на чтение потенциально не связанных данных, чтобы скрыть задержку, однако mmap
не поддерживает асинхронное чтение.
Более того, поскольку ОС может прозрачно вытеснять данные во внешнее хранилище, даже запросы только-для-чтения могут внезапно привести к блокировке, если они попытаются прочесть вытесненные данные. Иными словами, доступ к любым данным может привести к неожиданному зависанию ввода/вывода, поскольку СУБД не знает, находятся ли эти данные в памяти.
Разработчики СУБД могли бы избежать этих проблем, используя системные вызовы, перечисленные в разделе 2.2. Наиболее очевидным кажется использование mlock
, чтобы прикрепить те данные, с которыми СУБД собирается работать в ближайшее время. К сожалению, ОС обычно ограничивает количество памяти, которое может прикрепить каждый процесс, поскольку прикрепление может вызвать проблемы для конкурирующих процессов, и даже для самой ОС. СУБД также нужно тщательно отслеживать и откреплять неиспользуемые данные, чтобы ОС смогла их вытеснить.
Другим возможным решением является использование madvise
, чтобы подсказать ОС возможный шаблон доступа к данным во время запросов. Например, если СУБД подразумевает последовательный доступ, она может вызвать madvise
с флагом MADV_SEQUENTIAL
. В этом случае ОС вытеснит уже прочитанные данные, и загрузит те, которые будут прочитаны далее. Это решение гораздо проще, чем mlock
, но оно также даёт значительно меньше контроля, поскольку флаги всего лишь дают подсказки, которые ОС может игнорировать. Вдобавок, неверный флаг (например, если вы подсказали MADV_SEQUENTIAL
, а по факту доступ оказался случайным) может иметь серьёзные последствия для производительности. Мы продемонстрируем это в ходе наших экспериментов (раздел 4).
Еще одно решение заключается в создании дополнительных потоков для предварительной выборки (т.е. попытки доступа) к данным, чтобы именно они, а не основной поток, блокировались в случае ошибки доступа. Однако, хотя перечисленные подходы могут (частично) решить некоторые проблемы, все они создают значительную дополнительную сложность, которая сводит на нет профит от использования mmap
.
3.3 Проблема №3: обработка ошибок
Одной из ключевых обязанностей СУБД является обеспечение целостности данных, поэтому обработка ошибок имеет первостепенное значение. Например, некоторые СУБД (как SQL Server [6]) вычисляют контрольные суммы страниц данных, чтобы обнаружить возможные повреждения данных во время файлового ввода/вывода. При чтении данных из внешнего хранилища СУБД проверяет содержимое по контрольной сумме, сохранённой в заголовке. Однако при использовании mmap
СУБД необходимо проверять контрольные суммы при любом доступе к данным, поскольку ОС могла прозрачно вытеснить их с момента предыдущего обращения.
Аналогичным образом многие СУБД (включая некоторые упомянутые в разделе 2.3) написаны на языках с небезопасным доступом к памяти, а значит, ошибки работы с указателями могут привести к повреждению данных в памяти. Реализация буфера с поддержкой проверки целостности данных перед записью во внешнее хранилище помогло бы решить эту проблему, но mmap
просто запишет повреждённые данные в файл без всяких проверок.
Наконец, корректная обработка ошибок ввода/вывода становится гораздо сложнее при использовании mmap
. Там, где традиционный буфер позволяет разработчикам сосредоточить обработку ошибок в одном модуле, любой код, работающий с данными, отображёнными через mmap, может порождать сигналы SIGBUS
, с которыми СУБД придётся иметь дело с помощью громоздких обработчиков.
3.4 Проблема №4: низкая производительность
Самый большой и значительный недостаток mmap
связан с производительностью. Даже если разработчики СУБД могут аккуратно написать код так, чтобы преодолеть все остальные проблемы, мы убеждены, что mmap
всё равно останется узким местом, с которым невозможно ничего сделать без редизайна на уровне ОС.
Общепринятое мнение [28, 23, 29, 16, 17, 30] утверждает, что mmap
превосходит традиционный файловый ввод/вывод, поскольку позволяет избежать два основных источника накладных расходов. Во-первых, mmap
позволяет избежать явных системных вызовов read
/write
, поскольку ОС поддерживает отображение и обрабатывает промахи данных самостоятельно, за кулисами. Во-вторых, mmap
позволяет использовать указатели на данные непосредственно в кэше ОС, таким образом избегая лишнего копирования в буфер, находящийся в пространстве пользователя. И дополнительным бонусом, файловый ввод/вывод на основе mmap
позволяет уменьшить потребление памяти, поскольку данные не дублируются без необходимости в пространстве пользователя.
Учитывая сказанное, можно ожидать, что разрыв в производительности между mmap
и традиционным вводом/выводом будет увеличиваться по мере появления новых SSD-накопителей (например, PCIe 5.0 NVMe), которые обеспечат пропускную способность, сопоставимую с оперативной памятью [19]. Но к нашему удивлению оказалось, что механизм вытеснения данных в ОС не может масштабироваться более, чем на несколько потоков для рабочих нагрузок СУБД с объемом данных, превышающим объем внешнего хранилища с высокой пропускной способностью. Мы убеждены, что одной из основных причин, по которой эти проблемы с производительностью остались практически незамеченными, является исторически ограниченная производительность файлового ввода/вывода.
В частности, мы выявили три ключевых проблемы, с которыми сталкивается файловый ввод/вывод на основе mmap
: (1) конфликт таблиц страниц, (2) однопоточное вытеснение данных и (3) сбои TLB. И если первые две проблемы можно частично устранить относительно простыми настройками ОС, то сбои TLB являют гораздо более сложную проблему.
Напомним из раздела 2.1, что сбои TLB происходят во время вытеснения данных, когда ядру необходимо аннулировать сопоставления в удалённом TLB. И если очистка локального TLB относительно дёшева, то вызов межпроцессорных прерываний для синхронизации удалённых TLB может занимать тысячи циклов [39]. Обойти проблему можно лишь с помощью предложенных изменений микроархитектуры [39], либо обширными изменениями внутренностей ОС [15, 9, 10].
4. Экспериментальный анализ
Как объяснено в предыдущем разделе, некоторые из недостатков mmap
могут быть преодолены аккуратной реализацией, однако мы утверждаем, что присущие ему ограничения производительности не могут быть устранены без существенных изменений на уровне ОС. В этом разделе мы представляем наш экспериментальный анализ, который эмпирически демонстрирует эти проблемы.
Эксперименты проводились на компьютере с одним процессором AMD EPYC 7713 (64 ядра, 128 потоков) и 512 ГБ RAM, из которых 100 ГБ было доступно в Linux (v5.11) для кэша страниц. В качестве постоянного хранилища на компьютере было установлено 10 × 3.8 TB Samsung PM1733 SSD (с заявленной пропускной способностью чтения в 7000 МБ/с, и записи в 3800 МБ/с). С указанными дисками мы работали напрямую как с блочными устройствами, чтобы избежать возможных издержек со стороны файловой системы [19].
В качестве образца для сравнения мы использовали fio
[2] (v3.25) с опцией прямого ввода/вывода (O_DIRECT), чтобы обойти кэширование ОС. Мы анализировали только нагрузку чтением, что является наилучшим сценарием для СУБД на основе mmap
; иначе им пришлось бы реализовывать сложную защиту изменений (см раздел 3.1), что привело бы к существенным дополнительным накладным расходам [30]. В частности, мы проанализировали два распространённых сценария доступа к данным: (1) случайное чтение и (2) последовательное сканирование.
4.1 Случайное чтение
В первом эксперименте мы тестировали случайный доступ к области SSD размером 2 ТБ, чтобы смоделировать рабочую нагрузку от OLTP, превышающую объем оперативной памяти. Поскольку для кэша было выделено всего 100 ГБ, 95% всех обращений приводили к промахам кэша (т.е. нагрузка была ограничена именно вводом/выводом).
Рисунок 2а: Пропускная способность.
На рисунке 2а показано количество случайных чтений в секунду при 100 потоках. Образцовый fio
продемонстрировал стабильную производительность и достиг скорости около 900 тыс. чтений в секунду, что соответствует ожидаемой производительности при 100 ожидающих операций ввода/вывода и задержке NVMe примерно в 100 мкс. Другими словами, это демонстрирует, что fio
может полностью утилизировать пропускную способность SSD NVMe.
mmap
, с другой стороны, отработал значительно хуже, даже при использовании подсказки, которая соответствовала рабочей модели нагрузки. В нашем эксперименте мы наблюдали три различные фазы для MADV_RANDOM
. В течение первых 27 секунд mmap
вёл себя подобно fio
. Затем примерно на пять секунд его производительность упала почти до нуля, и наконец восстановилась примерно до половины производительности fio
. Это внезапное падение производительности произошло, когда заполнился кэш, что вынудило ОС начать вытеснять данные из памяти. Другие хинты для madvise
ожидаемо продемонстрировали ещё худшую производительность.
Рисунок 2b: Сбои TLB.
В разделе 3.4 мы перечислили три ключевых источника высоких накладных расходов при вытеснении страниц. Во-первых, это сбои TLB, которые мы измерили с помощью /proc/interrupt
и показали на рисунке 2b. Как уже упоминалось, сбои TLB обходятся дорого (т.е. требуют тысяч циклов [39]), поскольку приводят к межпроцессорным прерываниям для очистки TLB каждого ядра. Во-вторых, ОС использует единственный процесс (kswapd
) для удаления страниц, что в нашем эксперименте делает процессор ограничивающим фактором. Наконец, ОС должна синхронизировать таблицу страниц, что становится очень сложной задачей из-за множества параллельных потоков.
4.2 Последовательное сканирование
Последовательное сканирование — ещё один распространенный способ доступа к данным у СУБД, в частности при нагрузке OLAP. Поэтому мы также сравнили производительность сканирования fio
и mmap
при доступе к области SSD размером 2 ТБ. Сперва мы провели эксперимент с одиночным SSD, а затем повторили его с 10 SSD, объединёнными в программный RAID 0 (md).
Рисунок 3: Последовательное сканирование — 1 SSD (mmap: 20 потоков; fio: libaio, 1 поток, iodepth 256).
Результаты на рисунке 3 показывают, что fio
может использовать всю пропускную способность одного SSD-накопителя при сохранении стабильной производительности. Как и в предыдущем эксперименте, производительность mmap
изначально была аналогична производительности fio
, но затем произошло резкое падение производительности после заполнения кэша страниц, примерно через 17 секунд. Кроме того, флаги MADV_NORMAL
и MADV_SEQUENTIAL
работали ожидаемо лучше, чем MADV_RANDOM
.
Рисунок 4: Последовательное сканирование — 10 SSD (mmap: 20 потоков; fio: libaio, 4 потока, iodepth 256).
Рисунок 4, на котором показаны результаты эксперимента по последовательному сканированию с 10 SSD, ещё больше подчеркивает разрыв между тем, что теоретически может обеспечить современная флэш-память, и тем, чего можно добиться с помощью mmap
. Мы наблюдали примерно 20-кратную разницу в производительности между fio
и mmap
, при этом mmap
практически не показал улучшений по сравнению с результатами использования одного SSD.
Таким образом, мы обнаружили, что mmap
хорошо работает только на одном SSD и только в начальной фазе. Как только начинается вытеснение страниц, или если у вас несколько SSD, mmap
работает в 2-20 раз хуже, чем fio
. В связи с предстоящим выпуском PCIe 5.0 NVMe, который, как ожидается, удвоит пропускную способность каждого SSD-накопителя, наши результаты показывают, что mmap
не может достичь производительности традиционного файлового ввода/вывода при последовательном сканировании.
5. Сопутствующие работы
Насколько нам известно, не проводилось тщательного изучения вопросов, связанных с использованием файлового ввода/вывода на основе mmap
в современных СУБД. Ниже мы опишем некоторые из предыдущих исследовательских работ, в которых рассматривались различные аспекты mmap
.
Учитывая проблемы с обеспечением безопасности транзакций при использовании mmap
, в одном из направлений работы был введен новый вариант failure-atomic системного вызова msync
[31, 38]. Обычно, если система выходит из строя во время вызова msync
, СУБД не имеет возможности узнать, какие страницы были успешно записаны во внешнее хранилище. msync
с failure-atomic реализует тот же API, что и обычный msync
, но при этом гарантирует, что все данные записываются атомарно. В качестве побочного эффекта реализации, failure-atomic msync
отключает способность операционной системы прозрачно вытеснять страницы, что устраняет необходимость во многих механизмах безопасности, которые мы описали в разделе 3.1.
Tucana [28] и Kreon [29] — это экспериментальные СУБД модели ключ-значение, построенные на основе файлового ввода/вывода на основе mmap
. Однако они обе отмечают несколько основных проблем с mmap
(например, потерю детального контроля над планированием ввода/вывода), которые побудили Kreon реализовать свой собственный системный вызов (kmmap
). Эти системы также реализовали сложные схемы copy-on-write, чтобы обеспечить согласованность транзакций.
Другие исследовательские проекты творчески использовали mmap
, не ограничиваясь только лишь заменой буфера. Например, в одном проекте был использован механизм виртуальной подкачки операционной системы с помощью mmap
как простой способ миграции данных во вторичное хранилище [35]. RUMA использовала mmap
для "перепрошивки" отображений страниц для выполнения различных операций (например, сортировки) без физического копирования данных [33].
Наконец, для устранения расходов на отображение страниц, некоторые недавние проекты [18, 24, 25] вместо mmap
используют переключение указателя. Как мы уже отмечали на протяжении всей этой статьи, мы считаем, что эти облегченные методы управления буфером являются правильным подходом, поскольку они могут обеспечить производительность, аналогичную mmap, без его недостатков.
6. Заключение
В этой статье приводятся аргументы против использования mmap
для файлового ввода/вывода в СУБД. Несмотря на очевидные преимущества, мы продемонстрировали основные недостатки mmap
, и наш экспериментальный анализ подтверждает наши выводы об ограничениях производительности. В заключение мы хотим дать разработчикам СУБД следующий совет.
Когда вам не следует использовать mmap
в вашей СУБД:
- Вам необходимо выполнять изменения безопасно с точки зрения транзакций.
- Вам нужно обрабатывать ошибки доступа к страницам без блокировки на медленном вводе/выводе, или нужно точно контролировать, какие данные находятся в памяти.
- Вы заботитесь об обработке ошибок и хотите возвращать корректные результаты.
- Вам требуется высокая пропускная способность на быстрых постоянных устройствах хранения данных.
Когда вам возможно следует использовать mmap
в вашей СУБД:
- Ваш набор данных (или вся база данных) помещается в памяти, и при работе только читается. * Вам нужно срочно вывести продукт на рынок, и некогда заботиться о целостности данных и других технических трудностях в долгой перспективе.
- В иных случаях — никогда.
Благодарности
Эта статья является кульминацией многолетней нездоровой одержимости идеей о том, что разработчики неправильно используют mmap
в своих СУБД. Авторы хотели бы поблагодарить всех, кто внес свой вклад и предоставил полезные отзывы: Chenyao Lou ( PKU), David “Greasy” Andersen (CMU), Michael Kaminsky (BrdgAI), Thomas Neumann (TUM), Christian Dietrich (TUHH), Todd Lipcon (lipcon.org), и Саша Фёдорова (UBC).
Эта работа была поддержана (частично) NSF (IIS-1846158, III-1423210, DGE-1252522), исследовательскими грантами Google и Snowflake, а также стипендиальной программой Alfred P. Sloan Research Fellowship.
Ссылки
[1] Announcing InfluxDB IOx — The Future Core of InfluxDB Built with Rust and
Arrow.
[2] fio: Flexible I/O Tester.
[3] MongoDB MMAPv1 Storage Engine.
[4] RavenDB Storage Engine.
[5] RocksDB FAQ.
[6] SQL Server technical documentation.
[7] SQLite Memory-Mapped I/O.
[8] The InfluxDB storage engine and the Time-Structured Merge Tree.
[9] N. Amit. Optimizing the TLB Shootdown Algorithm with Page Access Tracking. In USENIX ATC, pages 27–39, 2017.
[10] N. Amit, A. Tai, and M. Wei. Don’t shoot down TLB shootdowns! In EuroSys, pages 35:1–35:14, 2020.
[11] D. L. Black, R. F. Rashid, D. B. Golub, C. R. Hill, and R. V. Baron. Translation Lookaside Buffer Consistency: A Software Approach. In ASPLOS, pages 113–122, 1989.
[12] P. A. Boncz, M. L. Kersten, and S. Manegold. Breaking the memory wall in MonetDB. Commun. ACM, 51(12):77–85, 2008.
[13] D. D. Chamberlin, M. M. Astrahan, M. W. Blasgen, J. Gray, W. F. K. III, B. G. Lindsay, R. A. Lorie, J. W. Mehl, T. G. Price, G. R. Putzolu, P. G. Selinger, M. Schkolnick, D. R. Slutz, I. L. Traiger, B. W. Wade, and R. A. Yost. A History and Evaluation of System R. Commun. ACM, 24(10):632–646, 1981.
[14] K. Chodorow. How MongoDB’s Journaling Works, 2012.
[15] A. T. Clements, M. F. Kaashoek, and N. Zeldovich. RadixVM: Scalable address spaces for multithreaded applications. In EuroSys, pages 211–224, 2013.
[16] A. Fedorova. Why mmap is faster than system calls, 2019.
[17] A. Fedorova. Getting storage engines ready for fast storage devices, 2020.
[18] G. Graefe, H. Volos, H. Kimura, H. A. Kuno, J. Tucek, M. Lillibridge, and A. C. Veitch. In-Memory Performance for Big Data. PVLDB, 8(1):37–48, 2014.
[19] G. Haas, M. Haubenschild, and V. Leis. Exploiting Directly-Attached NVMe Arrays in DBMS. In CIDR, 2020.
[20] G. Henry. Howard Chu on Lightning Memory-Mapped Database. IEEE Softw., 36(6):83–87, 2019.
[21] S. Idreos, F. Groffen, N. Nes, S. Manegold, K. S. Mullender, and M. L. Kersten. MonetDB: Two Decades of Research in Column-oriented Database Architectures. IEEE Data Eng. Bull., 35(1):40–45, 2012.
[22] H. V. Jagadish, D. F. Lieuwen, R. Rastogi, A. Silberschatz, and S. Sudarshan. Dalí: A High Performance Main Memory Storage Manager. In VLDB, pages 48–59, 1994.
[23] A. Kivity. Different I/O Access Methods for Linux, What We Chose for Scylla, and
Why, 2017.
[24] V. Leis, M. Haubenschild, A. Kemper, and T. Neumann. LeanStore: In-Memory Data Management beyond Main Memory. In ICDE, pages 185–196, 2018.
[25] T. Neumann and M. J. Freitag. Umbra: A Disk-Based System with In-Memory Performance. In CIDR, 2020.
[26] T. Neumann and G. Weikum. RDF-3X: a RISC-style Engine for RDF. PVLDB, 1(1):647–659, 2008.
[27] S. Papadopoulos, K. Datta, S. Madden, and T. G. Mattson. The TileDB Array Data Storage Manager. PVLDB, 10(4): 349–360, 2016.
[28] A. Papagiannis, G. Saloustros, P. González-Férez, and A. Bilas. Tucana: Design and Implementation of a Fast and Efficient Scale-up Key-value Store. In USENIX ATC, pages 537–550, 2016.
[29] A. Papagiannis, G. Saloustros, P. González-Férez, and A. Bilas. An Efficient Memory-Mapped Key-Value Store for Flash Storage. In SoCC, pages 490–502, 2018.
[30] A. Papagiannis, G. Xanthakis, G. Saloustros, M. Marazakis, and A. Bilas. Optimizing Memory-mapped I/O for Fast Storage Devices. In USENIX ATC, pages 813–827, 2020.
[31] S. Park, T. Kelly, and K. Shen. Failure-Atomic msync(): A Simple and Efficient Mechanism for Preserving the Integrity of Durable Data. In EuroSys, pages 225–238, 2013.
[32] A. Reece. Investigating Linux Performance with Off-CPU Flame Graphs, 2016.
[33] F. M. Schuhknecht, J. Dittrich, and A. Sharma. RUMA has it: Rewired User-space Memory Access is Possible! PVLDB, 9(10):768–779, 2016.
[34] D. G. Simmons. Re-examining our approach to memory mapping, 2020.
[35] R. Stoica and A. Ailamaki. Enabling Efficient OS Paging for Main-Memory OLTP Databases. In DaMoN, 2013.
[36] M. Stonebraker. Operating System Support for Database Management. Commun. ACM, 24(7):412–418, 1981.
[37] A. Valialkin. mmap may slow down your Go app, 2018.
[38] R. Verma, A. A. Mendez, S. Park, S. S. Mannarswamy, T. Kelly, and C. B. M. III. Failure-Atomic Updates of Application Data in a Linux File System. In FAST, pages 203–211, 2015.
[39] C. Villavieja, V. Karakostas, L. Vilanova, Y. Etsion, A. Ramírez, A. Mendelson, N. Navarro, A. Cristal, and O. S. Unsal. DiDi: Mitigating the Performance Impact of TLB Shootdowns Using a Shared TLB Directory. In PACT, pages 340–349, 2011.
[40] S. J. White and D. J. DeWitt. QuickStore: A High Performance Mapped Object Store. In SIGMOD, pages 395–406, 1994.