Restic: Comparação de abordagens para lidar com o uso de memória de índice

Criado em 20 dez. 2019  ·  55Comentários  ·  Fonte: restic/restic

NOTA PARA REVISORES DE CÓDIGO / COLABORADORES PRINCIPAIS

Leia este comentário para obter um resumo das alterações e informações estruturadas sobre cada uma dessas alterações. Este pode ser um ponto de partida melhor do que ler todos os comentários desta edição.

Descreva o problema

Conforme declarado em #1988, o consumo de memória do índice é um dos maiores problemas com o consumo de memória do restic. O ponto principal é a implementação real do armazenamento de índice na memória em internal/repository/index.go

Gostaria, portanto, de iniciar uma nova edição para discutir estratégias que possam remediar o alto consumo de memória.

Como ponto de partida, abri um branch index-alternatives que implementa as seguintes estratégias:

  • Padrão: índice como realmente usado em restic
  • Recarregar: Não salve nada. Para qualquer operação de índice, recarregue o arquivo e use a implementação real para o índice criado temporariamente.
  • Índice de pouca memória: armazene apenas dados de índice completos para blobs de árvore. Para blobs de dados, use apenas um IDSet para salvar quais blobs de dados estão presentes. Isso permite a maioria das operações de índice. Para os ausentes, faça como em Reload (pelo menos para blobs de dados)
  • Bolt: Use um DB no disco via bbolt

A estratégia de índice pode ser escolhida por
restic -i <default|reload|low-mem|bolt>

A filial é considerada WIP. Eu adicionei uma interface geral FileIndex que deve ser preenchida por novas estratégias de índice. Além disso, a implementação real apenas carrega arquivos de índice (arquivo por arquivo) no padrão Index struct e depois os recarrega em uma nova estrutura de dados, permitindo que o GC limpe. Os arquivos de índice recém-criados não são afetados, portanto, deve haver apenas um efeito para repositórios com dados nele.

Observe que a maioria das implementações são rápidas e sujas e perdem tratamento de erros graves, limpeza etc.

Mais estratégias de índice podem ser facilmente adicionadas adicionando uma nova implementação para FileIndex e adicionando-as em repository.go (onde o carregamento ocorre) e cmd/restic/global.go (onde o sinalizador -i é interpretado)

Agradeço receber feedback sobre as estratégias de índice propostas.
Nesta tarefa, também gostaria de coletar boas configurações de teste onde diferentes estratégias de índice possam ser comparadas entre si.

optimization

Comentários muito úteis

@rawtaz Obrigado pela sua resposta!

Você está certo que durante o desenvolvimento eu tentei me mover rápido e ser capaz de testar o que as coisas funcionam e o que não funciona. Por enquanto, parece que a mudança de código está madura o suficiente para discutir como ela pode ser melhor integrada ao mestre.

Acho que ajuda a discussão dar uma visão geral das mudanças que fiz e as razões pelas quais fiz cada mudança. A implementação abrange as seguintes alterações:

Principais mudanças:

  • a estrutura da base de código, ou seja, qual funcionalidade reside em internal/repository e internal/index
  • estrutura de definições de interface em internal/restic
  • estrutura interna do MasterIndex incluindo o manuseio de arquivos de índice "completos"
  • a estrutura de dados relacionada ao arquivo de índice
  • suporte para arquivos de índice de formato antigo ( index_old.go )
  • a nova estrutura de dados de índice na memória ( index_new.go )
  • mudança de acesso ao índice quando necessário

Pequenas alterações adicionais:

  • mudou Lookup e adicionou LookupAll
  • substituiu 'Store' por StorePack EDIT: é implementado em #2773
  • adicione CheckSetKnown para lidar com blobs conhecidos durante o backup EDIT: é implementado em #2773
  • remova o blob IDSet de checker.go e substitua pela funcionalidade de índice
  • use a nova funcionalidade de índice em 'list blobs'

As principais mudanças são IMO a chave para alcançar baixo consumo de memória, bom desempenho e um design de código mais limpo (em comparação com a implementação real). As pequenas mudanças são vitórias rápidas que também podem ser PRs independentes. No entanto, se forem considerados bons aprimoramentos, prefiro mantê-los no PR principal, caso contrário, eles provavelmente precisarão ser implementados duas vezes ou adiados.

Estrutura da base de código

