NUEVO: Helius adquiere Light Protocol
El precio de las escrituras baratas: ¿cómo lee RocksDB?
Blog/Ingeniería

El precio de las escrituras baratas: ¿cómo lee RocksDB?

Developer Experience Engineer0xIchigo en X0xIchigo en LinkedIn0xIchigo en GitHub
21 min de lectura

Leer desde un almacén de clave-valor parece sencillo: proporcionas una clave y obtienes un valor. Sin embargo, el artículo anterior de esta serie explica por qué esta idea es incorrecta. Los árboles LSM aceptan que las escrituras sean baratas porque posponen el trabajo de organizar los datos.

Las lecturas tienen que pagar ese costo.

El valor actual de una clave podría estar en la memtable, en un archivo del nivel 0 o en cualquiera de los niveles ordenados inferiores. El trabajo de la ruta de lectura es encontrar la versión más reciente de este valor sin revisar todos los lugares.

Este artículo explica los mecanismos que mantienen rápida esa búsqueda y el cálculo que determina su costo.

¿Cómo lee RocksDB una clave?

Una lectura en RocksDB avanza desde los datos más recientes hasta los más antiguos y se detiene en la primera versión de la clave que encuentra. Eso es todo; lo demás solo acelera el proceso.

El orden de búsqueda es fijo y refleja, en sentido inverso, la ruta de escritura descrita en el artículo anterior: recorre la memtable activa, las memtables inmutables, los archivos SST del nivel 0 y los archivos SST ordenados desde el nivel 1 hacia abajo.

RocksDB revisa primero la memtable activa porque contiene las escrituras más recientes, reside en memoria y usa una skiplist. Por eso, una consulta es una búsqueda ordenada en memoria que casi no requiere I/O. La búsqueda se detiene en cuanto encuentra la clave.

Después de la memtable activa, RocksDB revisa las memtables inmutables que ya se llenaron, pero todavía no se han volcado al disco. Las revisa a continuación, desde la más reciente hasta la más antigua.

Luego se revisa el disco. Cada archivo del nivel 0 debe revisarse por separado, desde el más reciente hasta el más antiguo, porque sus rangos de claves se superponen. Esto sucede porque cada archivo es una instantánea de lo que una memtable haya absorbido, por lo que la misma clave puede estar en dos archivos del nivel 0 al mismo tiempo.

Cada archivo del nivel 0 es otro lugar donde podría esconderse una clave. Precisamente por eso, la cantidad de archivos del nivel 0 penaliza las lecturas y RocksDB detiene las escrituras para mantenerla limitada.

Desde el nivel 1 hacia abajo, los archivos SST de un nivel determinado están ordenados y no se superponen, por lo que como máximo un archivo por nivel puede contener la clave. Encontrar ese archivo requiere una búsqueda binaria en los metadatos de los archivos. Después, la búsqueda desciende al interior del archivo. El bloque de índice localiza el bloque de datos correcto, y este contiene la clave o demuestra que el nivel no la tiene.

Los números de secuencia son importantes para la ruta de lectura de RocksDB porque detienen una búsqueda en la primera versión que encuentra. Esto garantiza que sea la versión más reciente. Por ejemplo, si una lectura encuentra un tombstone, tiene éxito e indica que la clave fue eliminada. Así, la búsqueda termina en el tombstone y deja el valor anterior en el disco.

¿Qué operaciones de lectura ofrece RocksDB?

RocksDB ofrece cuatro formas de leer, cada una con un uso diferente de la ruta de lectura: Get, MultiGet, Iterators y Snapshots.

Get

Get es la consulta puntual que rastreamos en la sección anterior. Es decir, busca una clave desde la versión más reciente hasta la más antigua, y la primera versión gana.

MultiGet

MultiGet agrupa consultas puntuales en lotes. Una lectura puede proporcionar a RocksDB un conjunto de claves, que este ordena y agrupa según los archivos que podrían tocar.

En la práctica, esto permite que RocksDB abra un solo SST, consulte una vez sus bloques de índice y filtro y agrupe el I/O. Es la misma ruta de lectura que Get, pero los costos son fijos y se amortizan en todo el lote.

Iteradores

Iterators recorre rangos, es decir, busca una clave y avanza o retrocede en orden.

En la práctica, un iterador es una fusión de k vías (es decir, combina k listas ordenadas diferentes en una única secuencia completamente ordenada). Cada fuente de datos activa —la memtable, cualquier memtable inmutable, cada archivo L0 y un archivo por cada nivel inferior— aporta su propio cursor ordenado, y un heap devuelve repetidamente el cursor que contiene la siguiente clave más pequeña.

