Diferentemente de outros serviços, a linguagem de consulta SQL++ até agora não tinha a opção de ajustar seu espaço de memória. Até agora.

Com o lançamento do Couchbase Server 7.0No entanto, o Query Service agora inclui uma cota de memória por solicitação.

Histórico

O principal motivo dessa disparidade entre o SQL++ (antigo N1QL) e outros serviços do Couchbase se resume a um simples fato: enquanto a maior parte do consumo de memória de serviços como Data ou Índice é o cache, e sempre que novos documentos chegam, há sempre algo que pode ser removido se o espaço estiver em alta demanda. A maior parte do serviço de consulta dependem de valores transitórios (documentos obtidos ou valores computados) que iniciam sua vida útil em um estágio de uma solicitação individual e depois expiram antes do término da solicitação.

No SQL++, não há nada para remover e substituir. Não há um ato de equilíbrio para manter o relógio funcionando. Se os recursos não estiverem disponíveis, a única opção é a falha.

Além disso, partes do Eventos, Índice e Pesquisa de texto completo são executados dentro do serviço de consulta. Eles usam recursos de memória do SQL++, mas o SQL++ não tem controle sobre eles.

Embora o consumo de memória do SQL++ não seja um problema em geral - As solicitações carregam e descartam documentos rapidamente e o mundo é um lugar feliz - de tempos em tempos, uma solicitação estranha e gananciosa aparece e estraga o jogo para todos. Esse problema precisava ser corrigido para Couchbase usuários.

Mas vamos desconsiderar (por enquanto) os componentes sobre os quais o SQL++ não tem controle e vamos considerar se um pool de valores transitórios em todo o nó seria desejável.

A operação desse pool de valores pode funcionar mais ou menos assim: Sempre que uma solicitação precisar de um valor, ela alocará o tamanho correspondente do pool global e, assim que terminar de usá-lo, o devolverá ao pool. Quando a memória se esgota, todas as alocações falham até que seja liberada memória suficiente.

Mas o que acontece quando um pedido ganancioso aparece? Ele pega o máximo que pode e não larga mais. E todos os outros pedidos frugais? Sem a possibilidade de expulsão, a única opção é o fracasso. As outras solicitações terminam uma a uma - com erro e com um erro - até que a culpada finalmente falha.

Esse comportamento é semelhante ao de um professor que manda toda a classe para a sala do diretor depois de ser atingida por um giz, em vez de investigar e mandar apenas o culpado.

O serviço de consulta agora tem olhos na parte de trás de sua cabeça e pode ver quem jogou o giz.

Insira a cota de memória por solicitação

Quando a cota de memória por solicitação é ativada, cada solicitação de consulta recebe seu próprio pool. O rastreamento de memória funciona como de costume, mas agora, quando o pool se esgota, ele é somente o culpado que falha.

"Mas uma configuração em todo o nó seria muito mais prático!" Estou ouvindo você dizer. E você está certo.

Implementamos um em sigilo: o serviço de consulta permite um número fixo de solicitações em execução a qualquer momento. Essa opção é controlada pelo parâmetro prestadores de serviços e a configuração padrão é quatro vezes o número de núcleos no nó de consulta. A cota geral de memória do nó corresponde ao número de servidores vezes a cota por solicitação.

As duas cotas estão intimamente interligadas: a cota por solicitação é explícita porque queríamos deixar claro que são as solicitações individuais que estão sendo rastreadas, e não o nó em sua totalidade.

Como posso usá-lo?

Há duas configurações para a cota de memória por solicitação no SQL++:

    • O /admin/settings parâmetro REST do nó cota de memória
    • O /query/service parâmetro REST da solicitação memory_quota

Essas configurações expressam em megabytes a quantidade máxima de memória que uma solicitação pode usar em um determinado momento.

O padrão cota de memória é zero, ou seja, a cota de memória é desativada. A cota memory_quota sempre substitui a configuração de todo o nó, desde que o valor solicitado não o exceda.

Vamos dar uma olhada em alguns exemplos. O comando abaixo define sua cota de memória como 10 MB para todo o nó e replica a configuração para todos os outros nós:

E esse comando define sua cota de memória para 10 MB para uma única solicitação, como você pode ver abaixo:

Por fim, este abaixo define sua cota de memória para 10 MB durante o período de cbq sessão:

Respostas e espaços-chave do sistema