Mudei a maioria das funcionalidades relacionadas ao índice de internal/repository para repository/index . IMO isso torna a estrutura de base de código mais clara e também separa claramente qual código está em qual diretório (em oposição à implementação real.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura da interface

Também separei as definições de interface em internal/restic para que fique claro o que está relacionado a Index e o que Repository . Isso também permite alterações mais claras das interfaces no futuro.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura do MasterIndex

Atualmente, MasterIndex combina estruturas de dados relacionadas a arquivos de índice e geralmente itera sobre todas essas subestruturas para operações de índice. Cada uma das estruturas de dados relacionadas ao arquivo de índice pode estar completa (ou seja, cheia o suficiente para salvar como um arquivo de índice completo no repositório) ou concluída (o que significa que já está presente como arquivo de índice).

Mudei completamente esse comportamento. Agora o MasterIndex usa um índice principal na memória onde a maioria das entradas do índice deve estar presente - especialmente todas aquelas que também estão presentes como arquivos de índice no repositório - e uma segunda estrutura de dados "inacabada" que é usada apenas para salvar o novo índice entradas e é inserido no índice principal na memória assim que o conteúdo é gravado como arquivo de índice no repositório.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura de dados relacionada ao arquivo de índice

Atualmente, existem três definições de estrutura de dados relacionadas ao arquivo de índice: uma definida por JSON em internal/index , uma definida por JSON em internal/repository e uma otimizada para pesquisa em internal/repository .
Decidi usar a estrutura de dados JSON de internal/index e adicionei os métodos necessários para usá-la no MasterIndex (e removi completamente as implementações de internal/repository ). Isso remove completamente a conversão de estruturas de dados internas para salvar arquivos de índice durante o backup. Por outro lado, as operações de pesquisa precisam fazer um loop sobre todas as entradas de índice. Portanto, é fundamental que essa estrutura de dados não contenha muitas entradas durante a pesquisa, ou seja, é inserida regularmente no índice principal.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Suporte para formato de arquivo de índice antigo

O suporte para novos formatos de arquivo de índice agora é terceirizado para index_old_format.go .

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Nova estrutura de dados de índice na memória ( index_new.go )

Esta é obviamente a maior mudança. Em princípio, uma grande lista de blob é mantida por tipo de blob. A lista é classificada por ID de blob de forma que a pesquisa possa ser feita usando a pesquisa binária. Como a otimização visa muitas entradas nesta lista, o tamanho de cada entrada é minimizado o máximo possível. Isso significa que o ID do pacote é armazenado em uma tabela separada e apenas uma referência é salva e o deslocamento e o comprimento são uint32 (limitando efetivamente o tamanho máximo do pacote a 4 GB). EDIT: essas duas otimizações são implementadas em #2781.
Um array ordenado simples já economiza a sobrecarga de memória que está dentro de map mas ainda gera alguma sobrecarga de memória e desempenho dentro de append : Um "buffer" extra é reservado para anexos futuros e os dados precisam ser copiado. Por isso, implementei uma matriz "paginada" com uma sobrecarga de memória pequena e limitada e sem necessidade de copiar para anexar.

Os testes que foram feitos indicam que este índice é um pouco mais rápido que a implementação padrão do MasterIndex, economizando mais de 75% da memória.

Pergunta aos mantenedores: Essa implementação de índice atende às suas expectativas?

Alterado Lookup e adicionado LookupAll

Lookup na verdade retorna mais de um resultado (mas não se eles estiverem espalhados por muitos arquivos de índice, BTW). Em muitos casos, apenas o primeiro resultado é usado. Portanto, alterei Lookup para retornar apenas um resultado e adicionei LookupAll para ser usado quando mais de um resultado for usado. Torna o código um pouco mais claro e também economiza alguns ciclos de CPU desnecessários.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Substituído 'Loja' por StorePack

Store armazena apenas um blob no índice, mas é chamado apenas para um conteúdo de pacote completo. Portanto, mudei para uma funcionalidade StorePack onde todo o conteúdo do pacote é salvo. Isso torna o código mais claro, armazenando no formato JSON muito mais fácil e também com melhor desempenho.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

adicione CheckSetKnown para lidar com blobs conhecidos durante o backup

Os testes mostraram que para muitos novos arquivos backup usa bastante memória para armazenar quais blobs foram adicionados durante esta execução de backup. Isso é para não salvar blobs duas vezes por protetores paralelos. Movi essa funcionalidade para o índice, pois essas informações de "blob conhecido" podem ser removidas assim que a entrada correspondente for adicionada ao índice. Com essa alteração, apenas alguns blobs são "conhecidos", mas não (ainda) no índice.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Remova o blob IDSet de checker.go e substitua pela funcionalidade de índice

O Checker cria seu próprio índice na memória e, além disso, mantém todos os IDs de blob em um IDSet adicional. Como todos os blobs já são conhecidos do índice, isso pode ser substituído pelo método Each do índice.
Isso economiza memória adicional em check .

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Use a nova funcionalidade de índice em 'blobs de lista'

Em vez de ler todo o índice na memória e depois fazer um loop sobre todas as entradas para imprimir a lista de blobs, cada arquivo de índice pode ser carregado separadamente na memória e fazer um loop sobre esses conteúdos. Torna list blobs muito menos com fome de memória.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Todos 55 comentários

Eu gosto de começar com a configuração que usei para testar durante a implementação:

Eu criei dados de teste com o seguinte script

#!/bin/sh
mkdir data
for i in `seq 1 1000`; do
        echo $i
        mkdir data/$i
        for j in `seq 1 100`; do
                echo $i$j > data/$i/$j
        done
done

Isso cria 100.000 pequenos arquivos diferentes em 1.000 diretórios. Portanto, isso deve fornecer muitos blobs de dados e ainda alguns blobs de árvore. Pode ser alterado para simular ainda mais arquivos.

Eu executei o backup inicial

restic -r /path/to/repo init
restic -r /path/to/repo backup /path/to/data

e fez um backup repetido com dados inalterados para todas as estratégias de índice:

/usr/bin/time restic -r /path/to/repo -i <strategy> backup /path/to/data

além de medir com o tempo, também adicionei o perfil de memória.
Aqui estão os resultados:

Índice padrão

Files:           0 new,     0 changed, 100000 unmodified
Dirs:            0 new,     0 changed,     2 unmodified
Added to the repo: 0 B  

processed 100000 files, 567.676 KiB in 0:08
snapshot d7a3f88b saved
13.14user 1.71system 0:11.16elapsed 133%CPU (0avgtext+0avgdata 90564maxresident)k
0inputs+64outputs (0major+1756minor)pagefaults 0swaps

(pprof) top  
Showing nodes accounting for 15.21MB, 97.56% of 15.59MB total
Dropped 84 nodes (cum <= 0.08MB)
Showing top 10 nodes out of 59
      flat  flat%   sum%        cum   cum%
   12.52MB 80.26% 80.26%    12.52MB 80.26%  github.com/restic/restic/internal/repository.(*Index).store
    1.01MB  6.46% 86.72%     1.01MB  6.50%  github.com/restic/chunker.NewWithBoundaries
    0.39MB  2.52% 89.23%     0.39MB  2.52%  reflect.New
    0.36MB  2.29% 91.53%     0.37MB  2.34%  github.com/restic/restic/internal/restic.NodeFromFileInfo
    0.29MB  1.89% 93.41%     1.10MB  7.05%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
    0.28MB  1.80% 95.22%     0.28MB  1.80%  bytes.makeSlice
    0.15MB  0.94% 96.15%     0.15MB  0.94%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
    0.09MB   0.6% 96.76%     0.09MB   0.6%  strings.(*Builder).grow
    0.08MB   0.5% 97.26%     0.08MB   0.5%  github.com/restic/restic/internal/restic.BlobSet.Insert
    0.05MB   0.3% 97.56%     0.38MB  2.46%  encoding/json.Marshal

Aqui, o índice já é a maior parte da memória. Você também pode ver que os relatórios de tempo de 90 MB usados. Esta é a configuração do coletor de lixo IMO sobre a qual já foi falado.

Recarregar índice

Isso parece funcionar. No entanto, ele exibia um ETA de cerca de 20 minutos enquanto usava constantemente 100% da CPU ... Isso pode ser facilmente explicado, pois constantemente relê (e descriptografa!) os arquivos de índice.
No entanto, considero que essa estratégia de índice não merece um olhar mais profundo....

Índice de pouca memória

<snip: same output as above>
processed 100000 files, 567.676 KiB in 0:08
snapshot f8822cee saved
13.47user 2.03system 0:11.90elapsed 130%CPU (0avgtext+0avgdata 109584maxresident)k
0inputs+72outputs (0major+1945minor)pagefaults 0swaps

(pprof) top
Showing nodes accounting for 7.68MB, 94.71% of 8.11MB total
Dropped 88 nodes (cum <= 0.04MB)
Showing top 10 nodes out of 63
      flat  flat%   sum%        cum   cum%
    4.84MB 59.68% 59.68%     4.84MB 59.68%  github.com/restic/restic/internal/restic.IDSet.Insert
    1.01MB 12.50% 72.18%     1.02MB 12.58%  github.com/restic/chunker.NewWithBoundaries
    0.41MB  5.05% 77.23%     0.41MB  5.05%  reflect.New
    0.35MB  4.26% 81.48%     0.35MB  4.26%  github.com/restic/restic/internal/restic.NodeFromFileInfo
    0.28MB  3.47% 84.95%     0.28MB  3.47%  bytes.makeSlice
    0.27MB  3.31% 88.26%     1.01MB 12.46%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
    0.19MB  2.33% 90.60%     0.19MB  2.33%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
    0.18MB  2.22% 92.81%     4.94MB 60.93%  github.com/restic/restic/internal/repository.(*IndexLowMem).store
    0.08MB  0.96% 93.78%     0.08MB  0.96%  github.com/restic/restic/internal/restic.BlobSet.Insert
    0.08MB  0.93% 94.71%     0.40MB  4.92%  encoding/json.Marshal

A memória total relatada pelo criador de perfil é reduzida pela metade! Além disso, o conjunto de dados de índice de 12,5 MB é substituído por um IDSet de menos de 5 MB. Eu estimaria que com essa abordagem o requisito de memória de índice pode ser reduzido para cerca de 40% !
Trade-Off é um tempo total de backup ligeiramente maior (13,5s em comparação com 13,1s com índice padrão).
Estou realmente interessado no desempenho dessa estratégia de índice em outras configurações de teste!

Índice de parafuso - primeira execução

<snip: same output as above>
processed 100000 files, 567.676 KiB in 0:30
snapshot 9ff3d71f saved
37.01user 3.25system 0:34.54elapsed 116%CPU (0avgtext+0avgdata 282816maxresident)k
0inputs+114096outputs (0major+5436minor)pagefaults 0swaps

(pprof) top
Showing nodes accounting for 2592.31kB, 90.31% of 2870.50kB total
Dropped 79 nodes (cum <= 14.35kB)
Showing top 10 nodes out of 81
      flat  flat%   sum%        cum   cum%
 1030.83kB 35.91% 35.91%  1037.16kB 36.13%  github.com/restic/chunker.NewWithBoundaries
  390.02kB 13.59% 49.50%   390.02kB 13.59%  reflect.New
  278.60kB  9.71% 59.20%   278.60kB  9.71%  github.com/restic/restic/internal/restic.NodeFromFileInfo
  274.66kB  9.57% 68.77%   933.59kB 32.52%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
  244.05kB  8.50% 77.27%   244.05kB  8.50%  bytes.makeSlice
  169.81kB  5.92% 83.19%   169.81kB  5.92%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
      80kB  2.79% 85.98%       80kB  2.79%  github.com/restic/restic/internal/restic.BlobSet.Insert
   44.17kB  1.54% 87.52%   292.20kB 10.18%  github.com/restic/restic/internal/archiver.(*TreeSaver).save
   40.16kB  1.40% 88.91%    40.16kB  1.40%  strings.(*Builder).grow
      40kB  1.39% 90.31%       40kB  1.39%  github.com/restic/restic/internal/restic.NewBlobBuffer

Ele também cria um arquivo my.db de 72 MB onde o índice é salvo.

Como esperado, o consumo de memória basicamente desapareceu!
Isso, no entanto, vem com uma grande compensação no tempo de backup (37s em comparação com 13,1s com índice padrão).
Acho que seria interessante ver como isso funciona com conjuntos de dados maiores. Não há ideia se esse problema de desempenho apenas adiciona um fator de cerca de 3 ou está crescendo mais do que linear.
Além disso, parece-me que muito mais testes e ajustes devem ser considerados com estratégias de índice baseadas em armazenamento ...

Índice de parafuso - segunda execução

Como na configuração do bolt o db não é deletado após a execução, executei novamente com um db já presente:

processed 100000 files, 567.676 KiB in 0:08
snapshot b364fba4 saved
14.26user 1.94system 0:12.07elapsed 134%CPU (0avgtext+0avgdata 133464maxresident)k
0inputs+88outputs (0major+2601minor)pagefaults 0swaps

O perfil de memória é quase idêntico ao da primeira execução, conforme o esperado.
Eu realmente me pergunto por que esta segunda corrida é muito mais rápida que a primeira. É o cache do fs que está funcionando agora? Ou o boltdb de alguma forma usa as informações do índice já armazenado para acelerar as coisas?

Geralmente, isso pode indicar que, com o índice baseado em armazenamento, podemos ter que pensar em um índice armazenado permanente, porque ele é capaz de aumentar bastante o desempenho!

Talvez valha a pena tentar um armazenamento de chave/valor baseado em LSM também. Os LSMs têm desempenho e compensações de espaço de disco bastante diferentes em comparação aos BTrees e podem funcionar melhor para o caso de uso de índice persistente. Eu pessoalmente usaria o RocksDB , mas provavelmente também existem lojas nativas Go boas o suficiente.

Obrigado por prototipar isso! Parece muito encorajador.

Os arquivos de índice são criptografados no momento, portanto, qualquer armazenamento em disco que usamos também deve ser criptografado. Eu olhei https://github.com/dgraph-io/badger - é Go nativo, baseado em LSM e suporta criptografia.

Índice de memória baixo parece bom para backups. Não tenho certeza se ele será dimensionado para restaurações de repositórios com um grande número de blobs.

Obrigado pela discussão até agora.
Devo admitir que não gastei muito tempo tentando outras opções de índice baseadas em armazenamento. (texugo ainda está na minha lista de tarefas)
O motivo é o seguinte: vejo que haverá alguns problemas a surgir ao tentar seriamente trazer isso para o estado de nível de produção. Vejo problemas sobre criptografia, como limpar arquivos e como lidar com operações abortadas e assim por diante.

É por isso que comecei a pensar em outra possibilidade de otimizar o índice na memória. O resultado já está no meu branch index-alternative e chamado low-mem2. A ideia é armazenar todos os dados do índice em tabelas ordenadas (economizando a sobrecarga do mapa) e tentar não armazenar nada duplicado. O ID do pacote, por exemplo, é salvo em uma tabela separada e referenciado apenas na tabela principal.
Não fiz muitos testes até agora, mas parece que o desempenho é um pouco pior que o índice padrão. Por outro lado, o consumo de memória é comparável ao low-mem, que economiza cerca de 60% em comparação com o índice padrão na minha configuração de teste, veja acima.
Considerando que para a maioria das operações (por exemplo, backup) são necessárias apenas informações específicas do índice, isso pode ser melhorado ainda mais.

Então eu sugiro o seguinte: vou tentar fazer um PR para substituir (ou substituir opcionalmente) o índice padrão pelo otimizado para memória (low-mem2). Esperamos que isso dê uma melhoria rápida sobre o problema de memória.
As possibilidades de usar um índice baseado em disco estão abertas para o futuro.

Qual é a sua opinião sobre isso? Talvez alguns dos principais mantenedores possam me dar uma dica sobre a direção certa para trabalhar. Obrigada!

Cortar o uso de memória pela metade parece uma solução razoável de curto prazo para mim. Atualmente, estou um pouco preocupado com um possível impacto no desempenho, mantendo os dados em uma matriz classificada. A implementação atual do MasterIndex simplesmente itera sobre uma lista contendo os índices. Para repositórios maiores, com alguns milhares de arquivos de índice, restic gasta uma grande fração do tempo para backups/verificações com iteração nos diferentes índices, consulte #2284.

Em relação ao seu branch low-mem2: estou um pouco confuso com as funções SortByBlob e SortByPack. Não deveria ser possível apenas classificar por blobID e, em seguida, apenas iterar em todo o índice para pesquisas com base no packID (como é feito pela implementação atual)? (Minha preferência seria livrar-se completamente do bloqueio assim que um índice fosse concluído)
O código atualmente é apenas capaz de converter um índice existente para o novo formato. Seria muito bom se pudesse converter um índice finalizado para o formato mais eficiente, para obter o consumo de memória aprimorado também durante o backup inicial.

@MichaelEischer : Obrigado por apontar esse problema no masterindex. Até onde posso ver, existem três maneiras possíveis de usar o índice:

  1. ler a partir de arquivos de índice e, em seguida, somente leitura
  2. criado durante a execução e então somente leitura
  3. Operações para modificar o índice (isso é apenas limpeza/recuperação)

Como 1. é o caso de uso primário e 2. é bastante semelhante, concordo que devemos otimizar para isso. Eu proporia também alterar a maneira como o índice mestre está funcionando e permitir que uma estrutura de índice seja preenchida pelos dados de índice de muitos arquivos de índice que foram lidos (e também para os arquivos de índice criados e usados ​​apenas como somente leitura).

Sobre low-mem2:

  • Adicionei SortByBlob e SortByPack, pois há um caso de uso para pacotes classificados. Mas também concordo que devemos tentar desbloquear operações paralelas no índice. Até onde posso ver, a escolha de pesquisar por blob ou pesquisar por pacote é determinada pelo comando principal. Portanto, mesmo se bloquearmos para recorrer, seria eficientemente não bloqueante para a operação principal. E, claro, devemos usar o RWMutex.
    Talvez pesquisar por pacotes em uma lista ordenada por blob também esteja ok, só precisa de alguns testes ..
  • Sobre as considerações de desempenho. O fato de que restic no momento apenas lê linearmente todos os arquivos de índice me deixa bastante confiante de que uma lista classificada de blob de todos os arquivos de índice e pesquisa de bisseção pode até superar a implementação atual para índices grandes, ou seja, para muitos arquivos de índice ;-)

Então, como continuamos? Como já mencionado, posso preparar um PR para trocar a implementação do índice/masterindex atual por uma otimizada.
@MichaelEischer Gostaria de receber feedback antecipado e sugestões de melhoria - posso fornecer algumas partes iniciais do trabalho para revisão? Talvez encontremos também algumas pessoas testando as mudanças em relação ao desempenho e uso de memória para cenários da vida real...

@aawsome Desculpe a resposta tardia. Eu poderia revisar as primeiras partes do trabalho, no entanto, também seria melhor obter alguns comentários sobre o design geral dos mantenedores principais. Para testar, tenho um repositório de 8 TB com 40 milhões de arquivos em mãos; Para meus testes de desempenho de índice, usei até agora o comando check que estressa muito o índice.