Esta regla que prioriza la versión más reciente se aplica en cada paso. Si varias fuentes ofrecen la misma clave, gana la versión más reciente y se descartan las demás.

Los valores ocultos y los tombstones se omiten a medida que avanza el iterador. Esto significa que un rango lleno de claves eliminadas es un rango que el iterador debe recorrer silenciosamente sin devolver nada. Un espacio de claves con muchas eliminaciones ralentiza los recorridos de formas que las consultas puntuales nunca parecen notar.

Snapshot

Un Snapshot es un número de secuencia capturado en un momento determinado. Cualquier lectura que lo incluya simplemente ignora las versiones posteriores a ese número y devuelve la versión más reciente igual o anterior a él. Esto permite lecturas coherentes de un momento específico mediante una comparación de enteros.

Crear un snapshot es prácticamente gratis porque el costo se acumula después; un snapshot activo todavía puede ver versiones que no se pueden recuperar, sin importar su antigüedad.

Tanto Get como MultiGet saben exactamente qué clave buscan. Por eso, la búsqueda puede hacer una pregunta más barata que “¿dónde está esta clave?”. En concreto: “¿qué archivos definitivamente no la tienen?”. Iterators nunca obtiene ese atajo. Responder la pregunta más barata es la única función del filtro de Bloom.

¿Qué es un filtro de Bloom?

Un filtro de Bloom responde una sola pregunta sobre un conjunto: ¿este elemento definitivamente no está aquí? La respuesta es “definitivamente no” o “quizás”, nunca un sí definitivo. Si un archivo definitivamente no contiene una clave, la búsqueda nunca lo abre, lo que ahorra I/O.

Cuando se construye un SST, cada clave pasa por varias funciones hash, y cada hash establece un bit en un arreglo compacto de bits (el bloque de filtro).

Durante la lectura, la consulta aplica el mismo hash a la clave buscada y revisa esos bits. Si algún bit no está establecido, la clave nunca se insertó, lo que significa que definitivamente no está aquí.

Si todos los bits están establecidos, la clave está probablemente presente. Es “probablemente” porque los hashes de otras claves pueden coincidir y cubrir los mismos bits.

Esto significa que puede haber falsos positivos: la búsqueda abre el archivo, revisa y no encuentra nada, después de consumir el I/O que el filtro debía ahorrar.

Con la configuración convencional de RocksDB de diez bits por clave, la tasa de falsos positivos es de aproximadamente uno por ciento. Diez bits ofrecen una probabilidad del 99 % de omitir un archivo que no contiene la clave. Un filtro para un SST de un millón de claves ocupa poco más de un megabyte, reside en memoria junto al índice y se consulta sin tocar el disco que protege. Pocas estructuras en la ingeniería de sistemas ofrecen tanto por tan poco.

Esto también explica por qué el artículo anterior de esta serie encontró un bloque de filtro de tamaño cero. Los filtros de Bloom son opcionales, se configuran mediante filter_policy con la cantidad de bits por clave que elija la aplicación, y la configuración predeterminada no establece ninguno.

Este valor predeterminado es razonable para una biblioteca que no puede conocer su carga de trabajo, pero es indefendible en producción. El filtro marca la diferencia entre que una clave ausente cueste una comprobación en memoria por archivo o una lectura del bloque de índice por archivo.

Una limitación es que los filtros de Bloom solo ayudan cuando la búsqueda conoce la clave exacta. Un filtro de Bloom puede responder si key:0301 está en este archivo, pero no puede responder si el archivo contiene algo entre key:0090 y key:0110, porque los hashes destruyen el orden.

Iterators, que nunca conoce de antemano su siguiente clave, no recibe ayuda de esta estructura. Los filtros de Bloom son una tecnología para consultas puntuales; las cargas de trabajo con muchos rangos deben buscar alivio en otra parte.

¿Qué ocurre con las búsquedas por prefijo de RocksDB?

La solución parcial de RocksDB al problema de los recorridos es cambiar lo que se procesa con hash. Un extractor de prefijos indica a RocksDB que las claves tienen una estructura significativa (por ejemplo, los primeros ocho bytes o todo lo que aparece antes de un separador). Cuando RocksDB sabe qué es un prefijo, puede crear filtros de Bloom sobre los prefijos en lugar de las claves completas, o junto con ellas. Un filtro que responde si un archivo contiene una clave que comienza con cierto prefijo puede ayudar a un iterador.

