Partilhar via


Estimar os requisitos de memória para tabelas Memory-Optimized

Aplica-se a:SQL ServerBanco de Dados SQL do AzureInstância Gerenciada SQL do Azure

As tabelas com otimização de memória exigem que exista memória suficiente para manter todas as linhas e índices na memória. Como a memória é um recurso finito, é importante que você entenda e gerencie o uso da memória em seu sistema. Os tópicos desta seção abrangem cenários comuns de uso e gerenciamento de memória.

É importante ter uma estimativa razoável das necessidades de memória de cada tabela com otimização de memória para que você possa provisionar o servidor com memória suficiente. Isso se aplica a novas tabelas e tabelas migradas de tabelas baseadas em disco. Esta seção descreve como estimar a quantidade de memória necessária para armazenar dados para uma tabela com otimização de memória.

Se você estiver considerando uma migração de tabelas baseadas em disco para tabelas com otimização de memória, consulte Determinando se uma tabela ou procedimento armazenado deve ser portado para In-Memory OLTP para obter orientação sobre quais tabelas são melhores para migrar. Todos os tópicos em Migrando para In-Memory OLTP fornecem orientação sobre a migração de tabelas baseadas em disco para tabelas otimizadas para a memória.

Orientação básica para estimar os requisitos de memória

No SQL Server 2016 (13.x) e versões posteriores, não há limite para o tamanho das tabelas com otimização de memória, embora as tabelas precisem caber na memória. No SQL Server 2014 (12.x), o tamanho de dados com suporte é de 256 GB para tabelas SCHEMA_AND_DATA.

O tamanho de uma tabela com otimização de memória corresponde ao tamanho dos dados mais alguma sobrecarga para cabeçalhos de linha. O tamanho da tabela com otimização de memória corresponde aproximadamente ao tamanho do índice clusterizado ou heap da tabela baseada em disco original.

Os índices em tabelas com otimização de memória tendem a ser menores do que os índices não agrupados em tabelas baseadas em disco. O tamanho dos índices não agrupados é da ordem de [primary key size] * [row count]. O tamanho dos índices de hash é [bucket count] * 8 bytes.

Quando há uma carga de trabalho ativa, é necessária memória extra para levar em conta o controle de versão de linha e várias operações. A quantidade necessária de memória depende da carga de trabalho, mas para ser seguro, a recomendação é começar com duas vezes o tamanho esperado de tabelas e índices otimizados para memória e observar o consumo real de memória. A sobrecarga do versionamento de linha sempre depende das características da carga de trabalho - especialmente transações de longa duração aumentam essa sobrecarga. Para a maioria das cargas de trabalho que usam bancos de dados maiores (por exemplo, maiores que 100 GB), a sobrecarga tende a ser limitada (25% ou menos).

Para obter mais informações sobre a possível sobrecarga de memória no mecanismo OLTP In-Memory, consulte Fragmentação de memória.

Cálculo detalhado dos requisitos de memória

Exemplo de tabela com otimização de memória

Considere o seguinte esquema de tabela com otimização de memória:

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  

Usando esse esquema, vamos determinar a memória mínima necessária para essa tabela com otimização de memória.

Memória para a tabela

Uma linha de tabela com otimização de memória tem três partes:

  • Carimbos de tempo
    Cabeçalho da linha/carimbos de data/hora = 24 bytes.

  • Ponteiros de índice
    Para cada índice de hash na tabela, cada linha tem um ponteiro de endereço de 8 bytes para a próxima linha do índice. Como há quatro índices, cada linha aloca 32 bytes para ponteiros de índice (um ponteiro de 8 bytes para cada índice).

  • de dados
    O tamanho da parte de dados da linha é determinado pela soma do tamanho do tipo de dados para cada coluna de dados. Em nossa tabela, temos cinco inteiros de 4 bytes, três colunas de caracteres de 50 bytes e uma coluna de caracteres de 30 bytes. Portanto, a parte de dados de cada linha é 4 + 4 + 4 + 4 + 4 + 50 + 50 + 30 + 50 ou 200 bytes.

A seguir está um cálculo de tamanho para 5.000.000 (5 milhões) linhas em uma tabela otimizada para memória. A memória total usada pelas linhas de dados é estimada da seguinte forma:

Memória para as linhas da tabela

A partir dos cálculos acima, o tamanho de cada linha na tabela otimizada para memória é 24 + 32 + 200, ou 256 bytes. Como temos 5 milhões de linhas, a tabela consome 5.000.000 * 256 bytes, ou 1.280.000.000 bytes - aproximadamente 1,28 GB.

