Pull to refresh

In-Memory OLTP в SQL Server 2014. Часть I

Reading time 9 min
Views 30K
Функциональность In-Memory OLTP (проект Hekaton) призвана ускорить обработку типовых OLTP-операций в SQL Server. Как известно, нагрузку, приходящуюся на сервер баз данных, будь то Microsoft SQL Server или Oracle/MySQL или SAP/Sybase или IBM DB2 и т.д., можно условно разбить на два класса: сбор данных и анализ того, что собрали, потому что зачем в противном случае было собирать? Первый класс задач называется OLTP (On-Line Transactional Processing). Для него характерны короткие обновляющие транзакции, затрагивающие относительно небольшое число таблиц в базе. Примеры таких приложений — операционный день в банке, биллинг в телекоме и т.д. Второй класс задач называется OLAP (On-Line Analytical Processing) и характеризуется массивным длительным чтением, охватывающим значительное количество таблиц и собирающим из них, как правило, по максимуму записей, изобилующим предикатами связывания, сортировки, группирования, агрегатными функциями и т.д. Как правило, структуры данных для разных классов задач разделяют, чтобы не мешали друг другу, и если первая структура базы строится с учетом многочисленных правил Кодда, то вторая, напротив, денормализована и выполняется по схеме «звезда (снежинка)».
Устремление СУБД в память проявилось в начале нового тысячелетия, когда выяснилось, что несмотря на закон Мура тактовая частота и быстродействие процессоров растут отнюдь не по экспоненте, а наоборот, выходят на плоскую прямую насыщения невзирая на ILP и прочие ухищрения. В то же время цены на оперативную память, когда-то неприлично дорогую, катастрофически снижаются и по сравнению с 90-ми упали в тысячи раз. Ага, сказали себе производители серверов баз данных. В 2005-м Oracle прикупил in-memory СУБД TimesTen, IBM в 2007-м — компанию Solid, а в Microsoft в данном случае ничего со стороны брать не стали, потому что решили воспитать в своем коллективе.

Начали с In-Memory OLAP. Первые плоды воспитания воплотились в ColumnStore-движке VertiPaq с выходом SQL Server 2008 R2 в виде PowerPivot для Excel и SharePoint. Как следует из названия, колоночный индекс устроен так, что каждая страница содержит в себе кусок колонки в высоко сжатом виде (~ в 2 раза лучше, чем при появившейся в SQL Server 2008 page-компрессии), чтобы утрамбовать больше данных в память. Традиционно от колоночных индексов выигрывает OLAP, потому что, как мы помним, это массивное чтение. Как правило, требуется прочитать вдоль по строкам (или по диапазону) все колонки, по которым справочники (таблицы измерений, лучи звезды) связаны с колонками внешних ключей в таблице фактов (ступице), чтобы построить между ними join (или semi-join). В SQL Server 2012 в этом плане произошли две полезные вещи. Во-первых, xVelocity (бывш. VertiPaq) появилась в Analysis Services в виде так называемых табличных моделей (Tabular Model), альтернативных традиционным многомерным, существовавшим еще, дай Б-г памяти, с SQL Server 7.0. Во-вторых, колоночный индекс перестал быть вещью в себе, и его стало возможно построить по мере надобности явно — в T-SQL появилась команда CREATE [NONCLUSTERED] COLUMNSTORE INDEX. На колоночные индексы сразу была наложена туча ограничений, самым жестоким из которых было, конечно, вот это — UPDATE statement failed because data cannot be updated in a table that has a nonclustered columnstore index. Consider disabling the columnstore index before issuing the UPDATE statement, and then rebuilding the columnstore index after UPDATE has completed. В SQL Server 2014 это зло побороли при помощи обновляемого кластерного колоночного индекса. Ну как колоночного… Чтобы сделать колоночный индекс обновляемым, на него навесили delta store и delete bitmap. Когда запись удаляется, она физически не исчезает, а на нее взводится флажок в delete bitmap. Когда происходит вставка записи, она попадает в delta store. И то, и другое — обычные B-Tree (rowstore) со всеми вытекающими плюсами и минусами. Есть фоновый процесс Tuple Mover, который ползает по delta store и конвертирует добавленные записи в сегменты columnstore, но, вообще говоря, чтение колоночного индекса означает чтение не только columnstore, но еще и этих двух друзей-довесков, потому как требуется отфильтровать удаленные записи и сделать union добавленных. Тем не менее, через две недели после своего выхода SQL Sever 2014 продемонстрировал рекордные результаты в независимых аналитических тестах TPC-H, заняв первые строчки в турнирных таблицах по весовым категориям 1, 3 и 10 ТБ объема БД в некластерном (standalone) зачете. Таким образом, будем считать, что с in-memory OLAP все обстоит замечательно и перейдем к in-memory OLTP.

