Como um banco de dados de pesquisa e análise, o Rockset capacita muitos aplicativos de personalização, detecção de anomalias, IA e vetores que precisam de consultas rápidas em dados em tempo actual. Rockset mantém índices invertidos para dados, permitindo executar consultas de pesquisa com eficiência sem verificar todos os dados. Também mantemos armazenamentos de colunas que permitem consultas analíticas eficientes. Leia em indexação convergente para saber mais sobre indexação no Rockset.
Os índices invertidos são a maneira mais rápida de encontrar as linhas que correspondem a uma consulta seletiva, mas depois que as linhas são identificadas, o Rockset precisa buscar os valores correlacionados de outras colunas. Isso pode ser um gargalo. Nesta postagem do weblog, falaremos sobre como tornamos essa etapa muito mais rápida, gerando uma aceleração de 4x nas consultas de pesquisa dos clientes.
Desempenho rápido de pesquisa para aplicativos modernos
Para muitos aplicativos em tempo actual, a capacidade de executar consultas de pesquisa com latência de milissegundos em altas consultas por segundo (QPS) é essencial. Por exemplo, veja como Outros enfeites usa Rockset como back-end para personalização em tempo actual.
Este weblog apresenta como melhoramos o desempenho da utilização e latência da CPU das consultas de pesquisa analisando cargas de trabalho e padrões de consulta relacionados à pesquisa. Aproveitamos o fato de que, para cargas de trabalho relacionadas à pesquisa, o conjunto de trabalho geralmente cabe na memória e nos concentramos em melhorar o desempenho da consulta na memória.
Analisando o desempenho da consulta de pesquisa no Rockset
Suponha que estejamos construindo o back-end para recomendações de produtos em tempo actual. Para isso, precisamos recuperar uma lista de produtos, dada uma cidade, que podem ser exibidos no website em ordem decrescente de probabilidade de serem clicados. Para conseguir isso, podemos executar o seguinte exemplo de consulta:
SELECT product_id, SUM(CAST(clicks as FLOAT)) / (SUM(CAST(impressions as FLOAT) + 1.0)) AS click_through_rate
FROM product_clicks p
WHERE metropolis = 'UNITED ST2'
GROUP BY product_id
ORDER BY click_through_rate DESC
Certas cidades são de specific interesse. Supondo que os dados das cidades acessadas com frequência cabem na memória, todos os dados de indexação são armazenados em RochasDB cache de bloco, o cache integrado fornecido pelo RocksDB. RocksDB é nosso armazenamento de dados para todos os índices.
O product_clicks
visualizar contém 600 milhões de documentos. Quando aplicado o filtro de cidade, são emitidos cerca de 2 milhões de documentos, o que representa aproximadamente 0,3% do whole de documentos. Existem dois planos de execução possíveis para a consulta.
- O otimizador baseado em custo (CBO) tem a opção de usar o armazenamento de colunas para ler as colunas necessárias e filtrar linhas desnecessárias. O gráfico de execução à esquerda da Figura 1 mostra que a leitura das colunas necessárias do armazenamento de colunas leva 5 segundos devido ao grande tamanho da coleção de 600 milhões de documentos.
Figura 1: Execução de consulta usando armazenamento de coluna à esquerda. Execução de consulta usando índice invertido/pesquisa à direita.
- Para evitar a varredura de toda a coluna, o CBO utiliza o índice invertido. Isso permite a recuperação apenas dos 2 milhões de documentos necessários, seguido pela busca dos valores de coluna necessários para esses documentos. O gráfico de execução está à direita da Figura 1.
O plano de execução ao usar o índice invertido é mais eficiente do que ao usar o armazenamento de colunas. O Price-Based mostly Optimizer (CBO) é sofisticado o suficiente para selecionar automaticamente o plano de execução apropriado.
O que está demorando?
Vamos examinar os gargalos no plano de execução do índice invertido mostrado na Figura 1 e identificar oportunidades de otimização. A consulta é executada principalmente em três etapas:
- Recupere os identificadores do documento do índice invertido.
- Obtenha os valores do documento usando os identificadores do Row Retailer. O armazenamento de linhas é um índice que faz parte do índice convergido, mapeando um identificador de documento para o valor do documento.
- Obtenha as colunas obrigatórias dos valores do documento (ou seja, product_id, cliques, impressões).
- A combinação das etapas 2 e 3 é chamada de
Add Fields operation
.
Conforme mostrado no gráfico de execução, a operação Adicionar campos exige muito da CPU e leva um tempo desproporcional na execução da consulta. É responsável por 1,1 segundos do tempo whole de CPU de 2 segundos para a consulta.
Por que isso está demorando?
Usos do Rockset RochasDB para todas as estratégias de indexação mencionadas acima. RocksDB utiliza um cache na memória, chamado cache de bloco, para armazenar na memória os blocos acessados mais recentemente. Quando o conjunto de trabalho cabe na memória, os blocos correspondentes ao armazenamento de linhas também estão presentes na memória. Esses blocos contêm vários pares de valores-chave. No caso do armazenamento de linhas, os pares assumem a forma de (identificador do documento, valor do documento). A operação Adicionar Campos é responsável por recuperar valores de documentos a partir de um conjunto de identificadores de documentos.
Recuperar um valor de documento do cache de bloco com base em seu identificador de documento é um processo que exige muita CPU. Isso ocorre porque envolve várias etapas, principalmente determinar qual bloco procurar. Isto é conseguido através de uma pesquisa binária em um Índice interno RocksDB ou realizando múltiplas pesquisas com um índice interno RocksDB multinível.
Observamos que há espaço para otimização com a introdução de um cache complementar na memória – uma tabela hash que mapeia diretamente identificadores de documentos para valores de documentos. Chamamos esse cache complementar de RowStoreCache.
RowStoreCache: complementando o cache de bloco RocksDB
O RowStoreCache é um cache complementar interno do Rockset ao cache de bloco RocksDB para o armazenamento de linhas.
- O RowStoreCache é um cache na memória que usa MVCC e atua como uma camada acima do cache de bloco RocksDB.
- O RowStoreCache armazena o valor do documento para um identificador de documento na primeira vez que o documento é acessado.
- A entrada do cache é marcada para exclusão quando o documento correspondente recebe uma atualização. No entanto, a entrada só é excluída quando todas as consultas anteriores que fazem referência a ela terminarem de ser executadas. Para determinar quando a entrada do cache pode ser removida, usamos a construção do número de sequência fornecida pelo RocksDB.
- O número de sequência é um valor que aumenta monotonicamente e aumenta em qualquer atualização do banco de dados. Cada consulta lê os dados em um número de sequência especificado, ao qual nos referimos como instantâneo do banco de dados. Mantemos uma estrutura de dados na memória de todos os instantâneos em uso atualmente. Quando determinamos que um instantâneo não está mais em uso porque todas as consultas que fazem referência a ele foram concluídas, sabemos que as entradas de cache correspondentes no instantâneo podem ser liberadas.
- Aplicamos uma política LRU no RowStoreCache e usamos políticas baseadas em tempo para determinar quando uma entrada de cache deve ser movida no acesso dentro da lista LRU ou removida dela.
Projeto e implementação.
A Figura 2 mostra o structure de memória do leaf pod, que é a unidade de execução primária para execução de consulta distribuída no Rockset.
Figura 2: Format de memória do pod leaf com o cache de bloco RocksDB e os caches RowStore. (RSC C1S1: RowStoreCache para o fragmento 1 da coleção 1.)*
No Rockset, cada coleção é dividida em N fragmentos. Cada fragmento está associado a uma instância do RocksDB responsável por todos os documentos e índices convergentes correspondentes dentro desse fragmento.
Implementamos o RowStoreCache para ter uma correspondência particular person com cada fragmento e uma lista world de LRU para impor políticas de LRU no pod folha.
Cada entrada no RowStoreCache contém o identificador do documento, o valor do documento, o número de sequência do RocksDB no qual o valor foi lido, o último número de sequência do RocksDB no qual a entrada foi atualizada e um mutex para proteger o acesso à entrada por vários tópicos simultaneamente. Para suportar operações simultâneas no cache, usamos folly::ConcurrentHashMapSIMD
.
Operações no RowStoreCache
RowStoreCache::Get(RowStoreCache, documentIdentifier, rocksDBSequenceNumber)
Esta operação é simples. Verificamos se o documentIdentifier está presente no RowStoreCache.
- Se estiver presente e o documento não tiver recebido nenhuma atualização entre o número de sequência em que foi lido e o número de sequência atual em que foi consultado, retornamos o valor correspondente. A entrada também é movida para o topo da lista world de entradas do LRU para que seja removida por último.
- Se não estiver presente, buscamos o valor correspondente ao identificador do documento na instância RocksDB e o configuramos no RowStoreCache.
RowStoreCache::Set(RowStoreCache, documentIdentifier, documentValue, rocksDBSequenceNumber)
- Se a operação get não encontrou o documentIdentifier no cache, tentamos definir o valor no cache. Como vários threads podem tentar inserir o valor correspondente a um documentIdentifier simultaneamente, precisamos garantir que inseriremos o valor apenas uma vez.
- Se o valor já estiver presente no cache, definimos o novo valor somente se a entrada não estiver marcada para ser excluída e a entrada corresponder a um número de sequência posterior ao já presente no cache.
GarantirLruLimites
- Quando uma entrada é adicionada à lista world de entradas LRU e precisamos recuperar memória, identificamos a entrada acessada menos recentemente e seu RowStoreCache correspondente.
- Em seguida, removemos a entrada do RowStoreCache correspondente e a desvinculamos da lista LRU world se as informações sobre atualizações na entrada do documento não forem relevantes.
Melhorias de desempenho com RowStoreCache
Melhorias de latência
A ativação do RowStoreCache na consulta de exemplo resultou em uma melhoria de 3x na latência da consulta, reduzindo-a de 2 segundos para 650 milissegundos.
Figura 3: Execução da consulta sem RowStoreCache à esquerda. Execução de consulta com RowStoreCache à direita.
A Figura 3 mostra que a operação “Adicionar campos” levou apenas 276 milissegundos com o RowStoreCache, em comparação com 1 segundo sem ele.
Melhorias no QPS
A execução da consulta de exemplo com filtros diferentes para a cidade com QPS alto mostrou uma melhoria no QPS de 2 consultas por segundo para 7 consultas por segundo, em linha com a diminuição da latência por consulta.
Isso representa uma melhoria de 3x no QPS para a consulta de exemplo.
A capacidade do RowStoreCache pode ser ajustada com base na carga de trabalho para obter desempenho supreme.
Observamos melhorias de desempenho semelhantes de até 4x na latência de consulta e QPS para diversas consultas de pesquisa de vários clientes usando o RowStoreCache.
Conclusão
Estamos constantemente nos esforçando para melhorar nossa estratégia de cache para alcançar o melhor desempenho de consulta. O RowStoreCache é uma nova adição à nossa pilha de cache, e os resultados mostraram que ele é eficaz na melhoria do desempenho da consulta de pesquisa, tanto nas métricas de latência quanto de QPS.
Autores do weblog: Nithin Venkatesh e Nathan Bronson, engenheiros de software program da Rockset.