
O preço das escritas baratas: como o RocksDB lê?
Índice
- Como o RocksDB lê uma chave?
- Quais operações de leitura o RocksDB oferece?
- Get
- MultiGet
- Iterators
- Snapshot
- O que é um filtro de Bloom?
- E as buscas por prefixo do RocksDB?
- O que é o cache de blocos do RocksDB?
- Quanto tombstones e cadeias de merge custam aos leitores?
- O triângulo da amplificação
- Um passo a passo do caminho de leitura
- Como a Solana sobrecarrega o caminho de leitura?
- Conclusão
Ler de um armazenamento de chave-valor parece simples: forneça uma chave e obtenha um valor. No entanto, o artigo anterior desta série explica por que esse raciocínio está errado. As árvores LSM fazem uma troca: as escritas são baratas porque adiam o trabalho de organizar os dados.
As leituras precisam pagar essa conta.
O valor atual de uma chave pode estar na memtable, em um arquivo no nível 0 ou em qualquer um dos níveis ordenados abaixo. O trabalho do caminho de leitura é encontrar a versão mais recente desse valor sem verificar todos os lugares.
Este artigo aborda os mecanismos que mantêm essa pesquisa rápida e a contabilidade que determina seu custo.
Como o RocksDB lê uma chave?
Uma leitura no RocksDB avança dos dados mais recentes para os mais antigos e para na primeira versão da chave que encontra. É só isso; todo o resto torna o processo mais rápido.
A ordem de pesquisa é fixa e espelha o caminho de escrita descrito no artigo anterior, mas ao contrário: ela percorre a memtable ativa, as memtables imutáveis, os arquivos SST no nível 0 e os arquivos SST ordenados do nível 1 para baixo.
O RocksDB verifica primeiro a memtable ativa porque ela contém as escritas mais recentes, reside na memória e usa uma skiplist. Como resultado, uma consulta é uma pesquisa ordenada em memória que praticamente não exige I/O. A pesquisa para assim que encontra a chave.
Depois da memtable ativa, o RocksDB verifica todas as memtables imutáveis que foram preenchidas, mas ainda não foram descarregadas em disco. Ele as verifica em seguida, da mais recente para a mais antiga.
Em seguida, o disco é verificado. Cada arquivo do nível 0 precisa ser verificado individualmente, do mais recente para o mais antigo, porque seus intervalos de chaves se sobrepõem. Isso acontece porque cada um é um snapshot do que uma memtable absorveu, de modo que a mesma chave pode estar em dois arquivos do nível 0 ao mesmo tempo.
Cada arquivo do nível 0 é um lugar distinto onde uma chave pode estar escondida. É exatamente por isso que a quantidade de arquivos no nível 0 sobrecarrega as leituras e que o RocksDB interrompe escritas para mantê-la limitada.
Do nível 1 para baixo, os arquivos SST em um determinado nível são ordenados e não se sobrepõem, portanto, no máximo um arquivo por nível pode conter a chave. Esse arquivo é encontrado por meio de uma pesquisa binária nos metadados dos arquivos. Depois, a pesquisa desce para o próprio arquivo. O bloco de índice localiza o bloco de dados correto, e o bloco de dados contém a chave ou mostra que esse nível não a possui.
Os números de sequência são importantes para o caminho de leitura do RocksDB porque fazem a pesquisa parar na primeira versão encontrada. Isso garante que essa versão seja a mais recente. Se uma leitura encontrar uma tombstone, por exemplo, ela será bem-sucedida e retornará que a chave foi excluída. Assim, a pesquisa termina na tombstone e deixa o valor mais antigo no disco.
Quais operações de leitura o RocksDB oferece?
O RocksDB oferece quatro formas de leitura, cada uma usando o caminho de leitura de maneira diferente: Get, MultiGet, Iterators e Snapshots.
Get
Get é a consulta pontual que acompanhamos na seção anterior. Ou seja, encontra uma chave em uma pesquisa do mais recente para o mais antigo, na qual a primeira versão prevalece.
MultiGet
MultiGet agrupa consultas pontuais em lotes. Uma leitura pode fornecer ao RocksDB um conjunto de chaves, que ele ordena e agrupa de acordo com os arquivos que elas podem acessar.
Na prática, isso permite que o RocksDB abra um único SST, consulte seus blocos de índice e filtro uma vez e agrupe o I/O em lotes. É o mesmo caminho de leitura de Get, mas os custos são fixos e amortizados pelo lote.
Iterators
Iterators percorrem intervalos, ou seja, buscam uma chave e avançam ou retrocedem em ordem.
Na prática, um iterator é uma mesclagem de k vias (ou seja, combina k listas ordenadas diferentes em uma única sequência totalmente ordenada). Cada fonte de dados ativa — a memtable, qualquer memtable imutável, cada arquivo L0 e um arquivo por nível mais profundo — contribui com seu próprio cursor ordenado, e um heap retorna repetidamente o cursor que contém a próxima menor chave.
Essa regra de priorizar a versão mais recente se aplica a cada etapa. Portanto, se várias fontes oferecerem a mesma chave, a versão mais recente prevalecerá e as demais serão descartadas.
Valores ocultos e tombstones são ignorados conforme o iterator avança. Isso significa que um intervalo cheio de chaves excluídas é um intervalo que o iterator precisa percorrer silenciosamente sem retornar nada. Um keyspace com muitas exclusões torna as varreduras lentas de maneiras que as consultas pontuais parecem nunca perceber.
Snapshot
Um Snapshot é um número de sequência capturado em um determinado momento. Qualquer leitura que o carregue simplesmente ignora versões mais recentes do que esse número e retorna a versão mais recente igual ou inferior a ele. Isso permite leituras consistentes de um ponto no tempo, implementadas como uma comparação de inteiros.
Criar um snapshot é praticamente gratuito porque o custo surge depois; um snapshot ativo ainda pode acessar versões que não podem ser recuperadas, por mais antigas que sejam.
Tanto Get quanto MultiGet sabem exatamente qual chave procuram. Isso significa que a pesquisa pode fazer uma pergunta mais barata do que “onde está esta chave?”. Mais precisamente: “quais arquivos definitivamente não a contêm?”. Iterators nunca contam com esse atalho. Responder à pergunta mais barata é todo o trabalho do filtro de Bloom.
O que é um filtro de Bloom?
Um filtro de Bloom responde a uma pergunta sobre um conjunto: este elemento definitivamente não está nele? A resposta é definitivamente não ou talvez, nunca um sim com certeza. Um arquivo que definitivamente não contém uma chave nunca é aberto pela pesquisa, economizando I/O.
Quando um SST é criado, ele passa cada chave por algumas funções de hash, e cada hash define um bit em um array compacto de bits (o bloco de filtro).
No momento da leitura, a consulta aplica hash à chave procurada da mesma maneira e verifica esses bits. Se algum bit não estiver definido, a chave nunca foi inserida, o que significa que ela definitivamente não está ali.
Se todos os bits estiverem definidos, a chave está provavelmente presente — provavelmente porque os hashes de outras chaves podem coincidentemente cobrir os mesmos bits.
Isso significa que podem ocorrer falsos positivos: a pesquisa abre o arquivo, verifica e não encontra nada, desperdiçando o I/O que o filtro existe para evitar.
Na configuração convencional do RocksDB de dez bits por chave, a taxa de falsos positivos é de aproximadamente 1%. Dez bits proporcionam 99% de chance de ignorar um arquivo que não contém a chave. Um filtro para um SST com um milhão de chaves ocupa pouco mais de um megabyte, reside na memória ao lado do índice e é consultado sem acessar o disco que protege. Poucas estruturas na engenharia de sistemas entregam mais por menos.
Isso também explica por que o artigo anterior desta série encontrou um bloco de filtro com tamanho zero. Os filtros de Bloom são opcionais, configurados por meio de filter_policy, com a quantidade de bits por chave escolhida pela aplicação, e a configuração padrão não define nenhum.
Esse padrão é justificável para uma biblioteca que não pode conhecer sua carga de trabalho, mas é indefensável em produção, onde o filtro representa a diferença entre uma chave ausente custar uma verificação na memória por arquivo e custar uma leitura de bloco de índice por arquivo.
Uma limitação é que os filtros de Bloom só ajudam quando a pesquisa conhece a chave exata. Um filtro de Bloom pode responder se key:0301 está neste arquivo, mas não pode responder se o arquivo contém algo entre key:0090 e key:0110, porque hashes destroem a ordem.
Iterators, que nunca conhecem a próxima chave com antecedência, não recebem nenhuma ajuda dessa estrutura. Filtros de Bloom são uma tecnologia para consultas pontuais; cargas de trabalho com muitos intervalos precisam buscar alívio em outro lugar.
E as buscas por prefixo do RocksDB?
A solução parcial do RocksDB para o problema das varreduras é mudar o que recebe hash. Um extrator de prefixo informa ao RocksDB que as chaves têm uma estrutura significativa (por exemplo, os primeiros oito bytes ou tudo antes de um separador). Quando o RocksDB sabe o que é um prefixo, ele pode criar filtros de Bloom sobre prefixos em vez de chaves inteiras, ou junto delas. Um filtro que responde se um arquivo contém uma chave que começa com determinado prefixo pode ajudar um iterator.
No entanto, esse atalho só funciona com um prefixo.
Uma varredura de determinado prefixo pode ignorar arquivos e blocos da mesma forma que consultas pontuais. Uma varredura que passa de um prefixo para o seguinte não recebe a mesma ajuda. Explicar como manter as garantias de ordem de iteração entre prefixos exige uma análise própria dos detalhes de configuração que tornam isso possível.
A busca por prefixo não torna as varreduras de intervalo baratas de modo geral.
Em vez disso, ela torna barata uma forma específica e extremamente comum de varredura de intervalo para determinado prefixo.
O que é o cache de blocos do RocksDB?
O cache de blocos é um cache em memória de blocos descompactados. Quando um bloco de dados é lido de um SST, ele é descompactado e mantido. Assim, a próxima leitura que precisar do mesmo bloco o encontrará sem acessar o disco. Isso significa que chaves acessadas com frequência se agrupam em blocos acessados com frequência, e esses blocos deixam de gerar custos de I/O.
Assim, os filtros de Bloom tentam manter as pesquisas fora dos arquivos, enquanto o cache de blocos torna barato revisitar os arquivos que elas precisam acessar.
Um detalhe é que existem dois caches.
O cache de blocos pertence ao próprio RocksDB e contém blocos descompactados que ele gerencia explicitamente. Abaixo dele está o cache de páginas do sistema operacional, que contém as páginas brutas e compactadas dos arquivos que o RocksDB leu recentemente. Os mesmos dados podem ocupar os dois ao mesmo tempo (ou seja, descompactados no cache de blocos e compactados no cache de páginas). É por isso que o consumo real de memória do RocksDB é sistematicamente subestimado e que discussões sobre dimensionamento de cache que ignoram o cache de páginas consideram apenas metade do sistema.
Os blocos de índice e filtro recebem tratamento especial. Eles são pequenos, consultados em quase todas as leituras que chegam ao arquivo correspondente e têm um custo catastrófico em caso de cache miss. Portanto, mantê-los residentes é praticamente inegociável.
A configuração determina se eles residem dentro do orçamento do cache de blocos ou fora dele, na própria memória dos leitores de tabela (ou seja, cache_index_and_filter_blocks).
Quanto tombstones e cadeias de merge custam aos leitores?
Tombstones e cadeias de merge são tipos de registro do caminho de escrita que afetam o caminho de leitura, mas só podem ser resolvidos pela compactação. Tombstones se acumulam até que a compactação as remova junto com os registros mortos que ocultam, e as cadeias de merge crescem até que a compactação as incorpore a valores consolidados.
Uma tombstone custa pouco para a consulta pontual que a encontra. Ou seja, a pesquisa para e fornece a resposta, pois “excluída” é um resultado válido. No entanto, um iterator não pode parar em uma tombstone. Em vez disso, precisa resolver a tombstone, ignorar as versões que ela oculta e continuar avançando até encontrar a próxima chave ativa. Um keyspace com muitas exclusões é um campo minado de entradas que precisam ser percorridas e descartadas sem retornar nada; varreduras podem executar muito trabalho arbitrário para produzir pouca saída. Consultas pontuais nunca percebem isso, enquanto varreduras de intervalo ficam indefesas.
Operandos de merge geram a mesma conta, mas na direção oposta. A leitura de uma chave mesclada não pode parar no registro mais recente porque um operando é uma descrição de uma alteração, não um valor. Portanto, a pesquisa precisa continuar coletando operandos até chegar a um valor completo, uma tombstone ou à base da árvore e, então, combinar a cadeia por meio do operador de merge.
O passo a passo do artigo anterior combinou uma cadeia de três elementos, mas nada no design limita a cadeia a três. Um contador mesclado dez mil vezes entre compactações exige uma leitura que reúna e combine dez mil operandos, e o caminho de escrita não fez nada para impedir isso.
O triângulo da amplificação
O triângulo da amplificação é uma relação de três concessões que rege todas as árvores LSM: amplificação de escrita, amplificação de leitura e amplificação de espaço. Melhorar quaisquer duas piora a terceira.
Athanassoulis et al. formalizaram essas concessões como a conjectura RUM (2016), segundo a qual um método de acesso pode minimizar, no máximo, dois destes fatores: sobrecarga de leitura, sobrecarga de atualização e sobrecarga de memória ou espaço. O terceiro absorve o custo. Árvores B ocupam uma posição (ou seja, leituras baratas e atualizações caras), enquanto árvores LSM ocupam outra (ou seja, atualizações baratas e leituras caras), e o triângulo mapeia o território entre elas.
Tudo neste artigo representou um dos vértices do triângulo se defendendo.
A amplificação de escrita é a proporção entre os bytes fisicamente gravados em disco e os bytes gravados logicamente pela aplicação. Um único Put é gravado uma vez no WAL, uma vez quando descarregado para L0 e sempre que a compactação o leva para um nível inferior. Isso significa que uma escrita lógica pode se transformar rotineiramente em dez a trinta escritas físicas ao longo de sua existência em uma LSM nivelada com uma dúzia de níveis. Embora o artigo anterior celebre o fato de cada uma dessas escritas ser sequencial, este artigo lamenta isso.
Amplificação de leitura se refere à quantidade de lugares que uma consulta precisa verificar para encontrar sua resposta. Para uma consulta pontual, o pior caso envolve cada memtable, arquivo L0 e um arquivo por nível mais profundo (ou seja, a pesquisa acompanhada no início deste artigo), e o custo efetivo é tudo o que passa pelos filtros de Bloom e pelo cache de blocos. Para uma varredura de intervalo, é a mesclagem de k vias que mantém todas as fontes abertas ao mesmo tempo. A amplificação de leitura é, em grande parte, o tema deste artigo: filtros, caches e extratores de prefixo atuando como mecanismos para reduzi-la.
A amplificação de espaço é a proporção entre o espaço em disco consumido e os dados lógicos armazenados. Versões antigas aguardando compactação, tombstones e os registros mortos que ocultam, cadeias de merge não consolidadas e todas as versões retidas por um snapshot ativo consomem espaço em disco com dados que a aplicação não pode mais ler ou não quer mais. Uma compactação nivelada bem ajustada mantém essa proporção próxima de 1,1x, enquanto outras configurações sacrificam muito mais espaço.
O triângulo determina que melhorar quaisquer dois vértices piora o terceiro. Ou seja:
- Compacte de forma agressiva: as leituras ficam mais rápidas e o espaço, mais eficiente, mas cada chave é regravada constantemente, aumentando a amplificação de escrita
- Compacte sem pressa: as escritas fluem, mas os arquivos se acumulam para as leituras verificarem, e dados obsoletos permanecem no disco
- Invista memória em filtros: caches e leituras melhoram, mas a memória é o quarto recurso silencioso pelo qual todos os vértices do triângulo competem
Não existe uma configuração chamada “rápida”. Existem apenas posições no triângulo que correspondem, ou não, a determinada carga de trabalho.
Um passo a passo do caminho de leitura
O exemplo a seguir reutiliza o projeto do passo a passo do artigo anterior, com estatísticas habilitadas.
Substitua o conteúdo de src/main.rs por:
use rocksdb::{BlockBasedOptions, Options, DB};
fn main() {
let with_bloom = std::env::args().nth(1).as_deref() == Some("bloom");
let path = if with_bloom { "/tmp/read-bloom" } else { "/tmp/read-nobloom" };
let mut opts = Options::default();
opts.create_if_missing(true);
opts.enable_statistics();
if with_bloom {
let mut bbt = BlockBasedOptions::default();
bbt.set_bloom_filter(10.0, false);
opts.set_block_based_table_factory(&bbt);
}
let db = DB::open(&opts, path).unwrap();
// Three L0 SSTs with fully overlapping ranges: file b holds keys where
// key % 4 == b, all spanning 0..600. Keys where key % 4 == 3 exist nowhere
for batch in 0..3u64 {
let mut k = batch;
while k < 600 {
let key = format!("key:{:04}", k);
db.put(key.as_bytes(), b"value").unwrap();
k += 4;
}
db.flush().unwrap();
}
// key:0404 lives only in the oldest file, meaning the search must get past
// the two newer files to reach it
let hit = db.get(b"key:0404").unwrap();
println!("get(key:0404) = {:?}", hit.map(|b| String::from_utf8(b).unwrap()));
// key:0303 is inside every file's range and inside no file
let miss = db.get(b"key:0303").unwrap();
println!("get(key:0303) = {:?}", miss);
if let Some(stats) = db.property_value("rocksdb.options-statistics").unwrap() {
for line in stats.lines() {
if line.contains("bloom.filter.useful")
|| line.contains("bloom.filter.full.positive")
|| line.contains("block.cache.data") {
println!("{}", line);
}
}
}
}Acima, temos três arquivos L0 descarregados cujos intervalos de chaves se sobrepõem completamente. Portanto, nenhum arquivo pode ser excluído apenas pelos metadados de intervalo, pois ambos os arquivos podem esconder qualquer uma das chaves.
O acerto (ou seja, key:0404) está apenas no arquivo mais antigo, forçando a pesquisa do mais recente para o mais antigo a lidar com dois arquivos que não o contêm antes de chegar ao arquivo que o contém.
A chave ausente (ou seja, key:0303) está dentro do intervalo de todos os arquivos, mas não está em nenhum deles. Isso significa que somente um filtro de Bloom poderia poupar essa leitura.
Execute como está e, depois, com o argumento bloom.
Sem filtros, a saída é:
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 3
rocksdb.block.cache.data.hit COUNT : 3
rocksdb.bloom.filter.useful COUNT : 0Temos seis acessos a blocos de dados para responder a duas consultas. O acerto leu um bloco de dados de cada um dos três arquivos, dois dos quais não foram úteis, totalizando três leituras do disco. A chave ausente verificou os mesmos três arquivos em seguida, e seus três acessos a blocos de dados foram cache hits apenas porque a primeira consulta tinha acabado de aquecer o cache. O cache de blocos fez seu trabalho e foi o único mecanismo que fez algo.
Com filtros, a saída é:
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 1
rocksdb.block.cache.data.hit COUNT : 0
rocksdb.bloom.filter.useful COUNT : 5
rocksdb.bloom.filter.full.positive COUNT : 1Temos um único acesso a bloco de dados no total. Podemos verificar manualmente o cálculo de useful ser 5:
- O acerto consultou o filtro do arquivo mais recente e o filtro do segundo arquivo, e ambos retornaram não. O filtro do arquivo mais antigo retornou o único
full.positive, que se mostrou um verdadeiro positivo. Isso nos dá dois arquivos ignorados - A chave ausente consultou os três filtros e recebeu três respostas negativas, o que significa três arquivos ignorados e nenhum arquivo aberto
Somando o acerto e a chave ausente, temos cinco verificações de arquivos. As cinco verificações que os filtros responderam usando a memória são as cinco leituras de blocos de dados que a execução sem filtros precisou realizar. Assim, dez bits por chave transformaram seis acessos a blocos em apenas um.
Na prática, este é o triângulo da amplificação em menor escala. A execução sem filtros pagou integralmente pela amplificação de leitura, a execução com filtros pagou por ela em memória (ou seja, algumas centenas de bytes de filtro por arquivo), e o caminho de escrita não pagou nada em nenhum dos casos. Toda decisão de ajuste no RocksDB é essa mesma troca em uma escala maior.
Como a Solana sobrecarrega o caminho de leitura?
O artigo anterior apresentou o validador como o teste de estresse do caminho de escrita. Ou seja, shreds chegando continuamente na velocidade da linha. O lado da leitura é a outra metade do acordo, porque um ledger que ninguém consegue ler é apenas uma forma cara de aquecer um data center.
Todas as afirmações deste artigo sobre ordem de pesquisa, filtros e design de chaves aparecem na forma como os dados da Solana são realmente consultados.
Dentro do validador, as leituras do Blockstore assumem duas formas diferentes: atender a solicitações de reparo e reproduzir ou transmitir um bloco.
Atender a solicitações de reparo significa que outros validadores estão solicitando shreds que perderam. Essa é uma consulta pontual por (slot, index). Reproduzir ou transmitir os dados de ledger de um bloco é uma varredura de intervalo sobre um determinado slot.
Como as chaves dos shreds começam com o número do slot, todos os shreds de um slot ficam contíguos no disco. Assim, “forneça o slot n” é uma caminhada sequencial e restrita do iterator, não uma dispersão de leituras pontuais.
O design de chaves do caminho de escrita é a otimização do caminho de leitura. Ou seja, o mesmo layout que prioriza o slot e fazia os descarregamentos produzirem arquivos L0 com pouquíssima sobreposição transforma leituras de blocos em uma única varredura contígua.
A camada RPC faz perguntas mais difíceis. getTransaction é uma consulta pontual por assinatura nos dados de status da transação. Esse é o caso para o qual os filtros de Bloom existem, pois uma assinatura ausente de determinado SST deve custar uma verificação na memória para ser descartada, não uma leitura de índice.
getSignaturesForAddress é a varredura baseada em prefixo apresentada anteriormente neste artigo. O índice de endereço para assinatura começa com o endereço, portanto, todo o histórico de uma conta ocupa um único intervalo contíguo de chaves, e toda a atividade de um endereço assume a forma de varredura de intervalo que a estrutura processa bem.
No entanto, as perguntas que as aplicações realmente fazem combinam as duas formas. Não existe uma primitiva para “forneça todas as transações deste endereço”. Esse tipo de pergunta se transforma em uma varredura por prefixo para encontrar as assinaturas e, depois, em uma consulta pontual por assinatura para obter as transações.
Na prática, uma consulta de intervalo se ramifica em centenas ou milhares de Gets, exatamente a carga de trabalho para a qual existe o processamento em lotes de MultiGet. Ou seja, ordene as assinaturas, agrupe-as pelos arquivos que acessam e consulte o índice e os filtros de cada arquivo uma vez.
Uma consulta que parece ser uma única chamada de API externamente aciona, no caminho de leitura, todos os mecanismos descritos neste artigo em sequência: iterator, ordenação por prefixo, filtros de Bloom, amortização em lotes e cache de blocos.
Nós criamos essa chamada de API.
Historicamente, os desenvolvedores precisavam executar no cliente um loop de getSignaturesForAddress e n chamadas a getTransaction, com a lógica de repetição que padrões n+1 sempre acabam exigindo.
getTransactionsForAddress reduz esse loop a um único método com vários recursos que correspondem aos mecanismos do caminho de leitura discutidos neste artigo.
As ordens crescente e decrescente são as duas direções do iterator. A paginação baseada em cursor devolve um token no formato slot:position, que representa a posição de uma chave. Portanto, retomar uma consulta paginada significa reposicionar uma varredura no ponto em que a anterior parou. Filtros de slot e tempo limitam o trabalho em vez de descartá-lo depois.
O sistema de arquivamento que atende a esse método mantém todo o histórico de transações da Solana no RocksDB — centenas de terabytes. Nós o migramos do ClickHouse para o RocksDB, e esse é o motivo desta série existir: o mecanismo de armazenamento descrito nestes artigos é aquele que ajustamos, column family por column family, para transformar toda a vida útil de uma conta em uma varredura contígua e a consulta de uma assinatura em uma única verificação na memória. É por isso que temos o sistema de arquivamento com melhor desempenho de toda a Solana.
Conclusão
O caminho de leitura é o cronograma de pagamento da árvore LSM, e este artigo o apresenta como uma conta detalhada. Uma consulta percorre os dados do mais recente para o mais antigo, passando pela memtable, por L0 e por todos os níveis ordenados, e para na primeira versão encontrada. Todos os outros recursos e ferramentas existem para minimizar a quantidade de acessos exigida por essa pesquisa: filtros de Bloom respondem na memória se uma chave definitivamente não está em um arquivo, para que ele permaneça fechado; o cache de blocos torna barato reabrir os arquivos que precisam ser abertos; e o design de prefixos transforma a única forma acessível de varredura de intervalo na forma de que as aplicações realmente precisam.
O triângulo da amplificação é o ledger que registra tudo isso: o custo de leitura é reduzido com memória, e o custo de escrita é pago antecipadamente em parcelas sequenciais. O custo de espaço se acumula silenciosamente em cada tombstone e versão oculta que a pesquisa precisa atravessar.
Observe que cada estrutura deste artigo apenas gerencia o custo de arquivos acumulados, versões obsoletas e cadeias de merge não consolidadas. Nada abordado aqui realmente o elimina. O mecanismo que o elimina é executado continuamente em segundo plano, e decidir com que agressividade ele opera é a decisão de ajuste mais importante do RocksDB. O próximo artigo desta série é sobre compactação: o que ela faz, os três estilos oferecidos pelo RocksDB e por que o Blockstore da Solana ficou conhecido por trocar de estilo e depois voltar ao anterior.
Se passar a tarde lendo um passo a passo sobre o caminho de leitura de um banco de dados é o seu tipo de programa, venha construir conosco. O caminho de leitura descrito aqui é o que operamos sobre todo o histórico da Solana, onde a diferença entre consultar um filtro de Bloom e abrir um arquivo é medida em hardware e dinheiro reais. Temos vagas abertas em toda a nossa equipe de engenharia. Veja todas as oportunidades em helius.dev/careers
Artigos relacionados
Assine a Helius
Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos


