Caixa para armazenamento de dados em aplicativos go

imagem

Uma breve observação sobre um banco de dados de valores-chave incorporado chamado Coffer, escrito em Golang. Se for muito breve: quando o banco de dados está parado, os dados estão no disco, quando são iniciados, os dados são copiados para a memória. A leitura vem da memória. Durante a gravação, os dados da memória são alterados e as alterações são gravadas no log do disco. O tamanho máximo dos dados armazenados é limitado pelo tamanho da RAM. A API permite criar cabeçalhos para registros do banco de dados e usá-los em transações, mantendo a consistência dos dados.

Mas primeiro, uma pequena introdução lírica. Era uma vez, quando a grama era mais verde, levei para incorporar um banco de dados de valores-chave para o aplicativo go. Olhando em volta e encontrando pacotes diferentes, de alguma forma não encontrei o que gostaria (subjetivamente) e apenas apliquei a solução com um banco de dados relacional externo. Ótima solução de trabalho. Mas como se costuma dizer, uma colher foi encontrada, mas o sedimento permaneceu. Antes de tudo, eu queria exatamente o nativo, escrito no banco de dados Go, diretamente nativo. E existem, basta olhar incrível-go. No entanto, não há um milhão deles. Isso é até surpreendente quando você considera que um programador é raro no mundo que não escreveu um banco de dados, estrutura ou jogo casual em sua vida.


Bem, você pode tentar empilhar sua bicicleta no joelho, com blackjack e outras guloseimas. Ao mesmo tempo, todo mundo sabe, ou pelo menos adivinha, que escrever até mesmo um simples banco de dados de valores-chave parece simples apenas à primeira vista. Mas, de fato, tudo é muito mais divertido (e aconteceu). E eu estava curioso sobre o ACID e preocupado com transações. As transações verdadeiras são mais prováveis ​​no sentido financeiro, porque Eu estava ocupado na fintech.


Segurança de dados


Considere o caso em que, durante a operação de um aplicativo com gravação ativa, a fonte de alimentação do computador foi coberta com uma bacia de cobre e o disco não quebrou. Se, neste momento, o aplicativo recebeu um ok do banco de dados, os dados desta operação não serão perdidos. Se o aplicativo recebeu uma resposta negativa, é claro que a operação não foi concluída. Bem, o caso em que o aplicativo enviou uma solicitação, mas não recebeu uma resposta: essa operação provavelmente não foi concluída, mas há uma pequena chance de a operação cair no log, mas exatamente no momento em que a resposta foi enviada, a energia foi desligada.


Como, no último caso, descobrir o que aconteceu com as últimas operações? Esta é uma pergunta interessante. Indiretamente, você pode adivinhar (tirar conclusões) observando o valor do registro de interesse após o lançamento de um novo aplicativo a partir do banco de dados. No entanto, se as operações forem frequentes o suficiente, receio que não ajude. Você pode ver o arquivo do último log (será o número mais alto), mas manualmente isso é inconveniente. Eu acho que, no futuro, você pode adicionar a capacidade de exibir logs na API (naturalmente, os logs nesse caso não devem ser excluídos).


Francamente, eu mesmo não puxei o fio da tomada, porque Eu não quero arriscar ferro para verificar o banco de dados. Nos testes, estrago apenas os arquivos de log normais e, nesse caso, tudo acontece como eu esperava. No entanto, não há experiência no uso prático do banco de dados, ele não funcionou no produto e há riscos. No entanto, para projetos de animais de estimação, acho que o banco de dados pode ser usado sem medo. Em geral, o aviso de isenção habitual, sem garantias.


No momento, o banco de dados não está protegido contra o uso em dois aplicativos diferentes (ou o mesmo, isso não importa aqui) configurado para funcionar com o mesmo diretório. Por favor, leve esse momento em consideração! E, no entanto, como o banco de dados é embutido, passando para ele algum tipo de tipo de referência nos argumentos, definitivamente não vale a pena alterá-lo em algum lugar na goroutine paralela.



Configuração


O banco de dados possui alguns parâmetros que podem ser configurados, no entanto, quase todos eles têm valores padrão, portanto, tudo pode ser ajustado em uma linha curta cof, err, wrn := Db(dirPath).Create() proibido) e aviso, que você pode conhecer, mas isso não interfere na operação do banco de dados.


Não vou desarrumar o texto com descrições complicadas, se necessário, observe-as no leia-me do repositório - github.com/claygod/coffer/blob/master/README_RU.md#config Observe o método Handler que conecta o manipulador para a transação, escreverei algumas linhas sobre ele mais abaixo, aqui apenas as listo:


  • Db (dirPath)
  • BatchSize (batchSize)
  • LimitRecordsPerLogfile (limitRecordsPerLogfile)
  • FollowPause (100 * time.Second)
  • LogsByCheckpoint (1000)
  • AllowStartupErrLoadLogs (true)
  • MaxKeyLength (maxKeyLength)
  • MaxValueLength (maxValueLength)
  • MaxRecsPerOperation (1.000.000)
  • RemoveUnlessLogs (true)
  • Memória Limit (100 * 1.000.000)
  • LimitDisk (1000 * 1.000.000)
  • Manipulador ("manipulador1" e manipulador1)
  • Manipulador ("manipulador2" e manipulador2)
  • Manipuladores (map [string] * manipulador)
  • Criar ()

