Cenário
Um tópico recente do fórum sobre o tamanho da área ocupada pela memória dos processos WebApp apontou que, em determinadas circunstâncias, a memória usada parecia significativa (nas centenas de megabytes por processo). À medida que a conversa continuava, ficou claro que o tópico sobre o uso da memória precisava de esclarecimentos. O objetivo deste documento é focar em como o número, o tipo e a estrutura das tabelas em uma aplicação Web afetam o uso da memória, que é de longe o fator mais significativo na memória total necessária por processo.
As informações neste documento correspondem à sua aplicação?
O consumo significativo de memória não afeta todas ou mesmo muitas aplicações web. Existem alguns aspectos simples a serem considerados para determinar se você precisa ser sensível ao consumo de memória e explorar maneiras de reduzi-lo.
Portanto, o ponto principal é que o consumo significativo de memória por processo requer análise em seu ambiente para passar de "é assim que funciona" para "temos um problema com o qual lidar". Esse limite pode ser ultrapassado ao usar o SQL, ter tabelas com comprimentos de linha muito grandes, ter muitas tabelas abertas na aplicação e carregar um número suficientemente grande de processos que se uniriam e excederiam razoavelmente a capacidade de memória de um servidor configurado.
Para ajudá-lo nessa análise, criamos o projeto básico AnalyzeTableMemoryUsage que você pode compilar em qualquer workspace. Discutiremos isso abaixo, com mais detalhes.
Então, se chamamos sua atenção, prossiga ...
O que usa memória e quando?
O maior consumidor de memória em um ambiente SQL está abrindo uma tabela. Para cada tabela aberta, a memória é alocada para o buffer de dados (igual ao comprimento da linha e alocado estaticamente), o cache de localização para ajuste de desempenho (igual ao comprimento da linha x o block size e alocado estaticamente) e cada instância do dicionário de dados no diretório aplicação (igual ao tamanho dos dados no buffer DD a qualquer momento, porque é alocado dinamicamente).
Existem alguns outros fatores envolvidos, como tamanho do argumento e como isso afeta a quantidade de dados que se move entre o buffer de dados e os buffers DD, mas abordaremos isso um pouco mais tarde. Por enquanto, vamos nos concentrar no básico:
Vamos dar um exemplo da tabela Customer nos exemplos instalados com o Studio. O comprimento total da linha é de apenas 1.222 bytes. Com o block size padrão de 10 linhas, o espaço de memória esperado para abrir esta tabela na sua aplicação seria 13.422 bytes (comprimento da linha x 11). Se adicionarmos três instâncias de dicionários de dados para a tabela do cliente, isso poderá usar até 3.666 bytes (comprimento da linha x número de instâncias de DD), mas isso só atingirá o pico quando as linhas que contiverem o máximo de dados estiverem na memória. Nesse nível, ainda estamos usando apenas um total de 17.008 bytes de memória nesta aplicação para a tabela Customer. Mesmo se tivéssemos 100 dessas tabelas, o uso da memória projetada por processo (para tabelas) seria de apenas 100 x 17.088 bytes, o que nos dá um total de aproximadamente 1,7 MB.
Obviamente, este não é um exemplo de tabela representativo. Executamos algumas estatísticas em uma workspace da aplicação fornecida por um desenvolvedor e descobrimos que, nas 74 tabelas usadas na aplicação, era comum ter tabelas com comprimentos de registro no intervalo de 17 KB (havia 7 delas) e o uso de memória padrão para cada um deles teria 191 KB (comprimento da linha x 11) e, quando totalizado, seria 1,3 MB. Adicione nossa suposição básica de três instâncias de DD para cada uma dessas tabelas e você poderá ter um pico adicional de 365 KB (mas novamente, apenas se as linhas na memória tiverem o comprimento máximo definido). Nessa workspace específica, o comprimento total da linha combinada para todas as 74 tabelas era de 140 KB com uma área de memória total projetada de 1. 5 MB para buffers de tabela e talvez outros 420 KB para buffers DD (assumindo 3 DDs por tabela e dados de tamanho máximo ao todo) - totalizando pouco menos de 2 MB. Aumente a aplicação quatro vezes mais e ainda não estamos no intervalo de 10 MB de memória necessário para todas as tabelas e dados.
Então, como pode haver relatórios de consumo de memória nas centenas de megabytes por processo?
Digite colunas grandes.
Embora cada aplicação seja diferente e, como desenvolvedores, cada um de nós tenha seus próprios hábitos, alguns princípios básicos geralmente entram em jogo:
Portanto, os comportamentos associados à combinação de colunas grandes e block size são multiplicados e, em seguida, multiplicados pelo número de processos na aplicação Web, é onde alguns desenvolvedores podem começar a ver um uso significativo da memória nas aplicações Web.
Vamos pegar o exemplo acima e alterar cada uma das 7 tabelas que tinham comprimentos de linha de 17 KB e assumir que, ao passar para o SQL, as colunas de texto foram alteradas para 1 MB (porque podemos - é essa mentalidade neste caso). Agora, adicionamos pelo menos 77 MB ao espaço de memória (por processo) e isso é apenas para o arquivo e para encontrar buffers de cache. Ainda não centenas de megabytes por processo, mas você pode ver como colunas muito grandes podem começar a somar.
Como podemos reduzir o consumo de memória?
Nota: Os fundamentos que discutiremos nesta seção são um pouco generalizados para que não fiquemos presos nas minúcias de diferentes tipos de dados e back-end. Tudo precisa ser levado para o contexto específico de instâncias individuais. Por exemplo, esses fatores podem limitar as opções disponíveis em alguns ambientes:
Portanto, lembre-se de que algumas das técnicas discutidas abaixo não podem ser usadas, ou não podem ser usadas na mesma extensão ou, mesmo se usadas, não resultam em uma redução significativa no uso de memória.
Block Size
Começaremos com block_size , porque ele multiplica o uso de memória da estrutura de dados subjacente para uma finalidade muito específica: o cache de localização para cada tabela. Também é o único aspecto que pode ser ajustado sem nenhuma alteração no próprio banco de dados. O único impacto potencial é o desempenho da aplicação e isso pode ser facilmente testado. Lembre-se de que a validade do cache de localização é temporária (apenas dentro do escopo do find_cache_timeout , que é padrão em 10 milissegundos); portanto, geralmente entra em jogo em loops de localização restritos (relatórios, processos em lote, preenchimento de listas de seleção etc.).
A primeira pergunta a ser feita em todas as tabelas é "com que frequência e em que circunstâncias as descobertas seqüenciais ocorrem?" Embora raro, existem algumas tabelas que nunca tiveram descobertas seqüenciais executadas, portanto, a alocação de um cache de localização significativo é simplesmente perda de memória. WebAppServerProps é um exemplo dessa tabela - as únicas descobertas feitas nessa tabela são FIND EQ. Obviamente, o tamanho mínimo do block é 2, portanto, não podemos recuperar toda a memória alocada para o cache de localização para essas tabelas, mas como o tamanho padrão do block é 10, ainda podemos reduzir o uso em 80%.
Para o banco de dados embedded, WebAppServerProps é de apenas 16 KB, portanto, o uso de memória padrão é de apenas 176 KB. Mas, com a coluna, altere para 1 MB quando convertido para o Microsoft SQL Server, que se expande para mais de 11 MB apenas nessa tabela. Podemos recuperar 8 MB disso ajustando o block size ao mínimo de 2.
As tabelas definidas como "extensões", usando a técnica de relacionamentos um para um, seriam outro exemplo de tabela que provavelmente usaria apenas EQs FIND; uma vez encontrada uma linha na tabela "principal", ela usa um valor de índice para FIND EQ para a tabela secundária e os dados estendidos.
As tabelas que possuem apenas EQs FIND são raras, mas ainda pode haver vantagens em ajustar o block size de outras tabelas para determinar se há algum desempenho significativo perdido se o cache de localização for reduzido. Você pode procurar tabelas com tamanhos de linhas grandes e reduzir o block size e ver se os usuários realmente sentirão a diferença (lembre-se, apenas loops apertados onde as descobertas acontecem dentro do tempo limite do cache de localização seriam auxiliados pelo cache de localização) os melhores candidatos à mudança seriam tabelas de pesquisa e outras que não são usadas muito ou raramente têm relatórios ou processos em lote associados ao seu uso.
A chave ao examinar possíveis alterações no block size não é investir tempo, a menos que uma redução no block size resulte em uma redução significativa no uso de memória. Lembre-se também de que, para tabelas com tamanhos de linha muito grandes, mesmo uma pequena redução no block size, digamos, passando do padrão de 10 para 6 ou 7, pode economizar memória significativa por processo sem alterar significativamente o perfil de desempenho. Para o teste, recomendamos definir o block size para o valor mínimo (2) e, em seguida, efetuar o backup conforme necessário. Configurá-lo como 2 fornecerá a indicação mais antiga da sensibilidade da mudança associada a qualquer tabela específica.
Embora os ajustes de block size possam resultar nas maiores reduções no uso de memória quando aplicados a tabelas com tamanhos de linhas grandes, eles também são a única alteração que pode ter um impacto associado ao desempenho.
Uma última técnica que pode ser usada para tabelas com tamanhos de linha muito grandes, mas que precisam de block size mais altos para ajudar o cache de localização de relatórios, processos em lote, grids e listas, é manter o block size definido baixo no arquivo table.int, mas ajuste-o mais alto dinamicamente quando necessário. Você pode definir Tags for this post