Xline 持久化存储设计与实现

news2024/9/9 0:09:59

01、引言

在 Xline 早期的原型阶段,我们采用了基于内存的存储来实现数据的持久化。这虽然简化了 Xline 原型设计的复杂度,提高了项目的开发和迭代速度,但带来的影响也是显著的:由于数据都存储在内存当中,因此一旦当进程 crash 后,节点的数据恢复需要依赖于从其他正常节点上拉取全量数据,这就需要较长的恢复时间。

基于此方面的考虑,Xline 在最新发布的版本 v0.3.0 中引入了一个 Persistent Storage Layer,来将数据持久化到磁盘当中,同时向上层调用方屏蔽掉无关的底层细节。

02、存储引擎选型

目前业界主流的存储引擎基本可分为基于 B+ Tree 的存储引擎和基于 LSM Tree 的存储引擎。他们有着各自的优势与劣势。

B+ Tree 读写放大分析

B+ Tree 在读取数据时,需要先沿着根节点,逐步向下层索引,直到最后访问到最底层的叶子结点,每层访问对应了一次磁盘 IO。而写入数据时,同样也沿着根节点向下搜索,找到对应的叶子结点后写入数据。

为了方便分析,我们进行相关约定,B+ Tree的block size为B,故每个内部节点包含O(B)个子节点,叶子节点包含O(B)条数据,假设数据集大小为N,则B+ Tree的高度为

写放大:B+ Tree的每次insert都会在叶子节点写入数据,不论数据实际大小是多少,每次都需要写入大小为 B 的数据块,因此写放大是 O(B)

读放大:B+ Tree的一次查询需要从根节点一路查到具体的某个叶子节点,所以需要等于层数大小的I/O,也就是

, 即读放大为

LSM Tree 读写放大分析

LSM Tree 在数据写入时,先以文件追加的形式写入一个内存文件 memtable(Level 0),当 memtable 达到固定大小时,将其转换成 immutable memtable,并合并到下一个 level 中。而对于数据的读取,则需要先在 memtable 中进行查找,当查找失败时,则向下逐层查找,直到找到该元素为止。LSM Tree 常采用 Bloom Filter 来优化读取操作,过滤掉那些不存在于数据库中的元素。

假设数据集大小为N,放大因子为k,最小层一个文件大小为B,每层文件的单个文件大小相同都为B,不过每层文件个数不同。

写放大:假设写入一个 record,在本层写满 k 次后会被 compact 到下一层。因此平均单层写放大应为

。一共有

层,故写放大为

读放大:最坏的情况下,数据被 compact 到最后一层,需要依次在每一层进行二分查找,直到在最后一层找到.

对于最高层 

,数据大小为 O(N), 需要进行二分查找,需要 

次磁盘读操作

对于次高层 

, 数据大小为 

, 需要进行 

次磁盘读操作

对于 

, 数据大小为 

,需要进行 

次磁盘读操作

……

以此类推,最终读放大为 R = 

总结

从读写放大的复杂度分析来看,基于 B+ Tree 的存储引擎更加适合读多写少的场景,而基于 LSM Tree 的存储引擎则更加适合写多读少的场景。

Xline 作为一款由 Rust 编写的开源分布式 KV 存储软件,在选择持久化存储引擎方面,需要有如下的考虑:

  1. 从性能方面:对于存储引擎而言,往往容易成为系统的性能瓶颈之一,因此必须选择高性能的存储引擎。而高性能的存储引擎必然要由高性能的语言来编写,同时要优先考虑异步实现。优先考虑 Rust 语言,其次是 C/C++ 语言。
  2. 从开发的角度:优先考虑 Rust 语言实现,这样能够在当前阶段减少一些额外的开发工作。
  3. 从维护的角度
    1. 考虑引擎的背后支持者:优先考虑大型商业公司,开源社区
    2. 业界需要有广泛使用,以便于在后期 debug、tuning 过程中能够有更多借鉴经验
    3. 知名度和受欢迎程度(github star)应当较高,以便于吸引优秀的贡献者参与
  4. 从功能角度:需要存储引擎提供事务语义,支持基本的 KV 相关操作,支持批处理操作等。