Se a sua cota de memória estiver definida (por qualquer meio), vários recursos do SQL++ poderão conter informações adicionais, inclusive:

    • Métricas
    • Controles
    • Espaços de chaves do sistema

Vamos dar uma olhada mais de perto em cada uma dessas respostas.

Respostas: Métricas

A seção de métricas da resposta contém um usedMemory mostrando a quantidade de memória do documento usada para executar a solicitação.

Se nenhuma memória de documento for usada, essa métrica será omitida. A mesma omissão ocorre com mutações ou errorCount também.

Respostas: Controles

A seção de controles da resposta também informa a cota de memória de acordo com suas configurações. Esta é a aparência:

Espaços-chave do sistema

Espaços de chaves do sistema - ambos system:active_requests e system:completed_requests - também contêm informações sobre a cota de memória. O usedMemory e memoryQuota também aparecem aqui. Dê uma olhada no exemplo abaixo:

Como a memória é usada, afinal?

Antes de nos aprofundarmos em alguns dos aspectos mecânicos da operação de cota de memória, devemos aprender um pouco sobre como uma solicitação usa a memória.

Como você provavelmente já deve ter adivinhado, o usedMemory foi introduzido para avaliar os requisitos de memória de uma instrução individual antes de permitir sua execução. Vamos fazer alguns experimentos e ver como ele se comporta, começando com este:

Como você pode ver acima, a memória usada não é o tamanho do conjunto de resultados.

Vamos tentar novamente, mas desta vez sem formatação. Dessa forma, o tamanho do conjunto de resultados é o mais próximo possível do tamanho dos dados no armazenamento:

Bem, também não é o tamanho dos dados obtidos.

Vamos tentar remover o custo de exibição dos resultados na tela:

Mesma consulta, mesmo formato, mas armazenamento diferente e, ainda assim, uma quantidade diferente de memória usada.

A conclusão: Para alguns tipos de declarações, o consumo de memória é mais uma função das circunstâncias dessa execução específica do que da declaração em si.

A operação da fase de execução da solicitação

A fase de execução de uma solicitação emprega um pipeline de operadores que são executados em paralelo. Cada operador recebe valores do estágio anterior, processa esses valores e os envia para o próximo.

A infraestrutura de troca de valores do operador inclui uma fila de valores para que cada operador não seja bloqueado pelo anterior ou pelo próximo. (Na verdade, o mecanismo de execução é mais complicado. Alguns operadores são incorporados a outros e alguns existem apenas para realizar o trabalho de orquestração, portanto, as filas de valores nem sempre estão envolvidas, mas ainda assim).

Por exemplo, uma consulta simples como esta:

usa um Varredura de índice para produzir chaves, que são enviadas para um Buscar para recuperar documentos do cache de valores-chave, que são enviados para um Filtro para excluir documentos que não se aplicam, e os que se aplicam são enviados para um Projeção para extrair campos e transformá-los em JSON (se necessário) e, por fim, passá-los para um Fluxo que os grava de volta no cliente.

Os valores que concluem o curso são eventualmente descartados durante a coleta de lixo.

No exemplo acima, se houvesse núcleos disponíveis para executar todos esses operadores em paralelo - e todos os operadores fossem executados exatamente na mesma velocidade - nunca haveria mais de cinco documentos atravessando o pipeline em um determinado momento, mesmo que a solicitação pudesse processar qualquer número de documentos.

É claro que um Escaneamento pode produzir chaves muito mais rapidamente do que um Buscar pode reunir documentos, e o marshalling pode ser caro. O envio de resultados pelo cabo de volta ao cliente pode ser lento, portanto, mesmo que haja núcleos disponíveis, as filas descritas acima serão usadas como buffers para valores que aguardam processamento ao longo da linha. Por sua vez, isso aumenta temporariamente a quantidade de memória que uma solicitação precisa para processar a sequência de valores de entrada.

Esse padrão explica por que tanto fazer o Projeção mais eficiente (pretty=false), ou Fluxo (enviar para um arquivo em vez de para o terminal) tem um efeito benéfico no consumo de memória: operadores mais rápidos significam menos valores presos nas filas de troca de valores.

Com o aumento da carga de solicitações, o kernel do SQL++ tem mais operadores para agendar, o que significa que, enquanto eles não são executados, a fila de valores para o operador anterior aumenta de tamanho, o que significa que é necessária ainda mais memória para processar solicitações individuais. Em suma, os nós carregados usam mais memória do que aqueles com pouca atividade.

