Поделиться через


Оценка требований к объему памяти для таблиц, оптимизированных для памяти

Область применения: SQL Server База данных SQL Azure Управляемый экземпляр SQL Azure

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

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

Если вы рассматриваете миграцию из дисковых таблиц в оптимизированные для памяти таблицы, ознакомьтесь с определением того, следует ли перенести таблицу или хранимую процедуру в In-Memory OLTP , чтобы получить рекомендации по переносу таблиц. Все разделы статьи Миграция на In-Memory OLTP содержат руководство по миграции таблиц с диска на оптимизированные для памяти.

Основные инструкции по оценке требований к памяти

В SQL Server 2016 (13.x) и более поздних версиях нет ограничений на размер оптимизированных для памяти таблиц, хотя таблицы должны соответствовать памяти. В SQL Server 2014 (12.x) поддерживаемый размер данных составляет 256 ГБ для таблиц SCHEMA_AND_DATA.

Размер оптимизированной для памяти таблицы соответствует размеру данных плюс определенный объем для размещения заголовков строк. Размер оптимизированной для памяти таблицы примерно соответствует размеру кластеризованного индекса или кучи исходной таблицы на основе дисков.

Индексы в таблицах, оптимизированных для памяти, как правило, меньше, чем некластеризованные индексы в дисковых таблицах. Размер некластеризованных индексов равен порядка [primary key size] * [row count]. Размер хэш-индексов — [bucket count] * 8 bytes.

При активной рабочей нагрузке требуется дополнительная память для учета управления версиями строк и различных операций. Требуемый объем памяти зависит от рабочей нагрузки, но для обеспечения безопасности рекомендация заключается в том, чтобы начать с двух раз ожидаемых размеров оптимизированных для памяти таблиц и индексов, а также наблюдать за фактическим потреблением памяти. Объем накладных расходов на версионирование строк всегда зависит от характеристик рабочей нагрузки; при этом длительные транзакции особенно увеличивают эти расходы. Для большинства рабочих нагрузок, использующих более крупные базы данных (например, больше 100 ГБ), издержки, как правило, ограничены (25 процентов или меньше).

Дополнительные сведения о потенциальных затратах на память в подсистеме OLTP In-Memory см. в разделе "Фрагментация памяти".

Вычисление точных требований к памяти

Пример оптимизированной для памяти таблицы

Рассмотрим следующую схему таблицы, оптимизированной для памяти:

CREATE TABLE t_hk
(  
  col1 int NOT NULL  PRIMARY KEY NONCLUSTERED,  

  col2 int NOT NULL  INDEX t1c2_index   
      HASH WITH (bucket_count = 5000000),  

  col3 int NOT NULL  INDEX t1c3_index   
      HASH WITH (bucket_count = 5000000),  

  col4 int NOT NULL  INDEX t1c4_index   
      HASH WITH (bucket_count = 5000000),  

  col5 int NOT NULL  INDEX t1c5_index NONCLUSTERED,  

  col6 char (50) NOT NULL,  
  col7 char (50) NOT NULL,   
  col8 char (30) NOT NULL,   
  col9 char (50) NOT NULL  

)   WITH (memory_optimized = on)  ;
GO  

С помощью этой схемы давайте определим минимальную память, необходимую для этой оптимизированной для памяти таблицы.

Память для таблицы

Строка таблицы, оптимизированная для памяти, состоит из трех частей:

  • Метки времени
    Заголовок строки или метки времени = 24 байта.

  • Указатели индекса
    Для каждого хэш-индекса в таблице каждая строка содержит 8-байтный адресный указатель на следующую строку в индексе. Так как существует четыре индекса, каждая строка выделяет 32 байта для указателей индекса (8-байтовый указатель для каждого индекса).

  • Данные
    Размер данных в строке определяется путем суммирования размера типа данных для каждого столбца данных. В нашей таблице имеется пять 4-байтных целых чисел, три 50-байтных символьных столбцов и один 30-байтный символьный столбец. Поэтому часть данных в каждой строке — это 4 + 4 + 4 + 4 + 4 + 50 + 50 + 30 + 50 или 200 байт.

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

Память для строк таблицы

Из вышеуказанных вычислений, размер каждой строки в таблице, оптимизированной для памяти, будет 24 + 32 + 200 или 256 байт. Так как у нас есть 5 миллионов строк, таблица потребляет 5 000 000 * 256 байт, или 1 280 000 000 байт - примерно 1,28 ГБ.

Память для индексов

Объем памяти для каждого хэш-индекса

Каждый хэш-индекс — это хэш-массив, состоящий из 8-байтных указателей адреса. Размер массива лучше всего определяется числом уникальных значений индекса для этого индекса. В текущем примере число уникальных значений Col2 является хорошей отправной точкой для размера массива для t1c2_index. Хэш-массив, который слишком велик, тратит память впустую. Хэш-массив, который слишком мал, замедляет эффективность из-за большого числа коллизий значений индексов, хэшируемых к одному и тому же индексу.

В хэш-индексах скорость поиска совпадений очень высока:

SELECT * FROM t_hk  
   WHERE Col2 = 3;

Некластеризованные индексы будут быстрее при поиске диапазона, например:

SELECT * FROM t_hk  
   WHERE Col2 >= 3;

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

SELECT COUNT(DISTINCT [Col2])  
  FROM t_hk;

Если вы создаете новую таблицу, необходимо оценить размер массива или собрать данные из тестирования до развертывания.

Сведения о том, как хэш-индексы работают в оптимизированных для памяти таблицах OLTP, см. в разделе "Хэш-индексы".