API


Na medida do possível, simplifiquei a API e, para uma base de valores-chave, não seja muito inteligente:


  • Iniciar - inicia o banco de dados
  • Stop - para o banco de dados
  • StopHard - uma parada independentemente das operações que estão sendo executadas no momento (talvez eu a remova)
  • Salvar - salva uma captura instantânea do estado atual do banco de dados
  • Escrever - adicione um registro ao banco de dados
  • WriteList - adicione vários registros ao banco de dados (modos estrito e opcional)
  • WriteListUnsafe - adicione vários registros ao banco de dados sem considerar a segurança dos dados
  • Leitura - obtenha um registro por chave
  • ReadList - obtenha uma lista de registros
  • ReadListUnsafe - obtenha uma lista de registros sem considerar a segurança dos dados
  • Excluir - excluir um registro
  • DeleteList - exclua vários registros no modo restrito / opcional
  • Transação - executar uma transação
  • Contagem - quantos registros no banco de dados
  • CountUnsafe - quantos registros no banco de dados (um pouco mais rápido, mas não seguro)
  • RecordsList - uma lista de todas as chaves do banco de dados
  • RecordsListUnsafe - uma lista de todas as chaves do banco de dados (um pouco mais rápido, mas não seguro)
  • RecordsListWithPrefix - uma lista de chaves com o prefixo especificado
  • RecordsListWithSuffix - uma lista de chaves com o fim especificado

Breves explicações para a API:


  • Modo estrito - faça tudo ou nada.
  • Modo opcional - faça tudo o que funcionar.
  • StopHard - talvez esse método deva ser removido da API até que seja decidido.
  • Todos os métodos RecordsList não são rápidos, porque No momento, não há índices na loja, enquanto essa é uma verificação completa.
  • Todos os métodos inseguros são mais rápidos, mas a consistência não é implícita ao usá-los. É lógico usá-los em um banco de dados parado para preenchimento rápido ou qualquer outra coisa na mesma linha.
  • O seguidor monitora a atualização regular do instantâneo do banco de dados, portanto, o método Save é mais provável em alguns casos especiais quando você definitivamente deseja criar um novo instantâneo (até que esse caso me venha à mente, mas talvez seja).