需求的优先级排列为:功能 > 维护 >= 性能 > 开发

我们主要调研了 Sled、ForestDB、RocksDB,bbolt 和 badger 等多个开源嵌入式数据库。其中,能够同时满足我们前面提到的四点要求的只有 RocksDB。RocksDB 由 Facebook 实现并开源,目前在业界有着良好的应用生产实践,同时版本依然保持着稳定的发布速度,在功能上也可以完美地覆盖我们的需求。

Xline 主要服务于跨云数据中心的一致性元数据管理,其工作场景主要是读多写少的场景。有些读者可能会有疑问,RocksDB 不是基于 LSM Tree 的存储引擎吗?而基于 LSM Tree 的存储引擎应当更加适合写多读少的应用场景,那为什么还要选择使用 RocksDB 呢?

的确,从理论上讲,最合适的存储引擎应当是基于 B+ Tree 的存储引擎。但考虑到像 Sled、ForestDB 等基于 B+ Tree 的嵌入式数据库缺少大型应用生产的实践,同时版本维护也处于停滞状态。经过了取舍后,我们选择了 RocksDB 作为 Xline 的存储后端。同时为了考虑到未来可能会有更加合适的存储引擎可供替换,我们在 Persistent Storage Layer 的设计上做了良好的接口分离与封装,可以最大程度地降低后期更换存储引擎的成本。

03、持久化存储层设计与实现

在开始讨论持久化存储层的设计与实现之前,我们需要先明确我们对持久化存储的需求预期:

  1. 正如前面所说,在做出相应的 trade-off 后,我们采用了 RocksDB 作为 Xline 的后端存储引擎。因此,我们不能排除未来会替换这一存储引擎的可能,StorageEnginne 的设计必须符合 OCP 原则,满足可配置,易替换的原则。
  2. 我们需要为上层使用者,提供基础的 KV 接口
  3. 要实现一套完备的 Recover 机制。

整体架构与写入流程

我们先来看看 Xline 当前的整体架构,如下图所示:

从上到下,Xline 的整体架构可以被划分为 访问层,共识模块,业务逻辑模块,存储 API 层和存储引擎层。其中存储API 层主要负责分别向业务模块和共识模块提供业务相关的 StorageApi,同时屏蔽底层的 Engine 的实现细节。而存储引擎层则负责实际数据的落盘操作。

我们以一次 PUT 请求为例,来看看数据的写入过程。当 client 向 Xline Server 发起一次 Put 请求时,会发生如下事情:

  1. KvServer 接收到用户发送来的 PutRequest 后,会先对请求进行合法性检查,检查通过后,通过自身 CurpClient 向 Curp Server 发起一次 propose 的 rpc 请求
  2. Curp Server 接收到 Propose 请求后,会先进入到 fast path 流程中。它会将请求中的 cmd 保存到 Speculative Executed Pool (aka. spec_pool)中,来判断是否与当前 spec_pool 中的命令是否冲突,冲突则返回 ProposeError::KeyConflict,并等待 slow path 完成,否则继续走当前的 fast_path
  3. 在 fast_path 中,一个命令如果既不冲突,又不重复,则会通过特定的 channel 通知后台的 cmd_worker 去执行。cmd_worker 一旦开始执行,会将对应的命令保存到 CommandBoard 中,以便 track 命令的执行情况。
  4. 当集群中的多个节点达成了共识后,则会提交状态机日志,并将这条日志持久化到 CurpStore 中,最后 apply 这条日志。在 apply 的过程中,会调用对应的 CommandExecutor,也就是业务模块中,各个 server 对应的 store 模块,将实际的数据通过 DB 持久化到后端数据库中。

接口设计

下图是 StorageApi 和 StorageEngine 两个 trait 以及相应的数据结构之间的相互关系

Storage Engine Layer

Storage Engine Layer 主要定义了 StorageEngine trait 以及相关的错误。

StorageEngine Trait 定义(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>;
}

相关的错误定义

#[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) 和 RocksEngine(engine/src/rocksdb_engine.rs) 则实现了 StorageEngine trait。其中 MemoryEngine 主要用于测试,而 RocksEngine 的定义如下:

/// `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
}

StorageApi Layer

业务模块

业务模块的 StorageApi 定义

/// 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>;
}


在业务模块,DB(xline/src/storage/db.rs) 负责将 StorageEngine 转换成为 StorageApi 供上层调用,它的定义如下:

/// 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 
}

在业务模块中的不同 Server 拥有自己的 Store 后端,其核心数据结构正是 StorageApi Layer 中的 DB。

共识模块

Curp 模块的 StorageApi 定义(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>;
}



而 RocksDBStorage(curp/src/server/storage/rocksdb.rs) 就是前面架构图中提到的 CurpStore,负责将 StorageApi 转换成底层的 RocksEngine 操作。

/// `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
}

实现相关

数据视图

在引入了 Persistent Storage Layer,Xline 中通过逻辑表 table 来分割不同的命名空间,目前它对应了底层的 Rocksdb 中的 Column Family。

当前有如下几张表:

  1. curp:存储 curp 相关的持久化信息,包括了 log entries,以及 voted_for 和对应的 term 信息
  2. lease: 保存了已授予的 lease 信息
  3. kv: 保存 kv 信息
  4. auth: 保存了当前 Xline 的 auth enable 情况以及相应的 enable revision
  5. user: 保存了 Xline 中添加的 user 信息
  6. role: 保存了 Xline 中添加的 role 信息
  7. meta: 保存了当前被 applied 的 log index

可扩展性

Xline 之所以将存储相关操作,拆分成了 StorageEngine 和 StorageApi 两个不同的 trait 并分散到两个不同的层级上,是为了隔离变化。StorageEngine trait 提供机制,StorageApi 则由上层的模块来定义,不同的模块可以有自己的定义,实现特定的存储策略。而 StorageApi 层的 CurpStore 和 DB 则负责实现这两个 trait 之间的转换。由于上层调用者不直接依赖于底层的 Storage Engine 相关内容,因此后面即便更换存储引擎也不会导致上层模块的代码需要做出大量的修改。

Recover 过程

对于 Recover 过程而言,重要不过两件事情,第一是 recover 哪些数据,第二是什么时候做 recover?我们先来看不同模块之间 recover 所涉及到的数据。

共识模块

在共识模块中,由于 RocksDBStorage 是专属于 Curp Server 使用的,因此可以直接将 recover 加入到相应的 StorageApi trait 中。具体实现如下:

#[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))
    }
}



对于共识模块而言,在 recover 过程中,会先从底层的 db 中加载 voted_for 以及相应的 term,这是处于共识算法的安全性保证,为了避免在同一个 term 内投出两次票。随后加载对应的 log entries。

业务模块

对于业务模块而言,不同的 Server 会拥有不同的 Store,它们共同依赖于底层 DB 所提供的机制。因此,对应的 recover 并不定义在 StorageApi 这个 trait,而是以独立的方法存在于 LeaseStore(xline/src/storage/lease_store/mod.rs)、AuthStore(xline/src/storage/auth_store/store.rs) 和 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(())
    }
}


其中,LeaseStore 和 AuthStore 的 recover 逻辑较为简单,这里不过多展开讨论,我们重点讨论 KvStore 的 recover 过程,其流程图如下

Recover 的时机

Xline 的 recover 时机主要位于系统的启动初期,会优先执行业务模块的 recover,随后是共识模块的 recover。其中由于 KvStore 的 recover 依赖于 LeaseStore 的 recover,因此 LeaseStore 的 recover 需要位于 KvStore 的 recover 之前,对应代码(xline/src/server/xline_server.rs)如下:

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?)
    }

共识模块的 recover 过程(curp/src/server/curp_node.rs)如下,其函数调用链为:XlineServer::start -> XlineServer::init_servers -> CurpServer::new -> CurpNode::new

// 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性能评估

在 v0.3.0 的新版本中,我们除了引入了 Persistent Storage Layer 以外,还对 CURP 的部分内容做了一些大型的重构。在重构完毕,添加新功能后,前不久通过了 validation test 和 Integration test。性能部分的测试信息,已经在Xlinev0.4.0 中释放出来。

性能报告请参考链接:

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