Sin embargo, este atajo solo funciona con un prefijo.

Un recorrido para un prefijo determinado puede omitir archivos y bloques de la misma manera que las consultas puntuales. Un recorrido que pasa de un prefijo al siguiente no recibe la misma ayuda. Mantener las garantías de orden de iteración entre prefijos merece un análisis detallado propio sobre las particularidades de configuración que lo hacen posible.

La búsqueda por prefijo no abarata los recorridos de rango en general.

Más bien, abarata una forma específica y muy común de recorrido de rango para un prefijo determinado.

¿Qué es la caché de bloques de RocksDB?

La caché de bloques es una caché en memoria de bloques sin comprimir. Cuando se lee un bloque de datos desde un SST, se descomprime y se conserva. Así, la próxima lectura que necesite el mismo bloque lo encuentra sin tocar el disco. Esto significa que las claves activas se agrupan en bloques activos, y estos bloques dejan de consumir I/O.

Por tanto, los filtros de Bloom intentan mantener las búsquedas fuera de los archivos, mientras que la caché de bloques abarata volver a visitar los archivos que deben abrirse.

Un detalle sutil es que existen dos cachés.

La caché de bloques pertenece a RocksDB y contiene bloques sin comprimir que administra de forma explícita. Debajo está la caché de páginas del sistema operativo, que contiene las páginas de archivo sin procesar y comprimidas que RocksDB leyó recientemente. Los mismos datos pueden ocupar ambas al mismo tiempo (es decir, sin comprimir en la caché de bloques y comprimidos en la caché de páginas). Por eso, el consumo real de memoria de RocksDB se subestima de forma crónica, y los debates sobre el tamaño de la caché que ignoran la caché de páginas solo consideran la mitad del sistema.

Los bloques de índice y filtro reciben un tratamiento especial. Son pequeños, se consultan en casi todas las lecturas que llegan a su archivo y no encontrarlos en caché resulta catastróficamente costoso. Por eso, mantenerlos residentes es casi innegociable.

Que residan dentro del presupuesto de la caché de bloques o fuera de él, en la memoria propia de los lectores de tablas, es una elección de configuración (es decir, cache_index_and_filter_blocks).

¿Cuánto les cuestan a los lectores los tombstones y las cadenas de fusión?

Los tombstones y las cadenas de fusión son tipos de registros de la ruta de escritura que afectan la ruta de lectura, pero solo la compactación puede resolverlos. Los tombstones se acumulan hasta que la compactación los elimina junto con los cadáveres que ocultan, y las cadenas de fusión crecen hasta que la compactación las combina en valores consolidados.

Un tombstone es barato para la consulta puntual que lo encuentra. La búsqueda se detiene y entrega la respuesta porque “eliminado” es un resultado válido. Sin embargo, un iterador no puede detenerse en un tombstone. Debe resolverlo, omitir las versiones que oculta y seguir avanzando hasta encontrar la siguiente clave activa. Un espacio de claves con muchas eliminaciones es un campo minado de entradas que deben recorrerse y descartarse sin devolver nada; recorrerlo puede requerir mucho trabajo arbitrario para producir pocos resultados. Las consultas puntuales nunca lo notan, mientras que los recorridos de rango no tienen defensa contra ello.

Los operandos de fusión generan el mismo costo, pero en la dirección contraria. Una lectura de una clave fusionada no puede detenerse en el registro más reciente porque un operando es una descripción de un cambio, no un valor. Por tanto, la búsqueda debe seguir recopilando operandos hasta llegar a un valor completo, un tombstone o la parte inferior del árbol. Después, debe combinar la cadena mediante el operador de fusión.

El recorrido del artículo anterior combinó una cadena de tres elementos, pero nada en el diseño limita la cadena a tres. Un contador fusionado diez mil veces entre compactaciones produce una lectura que recopila y combina diez mil operandos, y la ruta de escritura no hizo nada para impedirlo.

El triángulo de amplificación

El triángulo de amplificación representa una compensación entre tres factores que rige todos los árboles LSM: amplificación de escritura, amplificación de lectura y amplificación de espacio. Mejorar dos de ellos empeora el tercero.

Athanassoulis et al. formalizaron estas compensaciones como la conjetura RUM (2016), según la cual un método de acceso puede minimizar como máximo dos de estos costos: lectura, actualización y memoria o espacio. El tercero absorbe el costo. Los árboles B ocupan una posición (es decir, lecturas baratas y actualizaciones costosas), mientras que los árboles LSM ocupan otra (es decir, actualizaciones baratas y lecturas costosas). El triángulo representa el territorio entre ambas.

