1. Introdução
Na Internet, há muitas informações e debates sobre a escolha da abordagem sql / nosql, bem como os prós e contras de um ou outro armazenamento KV. O que você está lendo agora não é um manual ou agitação do rocksdb para usar este repositório e meu driver para ele. Gostaria de compartilhar o resultado intermediário da otimização do processo de desenvolvimento do NIF para Erlang. Este artigo apresenta um driver viável para o rocksdb, desenvolvido ao longo de algumas noites.
Assim, em um dos projetos, surgiu a tarefa de processamento confiável de um grande volume de eventos. Cada evento leva de 50 a 350 bytes, mais de 80 milhões de eventos são gerados por nó por dia. Só quero observar que os problemas de tolerância a falhas da entrega de mensagens aos nós não são considerados. Também uma das restrições de processamento é a alteração atômica e consistente do grupo de eventos.
Assim, os principais requisitos para o motorista são:
- Confiabilidade
- Desempenho
- Segurança (no sentido canônico)
- Funcionalidade:
- Todas as funções básicas de kv
- Famílias de colunas
- Transações
- Compressão de dados
- Suporte flexível de configuração de armazenamento
- Base de código mínima
Visão geral das soluções existentes
- O erocksdb é uma solução dos desenvolvedores leofs. As vantagens incluem a aprovação em um projeto real. Por contras - uma base de código desatualizada e falta de transacionalidade. Este driver é baseado no rocksdb 4.13.
- O rockse tem várias limitações, por exemplo, a falta de opções de configuração, mas o mais importante é que todas as chaves e valores devem ser cadeias. Ele entrou na revisão apenas como exemplo de um número de drivers que implementam um ou outro funcional e limitam outro.
- erlang-rocksdb é um projeto completo, cujo desenvolvimento começou em 2014. Como o erocksdb é usado em projetos reais. Possui uma grande base de código em C / C ++ e ampla funcionalidade. Esse driver é adequado para a prática geral e é usado na maioria dos projetos.
Após uma rápida análise da situação atual com os drivers erlang para o rocksdb, ficou claro que nenhum deles atende totalmente aos requisitos do projeto. Embora fosse possível usar o erlang-rocksdb, houve algumas noites livres e, após o desenvolvimento e implementação bem-sucedidos do filtro Bloom sobre Rust e curiosidade: é possível implementar todos os requisitos do projeto atual e implementar a maioria das funções no NIF em um curto período de tempo?
Rocker
Rocker é um NIF para Erlang, usando o wrapper Rust para rocksdb. Os principais recursos são segurança, desempenho e uma base de código mínima. Chaves e dados são armazenados em formato binário, o que não impõe restrições ao formato de armazenamento. No momento, o projeto é adequado para uso em soluções de terceiros.
O código fonte está no repositório do projeto .
Visão geral da API
Abertura da base
Trabalhar com o banco de dados é possível em dois modos:
O espaço total da chave. Nesse modo, todas as suas chaves serão colocadas em um conjunto. O Rocksdb permite que você configure com flexibilidade opções de armazenamento para tarefas atuais. Dependendo deles, o banco de dados pode ser aberto de duas maneiras:
usando um conjunto padrão de opções
rocker:open_default(<<"/project/priv/db_default_path">>) -> {ok, Db}.
O resultado desta operação será um ponteiro para trabalhar com o banco de dados, e o banco de dados será bloqueado para qualquer outra tentativa de abertura. O banco de dados será desbloqueado automaticamente imediatamente após a limpeza deste ponteiro.
- configure opções para a tarefa
{ok, Db} = rocker:open(<<"/project/priv/db_path">>, #{ create_if_missing => true, set_max_open_files => 1000, set_use_fsync => false, set_bytes_per_sync => 8388608, optimize_for_point_lookup => 1024, set_table_cache_num_shard_bits => 6, set_max_write_buffer_number => 32, set_write_buffer_size => 536870912, set_target_file_size_base => 1073741824, set_min_write_buffer_number_to_merge => 4, set_level_zero_stop_writes_trigger => 2000, set_level_zero_slowdown_writes_trigger => 0, set_max_background_compactions => 4, set_max_background_flushes => 4, set_disable_auto_compactions => true, set_compaction_style => universal }).
- Divisão em vários espaços. As chaves são armazenadas nas chamadas famílias de colunas e cada família de colunas pode ter opções diferentes. Vamos considerar um exemplo de abertura de um banco de dados com opções padrão para todas as famílias de colunas
{ok, Db} = case rocker:list_cf(BookDbPath) of {ok, CfList} -> rocker:open_cf_default(BookDbPath, CfList); _Else -> CfList = [], rocker:open_default(BookDbPath) end.
Remoção da base
Para excluir corretamente o banco de dados, você deve chamar rocker:destroy(Path).
Nesse caso, a base não deve ser usada.
Recuperação do banco de dados após uma falha
No caso de uma falha no sistema, o banco de dados pode ser restaurado usando o método rocker:repair(Path)
.Este processo consiste em 4 etapas:
- pesquisa de arquivo
- restaurar tabelas jogando WAL
- extrair metadados
- registro descritor
Criando uma família de colunas
Cf = <<"testcf1">>, rocker:create_cf_default(Db, Cf) -> ok.
Remoção da família de colunas
Cf = <<"testcf1">>, rocker:drop_cf(Db, Cf) -> ok.
Operações CRUD
Entrada de dados chave
rocker:put(Db, <<"key">>, <<"value">>) -> ok.
Obtendo dados por chave
rocker:get(Db, <<"key">>) -> {ok, <<"value">>} | notfound
Exclusão de dados importantes
rocker:delete(Db, <<"key">>) -> ok.
Entrada de dados chave no CF
rocker:put_cf(Db, <<"testcf">>, <<"key">>, <<"value">>) -> ok.
Recuperação de dados importantes no CF
rocker:get_cf(Db, <<"testcf">>, <<"key">>) -> {ok, <<"value">>} | notfound
Remoção de chave CF
rocker:delete_cf(Db, <<"testcf">>, <<"key">>) -> ok
Iteradores
Como você sabe, um dos princípios básicos do rocksdb é o armazenamento ordenado de chaves. Esse recurso é muito útil em tarefas reais. Para usá-lo, precisamos de iteradores de dados. O Rocksdb possui vários modos de transmissão de dados (exemplos de código detalhados podem ser encontrados nos testes ):
- Desde o início da tabela. O roqueiro é responsável por isso no iterador
{'start'}
- No final da tabela:
{'end'}
- A partir de um encaminhamento de chave específico
{'from', Key, forward}
- A partir de uma chave específica, retorne
{'from', Key, reverse}
Vale ressaltar que esses modos também funcionam para passar pelos dados armazenados nas famílias de colunas.
Crie um iterador
rocker:iterator(Db, {'start'}) -> {ok, Iter}.
Verificação do iterador
rocker:iterator_valid(Iter) -> {ok, true} | {ok, false}.
Criando um Iterador para CF
rocker:iterator_cf(Db, Cf, {'start'}) -> {ok, Iter}.
Criando um Iterador de Prefixo
O iterador de prefixo requer especificar explicitamente o tamanho do prefixo ao criar o banco de dados.
{ok, Db} = rocker:open(Path, #{ prefix_length => 3 }).
Um exemplo de criação de um iterador usando o prefixo "aaa":
{ok, Iter} = rocker:prefix_iterator(Db, <<"aaa">>).
Criando um Iterador de Prefixo para CF
Semelhante ao iterador de prefixo anterior, requer prefix_length
explícito para a família de colunas
{ok, Iter} = rocker:prefix_iterator_cf(Db, Cf, <<"aaa">>).
Obter o próximo item
O método retorna a próxima chave / valor, ou ok, se o iterador for concluído.
rocker:next(Iter) -> {ok, <<"key">>, <<"value">>} | ok
Transações
Uma ocorrência bastante comum é o requisito de registrar simultaneamente as alterações em um grupo de chaves. O Rocker permite combinar operações CRUD tanto dentro de um conjunto comum quanto no CF.
Este exemplo ilustra o trabalho com transações:
{ok, 6} = rocker:tx(Db, [ {put, <<"k1">>, <<"v1">>}, {put, <<"k2">>, <<"v2">>}, {delete, <<"k0">>, <<"v0">>}, {put_cf, Cf, <<"k1">>, <<"v1">>}, {put_cf, Cf, <<"k2">>, <<"v2">>}, {delete_cf, Cf, <<"k0">>, <<"v0">>} ]).
Desempenho
Você pode encontrar um teste de desempenho no conjunto de testes. Ele mostra cerca de 30k RPS para gravação e 200k RPS para leitura na minha máquina. Em condições reais, você pode esperar 15-20k RPS para gravação e cerca de 120k RPS para leitura, com um tamanho médio de dados de cerca de 1 Kb por chave e o número total de chaves é superior a 1 bilhão.
Conclusão
O desenvolvimento e a aplicação do Rocker em nosso projeto nos permitiram reduzir o tempo de resposta do sistema, aumentar a confiabilidade e reduzir o tempo de reinicialização. Essas vantagens foram obtidas com custos mínimos de desenvolvimento e implementação.
Concluí que, para projetos de Erlang que exigem otimização, o uso do Rust é ideal. Erlang consegue implementar rápida e eficientemente 95% do código, enquanto o Rust reescreve / adiciona 5% inibitórios sem comprometer a confiabilidade geral do sistema.
PS Existe uma experiência positiva no desenvolvimento da NIF para aritmética de precisão arbitrária em Erlang, que pode ser escrita em um artigo separado. Gostaria de esclarecer se o tópico do NIF sobre Rust é interessante para a comunidade?