O MasterIndex é usado para os casos 1 e 2. Mesclar todos os arquivos de índice na memória para um índice estático que seria suficiente para o caso 1 deve ser bastante fácil de implementar. Uma única lista ordenada definitivamente será mais rápida que o índice atual quando atingir uma certa quantidade de arquivos de índice O(log n) vs. O(n) . A adição dinâmica de arquivos de índice completos/finalizados (caso 2) não combina facilmente com uma única lista de blobs classificada, pois precisaria ser redefinida após cada índice adicionado. Recorrer totalmente a um índice com centenas de milhões de entradas várias vezes parece uma péssima ideia para mim. [EDIT] Apenas mesclar duas listas já ordenadas também funciona e basicamente equivale a uma cópia completa da lista existente. Com cem milhões de entradas, isso provavelmente ainda leva muito tempo [/EDIT] . Aumentar dinamicamente essa lista também exigiria alguma maneira de abrir espaço para novas entradas sem ter que manter uma cópia completa do buffer de lista antigo e novo na memória.

As alternativas para uma lista ordenada que me vêm à mente agora seriam algum tipo de árvore ou um mapa de hash. O primeiro provavelmente exigiria alguns ponteiros adicionais na estrutura de dados e o último seria bastante semelhante à implementação atual. Eu pensei em substituir o MasterIndex por um único hashmap grande, mas agora a estratégia de crescimento para o buffer de backup do hashmap me impediu de tentar: Go dobra o tamanho do buffer de backup quando um determinado fator de carga é atingido, o que no final pode exigir a manutenção temporária de um novo buffer de backup vazio junto com o antigo na memória. Esse cenário provavelmente exigiria muito mais memória do que a implementação atual. Isso deve ser resolvido usando dois níveis de hashmaps em que o primeiro é indexado usando os primeiros bits do ID do blob. Só não sei como lidar com eficiência com IDs de blob que não são distribuídos uniformemente e, portanto, ainda podem levar a grandes desequilíbrios nos hashmaps de segundo nível. Pode valer a pena procurar as estruturas de dados de índice usadas por bancos de dados ou borg-backup.

Caso 3 (prune/rebuild-index/find) é tratado por index/Index.New agora, então isso não deve colidir com alterações no MasterIndex.

ListPack métodos em Index/MasterIndex parecem código morto para mim, restic ainda compila quando eu removo esse método (apenas o teste Index precisa de alguns ajustes). Os únicos usos de um método ListPack que encontrei estão na estrutura Repository . Isso tornaria o SortByPack desnecessário e evitaria a complexidade associada ao recurso.

A pesquisa de pacotes em uma lista classificada por blob funcionaria, mas provavelmente um pouco mais lenta do que a implementação atual. A implementação atual itera sobre todos os índices e para quando um índice contendo os pacotes é encontrado, o que em média requer percorrer metade da lista. A verificação da lista classificada por blob sempre exigiria uma verificação completa. Por outro lado, a varredura deve ser mais rápida, de modo que o desempenho geral deve ser mais ou menos comparável.

@aawsome Então, agora, o foco das alterações seria otimizar o uso de memória do índice junto com o desempenho do masterIndex, enquanto adia as alterações fundamentais, como usar um banco de dados em disco para mais tarde?

Em relação ao caso 3: Pretende substituir também index/Index que é agrupado por packID? Meu entendimento atual do código restic é que esses seriam os únicos usuários em potencial de ListPack .
No momento, só consigo encontrar dois usos ativos de index/Index.Packs :

  • cmd_list/runList que só precisa de algum tipo de lista de blob e não se importa com a classificação
  • cmd_prune/pruneRepository que o usa para estatísticas, itera sobre todos os blobs para encontrar duplicatas e tomar a decisão de qual pacote reescrever ou remover. Apenas o último uso é um pouco mais complexo de evitar, pois exigiria alguns hashmaps adicionais para acompanhar os blobs usados ​​por pacote.

Sua solicitação de mesclagem "Otimizar índice" adicionaria outro uso que realmente requer um índice agrupado por arquivos de pacote.

Em relação a um MasterIndex otimizado: Meu último comentário analisou principalmente o uso de uma única estrutura de dados de índice grande que provavelmente precisaria usar o particionamento por chave para evitar grandes picos no uso de memória ao adicionar novas entradas. Essa maneira de particionar tem a desvantagem de que adicionar um novo índice provavelmente requer tocar em todas as partições. Além disso, desequilíbrios no tamanho das partições podem causar mais problemas.

No entanto, há também outra opção (que é ligeiramente inspirada em árvores de mesclagem estruturadas em log): Mantenha o número de entradas no MasterIndex aproximadamente constante em um valor c. Isso significaria que um novo índice após a finalização seria mesclado em uma das partes de índice existentes. Dessa forma, apenas uma parte do índice é modificada por vez. Para mapas de hash, isso pode ser usado para manter o fator de carga bastante alto, pois cada parte seria preenchida antes de usar a próxima. Para matrizes classificadas, alguma outra estratégia parece mais razoável: mesclar partes de índice em maiores com aproximadamente 1k, 2k, 4k, 8k, ... entradas enquanto apenas mescla a maior classe de tamanho atual quando existe uma certa constante de partes de índice com esse tamanho. Isso deve fornecer O(log n) complexidade para adicionar incrementalmente novos arquivos de índice. Dependendo da constante c, adicionar novos arquivos de índice é bastante barato (para pequenas partes de índice) ou a pesquisa é rápida (baixo número de partes de índice).

@aawsome Então, agora, o foco das alterações seria otimizar o uso de memória do índice junto com o desempenho do masterIndex, enquanto adia as alterações fundamentais, como usar um banco de dados em disco para mais tarde?

Sim, esse é o meu foco. Podemos discutir a adição de opções para o comando backup para salvar apenas blobs de árvore no índice (e, assim, salvar estupidamente cada novo blob no instantâneo) ou talvez carregar apenas os blobs de dados usados ​​no instantâneo anterior no índice (e, portanto, apenas ter deduplicação para blobs nos instantâneos anteriores). Isso permitiria que os clientes com pouca memória fizessem backup em grandes repositórios seguidos de operações de remoção em máquinas de grande memória para fazer a desduplicação completa.
Vou adiar meu trabalho no índice baseado em disco.

Em relação ao caso 3: Pretende substituir também index/Index que é agrupado por packID?

Eu acho que isso é possível e deve ser o objetivo da nova implementação. No entanto, não analisei em detalhes - obrigado por suas dicas!

Sobre os detalhes da implementação: Vou precisar de um pouco de tempo para ler e pensar sobre isso. Muito obrigado por suas idéias e apoio!

Conforme anunciado, preparei uma versão inicial do índice refatorado, veja refactor-index
Além disso, alguns internos são ligeiramente alterados para usar o novo índice. Por exemplo check agora deve exigir muito menos memória do que antes...

Ainda é WIP, mas quase todos os testes já passam e, pelos meus testes, parece muito bom.
Aqui estão os pontos abertos:

  • Corrigir testes para índice (esta parte ainda não compila)
  • refatorar rebuild-index e prune para usar o novo MasterIndex e o novo índice na memória
  • testes de performance

A implementação usa um array grande (classificado) para armazenar o índice e o formato IndexJSON para manter os itens criados na execução atual.
Ao fazer backup, o IndexJSON é armazenado regularmente no repositório e adicionado ao índice principal na memória.
Ao carregar o índice do repositório, cada arquivo de índice é carregado no formato IndexJSON e então adicionado ao índice principal na memória.

Portanto, a função crucial é AddJSONIndex em internal/index/index_new.go , começando na linha 278.

Sobre seus comentários @MichaelEischer Eu realmente não posso estimar os impactos de anexar à tabela indexável, veja a linha 329 em internal/index/index_new.go . Isso significa que toda a tabela deve ser copiada regularmente (você mencionou os "picos de memória")? Ou isso é bem tratado pelo gerenciamento de memória do go e do sistema operacional?

Além disso, depois de inserir a tabela precisa ser classificada novamente (pelo menos antes de pesquisá-la pela primeira vez). Acho que a implementação atual deve evitar operações de classificação desnecessárias, mas isso precisa de testes de desempenho. Alguém pode ajudar nesses testes?

@aawsome :

Alguém pode ajudar nesses testes?

Estou disposto a ajudar com testes de desempenho. O que exatamente você quer testar?

@dimejo :
Muito obrigado pela sua ajuda!

Eu preferiria comparações 1:1 (ou seja, a execução idêntica com ambas as ramificações) entre a ramificação mestre (ou última versão) e minha ramificação.
Estou interessado em diferenças sobre uso de memória e desempenho (tempo total e uso de CPU) para grandes repositórios (muitos arquivos, muitos instantâneos, etc). Usar /usr/bin/time -v está ok, talvez habilitar a criação de perfil (perfil de memória e perfil de CPU) possa ajudar a explicar melhor as diferenças.
O back-end não importa nada - o back-end local é bom, mas não é obrigatório.

Eu acho que os seguintes comandos devem pelo menos ser testados:

  • backup 1. execução inicial 2. outra execução com (quase) arquivos inalterados; 3. outra corrida com --force
  • check
  • restore
  • talvez mount
  • list blobs
  • cat blob
  • find --blob

[edit: lista adicionada, comandos cat e find]

Por favor, escreva se você não entender o que quero dizer ou se precisar de instruções mais detalhadas sobre como executar esses testes!

Vou tentar fazer todos os testes neste fim de semana, mas preciso de ajuda para habilitar o perfil. Não sou programador e seguir os conselhos da internet não deu muito certo :/

Você pode começar com
/usr/bin/time -v restic <command>

Para habilitar a criação de perfil, primeiro compile restic com a opção debug :
go run build.go --tags debug
e então use o sinalizador --mem-profile ou --cpu-profile , por exemplo:
restic --mem-profile . <command>

Isso cria um arquivo .pprof que pode ser analisado com go tool pprof , por exemplo:
go tool pprof -pdf mem.pprof restic
retorna um bom pdf.

Impressionante! Achei que seria muito mais complexo habilitar a criação de perfil. Vai experimentar e reportar.

@aawsome Obrigado pelo seu trabalho, são realmente muitas mudanças.

Fiz alguns testes com o comando check e algo estranho acontece. Usando o mestre restic, o comando é concluído em 6 minutos (327,25 real 613,08 usuário 208,48 sys), com as alterações de índice leva mais de uma hora (4516,77 real 896,17 usuário 531,71 sys). O tempo é gasto em algum lugar depois de imprimir "verificar instantâneos, árvores e bolhas", mas ainda não tive tempo de dar uma olhada mais de perto.

Na minha opinião, seu commit com as mudanças de índice é muito grande para ser revisto, mil linhas de código adicionado junto com duas mil exclusões é realmente muito.
Eu notei pelo menos quatro mudanças individuais:

  • Index.Lookup não retorna mais uma lista, mas apenas um blob
  • Muito código de manipulação de índice é extraído do Repositório
  • O masterIndex mantém apenas um único índice inacabado
  • O índice real de memória baixa

Eu tenho alguns comentários (incompletos) sobre o próprio código, mas ainda não pensei muito sobre a arquitetura geral do código:

struct IndexLowMem2.byFile : Levei algum tempo para entender o nome da variável. Por favor, altere para byPackFile ou similar para ser mais descritivo.

IndexLowMem2/FindPack : Meu entendimento de restic.IDs.Find é que ele espera uma lista ordenada de IDs. No entanto, idx.packTable não parece ser classificado, pois isso quebraria o packIndex em blobWithoutType .

Condição de corrida em index_new/FindBlob : O índice pode ser recorrido entre a chamada para SortByBlob e readquirir o bloqueio de leitura. Atualmente não vejo uma boa maneira de como isso pode ser corrigido ao usar um RWMutex. O mesmo problema também existe em FindBlobByPack .
IndexLowMem2/Lookup : A chamada para FindBlob e acessar o blobTable não é atômica.

