Não encontrou o que procurava? Dê uma olhada nessas páginas!

Blog

Uso de memória em aplicações DataFlex Web

15 de Julho de 2020
Por Stephen W. Meeley, Desenvolvimento

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.

  • O banco de dados embedded possui comprimentos de linha limitados e usa apenas um buffer por tabela aberta; portanto, é extremamente improvável que você experimente um consumo de memória significativo, a menos que esteja usando um back-end SQL.
     
  • Se sua aplicação usar um conjunto relativamente pequeno de tabelas e tamanhos de linha relativamente pequenos, as chances de sua aplicação consumir quantidades significativas de memória também serão muito baixas, mesmo com um servidor SQL. Você ainda pode encontrar as informações abaixo sobre o que consome memória, interessante e surpreendentemente útil.
     
  • Mesmo que suas aplicações Web consumam quantidades significativas de memória por processo, talvez não seja um problema que precise ser solucionado se o uso alto de memória não estiver causando problemas.

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:

  • Cada tabela possui um buffer de dados = comprimento da linha
     
  • Cada tabela possui um cache de localização configurável = block size X comprimento da linha
    • Observe que BLOCK_SIZE possui um valor padrão 10 (isso pode ser definido no arquivo table.int) e o cache de localização também pode ser controlado em tempo de execução por meio do atributo DF_FILE_BLOCK_SIZE.
       
  • Cada instância do DD possui um buffer baseado nos dados reais da memória, sendo o máximo o comprimento da linha (mas, também limitado pelo tamanho do argumento).
     

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:

  • As colunas de texto sempre alocam no mínimo 16 KB 
  • (max) colunas sempre alocam no mínimo 16 KB
    • Observe que eles são listados separadamente, porque, mesmo que as colunas de texto tenham como padrão os tipos de dados (máx.), eles também podem ser mapeados para colunas ASCII. 
  • A coluna de dados em WebAppServerProps é definida como 1 MB quando convertida para Microsoft SQL Server 
  • Os desenvolvedores, incluindo os da nossa equipe de desenvolvimento Data Access, tendem a dimensionar as colunas maiores "apenas por precaução" (por exemplo, a área de dados do WebAppServerProps realmente precisava ter 1 MB?) 

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:

  • Tipos de dados disponíveis (eles podem ser alterados mesmo nas revisões de um back-end específico) 
  • Tamanhos de tipos de dados na memória (especialmente quando tipos Unicode entram em cena) 
  • Limitações de comprimento de linha (geralmente estão vinculadas aos tipos de dados usados) 
  • Quem possui os dados (convertidos do DataFlex ou "nativo" e usados ​​por outras aplicações)? 
  • Quanta diferença faria uma mudança? 

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 DF_FILE_BLOCK_SIZE fora de uma Structure_Start ... Structure_End de operação e irá mudar imediatamente a alocação de memória para o cache de descoberta e depois reduzi-la quando definido de volta para o valor original, algo assim ... 


Integer iBaseBlockSize iTempBlockSize

Move 10 to iTempBlockSize // this can be whatever value you want for desired performance

Get_Attribute DF_FILE_BLOCK_SIZE of {tableNumber} to iBaseBlockSize
// {tableNumber} is likely going to be the main DD for the operation
// all the related files will be found with Find EQs and won’t benefit from a block size adjustment.

Set_Attribute DF_FILE_BLOCK_SIZE of {tableNumber} to iTempBlockSize
// keep in mind that whatever process is responding to this request will immediately
// allocate more memory once set

// do stuff

Set_Attribute DF_FILE_BLOCK_SIZE of {tableNumber} to iBaseBlockSize
// once the operation is complete we still need to set the find cache back to its starting point
// or the memory allocated for the responding process will stay at the higher level
// the value in the table.int file is only read upon opening the table.

Nota: A técnica acima pode ser muito útil quando o desempenho de um processo específico se beneficiaria de um cache de localização significativamente aumentado, independentemente do impacto no uso da memória.

Tipos de dados

Observe que geralmente apenas colunas de texto e binárias entram em jogo durante essa discussão sobre tipos de dados porque, por padrão, elas são mapeadas para os tipos de dados de back-end que armazenam dados grandes, por exemplo, os tipos de dados (máximos) no Microsoft SQL Server. Há casos em que os desenvolvedores podem ter mapeado o que normalmente seriam colunas ASCII para os tipos de dados de back-end maiores, para que eles também possam entrar em ação.

Usaremos os tipos de dados do Microsoft SQL Server nesta seção, mas todos os bancos de dados de back-end têm comportamentos semelhantes e não estamos preocupados com os tipos de dados numéricos ou de data.

O DataFlex possui mapeamentos padrões para os tipos de dados usados ​​no banco de dados embedded para o back-end do SQL:

  • ASCII = char
    • Eles também podem ser mapeados para varchar, nchar e nvarchar
    • No DataFlex 20, o padrão é nchar para suportar dados Unicode

 

  • Texto = varchar (máx.)
    • Também podem ser text, ntext e nvarchar (max)
    • No DataFlex 20, o padrão é nvarchar (max) para suportar dados Unicode

 

  • Binário = varbinário (máx)
    • Estes também podem ser de imagem e binários
    • Nenhuma alteração no DataFlex 20 de revisões anteriores

 

Como mencionado anteriormente, os tipos de dados (máx) sempre alocam no mínimo 16 KB, mesmo que sejam apenas para dados muito menores. Portanto, a primeira condição a examinar são as tabelas que usam um grande número de colunas de texto definidas para tamanhos que se ajustariam aos tipos de dados usados ​​para dados ASCII.