Todo lo descrito en este artículo ha sido una esquina del triángulo defendiéndose.

La amplificación de escritura es la relación entre los bytes escritos físicamente en el disco y los bytes que la aplicación escribe de forma lógica. Un solo Put se escribe una vez en el WAL, otra cuando se vuelca a L0 y cada vez que la compactación lo desplaza a un nivel inferior. Esto significa que una escritura lógica podría convertirse habitualmente en entre diez y treinta escrituras físicas durante su vida útil en un LSM por niveles con una docena de niveles. Aunque el artículo anterior celebra que cada una de esas escrituras sea secuencial, este artículo lo lamenta.

La amplificación de lectura se refiere a la cantidad de lugares que una consulta debe revisar para encontrar su respuesta. Para una consulta puntual, el peor caso incluye cada memtable, cada archivo L0 y un archivo por nivel inferior (es decir, la búsqueda descrita al comienzo de este artículo). El costo real es todo lo que logre pasar los filtros de Bloom y la caché de bloques. Para un recorrido de rango, es la fusión de k vías que mantiene abiertas todas las fuentes a la vez. Este artículo trata principalmente sobre la amplificación de lectura: filtros, cachés y extractores de prefijos que actúan como mecanismos para reducirla.

La amplificación de espacio es la relación entre el espacio en disco consumido y los datos lógicos almacenados. Las versiones antiguas que esperan la compactación, los tombstones y los cadáveres que ocultan, las cadenas de fusión sin combinar y todas las versiones que mantiene un snapshot activo consumen espacio en disco para datos que la aplicación ya no puede leer o ya no quiere. Una compactación por niveles bien ajustada mantiene esta relación cerca de 1,1x, mientras que otras configuraciones sacrifican mucho más espacio.

El triángulo establece que mejorar dos esquinas empeora la tercera. Es decir:

  • Compacta de forma agresiva: las lecturas se aceleran y el espacio se reduce, pero cada clave se reescribe constantemente y aumenta la amplificación de escritura
  • Compacta con poca frecuencia: las escrituras fluyen, pero los archivos se acumulan para que las lecturas los revisen y los datos inactivos permanecen en el disco
  • Invierte memoria en filtros: las cachés y las lecturas se recuperan, pero la memoria es el cuarto recurso silencioso por el que compiten todas las esquinas del triángulo

No existe una configuración llamada “rápida”. Solo hay posiciones en el triángulo que se ajustan, o no, a una carga de trabajo determinada.

Un recorrido por la ruta de lectura

A continuación, reutilizaremos el proyecto del recorrido del artículo anterior, con las estadísticas habilitadas.

Reemplaza el contenido de src/main.rs por:

Código
use rocksdb::{BlockBasedOptions, Options, DB};

fn main() {
	let with_bloom = std::env::args().nth(1).as_deref() == Some("bloom");
	let path = if with_bloom { "/tmp/read-bloom" } else { "/tmp/read-nobloom" };

	let mut opts = Options::default();
	opts.create_if_missing(true);
	opts.enable_statistics();

	if with_bloom {
		let mut bbt = BlockBasedOptions::default();

		bbt.set_bloom_filter(10.0, false);
		opts.set_block_based_table_factory(&bbt);
	}

	let db = DB::open(&opts, path).unwrap();

	// Three L0 SSTs with fully overlapping ranges: file b holds keys where
	// key % 4 == b, all spanning 0..600. Keys where key % 4 == 3 exist nowhere
	for batch in 0..3u64 {
		let mut k = batch;

		while k < 600 {
			let key = format!("key:{:04}", k);
			db.put(key.as_bytes(), b"value").unwrap();
			k += 4;
		}

		db.flush().unwrap();
	}

	// key:0404 lives only in the oldest file, meaning the search must get past
	// the two newer files to reach it
	let hit = db.get(b"key:0404").unwrap();
	println!("get(key:0404) = {:?}", hit.map(|b| String::from_utf8(b).unwrap()));

	// key:0303 is inside every file's range and inside no file
	let miss = db.get(b"key:0303").unwrap();
	println!("get(key:0303) = {:?}", miss);

	if let Some(stats) = db.property_value("rocksdb.options-statistics").unwrap() {
		for line in stats.lines() {
			if line.contains("bloom.filter.useful")
				|| line.contains("bloom.filter.full.positive")
				|| line.contains("block.cache.data") {
				println!("{}", line);
			}
		}
	}
}