A operação de cota de memória

Para fins da discussão anterior, ignorei todos os casos em que a memória cresce sem que os valores sejam trocados: hash JOINs, ORDER BYs e GROUP BYs são alguns exemplos que me vêm à mente.

Esses casos específicos são tratados pelo primeiro modo de operação da cota de memória: o buffer de classificação, agregação ou hash cresce além de um limite específico, a cota de memória gera um erro e a solicitação falha.

No entanto, como vimos, há várias circunstâncias que fazem com que o consumo de memória aumente sem falhas por parte da solicitação.

Nesses casos, o recurso Cota de memória emprega técnicas para tentar controlar o uso da memória e ajudar as solicitações a serem concluídas sem a necessidade de recursos excessivos.

O recurso de batimento cardíaco do consumidor

Um pipeline funciona bem se tanto os produtores quanto os consumidores avançarem no mesmo ritmo.

Se o produtor não executar, a solicitação simplesmente ficará parada. No entanto, se o consumidor não executar, não apenas a solicitação fica parada, mas a fila de valores do produtor também aumenta de tamanho.

Para combater essa possibilidade, o operador do consumidor é equipado com um heartbeat, que é monitorado pelo produtor. Quando o consumidor estiver esperando, mas não tentar receber valores após um número definido de operações de envio bem-sucedidas por parte do produtor, o produtor cederá até que o consumidor consiga executar.

Essa abordagem não é uma ciência exata, já que, infelizmente, a linguagem usada para desenvolver o SQL++ não permite que ele ceda a operadores específicos. Mas ela funciona como um esforço cooperativo: se um número suficiente de produtores ceder, todos os consumidores terão uma chance justa de ter tempo de kernel, o que significa que o uso da memória deve diminuir naturalmente.

A cota por operador

Como a produção não é uma ciência exata, os operadores individuais podem acumular um uso substancial de memória, mesmo quando os consumidores conseguem ser executados de tempos em tempos, porque os consumidores individuais ainda recebem menos tempo de kernel do que seus produtores.

Para resolver essa disparidade, o SQL++ também tem um pool de memória por produtor. Quando esse pool se esgota, um produtor cede (e não falha), retomando as operações quando o consumidor recebe um valor.

Essa produção faz com que os produtores anteriores esgotem seu próprio pool e produzam, permitindo assim que toda a solicitação avance sem consumir todo o pool de solicitações, possivelmente (mas não necessariamente) à custa da taxa de transferência.

Truques diversos

Até este ponto, as consultas dependeram do coletor de lixo para retornar a memória de valor ao heap, e o gerenciador de memória aloca estruturas de valor.

Como parte do esforço de rastreamento da memória, introduzimos técnicas para marcar a memória como não utilizada antes que o próprio coletor de lixo obtenha tempo de CPU e consiga processar todos os valores não utilizados pendentes.

Há também pequenos pools ad hoc para armazenar algumas estruturas de valores não utilizadas, já alocadas e disponíveis para reutilização, de modo que o coletor de lixo não precise ser usado repetidamente para tipos específicos de alocação dinâmica de memória e, em vez disso, fique livre para processar a memória que interessa.

Conclusão

Antes da versão 7.0 do Couchbase Server, o Query Service tinha um histórico de ser um pouco laissez faire com o uso de memória por solicitação. Agora, ele tem um conjunto claro de incentivos e incentivos para manter o uso da memória sob controle. Espero que isso seja útil para você.

Não se limite a ler sobre isso; experimente você mesmo:
Download do Couchbase Server 7

 

Autor

Postado por Marco Greco, arquiteto de software, Couchbase

Em uma vida anterior, Marco foi CTO, físico de radiação, arquiteto de software, administrador de sistemas, DBA, instrutor e faz-tudo na maior clínica de radioterapia da Itália. Depois de mudar de carreira e de país, ele passou mais de duas décadas em vários cargos de suporte e desenvolvimento na Informix, primeiro, e na IBM, depois, antes de finalmente mergulhar de cabeça e entrar para a Couchbase, para ajudá-los a fazer do N1QL um ouro. Ele é detentor de várias patentes e é autor de seus próprios projetos de código aberto.

Deixar uma resposta