05、往期推荐

【寻人启事】达坦科技持续招人ing

Xline v0.4.0: 一个用于元数据管理的分布式KV存储

数据库隔离级别及MVCC

欢迎回复邮箱info@dantenlord.io加群了解更多信息~

Xline是一个用于元数据管理的分布式KV存储。Xline项目以Rust语言写就,欢迎大家参与我们的开源项目!

GitHub链接https://github.com/datenlord/Xline

Xline 官网www.xline.cloud

Xline Discordhttps://discord.gg/XyFXGpSfvb

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/584386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TOOM舆情监控黑科技:AI破解人心,预测社会未来!

近年来&#xff0c;随着人工智能技术的快速发展&#xff0c;舆情监控正逐渐进入一个全新的时代。利用人工智能技术&#xff0c;舆情监控能够洞察人心&#xff0c;预测社会未来的走向&#xff0c;这项黑科技引发了广泛的关注和探讨。 舆情监控是通过对社会舆论的搜集、分析和评…

盛元广通生物样本库管理系统

生物样本库管理系统&#xff0c;作为一种高效、智能化的信息管理工具&#xff0c;在现代生物研究中扮演着重要的角色。随着科学技术的不断进步&#xff0c;生物样本的采集、保存和管理变得越来越重要&#xff0c;而盛元广通生物样本库管理系统正是为了解决这一问题而应运而生的…

无缝对接多语言:参数校验的终极指南(一)!

前言 在此之前&#xff0c;写过在两篇文章&#xff0c;是关于如何在 SpringBoot 内实现统一参数校验和自定义校验注解的。毕竟作为后端来讲&#xff0c;对于前端传来的数据&#xff0c;需要保持高度的警惕。避免出现异常数据&#xff0c;导致系统异常。统一参数校验和自定义校验…

现在的00后,真是卷死了呀,辞职信已经写好准备提交了·····

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;四月份春招我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪22K&#xff0c;都快接近我了。 后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了…

自从用了 EasyExcel,导入导出 Excel 更简单了!

EasyExcel 在做excel导入导出的时候,发现项目中封装的工具类及其难用,于是去gitHub上找了一些相关的框架,最终选定了EasyExcel。之前早有听闻该框架&#xff0c;但是一直没有去了解&#xff0c;这次借此学习一波&#xff0c;提高以后的工作效率。 实际使用中&#xff0c;发现…

服了呀,00后怎么这么卷....

现在的小年轻真的卷得过分了。前段时间我们公司来了个00年的&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 最近和他聊了一次天&#xff0c;原来这位小老弟家里条…

FDA辅料数据库-在线查询网址

在药物的制剂研发过程中&#xff0c;辅料是不可或缺的。然而&#xff0c;在使用辅料时需要明确其安全使用量&#xff0c;这并非易事。因此&#xff0c;美国FDA 辅料数据库&#xff08;IID&#xff09;成为了一个重要的参考标准&#xff0c;对于制剂开发提供了帮助。 如果研发人…

《汇编语言》- 读书笔记 - 第7章- 更灵活的定位内存地址的方法

《汇编语言》- 读书笔记 - 第7章- 更灵活的定位内存地址的方法 7.1 and 和 or 指令7.2 关于 ASCII 码7.3 以字符形式给出的数据程序 7.1 7.4 大小写转换的问题7.5 [bxidata] &#xff08;变量 固定偏移量&#xff09;问题 7.1 7.6 用[bxidata]的方式进行数组的处理7.7 SI 和 D…

软件测试测试环境搭建很难?一天学会这份测试环境搭建教程

如何搭建测试环境&#xff1f;这既是一道高频面试题&#xff0c;又是困扰很多小伙伴的难题。因为你在网上找到的大多数教程&#xff0c;乃至在一些培训机构的课程&#xff0c;都不会有详细的说明。 你能找到的大多数项目&#xff0c;是在本机电脑环境搭建环境&#xff0c;或是…

Linux服务器上如何安装OpenCV的库?

Linux上安装OpenCV其实挺简单的。对于Python来说&#xff0c;可以直接使用pip进行安装&#xff0c;如&#xff1a; pip3 install opencv-python 当然&#xff0c;如果你是想在C或者Java内作为外部包使用&#xff0c;你可以考虑编译安装。 安装依赖 首先是依赖安装问题&#…