Memória para índices

Memória para cada índice de hash

Cada índice de hash é uma matriz de hash de ponteiros de endereço de 8 bytes. O tamanho da matriz é melhor determinado pelo número de valores de índice exclusivos para esse índice. No exemplo atual, o número de valores únicos da Col2 é um bom ponto de partida para definir o tamanho da matriz para o t1c2_index. Uma matriz de hash muito grande desperdiça memória. Uma matriz hash que é demasiado pequena diminui o desempenho, pois existem demasiadas colisões devido a valores de índice que são mapeados para a mesma entrada de índice.

Os índices de hash permitem pesquisas de igualdade muito rápidas, tais como:

SELECT * FROM t_hk  
   WHERE Col2 = 3;

Os índices não agrupados são mais rápidos para pesquisas de intervalo, como:

SELECT * FROM t_hk  
   WHERE Col2 >= 3;

Se você estiver migrando uma tabela baseada em disco, poderá usar o seguinte para determinar o número de valores exclusivos para o índice t1c2_index.

SELECT COUNT(DISTINCT [Col2])  
  FROM t_hk;

Se você estiver criando uma nova tabela, precisará estimar o tamanho da matriz ou coletar dados do teste antes da implantação.

Para obter informações sobre como os índices de hash funcionam em In-Memory tabelas com otimização de memória OLTP, consulte Hash Indexes.

Definindo o tamanho da matriz de índice de hash

O tamanho da matriz de hash é definido por (bucket_count= value) onde value é um valor inteiro maior que zero. Se value não for uma potência de 2, a bucket_count real é arredondada para a próxima potência mais próxima de 2. Em nossa tabela de exemplo, (bucket_count = 5000000), como 5.000.000 não é uma potência de 2, a contagem real de buckets arredonda para 8.388.608 (2^23). Você deve usar esse número, não 5.000.000 ao calcular a memória necessária para a matriz de hash.

Assim, em nosso exemplo, a memória necessária para cada matriz de hash é:

8.388.608 * 8 = 2^23 * 8 = 2^23 * 2^3 = 2^26 = 67.108.864 ou aproximadamente 64 MB.

Como temos três índices de hash, a memória necessária para os índices de hash é de 3 * 64 MB = 192 MB.

Memória para índices não clusterizados

Os índices não agrupados são implementados como árvores Bw, com os nós internos a conter o valor de índice e ponteiros para nós subsequentes. Os nós folha contêm o valor do índice e um ponteiro para a linha da tabela na memória.

Ao contrário dos índices de hash, os índices não clusterizados não têm um tamanho de bucket fixo. O índice cresce e encolhe dinamicamente com os dados.

A memória necessária para índices não clusterizados pode ser calculada da seguinte forma:

  • Memória alocada a nós não-folha
    Para uma configuração típica, a memória alocada para nós não-folha é uma pequena porcentagem da memória total consumida pelo índice. Isso é tão pequeno que pode ser ignorado com segurança.

  • Memória para nós de folha
    Os nós de folha têm uma linha para cada chave exclusiva na tabela, apontando para as linhas de dados correspondentes a essa chave. Se você tiver várias linhas com a mesma chave (ou seja, tiver um índice não exclusivo não clusterizado), haverá apenas uma linha no nó da folha do índice que aponta para uma das linhas com as outras linhas vinculadas entre si. Assim, a memória total necessária pode ser aproximada por:

    • memóriaParaÍndiceNãoAgrupado = (tamanhoDoApontador + soma(dimensõesDosTiposDeDadosDasChaves)) * linhasComChavesÚnicas

Os índices não clusterizados são melhores quando usados para pesquisas de intervalo, como exemplificado pela seguinte consulta:

SELECT * FROM t_hk  
   WHERE c2 > 5;  

Memória para versionamento de linha

Para evitar bloqueios, In-Memory OLTP usa simultaneidade otimista ao atualizar ou excluir linhas. Isso significa que, quando uma linha é atualizada, outra versão da linha é criada. Além disso, as exclusões são lógicas - a linha existente é marcada como excluída, mas não removida imediatamente. O sistema mantém as versões de linha antigas (incluindo linhas excluídas) disponíveis até que todas as transações que poderiam usar a versão concluam a execução.

Como pode haver muito mais linhas na memória a qualquer momento aguardando que o ciclo de coleta de lixo libere sua memória, você deve ter memória suficiente para acomodar essas outras linhas.

O número de linhas extras pode ser estimado calculando o número máximo de atualizações e exclusões de linhas por segundo e, em seguida, multiplicando isso pelo número de segundos que a transação mais longa leva (mínimo de 1).

