Diseño e implementación de almacenamiento persistente Xline

01. Introducción

En la primera fase de prototipo de Xline, usamos almacenamiento basado en memoria para lograr la persistencia de los datos. Si bien esto simplifica la complejidad del diseño del prototipo Xline y mejora la velocidad de desarrollo e iteración del proyecto, el impacto también es significativo: dado que los datos se almacenan en la memoria, una vez que el proceso falla, la recuperación de datos del nodo debe depender de Para extraer la cantidad total de datos de otros nodos normales, se requiere un largo tiempo de recuperación.

En base a esta consideración, Xline introdujo una capa de almacenamiento persistente en la última versión v0.3.0 para conservar los datos en el disco, mientras protege a la persona que llama de nivel superior de los detalles subyacentes irrelevantes.

02. Selección del motor de almacenamiento

En la actualidad, los principales motores de almacenamiento de la industria se pueden dividir básicamente en motores de almacenamiento basados ​​en árboles B+ y motores de almacenamiento basados ​​en árboles LSM. Tienen sus propias ventajas y desventajas.

Análisis de amplificación de lectura y escritura de árboles B+

Cuando B+ Tree lee datos, debe seguir el nodo raíz e indexar gradualmente hacia la capa inferior hasta que finalmente accede al nodo de hoja inferior.Cada acceso a la capa corresponde a una E/S de disco. Al escribir datos, también busca el nodo raíz y escribe los datos después de encontrar el nodo hoja correspondiente.

Para facilitar el análisis, hacemos un acuerdo relevante: el tamaño del bloque del árbol B+ es B, por lo que cada nodo interno contiene nodos secundarios O(B), y el nodo hoja contiene piezas de datos O(B). el tamaño del conjunto de datos es N, entonces B+ Tree la altura de

Amplificación de escritura: Cada inserción de B+ Tree escribirá datos en el nodo hoja, independientemente del tamaño real de los datos, cada vez que se necesite escribir un bloque de datos de tamaño B, por lo que la amplificación de escritura es O(B)

Amplificación de lectura: una consulta de B+ Tree debe buscar desde el nodo raíz hasta un nodo hoja específico, por lo que se requiere una E/S igual al número de capas, es decir,

, es decir, la amplificación de lectura es

Análisis de amplificación de lectura y escritura del árbol LSM

Cuando LSM Tree escribe datos, primero escribe un archivo de memoria memtable (Nivel 0) en forma de archivo adjunto. Cuando el memtable alcanza un tamaño fijo, se convierte en memtable inmutable y se fusiona en el siguiente nivel. Para leer datos, primero debe buscar en el memtable, y cuando la búsqueda falla, busque capa por capa hasta que encuentre el elemento. LSM Tree a menudo usa Bloom Filter para optimizar las operaciones de lectura y filtrar elementos que no existen en la base de datos.

Suponga que el tamaño del conjunto de datos es N, el factor de ampliación es k, el tamaño de un archivo en la capa más pequeña es B y el tamaño de un solo archivo en cada capa es el mismo que B, pero el número de archivos en cada capa es diferente.

Amplificación de escritura: suponiendo que se escribe un registro, se compactará a la siguiente capa después de que la capa actual se escriba k veces. Por lo tanto, la amplificación de escritura de una sola capa promedio debe ser

. Un total de

capa, por lo que la amplificación de escritura es

Amplificación de lectura: en el peor de los casos, los datos se compactan en la última capa y se debe realizar una búsqueda binaria en cada capa hasta que se encuentra en la última capa.

para el nivel superior 

, el tamaño de los datos es O(N), y se requiere una búsqueda binaria, que requiere 

lectura de disco secundario

para el segundo nivel 

, el tamaño de los datos es 

, necesidad de llevar a cabo 

lectura de disco secundario

para 

, el tamaño de los datos es 

, necesidad de llevar a cabo 

lectura de disco secundario

……

Por analogía, la amplificación de lectura final es R = 

Resumir

