Disk saved flattens after cap 3 while the worst-case rebuild keeps climbing

O EIP-8188 adiciona um carimbo de data/hora visível por consenso a cada conta e slot de armazenamento, registrando quando cada um sofreu a última mutação. Suas regras de precificação usam esse campo para cobrar mais pela gravação no estado inativo de gravação, mas o campo em si é a parte que importa aqui porque fornece a cada cliente o mesmo sinal para qual estado sofreu mutação recentemente (quente) e qual não sofreu mutação há muito tempo (frio). O EIP não exige deliberadamente nenhuma arquitetura de armazenamento. Ele apenas entrega aos clientes os metadados e deixa em aberto o que fazer com eles.

Este artigo pega esse sinal e mede uma coisa que um cliente pode fazer com ele: separar fisicamente o estado frio do quente, mantendo um conjunto menor de trabalho a quente no banco de dados principal e armazenando o restante frio em um arquivo simples mais barato. A questão é quanto disco realmente economiza em um nó real e qual forma de fazer a separação vence.

Executamos um experimento de três etapas em um nó real da mainnet go-ethereum (geth) para ver quanto disco você economiza ao extrair o estado “frio” do banco de dados quente. Isso descreve o que cada etapa faz e o que realmente mediu.

As três etapas:

  • Linha de base. O nó geth normal.
  • Injeção de período. Marque cada conta e slot com a data em que foi usado pela última vez, para que possamos saber o que está frio.
  • Mova o estado inativo para fora. Puxe as partes frias do banco de dados principal para um arquivo simples, reduzindo o que o nó precisa manter quente.

Tudo abaixo é medido em um diretório de dados: mainnet no bloco 19.999.256.


Como geth armazena o estado

O estado é cada conta (saldo, nonce, código, raiz de armazenamento) mais todos os slots de armazenamento do contrato. geth o mantém duas vezes dentro de seu armazenamento de valores-chave (PebbleDB).

1. Merkle-Patricia Trie (MPT). A árvore autenticada cujo hash raiz é a raiz de estado do bloco.

   account trie  (root hash = stateRoot)
            |
         (branch)
        /        \
  (extension)   (branch)
      |          /     \
   (branch)   (leaf)  (leaf)
    /    \
 (leaf) (leaf)

   every contract account's storage is its own trie of the same shape.

2. O instantâneo. Uma cópia simples de chave-valor apenas das folhas, para que uma leitura não precise percorrer a árvore.

   accountHash             -> account
   accountHash + slotHash  -> slot value

Etapa 1: linha de base

O nó no bloco 19.999.256. Os tamanhos são bytes lógicos (soma do conteúdo do registro), a menos que indicado. O PebbleDB compactado em disco físico tem cerca de 251,75 GB.

componente contar tamanho
experimentar nós (conta) 334,7 milhões 38,81GB
testar nós (armazenamento) 1.560,7 milhões 109,33GB
tente total 1.895,4 milhões de nós 148,13GB
instantâneo, conta (chaves/valores) 7,58/3,77GB
instantâneo, armazenamento (chaves/valores) 69,74/13,32GB
total do instantâneo ~1,15 B de registros 101,38GB

Etapa 2: injeção menstrual

Para cada conta e slot de armazenamento, armazenamos o período mais recente em que foi gravado. Isso vai apenas para o instantâneo. Um “período” é apenas uma janela de tempo, aqui 1.314.000 blocos (cerca de seis meses). Uma folha é inativa se não tiver sido escrita há pelo menos minAge períodos (aqui são 2, ou seja, 1 ano de inatividade no total).

Neste experimento, usamos Xatu como fonte de dados primária.

    access-history source                     injector             snapshot (pebble)

   +---------------------+  (key, block)   +-------------+   read -> set period -> write
   | addr A wrote @ blk  | --------------> | period =    |   +---------------------------+
   | slot S wrote @ blk  |                 | (blk - fork)| ->| accountHash -> (acct, P)  |
   | ...                 |                 |   / perLen  |   | slotHash    -> (value, P) |
   +---------------------+                 +-------------+   +---------------------------+
                                                              (only ever raises a period)

