etcd vs PostgreSQL
Jinhua Luo
March 17, 2023
Antecedentes Históricos
PostgreSQL
PostgreSQL fue desarrollado originalmente en 1986 bajo el liderazgo del profesor Michael Stonebraker en la Universidad de California, Berkeley. A lo largo de varias décadas de desarrollo, PostgreSQL se ha convertido en el sistema de gestión de bases de datos relacionales de código abierto líder disponible hoy en día. Su licencia permisiva permite a cualquier persona usar, modificar y distribuir PostgreSQL libremente, ya sea para fines privados, comerciales o de investigación académica.
PostgreSQL ofrece un sólido soporte tanto para el procesamiento analítico en línea (OLAP) como para el procesamiento de transacciones en línea (OLTP), con potentes capacidades de consulta SQL y una amplia gama de extensiones que le permiten satisfacer casi todas las necesidades comerciales. Como resultado, ha ganado una atención creciente en los últimos años. De hecho, la escalabilidad y el alto rendimiento de PostgreSQL le permiten replicar la funcionalidad de prácticamente cualquier otro tipo de base de datos.
Fuente de la imagen (siguiendo el acuerdo de licencia CC 3.0 BY-SA): https://en.wikibooks.org/wiki/PostgreSQL/Architecture
etcd
¿Cómo surgió etcd y qué problema resuelve?
En 2013, el equipo de la startup CoreOS desarrolló un producto llamado Container Linux. Es un sistema operativo ligero y de código abierto que prioriza la automatización y la implementación rápida de servicios de aplicaciones. Container Linux requiere que las aplicaciones se ejecuten en contenedores y proporciona una solución de gestión de clústeres, lo que facilita a los usuarios la gestión de servicios como si estuvieran en una sola máquina.
Para garantizar que los servicios de los usuarios no experimentaran tiempo de inactividad debido a un reinicio del nodo, CoreOS necesitaba ejecutar múltiples réplicas. Pero, ¿cómo coordinarían entre múltiples réplicas y evitarían que todas las réplicas se volvieran inaccesibles durante los cambios?
Para abordar este problema, el equipo de CoreOS necesitaba un servicio de coordinación que pudiera almacenar información de configuración del servicio y proporcionar capacidades de bloqueo distribuido, entre otras cosas. Entonces, ¿cuál fue su enfoque? Primero analizaron el escenario comercial, los puntos de dolor y los objetivos principales. Luego, seleccionaron una solución que se alineara con sus objetivos, evaluando si elegir una solución de la comunidad de código abierto o desarrollar su propia herramienta personalizada. Este enfoque es un método universal de resolución de problemas que a menudo se emplea cuando se enfrentan problemas desafiantes, y el equipo de CoreOS siguió el mismo principio.
Un servicio de coordinación idealmente necesita cumplir los siguientes cinco objetivos:
- Alta disponibilidad con múltiples réplicas de datos
- Consistencia de datos con verificación de versiones entre réplicas
- Capacidad de almacenamiento mínima: el servicio de coordinación debe almacenar solo información de configuración de metadatos críticos para servicios y nodos pertenecientes a la configuración del plano de control, en lugar de datos relacionados con el usuario. Este enfoque minimiza la necesidad de fragmentación de datos para el almacenamiento y evita un diseño excesivo.
- Funcionalidad para CRUD (crear, leer, actualizar y eliminar), así como un mecanismo para escuchar cambios en los datos. Debe almacenar la información de estado de los servicios, y cuando haya cambios o anomalías en los servicios, debe enviar rápidamente el evento de cambio al plano de control. Esto ayuda a mejorar la disponibilidad del servicio y reduce el sobrecosto de rendimiento innecesario para el servicio de coordinación.
- Simplicidad operativa: el servicio de coordinación debe ser fácil de operar, mantener y solucionar problemas. Una interfaz fácil de usar puede reducir el riesgo de errores, disminuir los costos de mantenimiento y minimizar el tiempo de inactividad.
Desde la perspectiva del Teorema CAP, etcd pertenece al sistema CP (Consistencia y Tolerancia a Particiones).
Como componente central de un clúster de Kubernetes, kube-apiserver utiliza etcd como su almacenamiento subyacente.
Por un lado, etcd se utiliza para la persistencia en la creación de objetos de recursos en un clúster de k8s. Por otro lado, es el mecanismo de observación de datos de etcd lo que impulsa el trabajo de Informer de todo el clúster, permitiendo la orquestación continua de contenedores.
Por lo tanto, desde una perspectiva técnica, las razones principales por las que Kubernetes utiliza etcd son:
- etcd está escrito en el lenguaje Go, que es consistente con la pila tecnológica de k8s, tiene un bajo consumo de recursos y es extremadamente fácil de implementar.
- La fuerte consistencia, la observación, el arrendamiento y otras características de etcd son dependencias principales de k8s.
En resumen, etcd es una base de datos de clave-valor distribuida diseñada específicamente para la gestión y distribución de configuraciones. Como software nativo de la nube, ofrece usabilidad inmediata y alto rendimiento, lo que lo hace superior a las bases de datos tradicionales en esta área específica de necesidad.
Para hacer una comparación objetiva entre etcd y PostgreSQL, que son dos tipos diferentes de bases de datos, es importante evaluarlas en el contexto del mismo requisito. Por lo tanto, este artículo solo discutirá las diferencias entre los dos en términos de su capacidad para cumplir con los requisitos de gestión de configuraciones.
Modelo de Datos
Diferentes bases de datos tienen diferentes modelos de datos que presentan a los usuarios, y este factor determina la idoneidad de la base de datos para varios escenarios.
Clave-valor vs SQL
El modelo de datos clave-valor es un modelo popular en NoSQL, que también es adoptado por etcd. ¿Cómo se compara este modelo con SQL y cuáles son sus ventajas?
Primero, echemos un vistazo a SQL.
Las bases de datos relacionales mantienen los datos en tablas y proporcionan una forma eficiente, intuitiva y flexible de almacenar y acceder a información estructurada.
Una tabla, también conocida como relación, está compuesta por columnas que contienen una o más categorías de datos, y filas, también conocidas como registros de tabla, que incluyen un conjunto de datos que define las categorías. Las aplicaciones recuperan datos mediante consultas que emplean operaciones como "proyectar" para identificar atributos, "seleccionar" para identificar tuplas y "unir" para combinar relaciones. El modelo relacional para la gestión de bases de datos fue desarrollado en 1970 por Edgar Codd, un científico informático de IBM.
Fuente de la imagen (cumpliendo con el acuerdo de licencia CC 3.0 BY-SA): https://en.wikipedia.org/wiki/Associative_entity
Los registros en una tabla no tienen identificadores únicos porque las tablas están diseñadas para acomodar múltiples filas duplicadas. Para habilitar consultas clave-valor, se debe agregar un índice único al campo que sirve como clave en la tabla. El índice predeterminado de PostgreSQL es btree, que, similar a etcd, puede realizar consultas de rango en claves.
El lenguaje de consulta estructurado (SQL) es un lenguaje de programación para almacenar y procesar información en una base de datos relacional. Una base de datos relacional almacena información en forma tabular, con filas y columnas que representan diferentes atributos de datos y las diversas relaciones entre los valores de los datos. Puede usar declaraciones SQL para almacenar, actualizar, eliminar, buscar y recuperar información de la base de datos. También puede usar SQL para mantener y optimizar el rendimiento de la base de datos.
PostgreSQL ha ampliado SQL con numerosas extensiones, convirtiéndolo en un lenguaje Turing-completo. Esto significa que SQL puede realizar cualquier operación compleja, facilitando la ejecución de la lógica de procesamiento de datos completamente en el lado del servidor.
En comparación, etcd está diseñado como una herramienta de gestión de configuraciones, con datos de configuración típicamente representados como una tabla hash. Es por eso que su modelo de datos está estructurado en formato clave-valor, creando efectivamente una única tabla global grande. Se pueden realizar operaciones CRUD en esta tabla, que tiene solo dos campos: una clave única con información de versión y un valor sin tipo. Como resultado, los clientes deben recuperar el valor completo para su posterior procesamiento.
En general, la estructura clave-valor de etcd simplifica SQL y es más conveniente e intuitiva para la tarea específica de gestión de configuraciones.
MVCC (Control de Concurrencia Multi-Versión)
MVCC es una característica esencial para el versionado de datos en la gestión de configuraciones. Permite:
- Consultar datos históricos
- Determinar la antigüedad de los datos comparando versiones
- Observar datos, lo que requiere el versionado para habilitar notificaciones incrementales
Tanto etcd como PostgreSQL tienen MVCC, pero ¿cuáles son las diferencias entre ellos?
etcd utiliza un contador de versión de 64 bits globalmente incremental para gestionar su sistema MVCC. No hay necesidad de preocuparse por el desbordamiento. El contador está diseñado para manejar un gran número de actualizaciones, incluso si ocurren a un ritmo de millones por segundo. Cada vez que se crea o actualiza un par clave-valor, se le asigna un número de versión. Cuando se elimina un par clave-valor, se crea una tumba con un número de versión restablecido a 0. Esto significa que cada cambio produce una nueva versión, en lugar de sobrescribir la anterior.
Además, etcd retiene todas las versiones de un par clave-valor y las hace visibles para los usuarios. Los datos clave-valor nunca se sobrescriben, y las nuevas versiones se almacenan junto con las existentes. La implementación de MVCC en etcd también proporciona separación de lectura-escritura, lo que permite a los usuarios leer datos sin bloqueos, haciéndolo adecuado para casos de uso intensivos en lectura.
La implementación de MVCC en PostgreSQL difiere de la de etcd en que no se centra en proporcionar números de versión incrementales, sino en implementar transacciones y diferentes niveles de aislamiento de manera transparente para el usuario. MVCC es un mecanismo de bloqueo optimista que permite actualizaciones concurrentes. Cada fila en una tabla tiene un registro de ID de transacción, con xmin
representando el ID de transacción de la creación de la fila y xmax
representando el ID de transacción de la actualización de la fila.
- Las transacciones solo pueden leer datos que ya se han comprometido antes que ellas.
- Al actualizar datos, si se encuentra un conflicto de versión, PostgreSQL reintentará con un mecanismo de coincidencia para determinar si la actualización debe continuar.
Para ver un ejemplo, consulte el siguiente enlace: https://devcenter.heroku.com/articles/postgresql-concurrency
Desafortunadamente, usar IDs de transacción para el control de versiones de datos de configuración en PostgreSQL no es posible por varias razones:
- Los IDs de transacción se asignan a todas las filas involucradas en la misma transacción, lo que significa que el control de versiones no se puede aplicar a nivel de fila.
- No se pueden realizar consultas históricas, y solo se puede acceder a la última versión de una fila.
- Debido a su naturaleza de contador de 32 bits, los IDs de transacción son propensos a desbordarse y restablecerse durante la limpieza.
- No es posible implementar funcionalidad de observación basada en IDs de transacción.
Como resultado, PostgreSQL requiere métodos alternativos para el control de versiones de datos de configuración, ya que no hay soporte incorporado.
Interfaz del Cliente
El diseño de una interfaz de cliente es un aspecto crítico cuando se trata de determinar el costo y el consumo de recursos asociados con su uso. Al analizar las diferencias entre las interfaces, se pueden tomar decisiones informadas al seleccionar la opción más adecuada.
Las API de kv/watch/lease de etcd han demostrado ser particularmente hábiles para gestionar configuraciones. Sin embargo, ¿cómo se pueden implementar estas API en PostgreSQL?
Desafortunadamente, PostgreSQL no proporciona soporte incorporado para estas API, y es necesario encapsularlas para implementarlas. Para analizar su implementación, examinaremos el proyecto pg_watch_demo desarrollado por mí: pg_watch_demo.
gRPC/HTTP vs TCP
PostgreSQL sigue una arquitectura de múltiples procesos, donde cada proceso maneja solo una conexión TCP a la vez. Utiliza un protocolo personalizado para ofrecer funcionalidad mediante consultas SQL y sigue un modelo de interacción de solicitud-respuesta (similar a HTTP/1.1, que maneja solo una solicitud a la vez y requiere canalización para procesar múltiples solicitudes simultáneamente). Sin embargo, dado el alto consumo de recursos y la eficiencia relativamente baja, un proxy de grupo de conexiones (como pgbouncer) es crucial para mejorar el rendimiento, especialmente en escenarios con alto QPS.
Por otro lado, etcd está diseñado en una arquitectura de múltiples corrutinas en Golang y ofrece dos interfaces fáciles de usar: gRPC y RESTful. Estas interfaces son fáciles de integrar y son eficientes en términos de consumo de recursos. Además, cada conexión gRPC puede manejar múltiples consultas concurrentes, lo que garantiza un rendimiento óptimo.
Definición de Datos
etcd
message KeyValue {
bytes key = 1;
// Número de revisión cuando se creó la clave
int64 create_revision = 2;
// Número de revisión cuando se modificó por última vez la clave
int64 mod_revision = 3;
// Contador incremental que aumenta cada vez que se actualiza la clave.
// Este contador se restablece a cero cuando se elimina la clave y se utiliza como una tumba.
int64 version = 4;
bytes value = 5;
// El objeto de arrendamiento utilizado por la clave para TTL. Si el valor es 0, entonces no hay TTL.
int64 lease = 6;
}
PostgreSQL
PostgreSQL necesita usar una tabla para simular el espacio de datos global de etcd:
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
-- Equivalente a `create_revision` y `mod_revision`
-- Aquí, se utiliza un tipo de secuencia incremental de enteros grandes para simular la revisión
revision bigserial,
-- Tumba
tombstone boolean NOT NULL DEFAULT false,
-- Índice compuesto, buscar por clave primero, luego por revisión
primary key(key, revision)
);
get
etcd
La API get
de etcd tiene una amplia gama de parámetros:
- Consultas de rango, por ejemplo, establecer
key
como/abc
yrange_end
como/abd
recuperará todos los pares clave-valor con/abc
como prefijo. - Consultas históricas, especificando
revision
o un rango demod_revision
. - Ordenación y limitación del número de resultados devueltos.
message RangeRequest {
...
bytes key = 1;
// Consultas de rango
bytes range_end = 2;
int64 limit = 3;
// Consultas históricas
int64 revision = 4;
// Ordenación
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
PostgreSQL puede realizar la función get de etcd a través de SQL, e incluso proporcionar funcionalidades más complejas. Dado que SQL en sí es un lenguaje en lugar de una interfaz de parámetros fijos, es altamente versátil. Aquí mostramos un ejemplo simple de recuperación del último par clave-valor. Dado que la clave principal es un índice combinado, se puede buscar rápidamente por rango, lo que resulta en una recuperación de alta velocidad.
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;
// si se debe responder con los datos del par clave-valor antes de la actualización de esta solicitud `Put`.
bool prev_kv = 4;
bool ignore_value = 5;
bool ignore_lease = 6;
}
PostgreSQL
Al igual que en etcd, PostgreSQL no ejecuta cambios en el lugar. En su lugar, se inserta una nueva fila y se le asigna una nueva revisión.
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
Similar a etcd, la eliminación en PostgreSQL no modifica los datos en el lugar. En su lugar, se inserta una nueva fila con el campo tombstone establecido en true para indicar que es una tumba.
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 el rango de claves a observar
bytes range_end = 2;
// Revisión inicial para la observación
int64 start_revision = 3;
...
}
message WatchResponse {
ResponseHeader header = 1;
...
// Para eficiencia, se pueden devolver múltiples eventos
repeated mvccpb.Event events = 11;
}
PostgreSQL
PostgreSQL no viene con una función de observación incorporada, y en su lugar, requiere una combinación de triggers y canales para lograr una funcionalidad similar. Al usar pg_notify
, los datos se pueden enviar a todas las aplicaciones que están escuchando un canal específico.
-- función de trigger 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);
-- Extraer el nombre del canal para distribución desde la clave
channel = (SELECT SUBSTRING(NEW.key, '/(.*)/'));
-- Si una aplicación está observando el canal, enviar un evento a través de él
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; -- El resultado se ignora ya que es un trigger AFTER
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER notify_config_change
AFTER INSERT ON config
FOR EACH ROW EXECUTE FUNCTION notify_config_change();
Dado que la función de observación está encapsulada, las aplicaciones cliente también deben implementar la lógica correspondiente. Usando Golang como ejemplo, se deben seguir los siguientes pasos:
- Iniciar la escucha: Cuando comienza la escucha, todos los datos de notificación se almacenan en caché tanto en PostgreSQL como en el nivel de canal de Golang.
- Recuperar todos los datos usando get_all(key_prefix, revision): Esta función lee todos los datos existentes a partir de la revisión especificada. Para cada clave, solo se devolverán los datos de la última revisión, eliminando automáticamente cualquier dato eliminado. Si no se especifica la revisión, devuelve los últimos datos de todas las claves con el
key_prefix
dado. - Observar nuevos datos, incluyendo cualquier notificación que pueda haberse almacenado en caché entre el primer y el segundo paso, para evitar perder cualquier dato nuevo que pueda ocurrir durante esta ventana de tiempo. Ignorar cualquier revisión que ya se haya leído en el segundo paso.
func watch(l *pq.Listener) {
for {
select {
case n := <-l.Notify:
if n == nil {
log.Println("listener reconectado")
log.Printf("obtener todas las rutas desde la revisión %d incluyendo tumbas...\n", latestRev)
// Al reconectar, reanudar la transmisión basada en la revisión antes de la desconexión.
str := fmt.Sprintf(`select * from get_all_from_rev_with_stale('/routes/', %d)`, latestRev)
rows, err := db.Query(str)
...
continue
}
...
// mantener un estado que registre la última revisión que ha recibido
updateRoute(cfg)
case <-time.After(15 * time.Second):
log.Println("No se recibieron eventos durante 15 segundos, verificando conexión")
go func() {
// Si no se reciben eventos durante un período prolongado, verificar la salud de la conexión
if err := l.Ping(); err != nil {
log.Println("error de ping del listener: ", err)
}
}()
}
}
}
log.Println("obtener todas las rutas...")
// Al inicializar, la aplicación debe obtener todos los pares clave-valor actuales y luego monitorear incrementalmente las actualizaciones a través de watch
rows, err := db.Query(`select * from get_all('/routes/')`)
...
go watch(listener)
transacción
etcd
Las transacciones de etcd son una colección de múltiples operaciones con verificaciones condicionales, y las modificaciones realizadas por la transacción se confirman atómicamente.
message TxnRequest {
// Especificar la condición de ejecución de la transacción
repeated Compare compare = 1;
// Operaciones a ejecutar si se cumple la condición
repeated RequestOp success = 2;
// Operaciones a ejecutar si no se cumple la condición
repeated RequestOp failure = 3;
}
PostgreSQL
El comando DO
en PostgreSQL permite la ejecución de cualquier comando, incluidos procedimientos almacenados. Admite múltiples lenguajes, incluidos lenguajes incorporados como PL/pgSQL y Python. Con estos lenguajes, se pueden implementar cualquier juicio condicional, bucles y otra lógica de control, lo que lo hace más versátil que 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
En etcd, es posible crear un objeto de arrendamiento que las aplicaciones deben renovar periódicamente para evitar que expire. Cada par clave-valor puede estar vinculado a un objeto de arrendamiento, y cuando el objeto de arrendamiento expira, todos los pares clave-valor asociados también expiran, eliminándolos automáticamente.
message LeaseGrantRequest {
// TTL del arrendamiento
int64 TTL = 1;
int64 ID = 2;
}
// Renovación del arrendamiento
message LeaseKeepAliveRequest {
int64 ID = 1;
}
message PutRequest {
bytes key = 1;
bytes value = 2;
// ID del arrendamiento, utilizado para implementar TTL
int64 lease = 3;
...
}
PostgreSQL
- En PostgreSQL, un arrendamiento se puede mantener a través de una clave externa. Al consultar, si hay un objeto de arrendamiento asociado que ha expirado, se considera una tumba.
- Las solicitudes de keepalive actualizan la marca de tiempo
last_keepalive
en la tabla de arrendamiento.
CREATE TABLE IF NOT EXISTS config (
key text,
value text,
...
-- Usar una clave externa para especificar el objeto de arrendamiento asociado.
lease int64 references lease(id),
);
CREATE TABLE IF NOT EXISTS lease (
id text,
ttl int,
last_keepalive timestamp;
);
Comparación de Rendimiento
PostgreSQL necesita simular varias API de etcd a través de encapsulación. Entonces, ¿cómo es su rendimiento? Aquí están los resultados de una prueba simple:https://github.com/kingluo/pg_watch_demo#benchmark.
Los resultados muestran que el rendimiento de lectura y escritura es casi idéntico, con PostgreSQL incluso superando a etcd. Además, la latencia desde que ocurre una actualización hasta que la aplicación recibe el evento determina la eficiencia de la distribución de actualizaciones, y tanto PostgreSQL como etcd tienen un rendimiento similar. Cuando se probó en la misma máquina tanto para el cliente como para el servidor, la latencia de watch fue inferior a 1 milisegundo.
PostgreSQL, sin embargo, tiene algunas deficiencias que vale la pena mencionar:
- El registro WAL para cada actualización es más grande, lo que resulta en el doble de I/O de disco en comparación con etcd.
- Consume más CPU en comparación con etcd.
- Notify basado en canales es un concepto a nivel de transacción. Al actualizar el mismo tipo de recurso, la actualización se envía al mismo canal, y las solicitudes de actualización compiten por bloqueos de exclusión mutua, lo que resulta en solicitudes serializadas. En otras palabras, usar canales para implementar watch afectará el paralelismo de las operaciones de put.
Esto destaca que para lograr los mismos requisitos, necesitamos invertir más en aprender y optimizar PostgreSQL.
Almacenamiento
El rendimiento está determinado por el almacenamiento subyacente, y cómo se almacenan los datos determina los requisitos de recursos de memoria, disco y otros recursos de la base de datos.
etcd
Diagrama de arquitectura del almacenamiento de etcd:
etcd primero escribe las actualizaciones en el registro de escritura anticipada (WAL) y las vacía en el disco para garantizar que las actualizaciones no se pierdan. Una vez que el registro se escribe correctamente y es confirmado por la mayoría de los nodos, los resultados se pueden devolver al cliente. etcd también actualiza asincrónicamente TreeIndex y BoltDB.
Para evitar que el registro crezca infinitamente, etcd periódicamente toma una instantánea del almacenamiento, y los registros anteriores a la instantánea se pueden eliminar.
etcd indexa todas las claves en memoria (TreeIndex), registrando la información de versión de cada clave, pero solo mantiene un puntero a BoltDB (revisión) para el valor.
El valor correspondiente a la clave se almacena en el disco y se mantiene utilizando BoltDB.
Tanto TreeIndex como BoltDB utilizan la estructura de datos btree, conocida por su eficiencia en búsquedas y búsquedas de rango.
Diagrama de estructura de TreeIndex:
(Fuente de la imagen: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado bajo CC 4.0 BY-SA)
Cada clave se divide en diferentes generaciones, con cada eliminación marcando el final de una generación.
El puntero al valor está compuesto por dos enteros. El primer entero main
es el ID de transacción de etcd, mientras que el segundo entero sub
representa el ID de actualización de esta clave dentro de esa transacción.
Boltdb admite transacciones e instantáneas, y almacena el valor correspondiente a la revisión.
(Fuente de la imagen: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado bajo CC 4.0 BY-SA)
Ejemplo de escritura de datos:
Escribir key="key1", revision=(12,1), value="keyvalue5"
. Notar los cambios en las partes rojas de treeIndex y BoltDB:
(Fuente de la imagen: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado bajo CC 4.0 BY-SA)
Eliminar key="key", revision=(13,1)
crea una nueva generación vacía en treeIndex y genera un valor vacío en BoltDB con key="13_1t"
.
Aquí, la t
significa "tumba". Esto implica que no se puede leer la tumba porque el puntero en treeIndex es (13,1)
, pero en BoltDB es 13_1t
, que no se puede coincidir.
(Fuente de la imagen: https://blog.csdn.net/H_L_S/article/details/112691481, licenciado bajo CC 4.0 BY-SA)
Vale la pena mencionar que etcd programa tanto lecturas como escrituras a BoltDB utilizando una sola goroutine para reducir el I/O de disco aleatorio y mejorar el rendimiento de I/O.
PostgreSQL
Diagrama de arquitectura del almacenamiento de PostgreSQL:
Similar a etcd, PostgreSQL agrega las actualizaciones a un archivo de registro primero, y espera a que el registro se vacíe correctamente en el disco antes de considerar la transacción completa. Mientras tanto, las actualizaciones se escriben en la memoria compartida shared_buffer.
El shared_buffer es un área de memoria compartida por todas las tablas e índices en PostgreSQL, y sirve como un mapeo para estos objetos.
En PostgreSQL, cada tabla consta de múltiples páginas, con cada página de 8 KB de tamaño y que contiene múltiples filas.
Además de las tablas, los índices (como los índices btree) también están compuestos por páginas de tabla en el mismo formato. Sin embargo, estas páginas son especiales y están interconectadas para formar una estructura de árbol.
PostgreSQL está equipado con un proceso de checkpoint que periódicamente vacía todas las páginas de tabla e índice modificadas en el disco. Antes de cada checkpoint, los archivos de registro se pueden eliminar y reciclar para evitar que el registro crezca indefinidamente.
Estructura de página:
(Fuente de la imagen: https://en.wikibooks.org/wiki/PostgreSQL/Page_Layout, licenciado bajo CC 3.0 BY-SA)
Estructura de índice btree:
(Fuente de la imagen: https://en.wikibooks.org/wiki/PostgreSQL/Index_Btree, licenciado bajo CC 3.0 BY-SA)
Para mejorar el rendimiento de lectura, ciertas declaraciones SQL en PostgreSQL consideran usar mapas de bits para leer secuencialmente páginas dispersas, mejorando así el rendimiento de I/O.
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;
QUERY PLAN
------------------------------------------------------------------------------
Bitmap Heap Scan