Del análisis de la complejidad de la amplificación de lectura-escritura, el motor de almacenamiento basado en B+ Tree es más adecuado para escenarios con más lecturas y menos escrituras, mientras que el motor de almacenamiento basado en LSM Tree es más adecuado para escenarios con más escrituras y menos lecturas. .

Xline, como software de almacenamiento KV distribuido de código abierto escrito por Rust, necesita las siguientes consideraciones al elegir un motor de almacenamiento persistente:

  1. En términos de rendimiento : para los motores de almacenamiento, a menudo es fácil convertirse en uno de los cuellos de botella de rendimiento del sistema, por lo que se debe seleccionar un motor de almacenamiento de alto rendimiento. Un motor de almacenamiento de alto rendimiento debe escribirse en un lenguaje de alto rendimiento y se debe dar prioridad a la implementación asíncrona. Se da prioridad al lenguaje Rust, seguido del lenguaje C/C++.
  2. Desde la perspectiva del desarrollo : dar prioridad a la implementación del lenguaje Rust, que puede reducir algo de trabajo de desarrollo adicional en la etapa actual.
  3. Desde el punto de vista del mantenimiento :
    1. Considere a los partidarios detrás del motor: dé prioridad a las grandes empresas comerciales, comunidades de código abierto
    2. La industria debe ser ampliamente utilizada para que se pueda aprender más experiencia en el proceso posterior de depuración y ajuste.
    3. La visibilidad y la popularidad (estrella de github) deben ser altas para atraer a buenos contribuyentes para que participen.
  4. Desde un punto de vista funcional : se requiere que el motor de almacenamiento proporcione semántica de transacciones, admita operaciones básicas relacionadas con KV y admita operaciones por lotes, etc.

Los requisitos se priorizan como: Funcionalidad > Mantenimiento >= Desempeño > Desarrollo

Principalmente investigamos varias bases de datos integradas de código abierto como Sled, ForestDB, RocksDB, bbolt y badger. Entre ellos, solo RocksDB puede cumplir con los cuatro requisitos que mencionamos anteriormente. RocksDB es implementado y de código abierto por Facebook, actualmente cuenta con buenas prácticas de producción de aplicaciones en la industria, al mismo tiempo, la versión aún mantiene una velocidad de lanzamiento estable y puede cubrir perfectamente nuestras necesidades en términos de funciones.

Xline sirve principalmente para la gestión coherente de metadatos de centros de datos entre nubes, y sus escenarios de trabajo son principalmente escenarios con más lecturas y menos escrituras. Algunos lectores pueden tener preguntas, ¿no es RocksDB un motor de almacenamiento basado en LSM Tree? El motor de almacenamiento basado en LSM Tree debería ser más adecuado para escenarios de aplicaciones con más escrituras y menos lecturas, entonces, ¿por qué elegir RocksDB?

De hecho, en teoría, el motor de almacenamiento más adecuado debería ser un motor de almacenamiento basado en B+ Tree. Sin embargo, teniendo en cuenta que las bases de datos integradas basadas en B+ Tree, como Sled y ForestDB, carecen de la práctica de la producción de aplicaciones a gran escala, el mantenimiento de la versión también está estancado. Después de hacer concesiones, elegimos RocksDB como backend de almacenamiento para Xline. Al mismo tiempo, para considerar que puede haber motores de almacenamiento más adecuados para reemplazar en el futuro, hemos realizado una buena separación de interfaz y empaquetado en el diseño de la capa de almacenamiento persistente, lo que puede minimizar el costo de reemplazar el almacenamiento. motor más tarde.

03. Diseño e implementación de capa de almacenamiento persistente

Antes de comenzar a analizar el diseño y la implementación de la capa de almacenamiento persistente, debemos aclarar nuestros requisitos y expectativas para el almacenamiento persistente:

  1. Como se mencionó anteriormente, después de hacer la compensación correspondiente, adoptamos RocksDB como el motor de almacenamiento de back-end de Xline. Por lo tanto, no podemos descartar la posibilidad de reemplazar este motor de almacenamiento en el futuro.El diseño de StorageEnginne debe ajustarse al principio OCP y cumplir con los principios de configurabilidad y fácil reemplazo.
  2. Necesitamos proporcionar una interfaz KV básica para usuarios de nivel superior
  3. Para lograr un mecanismo de recuperación completo.