Как уже говорилось, Hekaton — это не кодовое название очередной версии SQL Server, как были Денали, Катмай, Юкон и т.д., а, собственно, проект по разработке in-memory движка, т.е. составной части продукта. Этот компонент является наиболее ярким нововведением не только в текущей версии, но и, возможно, в масштабе всей линейки продуктов, начиная с 16-битного Ashton-Tate/Microsoft SQL Server 1.0, увидевшего свет 25 лет назад. Гекатон — слово греческое и означает сто или сам-сто = в сто раз, что кагбэ намекает, что это не придрацца круче в сравнении с тем, у кого всего в десять. Хотелось бы сразу предостеречь от заблуждения, что Гекатон — некий расширенный вариант dbcc pintable, потому что с прикрепленной к памяти таблицей работа происходит, как с обычной дисковой, включая планы выполнения, обеспечение транзакционной целостности с помощью блокировок и т.д. Гекатон — компактное самостоятельное ядро, интегрированное внутрь исполнительного механизма SQL Server, характеризующееся по сравнению с традиционным database engine отсутствием интепретируемых планов выполнения, блокировок как средства обеспечения логической целостности данных и латчей для физической целостности. Напомню, что латчи (не знаю, как они идеологически верно переводятся на русский, наверно, защелки) — это легковесные блокировки, которые накладываются на страницы данных, индексные страницы, какие-то служебные структуры нпосредственно в момент их чтения или изменения в памяти в отличие от блокировок, которые могут действовать на протяжении всей транзакции. Поэтому, не погружаясь в детали, можно считать, что дедлатчей не бывает. Бывают нюансы, но не такие болезненные. Другое отличие состоит в том, что блокировками можно управлять (при пом. хинтов, уровня изоляции). Латчи находятся в сугубом ведении SQL Server. Я не буду подробно вдаваться в их внутреннее устройство, желающие могут обратиться к BOL или на страницу Евгения Хабарова. Я вообще не буду ударяться в теорию, давайте лучше перейдем к примерам.

Создадим БД и в ней файл-группу под in-memory OLTP.

CREATE DATABASE hekaton_test
ALTER DATABASE hekaton_test ADD FILEGROUP fg_hekaton_test CONTAINS MEMORY_OPTIMIZED_DATA 
ALTER DATABASE hekaton_test ADD FILE (name='fg_hekaton_test1', filename='c:\Temp\fg_hekaton_test1') TO FILEGROUP fg_hekaton_test     

Скрипт 1

Это файлстримоподобная файл-группа, в нее будут персиститься данные из памяти, а в процессе recovery. соответственно, читаться взад. Подобно clustered columnstore она состоит из дата-файлов, в которые последовательно пишутся вставленные в рез-те insert или update записи и дельта-файлов, в которых хранятся идентификаторы удаленных записей. Сначала изменения, как водится, отражаются в памяти, а при checkpoint блоками по 256К (в случае data) и 4К (delta) скидывается на диск, о чем делается отметка в журнале транзакций. Слияние пар data-delta происходит автоматически при достижении определенного размера и в зависимости от размера оперативной памяти, а также может делаться вручную процедурой sys.sp_xtp_merge_checkpoint_files. Подробнее об этом процессе можно прочитать здесь.
Таким образом, размещение таблиц в памяти не означает, что если сервер отрубился, все, что нажито непосильным трудом, все будет потеряно. In-Memory OLTP — полностью транзакционная технология и поддерживает средства отказоустойчивости, включая AlwaysOn.

В свежесозданной БД создадим таблицу, оптимизированную для работы в памяти.

use hekaton_test
CREATE TABLE dbo.ShoppingCart ( 
   ShoppingCartId int not null primary key nonclustered hash with (bucket_count=2000000), 
   UserId int not null index ix_UserId nonclustered hash with (bucket_count=1000000), 
   CreatedDate datetime2 not null, 
   TotalPrice money 
) 
WITH (MEMORY_OPTIMIZED=ON)  

Скрипт 2