IndexLowMem2/AddJSONIndex : Chamadas para anexar uma fatia alocam um novo buffer quando o antigo é muito pequeno e, em seguida, copiam todos os dados para o novo buffer. Como o método primeiro coleta as novas entradas packTable/indexTable e as anexa de uma só vez, isso não deve ser muito ineficiente.
As fatias packTable em idx.byFile , portanto, apontarão para versões antigas do array packTable quando append aloca um novo buffer. Isso provavelmente desperdiçará muita memória para grandes packCounts . Você pode simplesmente adicionar um endPackIndex a FileInfo além de startPackIndex e então acessar diretamente o packTable .
Anexar a it.blobTable parece um pouco caro, pois isso requer classificação mais tarde. Deve ser possível pré-classificar as novas entradas table e, em seguida, apenas mesclar as duas listas classificadas (no entanto, mesclar no local pode ser um pouco complicado).

MasterIndex.Lookup/Has/Count/... : Após a pesquisa em mi.mainIndex , mi.idx pode ser salvo e adicionado ao mainIndex antes que o idxMutex seja adquirido.

MasterIndex.Save : Isso parece bloquear o masterIndex enquanto o upload do índice está pendente. A implementação antiga em Repository.SaveIndex parece funcionar sem bloqueio.

Ah, eu encontrei o problema: agora a chamada para PrepareCache está comentada. PrepareCache também configura a função PerformReadahead do Cache. Sem isso, o Check precisa carregar cada blob de dados de árvore separadamente do back-end.

@MichaelEischer : Muito obrigado por fazer seus comentários valiosos sobre este estágio inicial de implementação :+1:

Eu sei que é muita mudança de código - no entanto, acho que também é uma simplificação da implementação do índice atual. Daí o grande número de arquivos/linhas de código excluídos.

Desculpe pelo problema PrepareCache - lembro-me de ter comentado para lidar com isso mais tarde, mas até agora só fiz testes locais de back-end e, portanto, o problema não surgiu.

Sobre seus comentários corretos sobre o material byPack : Você está certo, isso é WIP e ainda não foi usado. Deixei porque pretendia usá-lo mais tarde por rebuild-index e prune . No entanto, vou me concentrar em fazer o caso de uso principal funcionar - talvez eu remova essa parte antes de fazer um PR.

Sobre a ordenação: No momento, o código deve fazer a ordenação apenas quando necessário, ou seja, na primeira chamada Lookup ou Has . Isso significa que ao carregar muitos arquivos de índice no índice na memória (que é a operação usual no início), nenhuma classificação ocorre. Isso só acontece quando todos os arquivos de índice já estão carregados. Durante o backup - quando mais arquivos de índice são gravados - as entradas são adicionadas ao índice na memória e sempre que um recurso é necessário. No entanto, este é um recurso de uma lista principalmente classificada. Eu não sei como sort.Sort lida com listas quase ordenadas, mas geralmente são usados ​​algoritmos de classificação que são muito eficientes para listas quase ordenadas.

Eu cuidarei dos outros problemas - obrigado especialmente por encontrar as condições de corrida!

Na minha opinião, seu commit com as mudanças de índice é muito grande para ser revisto, mil linhas de código adicionado junto com duas mil exclusões é realmente muito.

Isto.

Eu sei que é muita mudança de código - no entanto, acho que também é uma simplificação da implementação do índice atual. Daí o grande número de arquivos/linhas de código excluídos.

É quando você deve dividir suas alterações em PRs individuais separados, para que seja gerenciável e com uma intenção clara.

@rawtaz :
A intenção clara é um redesenho do índice usado em restic para se livrar dos problemas com a implementação atual.

IMO, a mudança não é tão grande, considerando que é uma reimplementação completa do índice (e observe: também reimplementando algumas coisas herdadas, veja index/index_old_format.go ).

No entanto, isso é WIP e tem alguns problemas que estou disposto a corrigir antes de pensar em como melhor integrar esse trabalho ao branch master.
Estou ansioso para obter ideias de como dividi-lo em partes menores - você pode me dar instruções sobre quais mudanças podem ser aceitas como PRs?

Fiz alguns testes com o comando check e algo estranho acontece. Usando o mestre restic, o comando é concluído em 6 minutos (327,25 real 613,08 usuário 208,48 sys), com as alterações de índice leva mais de uma hora (4516,77 real 896,17 usuário 531,71 sys). O tempo é gasto em algum lugar depois de imprimir "verificar instantâneos, árvores e bolhas", mas ainda não tive tempo de dar uma olhada mais de perto.

Isso deve ser corrigido agora.

struct IndexLowMem2.byFile : Levei algum tempo para entender o nome da variável. Por favor, altere para byPackFile ou similar para ser mais descritivo.

Está alterado agora.

IndexLowMem2/FindPack : Meu entendimento de restic.IDs.Find é que ele espera uma lista ordenada de IDs. No entanto, idx.packTable não parece ser classificado, pois isso quebraria o packIndex em blobWithoutType .

Toda a lógica do byPack não foi usada e você apontou corretamente que também havia alguns erros. Limpei este código morto.

Condição de corrida em index_new/FindBlob : O índice pode ser recorrido entre a chamada para SortByBlob e readquirir o bloqueio de leitura. Atualmente não vejo uma boa maneira de como isso pode ser corrigido ao usar um RWMutex. O mesmo problema também existe em FindBlobByPack .

Espero ter encontrado uma boa maneira de lidar com isso. Eu basicamente testo sortedByBlob duas vezes se estiver definido como false. Este problema, portanto, deve ser corrigido agora ..

IndexLowMem2/Lookup : A chamada para FindBlob e acessar o blobTable não é atômica.

Isso agora é feito usando o novo indexTable.Get atômico.

As fatias packTable em idx.byFile , portanto, apontarão para versões antigas do array packTable quando append aloca um novo buffer. Isso provavelmente desperdiçará muita memória para grandes packCounts . Você pode simplesmente adicionar um endPackIndex a FileInfo além de startPackIndex e então acessar diretamente o packTable .

Está corrigido agora. packTable até agora não foi usado...

MasterIndex.Lookup/Has/Count/... : Após a pesquisa em mi.mainIndex , mi.idx pode ser salvo e adicionado ao mainIndex antes que o idxMutex seja adquirido.

Isso está corrigido agora.

MasterIndex.Save : Isso parece bloquear o masterIndex enquanto o upload do índice está pendente. A implementação antiga em Repository.SaveIndex parece funcionar sem bloqueio.

Esta questão ainda está em aberto. Eu não encontrei uma boa maneira de lidar com isso com apenas um índice inacabado.
Também não sei o impacto disso em relação ao desempenho. Deve afetar apenas backup . Espero ver alguns testes se isso for relevante..

Fiz alguns testes com o comando check e algo estranho acontece. Usando o mestre restic, o comando é concluído em 6 minutos (327,25 real 613,08 usuário 208,48 sys), com as alterações de índice leva mais de uma hora (4516,77 real 896,17 usuário 531,71 sys). O tempo é gasto em algum lugar depois de imprimir "verificar instantâneos, árvores e bolhas", mas ainda não tive tempo de dar uma olhada mais de perto.

Isso deve ser corrigido agora.

Vou tentar nos próximos dias.

Condição de corrida em index_new/FindBlob : O índice pode ser recorrido entre a chamada para SortByBlob e a readquirição do bloqueio de leitura. Atualmente não vejo uma boa maneira de como isso pode ser corrigido ao usar um RWMutex. O mesmo problema também existe em FindBlobByPack .

Espero ter encontrado uma boa maneira de lidar com isso. Eu basicamente testo sortedByBlob duas vezes se estiver definido como falso. Este problema, portanto, deve ser corrigido agora ..

Isso ainda deixa a possibilidade de uma colisão entre SortByBlob e AddJSONIndex . A verificação de que o índice está classificado corretamente e a pesquisa de índice real devem ocorrer na mesma seção crítica. Infelizmente, o RWMutex não suporta um downgrade atômico de um bloqueio de gravação para um bloqueio de leitura, que seria a maneira mais fácil de manter o bloqueio após a classificação. Atualmente vejo duas possibilidades:

  • Pegue o readlock, verifique se o índice está ordenado e execute a busca. Se o índice não estiver classificado, libere o readlock e obtenha o writelock, classifique o índice e complete apenas essa pesquisa enquanto mantém o writelock.
  • Pegue o readlock, verifique se o índice está ordenado e execute a busca. Se o índice não estiver classificado, libere o readlock e obtenha o writelock, classifique o índice e tente novamente desde o início.

MasterIndex.Lookup/Has/Count/... : Após a pesquisa em mi.mainIndex , mi.idx pode ser salvo e adicionado ao mainIndex antes que o idxMutex seja adquirido.

Parece que você perdeu MasterIndex.Lookup .

MasterIndex.Save : Isso parece bloquear o masterIndex enquanto o upload do índice está pendente. A implementação antiga em Repository.SaveIndex parece funcionar sem bloqueio.

Esta questão ainda está em aberto. Eu não encontrei uma boa maneira de lidar com isso com apenas um índice inacabado.
Também não sei o impacto disso em relação ao desempenho. Deve afetar apenas backup . Espero ver alguns testes se isso for relevante..

Hmm, o principal bloqueador aqui é que você não pode obter o ID do pacote de índice antes que a chamada de upload seja concluída. Seria bastante fácil de resolver permitindo que vários índices fossem inacabados enquanto apenas o último pudesse ser gravado e os predecessores estivessem sendo carregados no momento.

Isso ainda deixa a possibilidade de uma colisão entre SortByBlob e AddJSONIndex . A verificação de que o índice está classificado corretamente e a pesquisa de índice real devem ocorrer na mesma seção crítica. Infelizmente, o RWMutex não suporta um downgrade atômico de um bloqueio de gravação para um bloqueio de leitura, que seria a maneira mais fácil de manter o bloqueio após a classificação. Atualmente vejo duas possibilidades:

* Take the readlock, check that the index is sorted and run the search. If the index is not sorted, release the readlock and get the writelock, sort the index and complete just that search while holding the writelock.

* Take the readlock, check that the index is sorted and run the search. If the index is not sorted, release the readlock and get the writelock, sort the index and retry from the start.