Задание размера массива хэш-индекса

Размер хэш-массива задается (bucket_count= value) , где value — это целочисленное значение больше нуля. Если value не является степенью двойки, фактическое значение bucket_count округляется до следующей ближайшей степени двойки. В нашем примере таблицы (bucket_count = 5000000), так как 5000 000 не является степенью числа 2, фактическое количество контейнеров округляется до 8 388 608 (2^23). Необходимо использовать это число, а не 5 000 000, при вычислении объема памяти, необходимого для хэш-массива.

Таким образом, для каждого хэш-массива в нашем примере потребуется памяти:

8,388,608 * 8 = 2^23 * 8 = 2^23 * 2^3 = 2^26 = 67 108 864 или приблизительно 64 МБ.

Так как у нас есть три хэш-индекса, память, необходимая для хэш-индексов, составляет 3 * 64 МБ = 192 МБ.

Память для некластеризованных индексов

Некластеризованные индексы реализуются в виде BW-деревьев, внутренние узлы которых содержат значения индекса и указатели на последующие узлы. Листовые узлы содержат значение индекса и указатель на строку таблицы в памяти.

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

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

  • Память, выделенная для неличевых узлов
    Для типичной конфигурации память, выделенная нелистьевым узлам, составляет небольшой процент от общей памяти, используемой индексом. Это настолько мало, что это можно спокойно игнорировать.

  • Память для листовых узлов
    Конечные узлы содержат по одной строке для каждого уникального ключа в таблице, и она указывает на строки данных с этим уникальным ключом. Если у вас есть несколько строк с одним ключом (то есть у вас есть некластеризованный индекс), есть только одна строка в конечном узле индекса, указывающая на одну из строк с другими строками, связанными друг с другом. Таким образом, общую память можно примерно вычислить следующим образом:

    • памятьДляНекластеризованногоИндекса = (размерУказателя + сумма(размерыТиповДанныхКлючевыхСтолбцов)) * строкиСУникальнымиКлючами

Некластеризованные индексы лучше всего подходят для поиска по диапазону, как показано в следующем примере запроса:

SELECT * FROM t_hk  
   WHERE c2 > 5;  

Память для управления версиями строк

Чтобы избежать блокировок, модуль In-Memory OLTP использует оптимистический параллелизм при обновлении или удалении строк. Это означает, что при обновлении строки создается другая версия строки. Кроме того, операции удаления являются логическими — существующая строка помечается как удаленная, но не удаляется немедленно. Система сохраняет старые версии строк (включая удаленные строки) до завершения всех транзакций, которые могли бы использовать эту версию.

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

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

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

rowVersions = durationOfLongestTransactionInSeconds * peakNumberOfRowUpdatesOrDeletesPerSecond

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

memoryForRowVersions = rowVersions * rowSize

Память для табличных переменных

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

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

Память для роста

Предыдущие вычисления оценивают потребности в памяти для таблицы в текущем виде. В дополнение к этому объему памяти необходимо оценить увеличение размера таблицы и предоставить достаточно памяти для ее будущего роста. Например, если вы ожидаете рост на 10%, вам нужно умножить предыдущие результаты на 1.1, чтобы получить общий объем памяти, необходимый для вашей таблицы.

Фрагментация памяти

Чтобы избежать затрат на выделение памяти и повысить производительность In-Memory, подсистема OLTP всегда запрашивает память из операционной системы SQL Server (SQLOS) с помощью блоков 64 КБ, называемых суперблоками.

Каждый суперблок содержит выделение памяти только в определенном диапазоне размеров, называемом размерным классом. Например, суперблок A может иметь выделение памяти в классе размера 1-16 байт, в то время как суперблок B может иметь выделение памяти в классе размера 17-32 байта и так далее.

По умолчанию суперблоки также секционируются логическим ЦП. Это означает, что для каждого логического ЦП существует отдельный набор суперблоков, далее разбитый по классу sizeclass. Это уменьшает конкуренцию за распределение памяти между запросами, выполняемыми на разных ЦП.

Когда движок In-Memory OLTP создает новое выделение памяти, он сначала пытается найти свободную память в существующем суперблоке для запрашиваемого размерного класса и для ЦП, обрабатывающего запрос. Если эта попытка выполнена успешно, значение в used_bytes столбце в sys.dm_xtp_system_memory_consumers для конкретного потребителя памяти увеличивается на запрошенный размер памяти, но значение в allocated_bytes столбце остается неизменным.

Если в существующих суперблоках нет свободной памяти, то новый суперблок выделяется и значение used_bytes увеличивается на запрошенный размер памяти, а значение в allocated_bytes столбце увеличивается на 64 КБ.

С течением времени, так как память в суперблоках выделяется и освобождается, общий объем памяти, потребляемой подсистемой OLTP In-Memory, может значительно превышать объем используемой памяти. Другими словами, память может стать фрагментированной.

Сборка мусора может уменьшить используемую память, но уменьшает только выделенную память, если один или несколько суперблоков становятся пустыми и освобождены. Это относится как к автоматической, так и к принудительной сборке мусора с помощью системной хранимой процедуры sys.sp_xtp_force_gc.

Если фрагментация памяти в In-Memory OLTP ядре и использование выделенной памяти становятся выше ожиданий, можно включить флаг трассировки 9898. Это изменяет схему секционирования суперблоков с каждого ЦП на узел NUMA, уменьшая общее количество суперблоков и потенциальную возможность фрагментации памяти.

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