Последняя опция как раз означает, что таблица будет размещена в памяти. Каждая MEMORY_OPTIMIZED таблица обязана иметь хотя бы один индекс.Общее число не должно превышать 8. Входящие в индек поля не должны быть nullable. Для входящих в индексы полей n(var)char должна использоваться коллация BIN2. Кластерных индексов, по определению, нет. По своей структуре классических B-Tree тоже. Для таблиц в памяти индексы бывают HASH (лучше подходит для точечного поиска) и RANGE (как следует из названия, лучше подходит для сканов по диапазонам).
Основным компонентом хэш-индекса служит так называемая таблица соответствий (mapping table), в одной колонке которой хранятся результаты применения хэш-функции к конкатенации полей, образующих индексный ключ, в другой — указатели на записи. Поскольку хэш-функция может давать одинаковые результаты (возникают коллизии) для совершенно разных значений аргумента (для близких они как раз обязаны различаться), это в действительности будут указатели на области памяти, где лежат цепочки переполнений. Каждая такая цепочка представяляет собой двунаправленный список. Основным параметром при создании хэш-индекса выступает bucket_count. Это число слотов в таблице соответствий. Чем меньше их будет, тем выше вероятность коллизии, тем длиннее цепочки переполнений будут расти из каждого хэша. Соответственно, очевидно, что оно должно быть не меньше, чем количество уникальных значений в индексном ключе. На самом деле, оно оценивается как количество уникальных значений, округленное вверх до следующей степени 2, и здесь подробно объясняется, почему.
Второй тип индекса, доступный in-memory, называется range (диапазонный) и очень напоминает классический кластерный. Его узлы образуют упорядоченную структуру, эффективную для сканов по диапазонам. Вместо B-Tree используется его модификация Bw-Tree, наиболее ярким отличием котороя, пожалуй, является то, что она не хранит указатели на повторяющиеся значения. Если в таблице имеется миллион одинаковых значений, классическое дерево будет тупо держать в листьях миллион указателей (на данные). Bw обходится в этом случае одним, что позволяет драматически (англоязычные авторы очень любят это слово) сэкономить место при засовывании этого хозяйства в память. Единственно, в этом случае снова возникают цепочки переполнений — мы же не храним указатели на все записи, как добраться до следующей с таким же значением ключа? Возникает ощущение, что никакой экономии нет, просто байты на ptr переложили с листьев в букеты. Но нет, читайте, почему это не так, здесь, а мы двинемся дальше и создадим еще одну таблицу.

CREATE TABLE dbo.UserSession ( 
   SessionId int not null primary key nonclustered hash with (bucket_count=400000), 
   UserId int not null, 
   CreatedDate datetime2 not null, 
   ShoppingCartId int, 
   index ix_UserId nonclustered hash (UserId) with (bucket_count=400000) 
) 
WITH (MEMORY_OPTIMIZED=ON, <b>DURABILITY=SCHEMA_ONLY</b>)      

Скрипт 3

Обратите внимание на последнюю опцию, выделенную жирным цветом. Она означает, что данные этой таблицы не будут сохранены между рестартами сервера (сама структура останется). Выше я говорил, что по поводу Гекатона существует заблуждение, будто все, что находится в памяти, при рестарте теряется. Так вот для данных таблиц это действительно так, но вы создаете их совершенно осознанно, чтобы уменьшить накладные расходы, в частности, на журналирование там, где это не нужно. Это своего рода аналог временных таблиц. К слову, табличные переменные в Гекатоне также поддерживаются. Они объявляются через предварительное создание табличного типа CREATE TYPE… AS TABLE… В отличие от обычных табличных переменных они хранятся, понятно, не в дисковой tempdb, а относятся к той базе, где были объявлены.

Размещаемые в памяти таблицы не поддерживают автоматическое обновление статистики, в частности, ALTER DATABASE… SET AUTO_UPDATE_STATISTICS ON. Также не работает
exec sp_autostats @tblname = 'ShoppingCart'
Index Name AUTOSTATS Last Updated
[ix_UserId] OFF NULL
[PK__Shopping__7A789AE57302F83B] OFF NULL
exec sp_autostats @tblname = 'ShoppingCart', @flagc = 'ON'
— Operations that require a change to the schema version, for example renaming, are not supported with memory optimized tables.
Однако статистику можно обновлять вручную: UPDATE STATISTICS dbo.ShoppingCart WITH FULLSCAN, NORECOMPUTE.
Вообще таблицы в памяти имеют тучу ограничений. Не поддерживается большинство табличных хинтов: Нет TABLOCK, XLOCK, PAGLOCK,… На NOLOCK не ругается, но и не реагирует, как будто его и нет. Динамические и keyset-курсоры молчаливо переводятся в static. Не поддерживаются операторы TRUNCATE TABLE и MERGE (когда таблица в памяти выступает назначением). Существуют ограничения на типы используемых полей. Подробно прочитать о них можно здесь, мы же, дабы не растраиваться, посмотрим, что у нас получилось.

SELECT name, description FROM sys.dm_os_loaded_modules WHERE description = 'XTP Native DLL' 

Скрипт 4

В каталоге установки SQL Server C:\Program Files\Microsoft SQL Server\...\DATA\xtp\11\ появилось две dll, которые называются xtp_t_11_<9 цифр>.dll. Это наши таблицы ShoppingCart и UserSession.
Встроенный компилятор преобразует T-SQL-определения таблиц и хранимых процедур в Cшный код (можно посмотреть в том же каталоге), из которого получается машинный. Соответствующие динамические библиотеки загружаются в память и линкуются внутрь SQL Serverного процесса. При рестарте SQL Server библиотеки компилируются и загружаются заново на основе каталожной информации из метаданных.

В следующей части планируется рассмотреть нативную компиляцию хранимых процедур, уровни изоляции транзакций в Гекатоне, блокировки, журналирование и общую производительность по сравнению с традиционными дисковыми объектами.
Tags:
Hubs:
+6
Comments 7
Comments Comments 7

Articles