Por exemplo, examinamos uma tabela que tinha um grande número de colunas (acima de 230) definidas como nVarChar (máx), mas todas com comprimento de DataFlex definido como 40. O tamanho do buffer resultante era superior a 3,5 MB e, quando combinado com o padrão block size de 10, resultou em uso de memória de quase 40 MB por processo. Se possível, alterar todas essas colunas para nVarChar (40) reduziria o buffer para menos de 28 KB e a memória total por processo para cerca de 305 KB. Obviamente, esse foi um caso extremo e, como mencionado, pode haver razões para os tipos de dados usados ​​que estão fora do seu controle como desenvolvedor.

O ponto principal é conscientizar todas as tabelas e tipos de dados usados ​​e que pode ser uma tarefa tediosa.

Criamos um projeto básico (AnalyzeTableMemoryUse.src) que pode ser solto em qualquer workspace para fornecer uma visão geral imediata de cada tabela na lista de arquivos e projetar o impacto do uso de memória para cada uma. Ele também procura colunas que podem ser desnecessariamente mapeadas para tipos de dados que usam alocações mínimas e aponta quaisquer tabelas que tenham colunas muito grandes (onde o block size pode entrar em jogo).

Apenas para dar um exemplo, executamos isso em umaworkspace de amostra convertida do WebOrder… 


Workspace: Order Entry Mobile Application
Data Path: C:\DataFlex Examples\DataFlex 19.1 Examples\WebOrderMobile\Data\
Date Analyzed: 06/02/2020

Table: MSSQLDRV:OrderSystem
Columns: 4
Block Size: 10
Total Buffer Size: 73
Expected Memory Use: 803

Table: MSSQLDRV:Vendor
Columns: 8
Block Size: 10
Total Buffer Size: 137
Expected Memory Use: 1,507

Table: MSSQLDRV:Inventory
Columns: 6
Block Size: 10
Total Buffer Size: 92
Expected Memory Use: 1,012
Table: MSSQLDRV:Customer
Columns: 15
Column ’Comments’ VarChar(max), length used = 1024, recommend change to VarChar
Block Size: 10
Total Buffer Size: 32,964
Expected Memory Use: 362,604
Potential Memory Reduction: 168,949

Table: MSSQLDRV:SalesPerson
Columns: 3
Block Size: 10
Total Buffer Size: 49
Expected Memory Use: 539

Table: MSSQLDRV:OrderHeader
Columns: 9
Block Size: 10
Total Buffer Size: 96
Expected Memory Use: 1,056

Table: MSSQLDRV:OrderDetail
Columns: 6
Block Size: 10
Total Buffer Size: 60
Expected Memory Use: 660

Table: MSSQLDRV:CodeType
Columns: 3
Column ’Comment’ VarChar(max), length used = 1024, recommend change to VarChar
Block Size: 10
Total Buffer Size: 16,423
Expected Memory Use: 180,653
Potential Memory Reduction: 168,949

Table: MSSQLDRV:CodeMast
Columns: 3
Block Size: 10
Total Buffer Size: 50
Expected Memory Use: 550

Table: MSSQLDRV:WebAppSession
Columns: 9
Block Size: 10
Total Buffer Size: 173
Expected Memory Use: 1,903

Table: MSSQLDRV:WebAppUser
Columns: 5
Block Size: 10
Total Buffer Size: 92
Expected Memory Use: 1,012

Table: MSSQLDRV:WebAppServerProps
Columns: 11
Block Size: 10
Total Buffer Size: 1,048,741
Expected Memory Use: 11,536,151

Total Expected Memory Use: 12,088,450
Potential Memory Reduction: 2,027,388

Tables to Examine for Further Optimization:
MSSQLDRV:WebAppServerProps / 11,536,15 

Qualquer tabela com colunas muito grandes será listada para um exame mais aprofundado, para que você possa ver facilmente quais tabelas podem se beneficiar mais com a redução no block size. De fato, apenas definir o block size no WebAppServerProps para o mínimo de dois altera o perfil para este ...


Table: MSSQLDRV:WebAppServerProps
Columns: 11
Block Size: 2
Total Buffer Size: 1,048,741
Expected Memory Use: 3,146,223

Total Expected Memory Use: 3,698,522
Potential Memory Reduction: 2,027,388 

... portanto, com uma alteração inconseqüente (nem sequer foi necessária uma recompilação), reduzimos o consumo de memória por processo de 12MB para 3,5 MB e, alterando o tipo de dados usado em outras duas colunas, reduzimos em outros 2 MB.

O que reserva o futuro?

O DataFlex 2020 apresentará alterações como resultado do uso do Unicode. As alocações de buffer aumentarão devido ao tamanho aumentado dos dados Unicode, portanto, estamos otimizando alguns tipos de dados para que, sempre que possível, eles não aloquem seus 16 KB completos (consulte as notas sobre texto e (máx) colunas acima )

A documentação do DataFlex 2020 abordará isso em detalhes.

Para revisões após o DataFlex 2020, veremos como os drivers de banco de dados do DataFlex alocam memória para buffers de cache e veremos se podemos torná-lo mais dinâmico.

Sobre AnalyzeTableMemoryUsage.src

O arquivo de origem AnalyzeTableMemoryUsage.src é um projeto básico que os desenvolvedores podem colocar em qualquer workspace para ajudá-los a estimar o uso de memória para uma determinada workspace.