Esse valor é então multiplicado pelo tamanho da linha para obter o número de bytes necessário para o controle de versão da linha.

rowVersions = durationOfLongestTransactionInSeconds * peakNumberOfRowUpdatesOrDeletesPerSecond

As necessidades de memória para linhas obsoletas são então estimadas multiplicando o número de linhas obsoletas pelo tamanho de uma linha de tabela com otimização de memória. Para obter mais informações, consulte Memória para a tabela.

memoryForRowVersions = rowVersions * rowSize

Memória para variáveis de tabela

A memória usada para uma variável de tabela é liberada somente quando a variável de tabela sai do escopo. As linhas excluídas, incluindo as linhas excluídas como parte de uma atualização, de uma variável de tabela não estão sujeitas à coleta de lixo. Nenhuma memória é liberada até que a variável table saia do escopo.

As variáveis de tabela definidas em um grande lote SQL em vez de em um procedimento armazenado e usadas em muitas transações podem consumir uma grande quantidade de memória. Como não são lixo coletado, as linhas excluídas em uma variável de tabela podem consumir muita memória e degradar o desempenho, já que as operações de leitura precisam passar pelas linhas excluídas.

Memória para crescimento

Os cálculos anteriores estimam suas necessidades de memória para a tabela como ela existe atualmente. Além dessa memória, você precisa estimar o crescimento da tabela e fornecer memória suficiente para acomodar esse crescimento. Por exemplo, se você prevê um crescimento de 10%, precisará multiplicar os resultados anteriores por 1,1 para obter a memória total necessária para sua tabela.

Fragmentação da memória

Para evitar a sobrecarga de chamadas de alocação de memória e melhorar o desempenho, o mecanismo OLTP In-Memory sempre solicita memória do sistema operacional SQL Server (SQLOS) usando blocos de 64 KB, conhecidos como superblocos.

Cada superbloco contém alocações de memória apenas dentro de um intervalo de tamanho específico, conhecido como classe de tamanho. Por exemplo, o superbloco A pode ter alocações de memória na classe de tamanho de 1-16 bytes, enquanto o superbloco B pode ter alocações de memória na classe de tamanho de 17-32 bytes, e assim por diante.

Por padrão, os superblocos também são particionados pela CPU lógica. Isso significa que, para cada CPU lógica, há um conjunto separado de superblocos, divididos por classe de tamanho. Isso reduz a contenção de alocação de memória entre solicitações executadas em CPUs diferentes.

Quando o mecanismo OLTP In-Memory faz uma nova alocação de memória, ele primeiro tenta encontrar memória livre em um superbloco existente para a classe de tamanho solicitada e para a CPU que processa a solicitação. Se essa tentativa for bem-sucedida, o used_bytes valor na coluna em sys.dm_xtp_system_memory_consumers para um consumidor de memória específico aumenta pelo tamanho de memória solicitado, mas o allocated_bytes valor na coluna permanece o mesmo.

Se não houver memória livre nos superblocos existentes, um novo superbloco é alocado e o valor no used_bytes aumenta pelo tamanho da memória solicitada, enquanto o valor na coluna allocated_bytes aumenta em 64 KB.

Com o tempo, à medida que a memória em superblocos é alocada e deslocalizada, a quantidade total de memória consumida pelo mecanismo OLTP In-Memory pode se tornar significativamente maior do que a quantidade de memória usada. Por outras palavras, a memória pode ficar fragmentada.

A coleta de lixo pode reduzir a memória usada, mas só reduz a memória alocada se um ou mais superblocos ficarem vazios e forem deslocalizados. Isso se aplica à coleta de lixo forçada e automática usando o procedimento armazenado do sistema sys.sp_xtp_force_gc.

Se a fragmentação de memória do mecanismo In-Memory OLTP e o uso de memória alocada se tornarem mais altos do que o esperado, poderá habilitar o sinalizador de rastreamento 9898. Isso altera o esquema de particionamento de superblocos de por CPU para um nó por NUMA, reduzindo o número total de superblocos e o potencial de alta fragmentação de memória.

Esta otimização é mais relevante para máquinas grandes com muitas CPUs lógicas. A contrapartida dessa otimização é um aumento potencial na contenção de alocação de memória resultante de menos superblocos, o que pode reduzir a taxa de transferência geral da carga de trabalho. Dependendo dos padrões de carga de trabalho, a redução na largura de banda devido ao particionamento de memória por NUMA pode ser perceptível ou não.