Arriba tenemos tres archivos L0 volcados cuyos rangos de claves se superponen por completo. Por tanto, no se puede excluir ningún archivo solo con los metadatos de rango, porque ambas claves podrían esconderse en cualquiera de ellos.

El acierto (es decir, key:0404) solo reside en el archivo más antiguo. Esto obliga a la búsqueda desde lo más reciente a procesar dos archivos que no lo contienen antes de llegar al que sí lo contiene.

El fallo (es decir, key:0303) está dentro del rango de todos los archivos, pero no aparece en ninguno. Por tanto, solo un filtro de Bloom podría ahorrar esa lectura.

Ejecútalo tal cual y luego con el argumento bloom.

Sin filtros, el resultado es:

Código
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 3
rocksdb.block.cache.data.hit COUNT : 3
rocksdb.bloom.filter.useful COUNT : 0

Tenemos seis accesos a bloques de datos para responder dos consultas. El acierto leyó un bloque de datos de cada uno de los tres archivos. Dos no fueron útiles, por lo que costó tres lecturas de disco. Después, el fallo revisó los mismos tres archivos, y sus tres accesos a bloques de datos solo fueron aciertos de caché porque la primera consulta acababa de calentarla. La caché de bloques hizo su trabajo, y fue lo único que lo hizo.

Con filtros, el resultado es:

Código
get(key:0404) = Some("value")
get(key:0303) = None
rocksdb.block.cache.data.miss COUNT : 1
rocksdb.block.cache.data.hit COUNT : 0
rocksdb.bloom.filter.useful COUNT : 5
rocksdb.bloom.filter.full.positive COUNT : 1

En total, tenemos un acceso a un bloque de datos. Podemos verificar manualmente el cálculo que da 5 para useful:

  • El acierto consultó el filtro del archivo más reciente y el del segundo archivo, y ambos respondieron que no. El filtro del archivo más antiguo devolvió el único full.positive, que resultó ser un verdadero positivo. Esto nos da dos omisiones
  • El fallo consultó los tres filtros y recibió tres respuestas negativas, lo que significa que tenemos tres omisiones y cero archivos abiertos

Si sumamos el acierto y el fallo, obtenemos cinco comprobaciones de archivos. Las cinco comprobaciones que el filtro respondió desde la memoria son las cinco lecturas de bloques de datos que tuvo que realizar la ejecución sin filtros. Así, diez bits por clave convirtieron seis accesos a bloques en uno.

En la práctica, este es el triángulo de amplificación a menor escala. La ejecución sin filtros pagó toda la amplificación de lectura. La ejecución con filtros la pagó con memoria (es decir, unos cientos de bytes de filtro por archivo). La ruta de escritura no pagó nada en ninguno de los casos. Cada decisión de ajuste en RocksDB es esta misma compensación a mayor escala.

¿Cómo somete Solana a presión la ruta de lectura?

El artículo anterior presentó al validador como la prueba de estrés de la ruta de escritura: shreds que llegan a velocidad de línea para siempre. El lado de lectura es la otra mitad del trato, porque un ledger que nadie puede volver a leer solo es una forma costosa de calentar un centro de datos.

Todas las afirmaciones de este artículo sobre el orden de búsqueda, los filtros y el diseño de claves aparecen en la forma en que se consultan los datos de Solana.

Dentro del validador, las lecturas del Blockstore tienen dos formas distintas: atender solicitudes de reparación y reproducir o transmitir un bloque.

Atender solicitudes de reparación significa que otros validadores piden shreds que no recibieron. Es una consulta puntual por (slot, index). Reproducir o transmitir el ledger de un bloque es un recorrido de rango sobre un slot determinado.

Como las claves de los shreds comienzan con el número de slot, todos los shreds de un slot se almacenan de forma contigua en el disco. Por eso, “dame el slot n” es un recorrido de iterador compacto y secuencial, no un conjunto disperso de lecturas puntuales.

El diseño de claves de la ruta de escritura es la optimización de la ruta de lectura. La misma disposición que prioriza el slot y hacía que los volcados produjeran archivos L0 con muy poca superposición también convierte las lecturas de bloques en un único recorrido contiguo.

La capa RPC plantea preguntas más difíciles. getTransaction es una consulta puntual por firma en los datos de estado de las transacciones. Este es el caso para el que existen los filtros de Bloom, porque descartar una firma ausente de un SST determinado debería costar una comprobación en memoria, no una lectura del índice.

