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

в 7:16, , рубрики: Hekaton, microsoft, метки:

В первой части мы кратко рассмотрели основные возможности SQL Server по in-memory обработке применительно к аналитическим и транзакционным и приложениям, сконцентрировавшись на последних, поскольку in-memory OLTP (Hekaton) является наиболее существенным нововведением в SQL Server 2014. В данной статье мы продолжим обзор функциональности Гекатона на примере ранее созданной БД.

Коль скоро мы говорим о транзакционной обработке в памяти, нелишним будет вспомнить акроним ACID: атомарность, целостность, изолированность и остаточность (эффекта), конкретно — литеру I. Поскольку добиться полной изоляции накладно, в СУБД поддерживаются уровни изоляции, допускающие ту или иную степень компромисса. Это классика жанра. По мере приближения к идеалу в SQL Server поддерживаются: грязное чтение (допускаются нарушения вида: первая транзакция изменяет данные, вторая читает изменения, первая откатывается, получается, что вторая прочла несуществующее), read committed (возможно неповторяющееся чтение: первая транзакция может изменить данные в масштабе чтения второй, и во второй раз вторая транзакция прочтет уже измененные данные), repeatable read (первая транзакция не может изменять записи в масштабе второй, но может вставлять новые — фантомы), serializable (самый строгий – фантомы не проходят). Когда-то давно, когда страницы в SQL Server были 2-килобайтные, в нем поддерживались только страничные блокировки, поэтому два последних уровня были синонимы. Полноценная поддержка блокировки записи появилась в 7.0 (1998). В 2005-м к перечисленным уровням добавился snapshot Isolation, что, строго говоря, уровнем изоляции не является, а превращает SQL Server из блокировочника в версионник. Как раз snapshot является основным для Гекатона, потому что optimistic concurrency выступает наиболее естественным подходом при работе с объектами в памяти. Имеется в виду — по своей сути, т.к. версии записей в данном случае не хранятся в tempdb. Каждая операция записи имеет непрерывно возрастающий номер транзакции, использующийся для последующих чтений. Незафиксированные записи хранятся в памяти, но до коммита не видны, поэтому грязных чтений не бывает. Старые записи удаляются в процессе сборки мусора и освобождают память.
Также в Гекатоне поддерживается Repeatable Read, которое (в отличие от дискового варианта) никого не блокирует. Когда где-нибудь в конце транзакции происходит повторное чтение, то, если записи изменились, транзакция отменяется с ошибкой 41305 “The current transaction failed to commit due to a repeatable read validation failure on table [name]”. Аналогично работает Serializable — в случае возникновения фантомов транзакция отменяется.
Уровень изоляции задается в блоке ATOMIC нативно скомпилированной хранимой процедуры (посмотрим ниже) или в табличном хинте Т-SQL запроса. Как вариант — с помощью датабазной опции MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT, которая при обращении к таблицам в памяти поднимает Read UnCommitted/ Committed до Snapshot. Если привычно написать

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
  UPDATE dbo.UserSession SET ShoppingCartId=3 WHERE SessionId=4 

будет ошибка Msg 41333, Level 16, State 1, Line 3
The following transactions must access memory optimized tables and natively compiled stored procedures under snapshot isolation: RepeatableRead transactions, Serializable transactions, and transactions that access tables that are not memory optimized in RepeatableRead or Serializable isolation.

В Т-SQL скрипте уровень изоляции задается хинтом:

BEGIN TRAN 
  UPDATE dbo.UserSession WITH (SNAPSHOT) SET ShoppingCartId=3 WHERE SessionId=4 
  UPDATE dbo.ShoppingCart WITH (SNAPSHOT) SET TotalPrice=100.00 WHERE ShoppingCartId=3 
COMMIT 
(1 row(s) affected)
(1 row(s) affected)

Теоретически Read Committed для memory optimized таблиц тоже поддерживается, но только в случае autocommit (single statement) транзакций.
Затраты на блокировки при этом минимальны, ради чего, собственно, все и затевалось.