Arquitectura general y proceso de escritura.

Echemos un vistazo a la arquitectura general actual de Xline, como se muestra en la siguiente figura:

De arriba a abajo, la arquitectura general de Xline se puede dividir en capa de acceso, módulo de consenso, módulo de lógica empresarial, capa de API de almacenamiento y capa de motor de almacenamiento. Entre ellos, la capa API de almacenamiento es principalmente responsable de proporcionar StorageApi relacionado con el negocio al módulo comercial y al módulo de consenso, respectivamente, mientras protege los detalles de implementación del motor subyacente. La capa del motor de almacenamiento es responsable de la operación de almacenamiento de datos real.

Tomemos una solicitud PUT como ejemplo para ver el proceso de escritura de datos. Cuando el cliente inicia una solicitud Put a Xline Server, sucederán las siguientes cosas:

  1. Después de que KvServer reciba la PutRequest enviada por el usuario, primero verificará la validez de la solicitud. Después de pasar la verificación, iniciará una solicitud de propuesta de rpc a Curp Server a través de su propio CurpClient.
  2. Después de que Curp Server reciba la solicitud Proponer, primero ingresará al proceso de ruta rápida. Guardará el cmd en la solicitud en el grupo de ejecución especulativa (también conocido como spec_pool) para determinar si entra en conflicto con el comando en el spec_pool actual y devolverá ProposeError::KeyConflict si ocurre el conflicto, y esperará a que se complete la ruta lenta. , de lo contrario, continúe con la ruta actual fast_path
  3. En fast_path, si un comando no entra en conflicto ni se repite, notificará al cmd_worker en segundo plano para que lo ejecute a través de un canal específico. Una vez que cmd_worker comience a ejecutarse, guardará el comando correspondiente en CommandBoard para rastrear la ejecución del comando.
  4. Cuando varios nodos en el clúster lleguen a un consenso, enviarán el registro de la máquina de estado, conservarán este registro en CurpStore y, finalmente, lo aplicarán. En el proceso de aplicación, se llamará al CommandExecutor correspondiente, es decir, el módulo de almacenamiento correspondiente a cada servidor en el módulo comercial, y los datos reales se almacenarán en la base de datos de back-end a través de DB.

diseño de interfaz

La siguiente figura muestra la relación entre los dos rasgos de StorageApi y StorageEngine y las estructuras de datos correspondientes.

Capa de motor de almacenamiento

La capa del motor de almacenamiento define principalmente el rasgo del motor de almacenamiento y los errores relacionados.

Rasgo StorageEngine 定义(engine/src/engine_api.rs):