getSignaturesForAddress es el recorrido con forma de prefijo descrito antes en este artículo. El índice de dirección a firma comienza con la dirección, por lo que todo el historial de una cuenta ocupa un rango contiguo de claves. Así, toda la actividad de una dirección adquiere la forma de recorrido de rango que la estructura procesa bien.

Sin embargo, las preguntas que realmente hacen las aplicaciones se apoyan en ambas operaciones. No existe una primitiva para “dame todas las transacciones de esta dirección”. Este tipo de pregunta se convierte en un recorrido por prefijo para obtener las firmas y luego en una consulta puntual por cada firma para obtener las transacciones.

En la práctica, una consulta de rango se expande en cientos o miles de Gets. Esta es precisamente la carga de trabajo para la que existe el procesamiento por lotes de MultiGet: ordena las firmas, agrúpalas según los archivos que tocan y consulta una vez el índice y los filtros de cada archivo.

Una consulta que desde fuera parece una única llamada API activa, en la ruta de lectura, todos los mecanismos descritos en este artículo, uno tras otro: iterador, ordenamiento por prefijo, filtros de Bloom, amortización por lotes y caché de bloques.

Creamos esa llamada API.

Históricamente, los desarrolladores tenían que ejecutar en el cliente un bucle de getSignaturesForAddress y n llamadas a getTransaction, con la lógica de reintentos que siempre terminan necesitando los patrones n+1.

getTransactionsForAddress condensa ese bucle en un único método con diversas funciones que se ajustan a los mecanismos de la ruta de lectura explicados en este artículo.

El orden ascendente y descendente son las dos direcciones del iterador. La paginación basada en cursores devuelve un token con el formato slot:position, que representa una posición de clave. Por eso, reanudar una consulta paginada significa volver a posicionar un recorrido donde se detuvo el anterior. Los filtros de slot y tiempo limitan el trabajo en lugar de descartarlo después.

El sistema de archivo que atiende ese método almacena en RocksDB todo el historial de transacciones de Solana: cientos de terabytes. Lo migramos de ClickHouse a RocksDB, y esa es la razón por la que existe esta serie: el motor de almacenamiento descrito en estos artículos es el que ajustamos, familia de columnas por familia de columnas, para convertir toda la vida de una cuenta en un recorrido contiguo y la búsqueda de una firma en una sola comprobación en memoria. Por eso tenemos el sistema de archivo con mejor rendimiento de toda Solana.

Conclusión

La ruta de lectura es el plan de pagos del árbol LSM, y este artículo la presenta como una factura detallada. Una consulta recorre desde lo más reciente hasta lo más antiguo, pasando por la memtable, L0 y todos los niveles ordenados, y se detiene en la primera versión que encuentra. Todas las demás funciones y herramientas existen para minimizar la cantidad de accesos que requiere esa búsqueda: los filtros de Bloom determinan desde la memoria si una clave definitivamente no está en un archivo, por lo que este permanece cerrado; la caché de bloques abarata volver a abrir los archivos que deben abrirse; y el diseño de prefijos convierte la única forma asequible de recorrido de rango en la forma que realmente necesitan las aplicaciones.

El triángulo de amplificación es el registro que contabiliza todo esto: el costo de lectura se reduce con memoria, mientras que el costo de escritura se paga antes en cuotas secuenciales. El costo de espacio se acumula silenciosamente en cada tombstone y cada versión oculta que la búsqueda deja atrás.

Ten en cuenta que todas las estructuras de este artículo solo administran el costo de los archivos acumulados, las versiones inactivas y las cadenas de fusión sin combinar. Nada de lo explicado aquí lo elimina. Lo que lo elimina se ejecuta continuamente en segundo plano, y decidir con qué agresividad lo hace es la decisión de ajuste más importante de RocksDB. El siguiente artículo de esta serie trata sobre la compactación: qué hace, los tres estilos que ofrece RocksDB y por qué el Blockstore de Solana cambió de estilo y luego volvió al anterior.

Si pasar la tarde leyendo un recorrido sobre la ruta de lectura de una base de datos te parece un buen plan, construye con nosotros. La ruta de lectura descrita aquí es la que operamos sobre todo el historial de Solana, donde la diferencia entre consultar un filtro de Bloom y abrir un archivo se mide en hardware y dinero reales. Tenemos vacantes en todo nuestro equipo de ingeniería. Consulta todos los puestos disponibles en helius.dev/careers

Suscríbete a Helius

Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos

Imagen ampliada