BEGIN TRAN
INSERT dbo.ShoppingCart VALUES (5,500,GETUTCDATE(),50.5) 
SELECT resource_type, resource_description, object_name(resource_associated_entity_id), request_mode, request_type, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID

Обратите внимание, что Гекатон «вешает» блокировку только Schema Stability на таблицу. Никаких Х и IX нет в помине:

In Memory OLTP в SQL Server 2014. Часть II
Рис.1

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

CREATE TABLE dbo.ShoppingCart_Disk ( 
   ShoppingCartId int not null 
primary key, 
   UserId int not null, 
   CreatedDate datetime2 not null, 
   TotalPrice money,
   INDEX ixUserid nonclustered (UserId)) 
...
BEGIN TRAN
INSERT dbo.ShoppingCart_Disk VALUES (5,500,GETUTCDATE(),50.5) 
SELECT resource_type, resource_description, case resource_type when 'object' then object_name(resource_associated_entity_id) else cast(resource_associated_entity_id as sysname) end, resource_associated_entity_id, request_mode, request_type, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID       

Что называется, почувствуйте разницу:

In Memory OLTP в SQL Server 2014. Часть II
Рис.2

Минимальные блокировки — одно из явных преимуществ Гекатона. Приложение выиграет от переноса в память, если оно выполнено в стиле OLTP, т.е. характеризуется относительно короткими транзакциями с высоким процентом конкуренции от многочисленных соединений, и проблема состоит в том, что подолгу висят блокировки или физические латчи и не отпускают ресурсы. Портация будет проще, если используются хранимые процедуры, и проблемы с производительностью удается выделить в относительно небольшое подмножество таблиц/ хранимых процедур. Разумеется, не все сценарии выигрывают. Предельный случай — одна таблица из одной записи, которую все обновляют новым значением. Стремление во что бы то ни стало запихнуть все и вся в память приведет к тому, что она тупо кончится. Кстати, довольно часто задают вопрос, что будет, если Гекатон сожрет всю выделенную ему память? В общем, понятно, что ничего хорошего: Msg 701, Level 17, State 103. There is insufficient system memory in resource pool 'default' to run this query. Что в этом случае делать — как правило, ничего. SQL Server довольно шустро чистит память, и на релизе мне его еще не удалось загнать в ступор, который лечится только рестартом (в отличие от СТР). Сборка мусора спроектирована как неблокирующая и кооперативная, что означает, что несмотря на специальный поток для этих целей, бОльшую часть работы выполняют пользовательские процессы, которые время от времени проверяют и помечают записи для последующего удаления, а затем и удаляют. Специализированный поток включается, когда пользовательская активность совсем мала
Как избежать переполнения в процессе работы? Очевидно, правильно оценивать действительность при переносе таблиц в память, тем более, что sp_spaceused никто не отменял.
Можно использовать Memory Optimization Advisor, который проводит статический анализ схемы, индексов и пр. на предмет потенциальных проблем, и более интеллектуальный AMR (Analysis, Migrate and Report) для динамического анализа по характеру использования (берет статистику из
Performance Data Warehouse). Можно превентивно ограничить Гекатон с помощью регулятора ресурсов:

CREATE RESOURCE POOL mem_optpool WITH (MAX_MEMORY_PERCENT = сколько не жалко)
EXEC sp_xtp_bind_db_resource_pool 'mydatabase', 'mem_optpool‘

Влияние Resource Governor относится только к памяти, выделяемой In-Memory OLTP аллокаторами, т.е. при отсутствии в БД объектов, оптимизированных для памяти, из пула ресурсов память не изымается. In-memory OLTP engine интегрирован в общий SQL Server Memory Manager, в чем вы можете убедиться, запустив

select type, name, memory_node_id, pages_kb/1024 as pages_MB from sys.dm_os_memory_clerks where type like '%xtp%‘

Первый товарищ будет память, аллоцированная системой, последний (memory_node=64) – выделенное административное соединение, и между ними — пользовательские БД с MEMORY_OPTIMIZED_DATA.

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

select m1.ShoppingCartId, m2.UserId, d.CreatedDate, d.TotalPrice from ShoppingCart m1 
join ShoppingCart_Disk d on m1.ShoppingCartId = d.ShoppingCartId 
join UserSession m2 on d.ShoppingCartId = m2.ShoppingCartId

delete from ShoppingCart
insert ShoppingCart select * from ShoppingCart_Disk

update d set TotalPrice = m.TotalPrice from ShoppingCart_Disk d join ShoppingCart m on d.ShoppingCartId = m.ShoppingCartId where m.UserID <= 100

Скрипт 1

Кроме таблиц, в памяти могут размещаться хранимые процедуры. Такие процедуры могут работать только с in-memory таблицами. Процедура обязательно создается с опцией SCHEMABINDING, что означает, что таблицы, на которые ссылается процедура, не могу быть модифицированы или удалены прежде нее. Традиционные процедуры по умолчанию выполняются с опцией EXECUTE AS CALLER. Для процедур в памяти она не поддерживается. Возможными вариантами являются: EXECUTE AS OWNER, EXECUTE AS 'user_name‘ или EXECUTE AS SELF (создатель). Существуют и другие ограничения – см.BOL, «Скомпилированные в собственном коде хранимые процедуры».
Тело процедуры заключается в блок BEGIN ATOMIC из стандарта ANSI SQL, и на данный момент это его единственное применение в T-SQL. Он отличается от BEGIN TRAN тем, что автоматически откатывается при ошибке в то время, как в транзакции необходимо использовать TRY / CATCH и ROLLBACK, т.к. по поводу SET XACT_ABORT ON имеются нюансы. Опциями конструкции служат упоминавшийся выше TRANSACTION ISOLATION LEVEL = SNAPSHOT | REPEATABLEREAD | SERIALIZABLE и LANGUAGE, в качестве которого можно использовать любой язык из sys.languages. Он определяет формат date|time и язык системных сообщений.

CREATE PROCEDURE dbo.usp_InsertSampleCarts @StartId int, @InsertCount int 
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER 
AS 
BEGIN ATOMIC 
WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english')
  IF @StartId is null SELECT @StartId = isnull(MAX(ShoppingCartId), 0) + 1 FROM dbo.ShoppingCart
  DECLARE @i int = 0
  WHILE @i < @InsertCount 
  BEGIN 
    INSERT INTO dbo.ShoppingCart VALUES (@StartId + @i, RAND() * 1000, GETUTCDATE(), NULL) 
    SET @i += 1 
  END
END 

Скрипт 2

Как и в случае таблиц, для процедур происходит нативная компиляция, которая превращает интерпретируемые команды T-SQL в код на С и далее — в машинный код, поэтому если мы сейчас повторим запрос Скрипт 4 из предыдущей части, то увидим, что добавилась 3-я dll, соответствующая свежесозданной процедуре.

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

CREATE PROCEDURE dbo.usp_InsertSampleCarts_Disk @StartId int = null, @InsertCount int 
AS BEGIN  
  IF @StartId is null SELECT @StartId = isnull(MAX(ShoppingCartId), 0) + 1 FROM dbo.ShoppingCart
  DECLARE @i int = 0
  WHILE @i < @InsertCount 
  BEGIN 
    INSERT INTO dbo.ShoppingCart_Disk VALUES 
         (@StartId + @i, RAND() * 1000, GETUTCDATE(), NULL) 
    SET @i += 1 
  END
END

Скрипт 3

И вставим туда и туда по миллиону записей:

SET NOCOUNT ON; DBCC FREEPROCCACHE WITH NO_INFOMSGS; DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS

exec dbo.usp_InsertSampleCarts null, 1000000
exec dbo.usp_InsertSampleCarts_Disk @InsertCount = 1000000
 

Как говорится, разница налицо:

In Memory OLTP в SQL Server 2014. Часть II
Рис.3

Из особенностей следует отметить, что SQL Server 2014 не поддерживает автоматическую перекомпиляцию нативных хранимых процедур. План фиксирован. Parameter sniffing не используется, все параметры полагаются unknown. Вместо рекомпиляции, возможно, придется удалить и заново создать процедуру при изменении данных (для генерации нового плана). Возможно, хватит рекомпиляции, которая происходит при старте сервера, переезде на резервный узел и т.д. Понятно, что чем свежее статистика по таблицам, тем корректнее будет построен план, а статистика, как мы помним из первой части, автоматически не обновляется. Необходимо делать вручную с помощью UPDATE STATISTICS или sp_updatestats. Последняя всегда обновляет статистику для оптимизированных по памяти таблиц (для обычных — по мере надобности).

Из наглядных вещей давайте посмотрим еще разницу в журналировании. В классическом варианте, если мы пишем миллион записей в таблицу с некластерным индексом (и обновления задевают какую-нибудь колонку этого индекса), мы получим два миллиона записей в журнал, что, естественно, сказывается на производительности. К тому же (write-ahead logging), все это помещается в буфер лога немедленно и в нагруженных системах приводит к высокой конкуренции за буфер. В случае Гекатона незафиксированные транзакции вообще не сбрасываются на диск, поэтому не нужно хранить undo-информацию или агрессивно вставлять в буфер лога. Операции над индексами не журналируются — они не хранятся между рестартами. Формируется только одна консолидированная запись журнала при фиксации, которая содержит необходимую информацию о всех деталях изменений, для всех записей, затронутых транзакцией. Смотрим.

declare @lsn nvarchar(46) = (select max([Current LSN]) from sys.fn_dblog(null, null)) 
exec dbo.usp_InsertSampleCarts_Disk @InsertCount = 100
select * from sys.fn_dblog(null, NULL) where [Current LSN] > @lsn order by [Current LSN] desc 
select count(1), sum([Log Record Length]) from sys.fn_dblog(null, NULL) where [Current LSN] > @lsn

set @lsn = (select max([Current LSN]) from sys.fn_dblog(null, null)) 
exec dbo.usp_InsertSampleCarts null, 100
select * from sys.fn_dblog(null, NULL) where [Current LSN] > @lsn order by [Current LSN] desc 
select count(1), sum([Log Record Length]) from sys.fn_dblog(null, NULL) where [Current LSN] > @lsn
 

In Memory OLTP в SQL Server 2014. Часть II
Рис.4

И видим, что вставка 100 записей в случае in-memory таблицы журналирована в виде всего двух записей в журнал типа LOP_HK (Log Operation Hekaton). Как уже отмечалось, эти записи консолидированные. Посмотреть, во что они на самом деле раскрываются, можно с помощью новой недокументированной функции, которую я позаимствовал у Kalen Delaney. В качестве Current LSN ставим значения из соответствующей колонки третьего резалтсета на Рис.4:

SELECT [current lsn], [transaction id], operation, operation_desc, tx_end_timestamp, total_size, object_name(table_id) AS TableName 
FROM sys.fn_dblog_xtp(null, null) 
WHERE [Current LSN] = '00000027:000001dd:0002'

SELECT [current lsn], [transaction id], operation, operation_desc, tx_end_timestamp, total_size, object_name(table_id) AS TableName 
FROM sys.fn_dblog_xtp(null, null) 
WHERE [Current LSN] = '00000027:000001dd:0001'
 

In Memory OLTP в SQL Server 2014. Часть II
Рис.5

Первая запись, как мы видим, состоит из 102-х записей: begin tx, 100 вставок, commit. Несмотря на это их суммарный объем (см. второй и четвертый результат Рис.4) в >10 раз компактней, чем в случае дисковой таблицы. Если взять non-durable table (DURABILITY=SCHEMA_ONLY), журналирования вообще не будет.

Автор: alexejs

Источник

Поделиться

* - обязательные к заполнению поля