Ops - Na verdade, eu estava tão focado em SortByBlobs que não considerei os métodos que são chamados :-(
Obrigado por suas sugestões! Eu usei a segunda opção agora.

Parece que você perdeu MasterIndex.Lookup .

Está corrigido agora.

Hmm, o principal bloqueador aqui é que você não pode obter o ID do pacote de índice antes que a chamada de upload seja concluída. Seria bastante fácil de resolver permitindo que vários índices fossem inacabados enquanto apenas o último pudesse ser gravado e os predecessores estivessem sendo carregados no momento.

Eu também consertei isso. Agora eu codifico/descriptografo o índice e o escrevo no índice principal enquanto bloqueado, mas escrevo o arquivo no back-end depois de liberar o bloqueio.

Além disso, encontrei outra otimização que reduz bastante o consumo de memória (e também corrige o problema de anexação): troquei o grande array por uma estrutura que implementa uma lista "paginada" de blobs. Aqui é mantida uma lista de "páginas" e se for necessário ampliá-la, apenas uma nova página é alocada. Isso permite anexar sem precisar copiar todos os elementos antigos. Além disso, a sobrecarga é fixa e o tamanho máximo da página, enquanto que com o acréscimo você obtém uma sobrecarga bastante grande (o array obtém uma "reserva de capacidade" crescente para garantir que não precise recopiar com muita frequência). Essa otimização parece economizar mais ~15-20% de memória.

Fico feliz em receber feedback e ainda mais gostaria de ver testes de desempenho com grandes repositórios,,

Aqui estão os resultados da minha primeira comparação.
Usei um diretório de dados com 550.000 arquivos (resulta em 550.000 blobs de dados) usando o script

#!/bin/sh
mkdir data
for i in `seq 1 550`; do
        echo $i
        mkdir data/$i
        for j in `seq 1 1000`; do
                echo $i$j > data/$i/$j
        done
done

Então eu fiz backup desse diretório de dados três vezes e executei check depois:

/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data
/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data
/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data -f
/usr/bin/time/restic -r /path/to/repo --mem-profile . check

Essas etapas eu executo uma vez com o branch master (compilado para restic.old ) e meu branch (compilado para restic.new ).
Acabei de usar meu laptop antigo com SSD local.

Aqui estão os resultados:
time_backup1_old.txt
mem_backup1_old.pdf
time_backup1_new.txt
mem_backup1_new.pdf

time_backup2_old.txt
mem_backup2_old.pdf
time_backup2_new.txt
mem_backup2_new.pdf

time_backup3_old.txt
mem_backup2_old.pdf
time_backup3_new.txt
mem_backup3_new.pdf

time_check_old.txt
mem_check_old.pdf
time_check_new.txt
mem_check_new.pdf

Para resumir:

  • A nova implementação de índice usa apenas 22% da memória usada pela implementação de índice im master (!!!)
  • com check o uso de memória relacionado ao índice é ainda mais reduzido (o IDSet foi removido)
  • Com a nova implementação, o uso de memória de índice não é mais o único consumidor de memória excepcional, fazendo com que as otimizações de outras partes valham a pena.
  • A velocidade e o uso da CPU foram comparáveis. O único caso em que a nova implementação foi um pouco mais lenta foi a terceira execução de backup que usa muito pesquisas de índice. No entanto, estamos falando de um repositório recém-gerado com apenas alguns arquivos de índice. Portanto, a principal desvantagem da implementação do índice master (percorrendo todos os arquivos de índice) não desempenhou um grande papel aqui. Acredito que as coisas mudem completamente com muitos arquivos de índice e que a nova implementação superará claramente a atual.

Finalmente terminei meus testes. Demorou mais do que o estimado porque tive que refazer meus testes por causa de alguns resultados misteriosos. Eu queria comparar os resultados entre 1 e vários núcleos. Curiosamente, a VM com apenas 1 vCPU foi muito mais rápida em quase todas as operações. Talvez @fd0 saiba o que está acontecendo.

resultados para VM1

1 vCPU
2 GB de RAM

Observe que executei 1 CPU e 1 perfil de memória para cada versão restic. O tempo mostrado (em segundos) nesta tabela é a média para ambas as execuções.

| | restic v.0.9.6 | restic novo-índice | diferença |
| :--- | ---: | ---: | ---: |
| init repositório | 2,24 | 2,27 | 1,12% |
| 1º reforço | 1574,36 | 1542,80 | -2,00% |
| 2º reforço | 556,95 | 541,71 | -2,74% |
| 3º backup (--force) | 1192,90 | 1195,53 | 0,22% |
| esqueça | 0,51 | 0,52 | 2,97% |
| podar | 43,87 | 44,02 | 0,34% |
| verificar | 30,77 | 31,59 | 2,68% |
| lista de bolhas | 2,92 | 3,36 | 14,90% |
| bolha de gato | 2,58 | 2,60 | 0,97% |
| encontrar blob | 22,86 | 21,17 | -7,39% |
| restaurar | 895,01 | 883,57 | -1,28% |

resultados para VM8

8 vCPU
32 GB de RAM

Observe que executei 1 CPU e 1 perfil de memória para cada versão restic. O tempo mostrado (em segundos) nesta tabela é a média para ambas as execuções.

| | restic v.0.9.6 | restic novo-índice | diferença |
| :--- | ---: | ---: | ---: |
| init repositório | 2,11 | 2,09 | -0,95% |
| 1º reforço | 1894,47 | 1832,65 | -3,26% |
| 2º reforço | 827,95 | 776,38 | -6,23% |
| 3º backup (--force) | 1414,60 | 1411,98 | -0,19% |
| esqueça | 0,60 | 0,56 | -6,72% |
| podar | 93,10 | 89,84 | -3,50% |
| verificar | 70,22 | 68,44 | -2,53% |
| lista de bolhas | 3,86 | 7,24 | 87,68% |
| bolha de gato | 3,88 | 3,69 | -4,90% |
| encontrar blob | 30,95 | 29,11 | -5,95% |
| restaurar | 1150,49 | 1089,66 | -5,29% |

Informação adicional

$ uname -r
5.4.15-200.fc31.x86_64
$restic-orig version
debug enabled
restic 0.9.6 (v0.9.6-40-gd70a4a93) compiled with go1.13.6 on linux/amd64
$ restic-newindex version
debug enabled
restic 0.9.6 (v0.9.6-43-g5db7c80f) compiled with go1.13.6 on linux/amd64

arquivos alterados adicionados para backup 1:

Files:       487211 new,     0 changed,     0 unmodified
Dirs:            2 new,     0 changed,     0 unmodified
Added to the repo: 62.069 GiB

arquivos alterados adicionados para backup 2:

Files:       166940 new,     0 changed, 487211 unmodified
Dirs:            0 new,     2 changed,     0 unmodified
Added to the repo: 6.805 GiB

arquivos alterados adicionados para backup 3:

Files:       654215 new,     0 changed,     0 unmodified
Dirs:            2 new,     0 changed,     0 unmodified
Added to the repo: 4.029 MiB

Histórico

logs_vm1_cpu.zip
logs_vm1_mem.zip
logs_vm8_cpu.zip
logs_vm8_mem.zip

@dimejo Muito obrigado por seus testes!

Primeiro em relação à sua pergunta sobre o aumento do tempo para a configuração de 8 CPUs: Você está comparando o "tempo do usuário" que é o total de segundos de CPU usados. Com o processamento paralelo, isso é (e deve ser maior) do que usar uma única CPU, pois sempre há alguma sobrecarga devido à paralelização. Na verdade, suas 8 execuções de CPU foram muito mais rápidas, veja o tempo decorrido.

Você usou o commit 5db7c80f para seus testes newindex. É possível que você teste novamente com o último commit 26ec33dd ? Deve haver outra diminuição no uso de memória e (se eu fiz tudo certo) talvez até melhor desempenho.

Em geral estou bastante satisfeito com estes resultados! Parece que minha implementação está usando muito menos memória enquanto ainda tem desempenho semelhante à implementação do índice original. (BTW: vou corrigir o problema obviamente aberto com list blobs )

Primeiro em relação à sua pergunta sobre o aumento do tempo para a configuração de 8 CPUs: Você está comparando o "tempo do usuário" que é o total de segundos de CPU usados. Com o processamento paralelo, isso é (e deve ser maior) do que usar uma única CPU, pois sempre há alguma sobrecarga devido à paralelização. Na verdade, suas 8 execuções de CPU foram muito mais rápidas, veja o tempo decorrido.

Obrigada pelo esclarecimento. Estúpido eu nem vi o tempo decorrido...

Você usou o commit 5db7c80f para seus testes de newindex. É possível que você teste novamente com o último commit 26ec33dd? Deve haver outra diminuição no uso de memória e (se eu fiz tudo certo) talvez até melhor desempenho.

Certo.

Resultados

Tempo decorrido

Observe que listei o tempo decorrido (em mm:ss) agora. Novamente mostrando o tempo médio para ambas as execuções.

| | v0.9.6 | novoíndice | newindex2 | diferença (v0.9.6 e newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| init repositório | 00:42,4 | 01:11,6 | 00:21,5 | -49,27% |
| 1º reforço | 26:42,7 | 26:58,2 | 31:00,4 | +16,08% |
| 2º reforço | 10:18,9 | 09:54,1 | 09:03,7 | -12,14% |
| 3º backup (--force) | 17:32,6 | 17:05,8 | 20:37,1 | +17,52% |
| esqueça | 00:00,8 | 00:00,9 | 00:00,8 | +0,61% |
| podar | 01:02,5 | 00:56,5 | 00:51,2 | -18,16% |
| verificar | 00:31,6 | 00:30,8 | 00:31,0 | -1,74% |
| lista de bolhas | 00:05,1 | 00:06,2 | 00:05,6 | +9,44% |
| bolha de gato | 00:02,2 | 00:02,3 | 00:02,1 | -3,39% |
| encontrar blob | 00:28,5 | 00:28,2 | 00:23,0 | -19,28% |
| restaurar | 13:02,0 | 12:23,3 | 14:09,1 | +8,58% |

uso de memória

Esta tabela mostra o uso total de memória (em MB) dos arquivos pprof. Espero que seja o correto.

| | v0.9.6 | novoíndice | newindex2 | diferença (v0.9.6 e newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| init repositório | 32,01 | 32,01 | 32,00 | -0,03% |
| 1º reforço | 227,50 | 178,48 | 174,77 | -23,18% |
| 2º reforço | 227,89 | 157,92 | 149,52 | -34,39% |
| 3º backup (--force) | 204,53 | 147,15 | 136,06 | -33,48% |
| esqueça | 32,25 | 32,09 | 32,26 | +0,03% |
| podar | 167,55 | 108,96 | 130,60 | -22,05% |
| verificar | 163,29 | 75,06 | 70,34 | -56,92% |
| lista de bolhas | 33,22 | 30,21 | 25,47 | -23,33% |
| bolha de gato | 113,79 | 48,86 | 32,23 | -71,68% |
| encontrar blob | 79,48 | 30,40 | 25,64 | -67,74% |
| restaurar | 226,08 | 176,54 | 198,19 | -12,33% |

Informação adicional

8 vCPU
32GB RAM
$ restic-newindex2 version
debug enabled
restic 0.9.6 (v0.9.6-44-g26ec33dd) compiled with go1.13.6 on linux/amd64

Histórico

logs_test3.zip

@aawsome : Ainda tenho problemas com o comando check não armazenando em cache os blobs da árvore. O código antigo chamado Repository.SetIndex que também acionou as chamadas para PrepareCache. Agora a chamada para SetIndex foi removida, no entanto, não consegui encontrar um substituto para a chamada para PrepareCache.

@dimejo Muito obrigado pelos testes! Isso fornece informações muito boas e os resultados em relação ao consumo de memória já parecem muito bons!

Tentei entender os resultados de consumo de memória para v0.9.6, 3º backup, mas não consegui reproduzir seus resultados. Pode ser que você tenha usado o consumo de memória do índice (102 MB) em vez da memória total (204 MB)?

Eu também não entendo por que newindex2 mostra maior consumo de memória do que newindex para prune e restore . No perfil de memória você pode ver que a memória usada pelo índice é menor e o maior consumo de memória se deve a encoding/json.Marshal e restic.NewBlobBuffer em prune e restorer.(*packCache).get em restauração (que basicamente não estão presentes no perfil da v0.9.6). Além disso, o consumo de memória mostrado por time é menor para newindex2 do que para newindex.
Portanto, acredito que newindex2 também tenha menor consumo de memória para esses dois comandos do que newindex, mas não entendo por que a criação de perfil mostra essas partes extras (não relacionadas ao índice) consumindo memória.

Sobre o tempo total: estou um pouco irritado que newindex e newindex2 diferem tanto e que as diferenças são às vezes positivas e às vezes negativas. Tentei ver os perfis de CPU de algumas execuções e não vi nenhum uso de CPU relacionado ao índice. Por exemplo, com as três execuções de backup, as partes que consomem CPU em todas as execuções (v0.9.6 e newindex2) são totalmente dominadas por sha256, chunker, syscall e runtime e não vejo por que deve haver diferenças devido a uma implementação de índice diferente.
Pode ser que a máquina não tenha potência de CPU constante e as diferenças não sejam causadas por diferentes implementações, mas por diferentes circunstâncias da máquina? Quando você comparou v0.9.6 e newindex, os resultados não variaram muito e, pelo que parece, essas duas execuções foram feitas aproximadamente ao mesmo tempo.
Com outras palavras: é possível que você execute novamente a medição de cpu para v0.9.6 e newindex2 ao mesmo tempo? Em seguida, poderíamos verificar se a variação nos tempos de CPU é comum e talvez obter melhores insights sobre as alterações de desempenho devido à implementação alterada.

@MichaelEischer Obrigado por relatar esse problema ainda não corrigido e desculpe por não funcionar até agora.
A chamada para PrepareCache está em internal/index/masterindex.go:Load que é chamada por internal/repository/repository.go:LoadIndex .
Vou ter que depurar essa parte do código.

Apenas deixando outros resultados ad-hoc da ramificação com um repositório grande (~ 500 GB / 1500 + instantâneos) + backup pequeno (1,3 GB). Muito promissor :+1:
default-restic.txt
patched.txt

@MichaelEischer Encontrei o problema de não usar o cache com o comando check : Como a verificação reconstrói a funcionalidade de masterindex.Load() o PrepareCache precisa ser chamado explicitamente (estava em repository.UseIndex() ) Isso foi corrigido agora.
Também corrigi o problema de desempenho com list blobs

@seqizz Obrigado por testar! Estou bastante satisfeito com o consumo de memória :smile:
Você tem uma explicação por que o tempo decorrido na execução corrigida é muito maior do que na execução de descanso padrão? Eu não posso explicar esse comportamento pelas mudanças de índice. Você viu a paralelização reduzida devido à implementação diferente ou este é um efeito que pode ser explicado de dentro do sistema que você usou?

BTW: os tempos totais de CPU parecem ser comparáveis ​​entre as duas execuções. Isto é exatamente o que eu estava esperando.

@aawsome Eu diria que este foi o efeito colateral do teste impróprio por mim. Mesmo que eu forget os instantâneos criados entre as execuções, parece que foi lento porque executei a versão corrigida primeiro. Talvez o cache do SO em nosso armazenamento (Minio) tenha afetado o tempo de resposta.

Agora, como uma execução de controle, criei o restic upstream (em vez da versão de lançamento anterior, já que construí sua ramificação no upstream) e executei essa versão upstream primeiro. Mostra que você pode desconsiderar os tempos decorridos, pois depende do pedido.

restic-upstream.txt
restic-patched.txt

Tentei entender os resultados de consumo de memória para v0.9.6, 3º backup, mas não consegui reproduzir seus resultados. Pode ser que você tenha usado o consumo de memória do índice (102 MB) em vez da memória total (204 MB)?

Boa captura - provavelmente um erro de cópia e passado. Já está corrigido no meu post acima.

Também não entendo por que newindex2 mostra maior consumo de memória do que newindex para remoção e restauração. No perfil de memória você pode ver que a memória usada pelo índice é menor e o maior consumo de memória é devido a encoding/json.Marshal e restic.NewBlobBuffer em pruneand restorer.(*packCache).get em restore (que basicamente não são presente no perfil para v0.9.6).
Portanto, acredito que newindex2 também tenha menor consumo de memória para esses dois comandos do que newindex, mas não entendo por que a criação de perfil mostra essas partes extras (não relacionadas ao índice) consumindo memória.

TBH, não tenho ideia do que aconteceu lá. Tentei reproduzir os resultados na mesma VM, mas não consegui.

Além disso, o consumo de memória mostrado por tempo é menor para newindex2 do que para newindex.

Eu comparei os resultados mostrados por tempo de todas as 4. execuções do meu teste (veja abaixo), e eles não parecem ser muito confiáveis. Para alguns testes, o consumo de memória entre a execução de perfis de CPU e memória varia de 15 a 20%.

restic-orig_test4_cpu_findblob: 253,12MB
restic-orig_test4_mem_findblob: 307,44MB
restic-newindex2_test4_cpu_catblob: 184,12MB
restic-newindex2_test4_mem_catblob: 216,56MB
restic-newindex2_test4_cpu_findblob: 174,81MB
restic-newindex2_test4_mem_findblob: 202,38MB

Pode ser que a máquina não tenha potência de CPU constante e as diferenças não sejam causadas por diferentes implementações, mas por diferentes circunstâncias da máquina? Quando você comparou v0.9.6 e newindex, os resultados não variaram muito e, pelo que parece, essas duas execuções foram feitas aproximadamente ao mesmo tempo.
Com outras palavras: é possível que você execute novamente a medição de cpu para v0.9.6 e newindex2 ao mesmo tempo? Em seguida, poderíamos verificar se a variação nos tempos de CPU é comum e talvez obter melhores insights sobre as alterações de desempenho devido à implementação alterada.

A VM que usei não possui núcleos dedicados. Especialmente com 8 núcleos, restic não está nem perto de usar toda a energia da CPU disponível. É por isso que eu pensei que não importaria muito para os meus testes. Já repeti o teste com CPUs dedicadas e os resultados são muito mais uniformes. Parece que foi isso que causou a discrepância.

Resultados

tempo decorrido (em mm:ss)

| | v0.9.6 | novoíndice | newindex2 | diferença (v0.9.6 e newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| init repositório | 00:26,2 | 00:48,7 | 00:58,4 | +122,79% |
| 1º reforço | 32:51,8 | 32:12,1 | 32:49,0 | -0,14% |
| 2º reforço | 08:40,2 | 08:31,2 | 08:30,3 | -1,90% |
| 3º backup (--force) | 21:40,5 | 21:43,6 | 21:41,0 | +0,04% |
| esqueça | 00:00,8 | 00:00,7 | 00:00,8 | -7,98% |
| podar | 00:43,7 | 00:43,8 | 00:42,2 | -3,56% |
| verificar | 00:26,3 | 00:26,9 | 00:26,4 | +0,30% |
| lista de bolhas | 00:03,5 | 00:04,4 | 00:04,6 | +29,46% |
| bolha de gato | 00:01,7 | 00:01,8 | 00:01,8 | +9,04% |
| encontrar blob | 00:17,7 | 00:17,3 | 00:16,7 | -5,95% |
| restaurar | 12:40,2 | 13:20,3 | 12:24,1 | -2,12% |

uso de memória (em MB)

| | v0.9.6 | novoíndice | newindex2 | diferença (v0.9.6 e newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| init repositório | 32,05 | 32,02 | 32,02 | -0,09% |
| 1º reforço | 242,38 | 170,70 | 161,80 | -33,25% |
| 2º reforço | 224,63 | 154,78 | 146,69 | -34,70% |
| 3º backup (--force) | 218,38 | 159,48 | 150,07 | -31,28% |
| esqueça | 32,01 | 32,24 | 32,05 | +0,12% |
| podar | 183,05 | 110,84 | 113,29 | -38,11% |
| verificar | 195,05 | 74,28 | 70,33 | -63,94% |
| lista de bolhas | 33,32 | 29,45 | 25,45 | -23,62% |
| bolha de gato | 92,94 | 49,02 | 33,13 | -64,35% |
| encontrar blob | 111,51 | 29,62 | 25,41 | -77,21% |
| restaurar | 292,94 | 217,41 | 206,77 | -29,42% |

Informação adicional

VM usada:

8 vCPU (dedicated CPUs)
32GB RAM
$ restic-orig version
debug enabled
restic 0.9.6 (v0.9.6-40-gd70a4a93) compiled with go1.13.6 on linux/amd64



md5-6ad2d5d28ba7c01c107571884b108e74



$ restic-newindex version
debug enabled
restic 0.9.6 (v0.9.6-43-g5db7c80f) compiled with go1.13.6 on linux/amd64



md5-6ad2d5d28ba7c01c107571884b108e74



$ restic-newindex2 version
debug enabled
restic 0.9.6 (v0.9.6-44-g26ec33dd) compiled with go1.13.6 on linux/amd64

Histórico

logs_test4.zip

@dimejo Bom trabalho - muito obrigado!
O desempenho do blob de lista (e uso de memória) é muito aprimorado após a confirmação 908c8977df05ea88dcad86c37f39d201468446ad.

Os outros resultados já parecem muito bons. Atualmente estou procurando se o índice pode economizar memória para backup e restore usando uma pequena refatoração desses comandos (como já feito com check ).

Então eu vou trabalhar nos testes e fazer um PR.
@rawtaz Minha pergunta sobre qual parte deste trabalho deve ser separada para tornar um PR aceitável ainda está em aberto. Vou precisar de algumas instruções aqui se as alterações forem muito grandes para os mantenedores lidarem.
Ainda estou convencido de que a reimplementação completa do índice aqui é a chave para esses bons resultados na redução do consumo de memória.

@aawsome Obrigado por corrigir o comando check. Vou dar uma chance.

Em relação às suas otimizações para o comando check , você pode querer dar uma olhada no meu pull request #2328 que consegue reduzir ainda mais o uso de memória e é muito mais rápido para repositórios contendo mais do que alguns snapshots.

Depois de observar a diferença entre master e refactor-index, tenho a impressão de que quase metade das alterações são causadas pela movimentação de código entre repository e index . No entanto, isso não fica claro ao analisar os commits. Esse movimento é realmente necessário?
Também não estou realmente convencido de ter uma referência ao repositório armazenado no MasterIndex. A maneira anterior de deixar o Repositório controlar o MasterIndex parece ser melhor separada na minha visão. Embora provavelmente seja uma boa ideia mover o fallback do formato de índice atual para o antigo para a implementação do índice.
Concordo que provavelmente não é possível reduzir muito a quantidade total de alterações sem perder a funcionalidade, mas provavelmente ajudaria muito apenas ter que olhar para um conjunto de commits com menos de algumas centenas de linhas cada.

@aawsome Ainda há um bug com o armazenamento em cache de treePacks. A linha https://github.com/aawsome/restic/blob/908c8977df05ea88dcad86c37f39d201468446ad/internal/repository/repository.go#L97 deve ser treePacks.Insert(pb.PackID) . Isso também deve reduzir um pouco mais o uso de memória.

A implementação atual em restic apenas armazena em cache os pacotes que contêm apenas árvores. Isso é equivalente à nova implementação para pacotes criados com versões recentes do restic. No entanto, em versões restic antigas (acho que < 0,8) dados e blobs de árvore são misturados. Esses blobs de dados não devem ser armazenados em cache. Portanto, sugiro adicionar uma segunda iteração em todos os blobs e remover os pacotes que contêm blobs de dados novamente.

@MichaelEischer
Obrigado por encontrar outro bug de cache - espero que agora seja realmente o último. E desculpe por não conseguir encontrá-lo; Até agora, usei apenas back-end local e não há teste padrão que verifique se o cache está configurado corretamente ... Talvez esse teste deva ser adicionado ...

Isso (e o antigo problema do formato do repositório) foi corrigido no último commit.

Muito obrigado também pelo seu outro comentário!
Sobre check Concordo que há ainda mais potencial de otimização de memória (e, claro, velocidade). Eu já estava pensando em tornar o índice capaz de salvar diferentes sinalizadores definidos pelo usuário para blobs. Dessa forma, você pode economizar IDSet s enormes ou mapas definidos pelo usuário, pois salva o ID do blob apenas uma vez. Isso é o que eu estava pensando ao otimizar prune em uma segunda etapa.
Seu PR #2328 pode ser facilmente adaptado para usar sinalizadores de índice em vez do mapa definido pelo usuário e obtemos uma redução de memória adicional.

Concordo com seu problema de design e removi repo de MasterIndex . Portanto, também os comandos Load e Save voltaram para repository.go tornando o diif para masterizar um pouco menor.
No entanto, não acho que o objetivo principal deva ser ter uma fusão tão pequena quanto possível. Eu gostaria de implementar o melhor design. Sobre onde localizar a implementação index já temos dois lugares no master: internal/index e internal/repository . Como as coisas relacionadas ao índice são bastante código, pensei que deveria ser separado de repository e então escolhi internal/index para ser o local para colocar tudo sobre o índice (e também definir uma nova interface em internal/restic/index.go ). Todo o código em index_new.go , index_old_format.go e master_index.go é escrito do zero e index.go é fortemente adaptado, mantendo a funcionalidade original usada em rebuild-index e prune .
Por outro lado, todas as coisas relacionadas ao índice (exceto carregar/salvar) são removidas de internal/repository .

Sobre como otimizar backup e restore :

  • Com backup eu adicionei a possibilidade de adicionar blobs "conhecidos" ao índice. Como eles são removidos assim que o blob "conhecido" é adicionado ao índice, isso deve economizar muita memória usada em backup . Espero que o uso da memória principal agora seja devido ao espaço reservado para o chunker paralelo, se você não tiver um repositório realmente ENORME.
  • Por restore não encontrei nenhuma otimização relacionada ao índice. Parece que todo o uso de memória se deve ao modo restore funciona internamente. Tenho a sensação de que existem possibilidades de otimização, mas isso deve ser feito em um trabalho separado.

Então, para mim, apenas trabalhar na implementação de teste está aberto (se não houver outros bugs relatados). Então eu vou fazer um PR.

@aawsome Oi, desculpe por demorar para responder. Sinto que não posso fornecer instruções detalhadas relevantes, mas o que eu estava pensando era dividir coisas como "refactoring code em preparação para alterações futuras" (como alterar o tratamento do índice) e "refactoring code para otimizar ou adicionar recursos " (como as otimizações reais e outras coisas que você está buscando aqui) em commits separados ou mesmo PRs (depende do código).

Estou tentando colocar você de olho nisso que tem coisas melhores a dizer do que eu, porque acho que seu trabalho aqui está parecendo interessante!

Além disso, agradeço que você tenha iniciado esta contribuição abrindo um problema (em vez de apenas abrir um PR), obrigado por isso! Sempre que possível, é bom termos uma discussão sobre sugestões antes que a implementação real seja feita. Ao fazer isso, podemos encontrar um terreno comum e estabelecer uma direção que tenha todos a bordo (e muitas vezes surgem questões ou perspectivas inesperadas que mudam como a implementação seria feita, o que resulta em menos desperdício de trabalho no final).

@rawtaz Obrigado pela sua resposta!

Você está certo que durante o desenvolvimento eu tentei me mover rápido e ser capaz de testar o que as coisas funcionam e o que não funciona. Por enquanto, parece que a mudança de código está madura o suficiente para discutir como ela pode ser melhor integrada ao mestre.

Acho que ajuda a discussão dar uma visão geral das mudanças que fiz e as razões pelas quais fiz cada mudança. A implementação abrange as seguintes alterações:

Principais mudanças:

  • a estrutura da base de código, ou seja, qual funcionalidade reside em internal/repository e internal/index
  • estrutura de definições de interface em internal/restic
  • estrutura interna do MasterIndex incluindo o manuseio de arquivos de índice "completos"
  • a estrutura de dados relacionada ao arquivo de índice
  • suporte para arquivos de índice de formato antigo ( index_old.go )
  • a nova estrutura de dados de índice na memória ( index_new.go )
  • mudança de acesso ao índice quando necessário

Pequenas alterações adicionais:

  • mudou Lookup e adicionou LookupAll
  • substituiu 'Store' por StorePack EDIT: é implementado em #2773
  • adicione CheckSetKnown para lidar com blobs conhecidos durante o backup EDIT: é implementado em #2773
  • remova o blob IDSet de checker.go e substitua pela funcionalidade de índice
  • use a nova funcionalidade de índice em 'list blobs'

As principais mudanças são IMO a chave para alcançar baixo consumo de memória, bom desempenho e um design de código mais limpo (em comparação com a implementação real). As pequenas mudanças são vitórias rápidas que também podem ser PRs independentes. No entanto, se forem considerados bons aprimoramentos, prefiro mantê-los no PR principal, caso contrário, eles provavelmente precisarão ser implementados duas vezes ou adiados.

Estrutura da base de código

Mudei a maioria das funcionalidades relacionadas ao índice de internal/repository para repository/index . IMO isso torna a estrutura de base de código mais clara e também separa claramente qual código está em qual diretório (em oposição à implementação real.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura da interface

Também separei as definições de interface em internal/restic para que fique claro o que está relacionado a Index e o que Repository . Isso também permite alterações mais claras das interfaces no futuro.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura do MasterIndex

Atualmente, MasterIndex combina estruturas de dados relacionadas a arquivos de índice e geralmente itera sobre todas essas subestruturas para operações de índice. Cada uma das estruturas de dados relacionadas ao arquivo de índice pode estar completa (ou seja, cheia o suficiente para salvar como um arquivo de índice completo no repositório) ou concluída (o que significa que já está presente como arquivo de índice).

Mudei completamente esse comportamento. Agora o MasterIndex usa um índice principal na memória onde a maioria das entradas do índice deve estar presente - especialmente todas aquelas que também estão presentes como arquivos de índice no repositório - e uma segunda estrutura de dados "inacabada" que é usada apenas para salvar o novo índice entradas e é inserido no índice principal na memória assim que o conteúdo é gravado como arquivo de índice no repositório.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Estrutura de dados relacionada ao arquivo de índice

Atualmente, existem três definições de estrutura de dados relacionadas ao arquivo de índice: uma definida por JSON em internal/index , uma definida por JSON em internal/repository e uma otimizada para pesquisa em internal/repository .
Decidi usar a estrutura de dados JSON de internal/index e adicionei os métodos necessários para usá-la no MasterIndex (e removi completamente as implementações de internal/repository ). Isso remove completamente a conversão de estruturas de dados internas para salvar arquivos de índice durante o backup. Por outro lado, as operações de pesquisa precisam fazer um loop sobre todas as entradas de índice. Portanto, é fundamental que essa estrutura de dados não contenha muitas entradas durante a pesquisa, ou seja, é inserida regularmente no índice principal.

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Suporte para formato de arquivo de índice antigo

O suporte para novos formatos de arquivo de índice agora é terceirizado para index_old_format.go .

Pergunta aos mantenedores: Você reconhece essa mudança principal?

Nova estrutura de dados de índice na memória ( index_new.go )

Esta é obviamente a maior mudança. Em princípio, uma grande lista de blob é mantida por tipo de blob. A lista é classificada por ID de blob de forma que a pesquisa possa ser feita usando a pesquisa binária. Como a otimização visa muitas entradas nesta lista, o tamanho de cada entrada é minimizado o máximo possível. Isso significa que o ID do pacote é armazenado em uma tabela separada e apenas uma referência é salva e o deslocamento e o comprimento são uint32 (limitando efetivamente o tamanho máximo do pacote a 4 GB). EDIT: essas duas otimizações são implementadas em #2781.
Um array ordenado simples já economiza a sobrecarga de memória que está dentro de map mas ainda gera alguma sobrecarga de memória e desempenho dentro de append : Um "buffer" extra é reservado para anexos futuros e os dados precisam ser copiado. Por isso, implementei uma matriz "paginada" com uma sobrecarga de memória pequena e limitada e sem necessidade de copiar para anexar.

Os testes que foram feitos indicam que este índice é um pouco mais rápido que a implementação padrão do MasterIndex, economizando mais de 75% da memória.

Pergunta aos mantenedores: Essa implementação de índice atende às suas expectativas?

Alterado Lookup e adicionado LookupAll

Lookup na verdade retorna mais de um resultado (mas não se eles estiverem espalhados por muitos arquivos de índice, BTW). Em muitos casos, apenas o primeiro resultado é usado. Portanto, alterei Lookup para retornar apenas um resultado e adicionei LookupAll para ser usado quando mais de um resultado for usado. Torna o código um pouco mais claro e também economiza alguns ciclos de CPU desnecessários.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Substituído 'Loja' por StorePack

Store armazena apenas um blob no índice, mas é chamado apenas para um conteúdo de pacote completo. Portanto, mudei para uma funcionalidade StorePack onde todo o conteúdo do pacote é salvo. Isso torna o código mais claro, armazenando no formato JSON muito mais fácil e também com melhor desempenho.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

adicione CheckSetKnown para lidar com blobs conhecidos durante o backup

Os testes mostraram que para muitos novos arquivos backup usa bastante memória para armazenar quais blobs foram adicionados durante esta execução de backup. Isso é para não salvar blobs duas vezes por protetores paralelos. Movi essa funcionalidade para o índice, pois essas informações de "blob conhecido" podem ser removidas assim que a entrada correspondente for adicionada ao índice. Com essa alteração, apenas alguns blobs são "conhecidos", mas não (ainda) no índice.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Remova o blob IDSet de checker.go e substitua pela funcionalidade de índice

O Checker cria seu próprio índice na memória e, além disso, mantém todos os IDs de blob em um IDSet adicional. Como todos os blobs já são conhecidos do índice, isso pode ser substituído pelo método Each do índice.
Isso economiza memória adicional em check .

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Use a nova funcionalidade de índice em 'blobs de lista'

Em vez de ler todo o índice na memória e depois fazer um loop sobre todas as entradas para imprimir a lista de blobs, cada arquivo de índice pode ser carregado separadamente na memória e fazer um loop sobre esses conteúdos. Torna list blobs muito menos com fome de memória.

Pergunta aos mantenedores: Posso manter essa alteração ou você prefere removê-la do índice PR?

Um pequeno resultado de teste @aawsome , não consigo fazer backup em um repositório existente após o último commit.
Valores a partir de time :

|---|Backup "Normal"| fix masterindex.Load() commit|último commit|
|---|---|---|---|
|Uso de memória|~650Mb|~320Mb|~55Mb|
|Tempo de backup|~12seg|~12seg|(desistiu após 5mins)|

O armazenamento informa que interrompe a atividade após consultar os índices. (Desculpe se isso foi intencional devido à mudança de formato)

@seqizz Obrigado por enviar o relatório de bug. Eu tenho que admitir que eu só testei o último commit com go run build.go -T que não apresentou nenhum problema... Eu consertei no commit 5b4009fcd275794e3ce05304fce255d5fa3e1864

@aawsome Obrigado por escrever esse comentário resumido . Uma das coisas que eu como desenvolvedor não-core (sou apenas um mantenedor ou coisas simples) é que, durante o tempo em que o tempo de desenvolvedor principal é gasto em outras partes do projeto, problemas como esse continuam crescendo. Então, sempre que um desenvolvedor principal olha para isso, é muito ou, em alguns casos, demais para se aprofundar no momento. Quanto menos houver para ler para eles, maiores são as chances de que possamos fazer bom uso de seu tempo. Tomei a liberdade de editar o post inicial desta edição para adicionar uma referência ao seu comentário resumido.

Em relação às suas perguntas, eu pessoalmente não posso respondê-las. Vou ter que adiá-los por um tempo, e peço gentilmente sua paciência. Nosso foco principal agora com o tempo disponível que temos são outras coisas, mas posso dizer que esse problema e seu PR estão no meu radar e estou tentando colocar os olhos nele eventualmente.

Eu acho que com seu comentário resumido, além de corrigir bugs (estou um pouco preocupado que possa haver mais alguns pequenos bugs à espreita) e o que você encontrar que não se desvie do que você escreveu em seu comentário, a coisa mais produtiva é para deixar isso descansar um pouco. O próximo passo seria colocar os olhos do desenvolvedor principal nele, e até que isso aconteça, provavelmente é mais contraproducente aumentar a discussão. Deixe-me saber se meu raciocínio com isso não parece fazer sentido, para que eu possa esclarecer o que quero dizer.

Finalmente encontrei tempo para testar o desempenho do comando de backup em um repositório de arquivos de 8 TB e 40 milhões. Para manter o tempo de execução geral baixo, acabei de fazer backup de um diretório de 6 GB. O uso da CPU de restic foi limitado usando GOMAXPROCS=1 .

mestre atual: cpu-idx-master.pdf
este pr: cpu-idx-table.pdf

Com este PR restic usa cerca de 6,5 GB de memória em vez de 18 GB para o mestre atual, o que é uma melhoria gigantesca. O índice leva cerca de 9 minutos para carregar em ambos os casos (o host não suporta AES-NI, então a descriptografia é um pouco lenta). O backup leva 34 minutos para este PR e 18 minutos para o mestre atual. Observar os gráficos de perfil da CPU mostra a diferença: a indexTable gasta 16 minutos apenas para classificar o índice na memória. O índice é grande o suficiente para que uma única chamada de classificação para o índice na memória leve cerca de 75 segundos (!) para ser concluída. Durante esse tempo, todo o processamento de backup é bloqueado, pois o índice é bloqueado exclusivamente.

O índice na memória provavelmente precisa de uma maneira de mesclar um novo índice sem recorrer a tudo, talvez até implementado de forma incremental. Isso deve ser possível classificando o novo índice antecipadamente e, em seguida, mesclando-o no MasterIndex com uma única varredura na indexTable.

@MichaelEischer Muito obrigado por fazer seu teste! Na verdade, a implementação atual precisa de um recurso da tabela de índice cada vez que um arquivo de índice é gravado. Então, se eu calculei corretamente, 13 novos arquivos de índice foram criados, certo?
A boa mensagem é que as pesquisas em IndexJSON (que não são classificadas e, portanto, exigem pesquisa linear) não aparecem, então essa parece ser uma boa maneira de lidar com arquivos de índice "inacabados"

Na verdade, meu julgamento é que recorrer a uma tabela quase ordenada com sort.Sort deve ser quase ideal. (No entanto, experimentar sort.Stable ou um mergesort puro ainda deve valer a pena tentar ...) Eu acho que o principal problema é: Se queremos ter um array de índice classificado e inserir coisas nele, precisamos para mover cerca de 6,5 GB de dados na memória.
Então proponho separar as entradas de índice lidas dos arquivos de índice daqueles recém-gerados (e já salvos). Isso cria três estruturas de dados na implementação do MasterIndex. Já alterei isso no meu branch refactor-index . Você se importaria de testar novamente sua configuração?

Quando eu zoom nos dados do perfil IndexJSON leva 1,22 segundos no total. Então isso parece bom por enquanto. O pior caso seria muitos arquivos pequenos, caso um mapa pudesse ser mais rápido.

A execução do backup na verdade criou 16 novos arquivos de índice. Os 75 segundos foram um palpite de uma corrida de check que só aciona a reclassificação uma vez. Tanto o quicksort (usado por sort.Sort) quanto o mergesort têm uma complexidade de tempo de melhor caso de O(n log n), portanto, os dados pré-ordenados _não melhoram o desempenho._ Como uma observação lateral, o índice na memória requer menos espaço do que o representação em disco ^^ .

Executei novamente o teste com o código atualizado e o desempenho é muito melhor: cpu-idx-table-split.pdf

No entanto, usar um índice separado para novas entradas de índice apenas oculta, mas não resolve o problema subjacente: imagine uma execução de backup que adiciona alguns milhões de novos arquivos ou um grande backup inicial. A implementação atual tem complexidade O(n * n*log n) . O primeiro n refere-se ao número de anexos ao índice que escala um pouco linear com a quantidade de novos dados de backup. E a última parte de n*log n é a já mencionada complexidade para ordenar o índice. Ao substituir o quicksort por uma mesclagem linear, isso é reduzido para O(n * n) que pode ser bom o suficiente para tamanhos de índice abaixo de várias dezenas de gigabytes. Além disso, não será dimensionado, mas acho que os repositórios Petabyte são um problema próprio. A mesclagem linear pode até ser implementada para funcionar de forma incremental. Eu tenho uma idéia de como reduzir a complexidade para O(n * log n) mas isso aumentaria os custos de pesquisa para O(log^2 n) e exigiria algumas alterações de código extensas.

Gostaria de saber se existe uma estrutura de dados que atenda perfeitamente às nossas necessidades:

  • deve ter baixa sobrecarga de memória
  • velocidade de consulta sublinear amortizada, de preferência log n ou menos
  • custos de inserção sublinear amortizados, de preferência log n ou menos. Com inserções n , isso levaria a custos de O(n * log n) para uma execução de backup completa

Ao descartar o requisito de uma representação realmente compacta na memória, os hashmaps se encaixam perfeitamente, pois suas consultas e atualizações acontecem em O(1) . No entanto, eles exigem muito mais memória: meu protótipo atual usa cerca de 70% mais memória do que este PR.

Uma solução seria combinar ambas as abordagens e coletar novas entradas em um hashmap e mesclá-las no array ordenado de tempos em tempos (por exemplo, quando o tamanho do hashmap atingir 10% do array ordenado).

Dito isso, provavelmente é melhor esperar pelo feedback do @fd0 antes de fazer alterações extensas. (Desculpe por aumentar a discussão novamente.)

Gostaria de saber se existe uma estrutura de dados que atenda perfeitamente às nossas necessidades:

Eu diria que existem "muitas" estruturas de dados que satisfazem mais ou menos essas necessidades, mas não sei quanto esforço seria mudar para elas em restic. Basicamente, qualquer estrutura de árvore B ou geralmente de árvore deve satisfazer essas necessidades (mapas de hash são realmente ineficientes para esses cenários, pois você também descobriu em suas medições).

Se não me engano, um B-Tree apenas garante que seus nós estejam pelo menos meio preenchidos. E isso exigiria ponteiros que adicionariam mais ou menos um ponteiro de 8 bytes para cada 48 bytes de dados. Se um B-Tree garantir 70% de uso de espaço em média, ele acabaria com uma sobrecarga de memória semelhante à minha implementação baseada em hashmap. Pode haver algumas variantes como B*-Trees que são mais eficientes.

@michaeldorner sim, essa é a "velha árvore B tradicional" - como você escreveu, existem muitas (eu vi pelo menos dez bem diferentes até agora) estruturas semelhantes a árvores B, cada uma com propriedades diferentes. Outras estruturas semelhantes a árvores bem conhecidas incluem, por exemplo, árvores vermelho-preto (populares para mapas que devem garantir tempo e espaço de inserção/exclusão consistentes; isso contrasta com mapas baseados em hash que podem sofrer "saltos" ou outros picos excessivos tanto em tempo e espaço eventualmente levando a sérios problemas de segurança/desempenho ). Existem também vários mapas "comprimidos" - sinta-se à vontade para procurá-los. Portanto, há realmente uma série de opções.

Embora eu não seja contra o uso de um pequeno arquivo temporário como discutido neste tópico e como, por exemplo, o sqlite faz - btw. O sqlite é altamente eficiente e eu o usei como armazenamento apenas para fins de uso de memória extremamente baixo, oferecendo uma taxa de transferência bastante alta, apesar de usar discos rígidos como cache temporário - sinta-se à vontade para experimentá-lo, pode surpreendê-lo.

Apenas meus 2 centavos: wink:.

A matriz ordenada e densamente compactada de entradas de índice é basicamente o padrão-ouro para uso de memória. Sim, você pode aplicar alguma compactação, mas isso só ajudaria com o tamanho/deslocamento do blob e o ID do pacote (aprox. 16 bytes) os 32 bytes restantes são o ID do blob (hashes sha256) que eu suponho ser basicamente incompressível. Não tenho certeza se o potencial para outro uso de memória 20-30% menor justificaria a complexidade ainda maior.

Árvores sempre requerem um (e algum espaço não utilizado como com B-Trees) ou dois ponteiros (red-black-tree e outros) por elementos que já adicionariam 8 ou 16 bytes de overhead ao aprox. 48 bytes da própria entrada de índice. Também não tenho certeza de quão bem o coletor de lixo Go lidaria com milhões ou mesmo bilhões de entradas de índice.

Para salvar a honra das tabelas de hash: diz-se que o hash de cuco (tempo de pesquisa constante) junto com 4 entradas por bucket atinge mais de 90% de utilização do espaço. Se você usar apenas dez dessas tabelas de hash, a sobrecarga total de memória também permanecerá abaixo de 33%, mesmo durante o crescimento de um hashmap.

Acho que a questão principal é mais se queremos uma única estrutura de dados (árvore/hashmap) com uma certa sobrecarga de memória. Ou se queremos a menor sobrecarga de um array empacotado classificado, que precisará de uma estrutura de dados auxiliar para fornecer uma velocidade de inserção razoável. Se essa estrutura de dados auxiliar contém apenas 10-20% dos dados gerais do índice, não importa muito se uma estrutura de dados tem 30% ou 70% de sobrecarga e, nesse caso, o hashmap interno do Go seria a solução mais simples.

O uso de um índice em disco tem sua própria parcela de problemas: se o índice for muito grande para ser mantido na memória, o desempenho do índice provavelmente implodirá nos HDDs (os SSDs devem ser muito melhores), a menos que o índice possa se beneficiar de localidade entre blobs para, por exemplo, uma pasta/arquivo (em uma lista totalmente ordenada de ids de blob, o padrão de acesso seria basicamente uniformemente aleatório).

@MichaelEischer obrigado pelo encerramento. Eu ainda encorajo você a tentar o cache de disco (apenas para ter um vislumbre do desempenho possível, não para ser incorporado no Restic) usando sqlite3 com configurações diferentes (por exemplo, com e sem índice, com restrições de memória máxima aumentadas do que o muito baixo padrões, usando apenas uma tabela como armazenamento tipo mapa de valor- chave , etc.) usando WAL, pois é significativamente mais lento para leituras (mas um pouco mais rápido para gravações).

O uso do cache de disco também pode se beneficiar significativamente do processamento orientado a fluxo, mas não sei se é uma boa opção para um teste rápido. Apenas uma ideia baseada na minha boa experiência com armazenamento de valor-chave baseado em sqlite3: wink:.

@aawsome Você poderia dividir sua otimização de "blobs de lista" em um PR separado?

Não pretendo mesclar a remoção blob IDSet de checker.go , pois o #2328 também fornece essa otimização, mas sem precisar envolver o índice mestre.

Isso deixa LookupAll como a última alteração menor. Essa função seria útil ao adicionar suporte para repositórios danificados para remoção. No entanto, sem algumas otimizações de desempenho de índice primeiro, provavelmente apenas reduzirá o desempenho geral do restic. Então isso vai ter que esperar.

Em relação às principais mudanças, elas também precisam ficar em espera por enquanto.

@dumblob O problema de usar sqlite ou qualquer outro banco de dados genérico para um índice em disco é que sem otimizações que fazem uso da localidade entre blobs em um arquivo (ou seja, blobs dentro de um arquivo acabam em pacotes próximos e geralmente são acessados ​​juntos), terminaremos com um acesso pseudo-aleatório a todas as partes do índice que é maior que a memória principal e, portanto, precisa carregar dados de locais aleatórios no disco. Essa é uma implementação genérica de banco de dados não tem chance alguma de fornecer um bom desempenho.

@aawsome Você poderia dividir sua otimização de "blobs de lista" em um PR separado?

Certo. PR virá em breve.

Isso deixa LookupAll como a última alteração menor. Essa função seria útil ao adicionar suporte para repositórios danificados para remoção. No entanto, sem algumas otimizações de desempenho de índice primeiro, provavelmente apenas reduzirá o desempenho geral do restic. Então isso vai ter que esperar.

Na verdade, a implementação real de Lookup é (parcialmente) LookupAll no sentido de que retorna todos os resultados dentro de um arquivo de índice, mas apenas os resultados do primeiro arquivo de índice que tem uma correspondência .

Como o caso usual é que não há duplicatas em um arquivo de índice, isso na verdade não fornece duplicatas, mesmo nos casos em que elas podem ser necessárias.

Vou preparar um PR para alterar Lookup e adicionar LookupAll onde for útil. Então podemos discutir se isso é útil ou não.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

mholt picture mholt  ·  4Comentários

christian-vent picture christian-vent  ·  3Comentários

shibumi picture shibumi  ·  3Comentários

cfbao picture cfbao  ·  3Comentários

fd0 picture fd0  ·  4Comentários