Um caso de uso simples:

 package main import ( "fmt" "github.com/claygod/coffer" ) const curDir = "./" func main() { // STEP init db, err, wrn := coffer.Db(curDir).Create() switch { case err != nil: fmt.Println("Error:", err) return case wrn != nil: fmt.Println("Warning:", err) return } if !db.Start() { fmt.Println("Error: not start") return } defer db.Stop() // STEP write if rep := db.Write("foo", []byte("bar")); rep.IsCodeError() { fmt.Sprintf("Write error: code `%d` msg `%s`", rep.Code, rep.Error) return } // STEP read rep := db.Read("foo") rep.IsCodeError() if rep.IsCodeError() { fmt.Sprintf("Read error: code `%v` msg `%v`", rep.Code, rep.Error) return } fmt.Println(string(rep.Data)) } 

Transações


Como mencionado acima, minha definição de transações pode não coincidir com a geralmente aceita na construção do DB, talvez elas estejam unidas apenas por uma idéia. Em uma implementação específica, uma transação é um determinado cabeçalho especificado no estágio de configuração do banco de dados (método Handler ). Quando invocamos uma transação com esse cabeçalho, o banco de dados bloqueia os registros com os quais o cabeçalho irá trabalhar e transfere seus valores atuais para o cabeçalho. O cabeçalho manipula esses dados conforme necessário e retorna os novos valores do banco de dados e os salva em cem. Depois disso, os registros são desbloqueados e ficam disponíveis para outras operações.


Existem exemplos no repositório que revelam a essência do uso de transações muito bem. Por curiosidade, fiz um pequeno exemplo financeiro, no qual existem operações de débito e crédito, transferência, compra e venda. Foi muito fácil escrever esse exemplo e, ao mesmo tempo, essa implementação na altura dos joelhos é bastante consistente e adequada para uso em várias soluções financeiras ou, por exemplo, em logística.


Um ponto importante: o código do manipulador não é armazenado no banco de dados. Eu tive a idéia de armazená-lo em um log, mas me pareceu um desperdício demais, por isso não o compliquei e, portanto, a responsabilidade pela consistência dos manipuladores entre diferentes partidas do banco de dados recai sobre o desenvolvedor do código que usa o banco de dados. Definitivamente, os manipuladores não podem ser alterados se o aplicativo e o banco de dados pararem de falhar. Nesse caso, você deve primeiro iniciar o banco de dados e depois pará-lo regularmente - um novo instantâneo de dados será criado. Para não ficar confuso, aconselho você a usar o número da versão no nome dos manipuladores.



Receba e processe respostas


O banco de dados retorna relatórios indicando o status da resposta e com os dados. Como existem muitos códigos, e escrever um comutador com o processamento de cada um deles é problemático, convém verificar por aprox. Isso não deve ser feito. O fato é que o código pode ter o status Ok, Erro, Pânico. Com Ok, tudo está claro, mas e os outros dois? Se o status for Erro, uma operação específica será concluída ou não será concluída. Este erro deve ser tratado adequadamente no aplicativo. No entanto, é possível trabalhar mais com o banco de dados (e necessário). Outra coisa Panic - o trabalho com o banco de dados deve ser descontinuado.


A verificação de IsCodeError simplifica o trabalho com todos os erros; portanto, se você não estiver interessado nos detalhes, continue trabalhando.
A verificação IsCodePanic abrange todos os casos em que o trabalho com o banco de dados deve ser interrompido.


No caso simples, um switch triplo é suficiente para processar a resposta:


  • IsCodeOk - continue trabalhando IsCodeOk
  • IsCodeError - registre o erro no relatório e trabalhe mais
  • IsCodePanic - registre o erro no relatório e pare de trabalhar com o banco de dados

Offtop


Para o nome, foi escolhida uma das opções de tradução da palavras para o inglês. Prefiro a box , é claro, mas essa é uma palavra muito popular, espero que o coffer também coffer .
O tópico com o ACID me parece bastante holístico, então eu diria que o Coffer está comprometido com isso, mas não é um fato, e não afirmo que ele tenha conseguido.



Desempenho


Escrevi imediatamente um banco de dados levando em consideração concorrência e concorrência. É nesse modo que mostra sua eficácia (embora isso provavelmente seja dito em voz alta). Nos resultados abaixo, o benchmark mostra uma largura de banda de 200k rps. É claro que este é um banco artificial, e a realidade será completamente diferente, porque depende muito do tamanho dos dados gravados, da quantidade de dados já gravados, do desempenho do ferro e da fase da lua. Mas a tendência é pelo menos compreensível. Se o banco de dados for usado no modo de thread único, cada solicitação será executada somente após receber a resposta da anterior, a velocidade será lenta e eu aconselho você a procurar outros bancos de dados, mas não o Coffer.


  • BenchmarkCofferTransactionSequence-4 2000 227928 ns / op
  • BenchmarkCofferTransactionPar32HalfConcurent-4 100000 4199 ns / op

A propósito, se alguém gasta tempo e se inclina para si mesmo como um repositório com o Coffer, se possível, execute o banco nele. Estou muito interessado em quais máquinas o desempenho mostrará no banco de dados. Primeiro de tudo, é claro que tudo depende do disco. Isso ficou especialmente claro para mim depois que comprei recentemente um novo Samsung EVO. Mas não se preocupe, isso não substitui um disco morto. A antiga Toshiba continua a funcionar corretamente e agora armazena meu arquivo de vídeo.


O relógio incorporado na memória ainda é um mapa simples, nem mesmo dividido em seções. Obviamente, pode ser ótimo melhorá-lo, por exemplo, para acelerar a seleção de chaves por prefixos e sufixos. Enquanto eu não fiz isso, tk. a principal funcionalidade, como digo o chip DB, vejo nas transações, e o gargalo no desempenho das transações estarão funcionando com o disco e, somente então, trabalhando com a memória.



Licença


Agora, a licença permite armazenar até dez milhões de registros no banco de dados, pareceu-me que esse é um número suficiente. Planos adicionais para o desenvolvimento do banco de dados estão em processo de formação.
Em geral, é interessante para mim usar o banco de dados como um pacote e focar principalmente em sua API.



Conclusões


Recentemente, frequentemente encontro a tarefa de escrever serviços com a característica de alta disponibilidade. Infelizmente, devido ao fato de isso quase sempre implicar a presença de várias instâncias, não vale a pena usar o banco de dados interno nesse caso. Resta a opção de um aplicativo ou serviço regular que existe em uma instância. Parece-me um caso mais raro, mas, no entanto, é bom; nesse caso, é bom ter um banco de dados que tente, sempre que possível, salvar os dados nele armazenados. O Coffer que criei está tentando resolver esse problema. Vamos ver como ele faz isso.



Agradecimentos


  • Para todos que lerem o artigo até o fim
  • Comentadores que desejam compartilhar suas opiniões
  • Enviado informações pessoais sobre erros de digitação e erros no texto
  • Vizinho ligar música à noite

Referências


Repositório de banco de dados
Descrição em russo

Source: https://habr.com/ru/post/pt477154/


All Articles