/// Write operation
#[non_exhaustive]
#[derive(Debug)]
pub enum WriteOperation<'a> {
    /// `Put` operation
    Put {  table: &'a str, key: Vec<u8>, value: Vec<u8> },
    /// `Delete` operation
    Delete { table: &'a str, key: &'a [u8] },
    /// Delete range operation, it will remove the database entries in the range [from, to)
    DeleteRange { table: &'a str, from: &'a [u8], to: &'a [u8] },
}

/// The `StorageEngine` trait
pub trait StorageEngine: Send + Sync + 'static + std::fmt::Debug {
    /// Get the value associated with a key value and the given table
    ///
    /// # Errors
    /// Return `EngineError::TableNotFound` if the given table does not exist
    /// Return `EngineError` if met some errors
    fn get(&self, table: &str, key: impl AsRef<[u8]>) -> Result<Option<Vec<u8>>, EngineError>;

    /// Get the values associated with the given keys
    ///
    /// # Errors
    /// Return `EngineError::TableNotFound` if the given table does not exist
    /// Return `EngineError` if met some errors
    fn get_multi(
        &self,
        table: &str,
        keys: &[impl AsRef<[u8]>],
    ) -> Result<Vec<Option<Vec<u8>>>, EngineError>;

    /// Get all the values of the given table
    /// # Errors
    /// Return `EngineError::TableNotFound` if the given table does not exist
    /// Return `EngineError` if met some errors
    #[allow(clippy::type_complexity)] // it's clear that (Vec<u8>, Vec<u8>) is a key-value pair
    fn get_all(&self, table: &str) -> Result<Vec<(Vec<u8>, Vec<u8>)>, EngineError>;

    /// Commit a batch of write operations
    /// If sync is true, the write will be flushed from the operating system
    /// buffer cache before the write is considered complete. If this
    /// flag is true, writes will be slower.
    ///
    /// # Errors
    /// Return `EngineError::TableNotFound` if the given table does not exist
    /// Return `EngineError` if met some errors
    fn write_batch(&self, wr_ops: Vec<WriteOperation<'_>>, sync: bool) -> Result<(), EngineError>;
}

Definiciones de errores relacionadas

#[non_exhaustive]
#[derive(Error, Debug)]
pub enum EngineError {
    /// Met I/O Error during persisting data
    #[error("I/O Error: {0}")]
    IoError(#[from] std::io::Error),
    /// Table Not Found
    #[error("Table {0} Not Found")]
    TableNotFound(String),
    /// DB File Corrupted
    #[error("DB File {0} Corrupted")]
    Corruption(String),
    /// Invalid Argument Error
    #[error("Invalid Argument: {0}")]
    InvalidArgument(String),
    /// The Underlying Database Error
    #[error("The Underlying Database Error: {0}")]
    UnderlyingError(String),
}

MemoryEngine(engine/src/memory_engine.rs) y RocksEngine(engine/src/rocksdb_engine.rs) implementan el rasgo StorageEngine. Entre ellos, MemoryEngine se usa principalmente para pruebas y RocksEngine se define de la siguiente manera:

/// `RocksDB` Storage Engine
#[derive(Debug, Clone)]
pub struct RocksEngine {
    /// The inner storage engine of `RocksDB`
    inner: Arc<rocksdb::DB>,
}

/// Translate a `RocksError` into an `EngineError`
impl From<RocksError> for EngineError {
    #[inline]
    fn from(err: RocksError) -> Self {
        let err = err.into_string();
        if let Some((err_kind, err_msg)) = err.split_once(':') {
            match err_kind {
                "Corruption" => EngineError::Corruption(err_msg.to_owned()),
                "Invalid argument" => {
                    if let Some(table_name) = err_msg.strip_prefix(" Column family not found: ") {
                        EngineError::TableNotFound(table_name.to_owned())
                    } else {
                        EngineError::InvalidArgument(err_msg.to_owned())
                    }
                }
                "IO error" => EngineError::IoError(IoError::new(Other, err_msg)),
                _ => EngineError::UnderlyingError(err_msg.to_owned()),
            }
        } else {
            EngineError::UnderlyingError(err)
        }
    }
}

impl StorageEngine for RocksEngine {
    /// omit some code
}

Capa StorageApi

módulo de negocios

Definición de StorageApi del módulo comercial

/// The Stable Storage Api
pub trait StorageApi: Send + Sync + 'static + std::fmt::Debug {
    /// Get values by keys from storage
    fn get_values<K>(&self, table: &'static str, keys: &[K]) -> Result<Vec<Option<Vec<u8>>>, ExecuteError>
    where
        K: AsRef<[u8]> + std::fmt::Debug;

    /// Get values by keys from storage
    fn get_value<K>(&self, table: &'static str, key: K) -> Result<Option<Vec<u8>>, ExecuteError>
    where
        K: AsRef<[u8]> + std::fmt::Debug;

    /// Get all values of the given table from the storage
    fn get_all(&self, table: &'static str) -> Result<Vec<(Vec<u8>, Vec<u8>)>, ExecuteError>;

    /// Reset the storage
    fn reset(&self) -> Result<(), ExecuteError>;

    /// Flush the operations to storage
    fn flush_ops(&self, ops: Vec<WriteOp>) -> Result<(), ExecuteError>;
}


En el módulo de negocios, DB(xline/src/storage/db.rs) es responsable de convertir StorageEngine en StorageApi para llamadas de nivel superior, y su definición es la siguiente:

/// Database to store revision to kv mapping
#[derive(Debug)]
pub struct DB<S: StorageEngine> {
    /// internal storage of `DB`
    engine: Arc<S>,
}

impl<S> StorageApi for DB<S>
where
    S: StorageEngine
{
    /// omit some code 
}

Diferentes servidores en el módulo comercial tienen su propio backend de tienda, y su estructura de datos central es la base de datos en la capa StorageApi.

módulo de consenso

Definición de StorageApi para el módulo Curp (curp/src/server/storage/mod.rs)

/// Curp storage api
#[async_trait]
pub(super) trait StorageApi: Send + Sync {
    /// Command
    type Command: Command;

    /// Put `voted_for` in storage, must be flushed on disk before returning
    async fn flush_voted_for(&self, term: u64, voted_for: ServerId) -> Result<(), StorageError>;

    /// Put log entries in the storage
    async fn put_log_entry(&self, entry: LogEntry<Self::Command>) -> Result<(), StorageError>;

    /// Recover from persisted storage
    /// Return `voted_for` and all log entries
    async fn recover(
        &self,
    ) -> Result<(Option<(u64, ServerId)>, Vec<LogEntry<Self::Command>>), StorageError>;
}



Y RocksDBStorage (curp/src/server/storage/rocksdb.rs) es el CurpStore mencionado en el diagrama de arquitectura anterior, que es responsable de convertir StorageApi en la operación RocksEngine subyacente.

/// `RocksDB` storage implementation
pub(in crate::server) struct RocksDBStorage<C> {
    /// DB handle
    db: RocksEngine,
    /// Phantom
    phantom: PhantomData<C>,
}

#[async_trait]
impl<C: 'static + Command> StorageApi for RocksDBStorage<C> {
    /// Command
    type Command = C;
    /// omit some code
}

relacionado con la implementación

vista de datos

Después de introducir la capa de almacenamiento persistente, Xline divide diferentes espacios de nombres a través de la tabla de tabla lógica, que actualmente corresponde a la familia de columnas en el Rocksdb subyacente.

Actualmente existen las siguientes tablas:

  1. curp: almacene información persistente relacionada con curp, incluidas las entradas de registro, vote_for y la información del término correspondiente
  2. arrendamiento: Guarde la información del arrendamiento otorgado
  3. kv: guardar información de kv
  4. auth: guarda la situación actual de habilitación de autenticación de Xline y la revisión de habilitación correspondiente
  5. usuario: guarda la información del usuario agregada en Xline
  6. rol: Guarde la información del rol agregado en Xline
  7. meta: guarda el índice de registro aplicado actualmente

escalabilidad

La razón por la que Xline divide las operaciones relacionadas con el almacenamiento en dos características diferentes, StorageEngine y StorageApi, y las distribuye en dos niveles diferentes, es para aislar los cambios. El rasgo StorageEngine proporciona el mecanismo, y StorageApi está definido por los módulos de nivel superior.Diferentes módulos pueden tener sus propias definiciones para implementar estrategias de almacenamiento específicas. CurpStore y DB en la capa StorageApi son responsables de la conversión entre estos dos rasgos. Dado que la persona que llama de nivel superior no depende directamente del contenido relacionado con el motor de almacenamiento subyacente, incluso si el motor de almacenamiento se reemplaza más tarde, el código del módulo de nivel superior no requerirá muchas modificaciones.

Proceso de recuperación

Para el proceso de recuperación, solo hay dos cosas importantes: la primera es qué datos recuperar y la segunda es cuándo recuperarlos. Primero veamos los datos involucrados en la recuperación entre diferentes módulos.

módulo de consenso

En el módulo de consenso, dado que Curp Server utiliza RocksDBStorage exclusivamente, la recuperación se puede agregar directamente al rasgo StorageApi correspondiente. La implementación específica es la siguiente:

#[async_trait]
impl<C: 'static + Command> StorageApi for RocksDBStorage<C> {
    /// Command
    type Command = C;
    /// omit some code
    async fn recover(
        &self,
    ) -> Result<(Option<(u64, ServerId)>, Vec<LogEntry<Self::Command>>), StorageError> {
        let voted_for = self
            .db
            .get(CF, VOTE_FOR)?
            .map(|bytes| bincode::deserialize::<(u64, ServerId)>(&bytes))
            .transpose()?;

        let mut entries = vec![];
        let mut prev_index = 0;
        for (k, v) in self.db.get_all(CF)? {
            // we can identify whether a kv is a state or entry by the key length
            if k.len() == VOTE_FOR.len() {
                continue;
            }
            let entry: LogEntry<C> = bincode::deserialize(&v)?;
            #[allow(clippy::integer_arithmetic)] // won't overflow
            if entry.index != prev_index + 1 {
                // break when logs are no longer consistent
                break;
            }
            prev_index = entry.index;
            entries.push(entry);
        }

        Ok((voted_for, entries))
    }
}



Para el módulo de consenso, durante el proceso de recuperación, se cargará vote_for y el término correspondiente desde la base de datos subyacente primero, que es la garantía de seguridad del algoritmo de consenso, para evitar votar dos veces en el mismo término. Luego cargue las entradas de registro correspondientes.

módulo de negocios

Para los módulos comerciales, diferentes servidores tendrán diferentes tiendas y todos se basan en el mecanismo proporcionado por la base de datos subyacente. Por lo tanto, la recuperación correspondiente no está definida en el rasgo StorageApi, pero existe en LeaseStore(xline/src/storage/lease_store/mod.rs), AuthStore(xline/src/storage/auth_store/store.rs) en un método independiente y KvStore (xline/src/storage/kv_store.rs).

/// Lease store
#[derive(Debug)]
pub(crate) struct LeaseStore<DB>
where
    DB: StorageApi,
{
    /// Lease store Backend
    inner: Arc<LeaseStoreBackend<DB>>,
}

impl<DB> LeaseStoreBackend<DB>
where
    DB: StorageApi,
{
    /// omit some code
    /// Recover data form persistent storage
    fn recover_from_current_db(&self) -> Result<(), ExecuteError> {
        let leases = self.get_all()?;
        for lease in leases {
            let _ignore = self
                .lease_collection
                .write()
                .grant(lease.id, lease.ttl, false);
        }
        Ok(())
    }
}

impl<S> AuthStore<S>
where
    S: StorageApi,
{
    /// Recover data from persistent storage
    pub(crate) fn recover(&self) -> Result<(), ExecuteError> {
        let enabled = self.backend.get_enable()?;
        if enabled {
            self.enabled.store(true, AtomicOrdering::Relaxed);
        }
        let revision = self.backend.get_revision()?;
        self.revision.set(revision);
        self.create_permission_cache()?;
        Ok(())
    }
}


Entre ellos, la lógica de recuperación de LeaseStore y AuthStore es relativamente simple, por lo que no discutiremos demasiado aquí, nos centraremos en el proceso de recuperación de KvStore, y su diagrama de flujo es el siguiente

Cuándo recuperarse

El tiempo de recuperación de Xline se encuentra principalmente en la etapa inicial del inicio del sistema, y ​​la recuperación del módulo comercial se ejecutará primero, seguida de la recuperación del módulo de consenso. Dado que la recuperación de KvStore depende de la recuperación de LeaseStore, la recuperación de LeaseStore debe ubicarse antes de la recuperación de KvStore. El código correspondiente (xline/src/server/xline_server.rs) es el siguiente:

impl<S> XlineServer<S>
where
    S: StorageApi,
{
    /// Start `XlineServer`
    #[inline]
    pub async fn start(&self, addr: SocketAddr) -> Result<()> {
        // lease storage must recover before kv storage
        self.lease_storage.recover()?;
        self.kv_storage.recover().await?;
        self.auth_storage.recover()?;
        let (kv_server, lock_server, lease_server, auth_server, watch_server, curp_server) =
            self.init_servers().await;
        Ok(Server::builder()
            .add_service(RpcLockServer::new(lock_server))
            .add_service(RpcKvServer::new(kv_server))
            .add_service(RpcLeaseServer::from_arc(lease_server))
            .add_service(RpcAuthServer::new(auth_server))
            .add_service(RpcWatchServer::new(watch_server))
            .add_service(ProtocolServer::new(curp_server))
            .serve(addr)
            .await?)
    }

El proceso de recuperación del módulo de consenso (curp/src/server/curp_node.rs) es el siguiente, y su cadena de llamada de funciones es: XlineServer::start -> XlineServer::init_servers -> CurpServer::new -> CurpNode:: nuevo

// utils
impl<C: 'static + Command> CurpNode<C> {
    /// Create a new server instance
    #[inline]
    pub(super) async fn new<CE: CommandExecutor<C> + 'static>(
        id: ServerId,
        is_leader: bool,
        others: HashMap<ServerId, String>,
        cmd_executor: CE,
        curp_cfg: Arc<CurpConfig>,
        tx_filter: Option<Box<dyn TxFilter>>,
    ) -> Result<Self, CurpError> {
        // omit some code
        // create curp state machine
        let (voted_for, entries) = storage.recover().await?;
        let curp = if voted_for.is_none() && entries.is_empty() {
            Arc::new(RawCurp::new(
                id,
                others.keys().cloned().collect(),
                is_leader,
                Arc::clone(&cmd_board),
                Arc::clone(&spec_pool),
                uncommitted_pool,
                curp_cfg,
                Box::new(exe_tx),
                sync_tx,
                calibrate_tx,
                log_tx,
            ))
        } else {
            info!(
                "{} recovered voted_for({voted_for:?}), entries from {:?} to {:?}",
                id,
                entries.first(),
                entries.last()
            );
            Arc::new(RawCurp::recover_from(
                id,
                others.keys().cloned().collect(),
                is_leader,
                Arc::clone(&cmd_board),
                Arc::clone(&spec_pool),
                uncommitted_pool,
                curp_cfg,
                Box::new(exe_tx),
                sync_tx,
                calibrate_tx,
                log_tx,
                voted_for,
                entries,
                last_applied.numeric_cast(),
            ))
        };   
        // omit some code
        Ok(Self {
            curp,
            spec_pool,
            cmd_board,
            shutdown_trigger,
            storage,
        })
    }



04性能评估

En la nueva versión v0.3.0, además de presentar la capa de almacenamiento persistente, también realizamos algunas refactorizaciones importantes en algunas partes de CURP. Después de refactorizar y agregar nuevas funciones, pasó la prueba de validación y la prueba de integración no hace mucho tiempo. La información de prueba de la parte de rendimiento se ha publicado en Xlinev0.4.0.

Para obtener el informe de rendimiento, consulte el enlace:

https://github.com/datenlord/Xline/blob/master/img/xline-key-perf.png

05. Recomendaciones pasadas

[Aviso de persona desaparecida] Datan Technology continúa reclutando personas

Xline v0.4.0: una tienda KV distribuida para la gestión de metadatos

Nivel de aislamiento de base de datos y MVCC

Bienvenido a responder el correo electrónico [email protected] para unirse al grupo para obtener más información ~

Xline es una tienda KV distribuida para la gestión de metadatos. El proyecto Xline está escrito en lenguaje Rust, ¡y todos son bienvenidos a participar en nuestro proyecto de código abierto!

Enlace de GitHub : https://github.com/datenlord/Xline

Sitio web oficial de Xline : www.xline.cloud

Discordia Xlinehttps://discord.gg/XyFXGpSfvb

Supongo que te gusta

Origin blog.csdn.net/DatenLord/article/details/130930079
Recomendado
Clasificación