LED屏控制卡

LED屏控制卡&#xff08;LED Screen Control Card&#xff09;是一种用于控制和管理LED显示屏的关键设备。它通常是一个硬件设备&#xff0c;具有处理器、存储器、接口和软件功能&#xff0c;用于接收、解码和显示图像、视频和其他多媒体内容。 以下是LED屏控制卡的一些重要特点…

JVM (基础概念、类加载过程、垃圾回收算法)

目录 一、JVM 是什么 二、JVM 运行流程 三、Java运行时数据区 1、程序计数器&#xff08;线程私有&#xff09; 2、栈区&#xff08;线程私有&#xff09; 3、堆 4、方法区 四、OOM内存溢出和内存泄漏 1、OOM内存溢出 2、内存泄漏 五、类加载过程 1、加载 2、连接…

PMP课堂模拟题目及解析(第14期)

131. 项目经理正在制定干系人参与计划&#xff0c;并识别到一位权力等级较高但在项目中兴趣较低的干系人&#xff0c;项目经理应该如何对待该干系人&#xff1f; A. 重点管理 B. 随时告知 C. 监督 D. 令其满意 132. 项目经理识别到项目干系人具有明显不同的需求和期望。…

不合格机器人工程专业讲师笔记-230529-

工作八年&#xff0c;最大的遗憾&#xff0c;就是对不起学生&#xff0c;对不起同事&#xff0c;对不起领导&#xff0c;各项工作都没有做好。 但是由于个人水平低&#xff0c;能力差&#xff0c;唯一能做的就是在忏悔中不断总结&#xff0c;避免一次又一次失败。 一万句&…

别让测试岗位的坑太大,10年老鸟亲身经历告诉你如何避开陷阱

测试岗位一直是IT行业中备受争议的一个职业&#xff0c;有人看重其重要性&#xff0c;有人则认为这是个巨坑。如果你也对测试岗位存在疑虑和担忧&#xff0c;那么这篇文章一定能帮到你&#xff01; 作者是一位在测试领域摸爬滚打了10年的老鸟&#xff0c;他深刻地理解了测试工…

行业报告 | 智能制造在中国—中国机器视觉产业链现状分析

文 | BFT机器人 导语 Introduction 智能制造装备是指具有感知、分析、推理、决策、控制功能的制造装备&#xff0c;它是先进制造技术、信息技术和智能技术的集成和深度融合&#xff0c;体现了制造业智能化、数字化和网络化的发展要求。智能制造装备的水平已成为当今衡量一个国家…

数据类岗位面试随想录

数据分析或者是偏向数据分析的数据开发岗&#xff0c;要求无非就是SQL、Python和业务相关的问题。 1 SQL问答 基本这些问题和期末考试的难度比&#xff0c;是简单的。和学校所教的比&#xff0c;基本超纲的问题只会有窗口函数。这一部分面试官一般不会问你难的问题&#xff0c…

通用支付系统设计

支付永远是一个公司的核心领域&#xff0c;因为这是一个有交易属性公司的命脉。那么&#xff0c;支付系统到底长什么样&#xff0c;又是怎么运行交互的呢?抛开带有支付牌照的金融公司的支付架构&#xff0c;下述链路和系统组成基本上符合绝大多数支付场景。其实整体可以看成是…

【人脸识别】insightface 使用记录和搭建服务注意点 从0到1

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.开始1.1 前置1.2 再次运行&#xff0c;人脸检测跑通 前言 人脸识别项目&#xff0c;再走一遍。之前是公司老人留下的&#xff0c;没部署过&#xff0c;没交付…

【HMS Core】Health Kit关于获取历史数据问题

【问题描述1】 应用已经开通了历史数据访问权限&#xff0c;同时用户在授权页面已经勾选了”历史数据“项&#xff0c;然后我们是调用healthkit的rest接口查询健康数据&#xff0c;那么是否用户授权之前一年的健康数据都能被查询到呢&#xff1f; 【解决方案】 当用户授予应用…