etcd vs PostgreSQL
Jinhua Luo
March 17, 2023
Contexto Histórico
PostgreSQL
O PostgreSQL foi originalmente desenvolvido em 1986 sob a liderança do professor Michael Stonebraker na Universidade da Califórnia, Berkeley. Ao longo de várias décadas de desenvolvimento, o PostgreSQL emergiu como o principal sistema de gerenciamento de banco de dados relacional de código aberto disponível hoje. Sua licença permissiva permite que qualquer pessoa use, modifique e distribua o PostgreSQL livremente, independentemente de ser para fins privados, comerciais ou de pesquisa acadêmica.
O PostgreSQL oferece suporte robusto para processamento analítico online (OLAP) e processamento transacional online (OLTP), com capacidades poderosas de consulta SQL e uma ampla gama de extensões que permitem atender a quase todas as necessidades comerciais. Como resultado, tem ganhado cada vez mais atenção nos últimos anos. Na verdade, a escalabilidade e o alto desempenho do PostgreSQL permitem que ele replique a funcionalidade de praticamente qualquer outro tipo de banco de dados.
Fonte da imagem (sob licença CC 3.0 BY-SA): https://en.wikibooks.org/wiki/PostgreSQL/Architecture
etcd
Como o etcd surgiu e qual problema ele resolve?
Em 2013, a equipe de startup CoreOS desenvolveu um produto chamado Container Linux. É um sistema operacional leve e de código aberto que prioriza a automação e a implantação rápida de serviços de aplicativos. O Container Linux exige que os aplicativos sejam executados em contêineres e fornece uma solução de gerenciamento de cluster, facilitando a gestão de serviços como se estivessem em uma única máquina.
Para garantir que os serviços dos usuários não sofressem interrupções devido à reinicialização de um nó, a CoreOS precisava executar várias réplicas. Mas como coordenar entre várias réplicas e evitar que todas se tornassem indisponíveis durante mudanças?
Para resolver esse problema, a equipe da CoreOS precisava de um serviço de coordenação que pudesse armazenar informações de configuração de serviços e fornecer capacidades de bloqueio distribuído, entre outras coisas. Então, qual foi a abordagem deles? Eles primeiro analisaram o cenário de negócios, os pontos problemáticos e os objetivos principais. Em seguida, selecionaram uma solução que se alinhava aos seus objetivos, avaliando se deveriam escolher uma solução da comunidade de código aberto ou desenvolver sua própria ferramenta personalizada. Essa abordagem é um método universal de resolução de problemas que é frequentemente empregado ao enfrentar desafios complexos, e a equipe da CoreOS seguiu o mesmo princípio.
Um serviço de coordenação ideal precisa atender aos seguintes cinco objetivos:
- Alta disponibilidade com múltiplas réplicas de dados
- Consistência de dados com verificação de versão entre réplicas
- Capacidade mínima de armazenamento: o serviço de coordenação deve armazenar apenas informações críticas de configuração de metadados para serviços e nós pertencentes à configuração do plano de controle, em vez de dados relacionados ao usuário. Essa abordagem minimiza a necessidade de fragmentação de dados para armazenamento e evita projetos excessivos.
- Funcionalidades para CRUD (criar, ler, atualizar e excluir), bem como um mecanismo para monitorar mudanças nos dados. Ele deve armazenar as informações de status dos serviços e, quando houver mudanças ou anomalias nos serviços, deve enviar rapidamente o evento de mudança para o plano de controle. Isso ajuda a melhorar a disponibilidade do serviço e reduzir a sobrecarga desnecessária de desempenho para o serviço de coordenação.
- Simplicidade operacional: o serviço de coordenação deve ser fácil de operar, manter e solucionar problemas. Uma interface fácil de usar pode reduzir o risco de erros, diminuir os custos de manutenção e minimizar o tempo de inatividade.
Do ponto de vista do Teorema CAP, o etcd pertence ao sistema CP (Consistência e Tolerância a Partições).
Como componente central de um cluster Kubernetes, o kube-apiserver usa o etcd como seu armazenamento subjacente.
Por um lado, o etcd é usado para persistência na criação de objetos de recursos em um cluster k8s. Por outro lado, é o mecanismo de monitoramento de dados do etcd que impulsiona o trabalho do Informer de todo o cluster, permitindo a orquestração contínua de contêineres.
Portanto, do ponto de vista técnico, as principais razões pelas quais o Kubernetes usa o etcd são:
- O etcd é escrito em Go, que é consistente com a pilha tecnológica do k8s, tem baixo consumo de recursos e é extremamente fácil de implantar.
- A forte consistência, o monitoramento (watch), o lease e outras funcionalidades do etcd são dependências principais do k8s.
Em resumo, o etcd é um banco de dados distribuído de chave-valor projetado especificamente para gerenciamento e distribuição de configurações. Como um software nativo da nuvem, ele oferece usabilidade pronta para uso e alto desempenho, tornando-o superior aos bancos de dados tradicionais nessa área específica de necessidade.
Para fazer uma comparação objetiva entre o etcd e o PostgreSQL, que são dois tipos diferentes de bancos de dados, é importante avaliá-los no contexto da mesma necessidade. Portanto, este artigo discutirá apenas as diferenças entre os dois em termos de sua capacidade de atender aos requisitos de gerenciamento de configurações.
Modelo de Dados
Diferentes bancos de dados têm diferentes modelos de dados que apresentam aos usuários, e esse fator determina a adequação do banco de dados para vários cenários.
Chave-valor vs SQL
O modelo de dados chave-valor é um modelo popular no NoSQL, que também é adotado pelo etcd. Como esse modelo se compara ao SQL e quais são suas vantagens?
Primeiro, vamos dar uma olhada no SQL.
Bancos de dados relacionais mantêm os dados em tabelas e fornecem uma maneira eficiente, intuitiva e flexível de armazenar e acessar informações estruturadas.
Uma tabela, também conhecida como relação, é composta por colunas que contêm uma ou mais categorias de dados, e linhas, também conhecidas como registros da tabela, que incluem um conjunto de dados que define as categorias. Aplicativos recuperam dados usando consultas que empregam operações como "projeção" para identificar atributos, "seleção" para identificar tuplas e "junção" para combinar relações. O modelo relacional para gerenciamento de bancos de dados foi desenvolvido em 1970 por Edgar Codd, um cientista da computação da IBM.
Fonte da imagem (em conformidade com a licença CC 3.0 BY-SA): https://en.wikipedia.org/wiki/Associative_entity
Registros em uma tabela não têm identificadores únicos porque as tabelas são projetadas para acomodar várias linhas duplicadas. Para permitir consultas chave-valor, um índice único deve ser adicionado ao campo que serve como chave na tabela. O índice padrão do PostgreSQL é o btree, que, semelhante ao etcd, pode realizar consultas de intervalo em chaves.
A linguagem de consulta estruturada (SQL) é uma linguagem de programação para armazenar e processar informações em um banco de dados relacional. Um banco de dados relacional armazena informações em forma tabular, com linhas e colunas representando diferentes atributos de dados e as várias relações entre os valores dos dados. Você pode usar instruções SQL para armazenar, atualizar, remover, pesquisar e recuperar informações do banco de dados. Você também pode usar o SQL para manter e otimizar o desempenho do banco de dados.
O PostgreSQL expandiu o SQL com inúmeras extensões, tornando-o uma linguagem Turing-completa. Isso significa que o SQL pode realizar qualquer operação complexa, facilitando a execução da lógica de processamento de dados inteiramente no lado do servidor.
Em comparação, o etcd é projetado como uma ferramenta de gerenciamento de configurações, com dados de configuração geralmente representados como uma tabela hash. É por isso que seu modelo de dados é estruturado como um formato chave-valor, criando efetivamente uma única grande tabela global. Operações CRUD podem ser realizadas nessa tabela, que possui apenas dois campos: uma chave única com informações de versão e um valor não tipado. Como resultado, os clientes devem recuperar o valor completo para processamento adicional.
No geral, a estrutura chave-valor do etcd simplifica o SQL e é mais conveniente e intuitiva para a tarefa específica de gerenciamento de configurações.
MVCC (Controle de Concorrência Multiversão)
O MVCC é um recurso essencial para o versionamento de dados no gerenciamento de configurações. Ele permite:
- Consultar dados históricos
- Determinar a idade dos dados comparando versões
- Monitorar dados, o que requer versionamento para permitir notificações incrementais
Tanto o etcd quanto o PostgreSQL têm MVCC, mas quais são as diferenças entre eles?
O etcd usa um contador de versão 64-bit globalmente incrementado para gerenciar seu sistema MVCC. Não há necessidade de se preocupar com estouro. O contador é projetado para lidar com um grande número de atualizações, mesmo que ocorram a uma taxa de milhões por segundo. Cada vez que um par chave-valor é criado ou atualizado, ele recebe um número de versão. Quando um par chave-valor é excluído, uma lápide (tombstone) é criada com um número de versão redefinido para 0. Isso significa que cada alteração produz uma nova versão, em vez de sobrescrever a anterior.
Além disso, o etcd retém todas as versões de um par chave-valor e as torna visíveis para os usuários. Os dados chave-valor nunca são sobrescritos, e novas versões são armazenadas junto com as existentes. A implementação do MVCC no etcd também fornece separação de leitura e escrita, o que permite que os usuários leiam dados sem bloqueio, tornando-o adequado para casos de uso intensivo de leitura.
A implementação do MVCC no PostgreSQL difere da do etcd, pois não se concentra em fornecer números de versão incrementados, mas sim em implementar transações e diferentes níveis de isolamento de forma transparente para o usuário. O MVCC é um mecanismo de bloqueio otimista que permite atualizações concorrentes. Cada linha em uma tabela tem um registro de ID de transação, com xmin
representando o ID da transação de criação da linha e xmax
representando o ID da transação de atualização da linha.
- As transações só podem ler dados que já foram confirmados antes delas.
- Ao atualizar dados, se um conflito de versão for encontrado, o PostgreSQL tentará novamente com um mecanismo de correspondência para determinar se a atualização deve prosseguir.
Para ver um exemplo, consulte o seguinte link: https://devcenter.heroku.com/articles/postgresql-concurrency
Infelizmente, usar IDs de transação para controle de versão de dados de configuração no PostgreSQL não é possível por várias razões:
- Os IDs de transação são atribuídos a todas as linhas envolvidas na mesma transação, o que significa que o controle de versão não pode ser aplicado no nível de linha.
- Consultas históricas não podem ser realizadas, e apenas a versão mais recente de uma linha pode ser acessada.
- Devido à sua natureza de contador de 32 bits, os IDs de transação são propensos a estouro e redefinição durante a limpeza (vacuum).
- Não é possível implementar funcionalidades de monitoramento (watch) com base em IDs de transação.
Como resultado, o PostgreSQL requer métodos alternativos para o controle de versão de dados de configuração, já que o suporte embutido não está disponível.
Interface do Cliente
O design de uma interface de cliente é um aspecto crítico quando se trata de determinar o custo e o consumo de recursos associados ao seu uso. Ao analisar as diferenças entre as interfaces, é possível fazer escolhas informadas ao selecionar a opção mais adequada.
As APIs de kv/watch/lease do etcd provaram ser particularmente eficazes no gerenciamento de configurações. No entanto, como implementar essas APIs no PostgreSQL?
Infelizmente, o PostgreSQL não fornece suporte embutido para essas APIs, e é necessário encapsulamento para implementá-las. Para analisar sua implementação, examinaremos o projeto pg_watch_demo desenvolvido por mim: pg_watch_demo.
gRPC/HTTP vs TCP
O PostgreSQL segue uma arquitetura de múltiplos processos, onde cada processo lida com apenas uma conexão TCP por vez. Ele usa um protocolo personalizado para fornecer funcionalidades por meio de consultas SQL e segue um modelo de interação de solicitação-resposta (semelhante ao HTTP/1.1, que lida com apenas uma solicitação por vez e requer pipeline para processar várias solicitações simultaneamente). No entanto, dado o alto consumo de recursos e a eficiência relativamente baixa, um proxy de pool de conexões (como pgbouncer) é crucial para melhorar o desempenho, especialmente em cenários com alto QPS.
Por outro lado, o etcd é projetado em uma arquitetura de múltiplas corrotinas em Golang e oferece duas interfaces amigáveis ao usuário: gRPC e RESTful. Essas interfaces são fáceis de integrar e são eficientes em termos de consumo de recursos. Além disso, cada conexão gRPC pode lidar com várias consultas simultâneas, o que garante um desempenho ideal.
Definindo Dados
etcd
message KeyValue {
bytes key = 1;
// Número de revisão quando a chave foi criada
int64 create_revision = 2;
// Número de revisão quando a chave foi modificada pela última vez
int64 mod_revision = 3;
// Contador incrementado que aumenta toda vez que a chave é atualizada.
// Este contador é redefinido para zero quando a chave é excluída e é usado como uma lápide.
int64 version = 4;
bytes value = 5;
// O objeto de lease usado pela chave para TTL. Se o valor for 0, então não há TTL.
int64 lease = 6;
}
PostgreSQL
O PostgreSQL precisa usar uma tabela para simular o espaço de dados global do etcd:
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
-- Equivalente a `create_revision` e `mod_revision`
-- Aqui, um tipo de sequência de incremento de inteiro grande é usado para simular a revisão
revision bigserial,
-- Lápide
tombstone boolean NOT NULL DEFAULT false,
-- Índice composto, pesquisa por chave primeiro, depois por revisão
primary key(key, revision)
);
get
etcd
A API get
do etcd tem uma ampla gama de parâmetros:
- Consultas de intervalo, por exemplo, definir
key
como/abc
erange_end
como/abd
recuperará todos os pares chave-valor com/abc
como prefixo. - Consultas históricas, especificando
revision
ou um intervalo demod_revision
. - Ordenação e limitação do número de resultados retornados.
message RangeRequest {
...
bytes key = 1;
// Consultas de intervalo
bytes range_end = 2;
int64 limit = 3;
// Consultas históricas
int64 revision = 4;
// Ordenação
SortOrder sort_order = 5;
SortTarget sort_target = 6;
bool serializable = 7;
bool keys_only = 8;
bool count_only = 9;
// Consultas históricas
int64 min_mod_revision = 10;
int64 max_mod_revision = 11;
int64 min_create_revision = 12;
int64 max_create_revision = 13;
}
PostgreSQL
O PostgreSQL pode realizar a função get do etcd por meio de SQL, e até fornecer funcionalidades mais complexas. Como o SQL em si é uma linguagem e não uma interface de parâmetros fixos, ele é altamente versátil. Aqui mostramos um exemplo simples de recuperação do par chave-valor mais recente. Como a chave primária é um índice composto, ele pode ser rapidamente pesquisado por intervalo, resultando em uma recuperação de alta velocidade.
CREATE FUNCTION get1(kk text)
RETURNS table(r bigint, k text, v text, c bigint) AS $$
SELECT revision, key, value, create_time
FROM config
where key = kk and tombstone = false
ORDER BY key, revision desc
limit 1
$$ LANGUAGE sql;
put
etcd
message PutRequest {
bytes key = 1;
bytes value = 2;
int64 lease = 3;
// se deve responder com os dados do par chave-valor antes da atualização desta solicitação `Put`.
bool prev_kv = 4;
bool ignore_value = 5;
bool ignore_lease = 6;
}
PostgreSQL
Assim como no etcd, o PostgreSQL não executa alterações no local. Em vez disso, uma nova linha é inserida e uma nova revisão é atribuída a ela.
CREATE FUNCTION set(k text, v text) RETURNS bigint AS $$
insert into config(key, value) values(k, v) returning revision;
$$ LANGUAGE SQL;
delete
etcd
message DeleteRangeRequest {
bytes key = 1;
bytes range_end = 2;
bool prev_kv = 3;
}
PostgreSQL
Semelhante ao etcd, a exclusão no PostgreSQL não modifica os dados no local. Em vez disso, uma nova linha é inserida com o campo tombstone definido como true para indicar que é uma lápide.
CREATE FUNCTION del(k text) RETURNS bigint AS $$
insert into config(key, tombstone) values(k, true) returning revision;
$$ LANGUAGE SQL;
watch
etcd
message WatchCreateRequest {
bytes key = 1;
// Especifica o intervalo de chaves a serem monitoradas
bytes range_end = 2;
// Revisão inicial para o monitoramento
int64 start_revision = 3;
...
}
message WatchResponse {
ResponseHeader header = 1;
...
// Para eficiência, vários eventos podem ser retornados
repeated mvccpb.Event events = 11;
}
PostgreSQL
O PostgreSQL não vem com uma função de monitoramento embutida e, em vez disso, requer uma combinação de gatilhos e canais para alcançar funcionalidades semelhantes. Usando pg_notify
, os dados podem ser enviados para todos os aplicativos que estão ouvindo um canal específico.
-- função de gatilho para distribuir eventos de put/delete
CREATE FUNCTION notify_config_change() RETURNS TRIGGER AS $$
DECLARE
data json;
channel text;
is_channel_exist boolean;
BEGIN
IF (TG_OP = 'INSERT') THEN
-- usar JSON para codificar
data = row_to_json(NEW);
-- Extrair o nome do canal para distribuição da chave
channel = (SELECT SUBSTRING(NEW.key, '/(.*)/'));
-- Se um aplicativo estiver monitorando o canal, enviar um evento por meio dele
is_channel_exist = NOT pg_try_advisory_lock(9080);
IF is_channel_exist THEN
PERFORM pg_notify(channel, data::text);
ELSE
PERFORM pg_advisory_unlock(9080);
END IF;
END IF;
RETURN NULL; -- O resultado é ignorado, pois este é um gatilho AFTER
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER notify_config_change
AFTER INSERT ON config
FOR EACH ROW EXECUTE FUNCTION notify_config_change();
Como a funcionalidade de monitoramento é encapsulada, os aplicativos clientes também devem implementar a lógica correspondente. Usando Golang como exemplo, as seguintes etapas devem ser seguidas:
- Iniciar a escuta: Quando a escuta começa, todos os dados de notificação serão armazenados em cache tanto no PostgreSQL quanto no nível do canal Golang.
- Recuperar todos os dados usando get_all(key_prefix, revision): Esta função lê todos os dados existentes a partir da revisão especificada. Para cada chave, apenas os dados da revisão mais recente serão retornados, com quaisquer dados excluídos automaticamente removidos. Se a revisão não for especificada, ela retorna os dados mais recentes para todas as chaves com o
key_prefix
fornecido. - Monitorar novos dados, incluindo quaisquer notificações que possam ter sido armazenadas em cache entre a primeira e a segunda etapa, para evitar perder quaisquer novos dados que possam ocorrer durante essa janela de tempo. Ignorar quaisquer revisões que já foram lidas na segunda etapa.
func watch(l *pq.Listener) {
for {
select {
case n := <-l.Notify:
if n == nil {
log.Println("listener reconectado")
log.Printf("obter todas as rotas a partir da rev %d incluindo lápides...\n", latestRev)
// Ao reconectar, retomar a transmissão com base na revisão antes da desconexão.
str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev)
rows, err := db.Query(str)
...
continue
}
...
// manter um estado que registra a revisão mais recente que recebeu
updateRoute(cfg)
case <-time.After(15 * time.Second):
log.Println("Nenhum evento recebido por 15 segundos, verificando conexão")
go func() {
// Se nenhum evento for recebido por um período prolongado, verificar a saúde da conexão
if err := l.Ping(); err != nil {
log.Println("erro de ping do listener: ", err)
}
}()
}
}
}
log.Println("obter todas as rotas...")
// Ao inicializar, o aplicativo deve obter todos os pares chave-valor atuais e, em seguida, monitorar incrementalmente as atualizações por meio do watch
rows, err := db.Query(`select * from get_all('/routes/')`)
...
go watch(listener)
transação
etcd
As transações do etcd são uma coleção de várias operações com verificações condicionais, e as modificações feitas pela transação são confirmadas atomicamente.
message TxnRequest {
// Especificar a condição de execução da transação
repeated Compare compare = 1;
// Operações a serem executadas se a condição for atendida
repeated RequestOp success = 2;
// Operações a serem executadas se a condição não for atendida
repeated RequestOp failure = 3;
}
PostgreSQL
O comando DO
no PostgreSQL permite a execução de qualquer comando, incluindo procedimentos armazenados. Ele suporta várias linguagens, incluindo linguagens embutidas como PL/pgSQL e Python. Com essas linguagens, qualquer julgamento condicional, loops e outras lógicas de controle podem ser implementadas, tornando-o mais versátil que o etcd.
DO LANGUAGE plpgsql $$
DECLARE
n_plugins int;
BEGIN
SELECT COUNT(1) INTO n_plugins FROM get_all('/plugins/');
IF n_plugins = 0 THEN
perform set('/routes/1', 'foobar');
perform set('/upstream/1', 'foobar');
...
ELSE
...
END IF;
END;
$$;
lease
etcd
No etcd, é possível criar um objeto de lease que os aplicativos devem renovar periodicamente para evitar que expire. Cada par chave-valor pode ser vinculado a um objeto de lease, e quando o objeto de lease expira, todos os pares chave-valor associados também expiram, excluindo-os automaticamente.
message LeaseGrantRequest {
// TTL do lease
int64 TTL = 1;
int64 ID = 2;
}
// Renovação do lease
message LeaseKeepAliveRequest {
int64 ID = 1;
}
message PutRequest {
bytes key = 1;
bytes value = 2;
// ID do lease, usado para implementar TTL
int64 lease = 3;
...
}
PostgreSQL
- No PostgreSQL, um lease pode ser mantido por meio de uma chave estrangeira. Ao consultar, se houver um objeto de lease associado que expirou, ele é considerado uma lápide.
- As solicitações de keepalive atualizam o carimbo de data/hora
last_keepalive
na tabela de lease.
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
...
-- Usar uma chave estrangeira para especificar o objeto de lease associado.
lease int64 references lease(id),
);
CREATE TABLE IF NOT EXISTS lease (
id text,
ttl int,
last_keepalive timestamp;
);
Comparação de Desempenho
O PostgreSQL precisa simular várias APIs do etcd por meio de encapsulamento. Então, como é o seu desempenho? Aqui estão os resultados de um teste simples: https://github.com/kingluo/pg_watch_demo#benchmark.
Os resultados mostram que o desempenho de leitura e escrita é quase idêntico, com o PostgreSQL até superando o etcd. Além disso, a latência desde a ocorrência de uma atualização até o aplicativo receber o evento determina a eficiência da distribuição da atualização, e tanto o PostgreSQL quanto o etcd têm desempenho semelhante. Quando testados na mesma máquina para o cliente e o servidor, a latência do watch foi inferior a 1 milissegundo.
O PostgreSQL, no entanto, tem algumas desvantagens que valem a pena mencionar:
- O log WAL para cada atualização é maior, resultando em duas vezes mais I/O de disco em comparação com o etcd.
- Consome mais CPU em comparação com o etcd.
- O Notify baseado em canais é um conceito de nível de transação. Ao atualizar o mesmo tipo de recurso, a atualização é enviada para o mesmo canal, e as solicitações de atualização competem por bloqueios de exclusão mútua, resultando em solicitações serializadas. Em outras palavras, usar canais para implementar o watch afetará o paralelismo das operações de put.
Isso destaca que, para alcançar os mesmos requisitos, precisamos investir mais em aprendizado e otimização do PostgreSQL.
Armazenamento
O desempenho é determinado pelo armazenamento subjacente, e como os dados são armazenados determina os requisitos de memória, disco e outros recursos do banco de dados.
etcd
Diagrama de arquitetura do armazenamento do etcd:
O etcd primeiro grava as atualizações no log de write-ahead (WAL) e as descarrega no disco para garantir que as atualizações não sejam perdidas. Uma vez que o log é gravado com sucesso e confirmado pela maioria dos nós, os resultados podem ser retornados ao cliente. O etcd também atualiza assincronamente o TreeIndex e o BoltDB.
Para evitar que o log cresça infinitamente, o etcd periodicamente tira um snapshot do armazenamento, e os logs anteriores ao snapshot podem ser excluídos.
O etcd indexa todas as chaves na memória (TreeIndex), registrando as informações de versão de cada chave, mas mantém apenas um ponteiro para o BoltDB (revisão) para o valor.
O valor correspondente à chave é armazenado no disco e mantido usando o BoltDB.
Tanto o TreeIndex quanto o BoltDB usam a estrutura de dados btree, conhecida por sua eficiência em pesquisas e pesquisas de intervalo.
Diagrama da estrutura do TreeIndex:
(Fonte da imagem: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado sob CC 4.0 BY-SA)
Cada chave é dividida em diferentes gerações, com cada exclusão marcando o fim de uma geração.
O ponteiro para o valor é composto por dois inteiros. O primeiro inteiro main
é o ID da transação do etcd, enquanto o segundo inteiro sub
representa o ID da atualização desta chave dentro dessa transação.
O Boltdb suporta transações e snapshots, e armazena o valor correspondente à revisão.
(Fonte da imagem: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado sob CC 4.0 BY-SA)
Exemplo de escrita de dados:
Escrevendo key="key1", revision=(12,1), value="keyvalue5"
. Observe as mudanças nas partes vermelhas do treeIndex e do BoltDB:
(Fonte da imagem: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado sob CC 4.0 BY-SA)
Excluindo key="key", revision=(13,1)
cria uma nova geração vazia no treeIndex e gera um valor vazio no BoltDB com key="13_1t"
.
Aqui, o t
significa "tombstone". Isso implica que você não pode ler a lápide porque o ponteiro no treeIndex é (13,1)
, mas no BoltDB, é 13_1t
, que não pode ser correspondido.
(Fonte da imagem: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado sob CC 4.0 BY-SA)
Vale a pena notar que o etcd agenda tanto leituras quanto escritas para o BoltDB usando uma única goroutine para reduzir o I/O de disco aleatório e melhorar o desempenho de I/O.
PostgreSQL
Diagrama de arquitetura do armazenamento do PostgreSQL:
Semelhante ao etcd, o PostgreSQL anexa as atualizações a um arquivo de log primeiro e espera que o log seja descarregado com sucesso no disco antes de considerar a transação concluída. Enquanto isso, as atualizações são gravadas na memória shared_buffer.
O shared_buffer é uma área de memória compartilhada por todas as tabelas e índices no PostgreSQL e serve como um mapeamento para esses objetos.
No PostgreSQL, cada tabela consiste em várias páginas, com cada página tendo 8 KB de tamanho e contendo várias linhas.
Além das tabelas, os índices (como índices btree) também são compostos por páginas de tabela no mesmo formato. No entanto, essas páginas são especiais e são interconectadas para formar uma estrutura de árvore.
O PostgreSQL é equipado com um processo de checkpoint que periodicamente descarrega todas as páginas de tabela e índice modificadas no disco. Antes de cada checkpoint, os arquivos de log podem ser excluídos e reciclados para evitar que o log cresça indefinidamente.
Estrutura da página:
(Fonte da imagem: https://en.wikibooks.org/wiki/PostgreSQL/Page_Layout, licenciado sob CC 3.0 BY-SA)
Estrutura do índice btree:
![Índice btree do PostgreSQL](https://static.api7.ai/uploads/2023/02/22