ComputePeriod(block) = (block - forkBlock) / blocksPerPeriod. Com forkBlock 17.371.256 e blocksPerPeriod 1.314.000, o head fica no período 2. A atualização só aumenta o período do registro, então a fonte pode emitir diferenças em qualquer ordem, em lotes, com novas tentativas.

Como é armazenado e por que quase não custa nada. O período é um campo RLP final opcional. Quando é 0 (o registro nunca foi gravado no intervalo rastreado), os bytes são idênticos a um registro legado, portanto, apenas os registros gravados recentemente aumentam.

   account record:
     before:  RLP( nonce, balance, storageRoot, codeHash )
     after:   RLP( nonce, balance, storageRoot, codeHash, period )   (+1 to 2 bytes)

   storage slot record:
     before:  RLP("value")                a plain string
     after:   RLP( "value", period )      a 2-item list  (+2 to 3 bytes)

Custo (etapa 1 a etapa 2).

tentar instantâneo
mudar nenhum, byte idêntico +0,2 a 0,3 GB (em aproximadamente 32,5 milhões de contas mais slots gravados recentemente)
como % 0% menos de 0,5% do instantâneo de 101 GB

Etapa 3: remover o estado inativo

Agora que cada folha carrega um último período usado (etapa 2), podemos extrair as partes frias do estado do banco de dados principal para um arquivo simples, deixando para o nó um conjunto menor de trabalho a quente.

O que movemos

Nós procuramos subárvores frias: um nó cujas folhas abaixo estão inativas (currentPeriod - leafPeriod >= 2usando os períodos da etapa 2). Quando encontramos uma, pegamos a maior subárvore fria possível, sob dois limites:

  • um boné em sua altura, para que uma subárvore nunca fique muito grande. A altura é contada a partir das folhas, então uma folha tem altura 1, um galho diretamente acima das folhas tem altura 2 e assim por diante, com no máximo 16 ^ (altura-1) folhas sob uma subárvore dessa altura.
  • um chão de altura 2, então nunca movemos uma única folha fria sozinha. Mover uma única folha custa um stub de 17 bytes mais um registro de arquivo e não economiza nenhum interior, uma perda líquida.
   BEFORE (in PebbleDB)                  AFTER

        N (cold subtree root)            N  ->  17-byte stub  --+
       / \                                                      |
  (branch)(branch)   interior nodes      subtree gone from      |
   / \     / \         (deleted)         PebbleDB                v
 leaf leaf leaf leaf  (all inactive)            nodearchive (flat file)
                                                +------------------------+
                                                | (leaf)(leaf)(leaf) ... |
                                                +------------------------+

O que armazenamos e lendo de volta

Para cada subárvore movida escrevemos seu só folhas para o arquivo simples e substitua toda a subárvore por um ponteiro de 17 bytes:

  • O stub, 17 bytes no PebbleDB: (0x00 marker | fileOffset:8 | size:8). O primeiro byte de um nó de teste real é 0xc0 ou superior, então 0x00 nunca pode ser confundido com um. O deslocamento e o tamanho colocam os registros desta subárvore no arquivo.
  • O arquivo deixa apenas: um registro RLP por folha, (pathToLeaf, leafValue). Não há nós internos, apenas as folhas e seus caminhos relativos. O interior é excluído do PebbleDB.
  • Lendo de volta: carregue os registros, reinsira cada um (path, value) em um novo mini-trie. A subárvore reconstruída é idêntica à original e seu hash deve corresponder ao esperado pelo stub, o que também funciona como uma verificação de corrupção.
   access a cold leaf:
     stub(offset, size) -> read records -> rebuild subtree in memory -> use it

Por que apenas sai: a alternativa ingênua

A mudança ingênua copia cada nó frio no arquivo simples como está, com ramificações internas e tudo. Ele libera o máximo do banco de dados principal, porque realoca tudo frio, mas o arquivo simples paga por isso. Medimos ambos no mesmo datadir, em bytes de valor lógico para que os dois sejam contados da mesma maneira:

ingênuo: cada nó frio, estrutura completa nossa: subárvores frias, apenas folhas (cap 3)
o que aparece no arquivo simples toda a subárvore, interior e tudo apenas as folhas, interior reconstruído no acesso
canhotos escritos 316,3 milhões 239,6 milhões
tentativa diminui de 148,1 GB para 32,7 GB (-115,5) 58,7 GB (-89,4)
arquivo simples 162,39GB 82,96GB cru / 41,58GB comprimido
disco total líquido +46,93GB (sobe) -6,43GB cru / -47,81 GB comprimido

A abordagem ingênua libera mais do banco de dados principal (-115 GB de tentativa contra nossos -89 GB), mas seu arquivo simples é de 162 GB, maior do que o que foi liberado, então o disco total vai acima em cerca de 47 GB. Armazenar folhas apenas mantém o arquivo simples pequeno o suficiente para que o disco total seja abaixo. Abandonar o interior e reconstruí-lo no acesso é o truque.

Encontrando as subárvores frias

Uma passagem de streaming percorre a tentativa e o instantâneo com carimbo de ponto lado a lado, rolando a resposta totalmente inativa das folhas. Quando uma subárvore fria está completa e dentro do piso e do topo, ela materializa a subárvore, escreve os registros das folhas, prepara o toco e exclui o interior, tudo na mesma passagem. A verificação do hash reconstruído é executada antes que qualquer coisa seja excluída, portanto, uma incompatibilidade anula aquela subárvore em vez de corromper o estado.

Compactando o arquivo

O arquivo simples está bruto no disco, mas é bem compactado. Comprimido em pedaços de aproximadamente 1 MB, um quadro zstd por pedaço mais uma pequena tabela de deslocamento, ele cai cerca de metade e ainda permite reconstruir uma única subárvore descompactando apenas seu pedaço. A compactação é a maior alavanca em todo o pipeline, portanto, os resultados abaixo relatam tanto o arquivo bruto quanto o compactado. Duas coisas que não funcionaram: compactar cada folha ou subárvore por conta própria, porque os blocos são muito pequenos para o zstd encontrar qualquer coisa, e um dicionário compartilhado treinado em registros de amostra, que movia o número em alguns pontos e às vezes piorava. Você precisa compactar muitas subárvores juntas.

Escolhendo a altura do boné

O piso de 2 significa cada tampa move as mesmas folhas. O limite apenas altera a forma como essas folhas são agrupadas: um limite mais alto mescla uma região fria em uma grande subárvore em vez de muitas subárvores pequenas, o que deixa menos stubs de 17 bytes para trás e exclui mais interior, de modo que o banco de dados principal diminui ainda mais. Varremos a tampa de 2 para 5, piso fixado em 2:

altura da tampa tente redução arquivo (bruto) arquivo (compactado) disco de rede (bruto) disco de rede (compactado) subárvores movidas reconstrução do pior caso (folhas)
2 -66,93 GB (-45,2%) 63,35 GB 31,39GB -3,58 GB (-1,4%) -35,54 GB (-14,1%) 295,1 milhões 16
3 -95,86 GB (-64,7%) 82,96GB 41,58GB -12,90 GB (-5,1%) -54,28 GB (-21,6%) 239,6 milhões 73
4 -107,37 GB (-72,5%) 89,19GB 44,79GB -18,18 GB (-7,2%) -62,58 GB (-24,9%) 158,4 milhões 295
5 -110,03 GB (-74,3%) 89,98GB 45,16GB -20,05 GB (-8,0%) -64,87 GB (-25,8%) 116,4 milhões 1118

As duas porcentagens usam linhas de base diferentes. A redução da tentativa é contra a experimente o tamanho (148,1 GB da etapa 1), já que a mudança exclui apenas os nós de teste e deixa o instantâneo em paz. O disco líquido colunas estão contra o total Pegada em disco de 251,75 GB.

Um limite mais alto sempre economiza mais disco, mas os ganhos diminuem rapidamente, cada passo valendo cerca de metade do anterior, enquanto a reconstrução do pior caso cresce no sentido contrário:

Esta é a verdadeira desvantagem na escolha de um limite. Um limite maior reduz ainda mais o disco, mas cada acesso que atinge o estado frio reconstrói toda a subárvore em que chega, e uma subárvore maior é uma reconstrução mais lenta. O Cap 2 reconstrói no máximo 16 folhas, o Cap 5 até mais de mil. Portanto, o ponto ideal não é o limite mais profundo. Cap 3 é o melhor equilíbrio: armazena a maior parte da economia de disco (-54,28 GB de um possível -64,87 GB no limite mais profundo), enquanto mantém a reconstrução pequena, no máximo 73 folhas. O Cap 4 troca outros 8 GB por uma reconstrução aproximadamente quatro vezes maior, e o Cap 5 não economiza quase nada para uma reconstrução após mil folhas.


Resumo de ponta a ponta

   STEP 1 baseline          STEP 2 period inject        STEP 3 move inactive out
   ---------------          --------------------        ------------------------
   trie     148.1 GB  --->  trie     148.1 GB  (same) -> trie     ~59 GB + 240 M stubs
   snapshot 101.4 GB  --->  snapshot 101.6 GB  (+0.3) -> snapshot 101.6 GB (same)

   no timestamps            every leaf has its          nodearchive 83 GB raw
                            last-used period            (42 GB compressed)
Linha de base da etapa 1 Etapa 2 pós-injeção Etapa 3 pós-mudança (piso 2/cap 3, melhor equilíbrio)
experimente nós 1.895,4 milhões 1.895,4 milhões 788,0 milhões (-58%)
instantâneo 101,38GB ~101,6 GB (+menos de 0,5%) ~101,6GB
arquivo externo 82,96 GB brutos / 41,58GB zstd
PebbleDB (físico, compactado) 251,75GB 251,75GB 155,89GB (-95,9GB)
disco total líquido vs linha de base ~+0,3 GB (+0,1%) -12,90 GB (-5,1%) bruto / -54,28 GB (-21,6%) compactado

O que tiramos disso:

  • A mudança reduz o banco de dados quente, excluindo nós internos frios e realocando folhas frias. Com a regra de piso 2/cap 3, o PebbleDB cai 95,9 GB e as folhas caem em um arquivo simples, você pode estacionar em um armazenamento mais barato.
  • O total no disco ainda diminui após a contagem do arquivo: -12,90 GB brutos e -54,28 GB (cerca de 22%) com a compactação fragmentada. O preço é reconstruir uma pequena subárvore em um raro acesso de estado frio, no máximo 73 folhas neste limite.
  • A altura do limite é uma compensação entre o tamanho do disco e o custo de reconstrução. Um limite mais profundo libera mais disco, mas aumenta a reconstrução do pior caso, e os ganhos do disco diminuem rapidamente enquanto o custo de reconstrução aumenta. O limite 3 é o melhor equilíbrio, o limite 4 é mais agressivo (mais disco, maior reconstrução) e o limite 5 mal vale a pena.

Perguntas abertas

Desempenho sob cargas de trabalho reais. Tudo acima é uma medição estática da pegada. O que não mede é o custo de desempenho da modificação do estado frio. Cada acerto em uma subárvore fria paga uma reconstrução: leia os registros folha, reconstrua a subárvore na memória e verifique o hash antes de retornar. Isso é barato em uma tampa rasa, no máximo 73 folhas na tampa 3, mas cresce com a tampa, até cerca de 300 folhas na tampa 4 e mais de mil na tampa 5, e isso acontece em cada acesso a uma subárvore fria. Esta é a principal razão pela qual um limite mais profundo não é automaticamente melhor, apesar de economizar mais disco.

Portanto, o vencedor da pegada não é automaticamente o vencedor da carga de trabalho. Um projeto que economiza mais disco ainda pode perder se um padrão de acesso comum continuar alcançando subárvores frias e pagando pela reconstrução. O experimento que ainda não realizamos é reproduzir blocos reais da rede principal, além de alguns padrões adversários que acessam deliberadamente o estado frio. A compactação adiciona uma segunda camada aqui, já que um acerto em um pedaço compactado também compensa uma descompactação desse pedaço. Portanto, os números apresentados neste documento devem servir apenas como referência para a área ocupada pelo armazenamento, e não para o desempenho.

Fontesethresear

By victor

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *