尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linera Storage Service 深度解析:基于 gRPC 的共享键值存储服务(RocksDB / 内存双后端)

Linera Storage Service 深度解析:基于 gRPC 的共享键值存储服务(RocksDB / 内存双后端) Linera Storage Service 深度解析基于 gRPC 的共享键值存储服务RocksDB / 内存双后端【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol导读linera-storage-service是 Linera 协议仓库中负责存储服务化的关键模块它把基于linera-views的 RocksDB 存储与内存存储包装成一个可通过 gRPC 远程访问的共享键值存储服务器并提供一个实现了KeyValueStore与KeyValueDatabase两个 trait 的客户端。阅读本文后你将掌握该服务的设计动机、gRPC 协议全貌、服务端双后端实现与全部 CLI 参数、客户端的命名空间与 root key 编码机制、以及它如何应对 gRPC 4MB 消息上限大值分块传输这一核心工程问题。模块定位为什么要做存储服务化在 Linera 架构中linera-views 提供了统一的键值存储抽象KeyValueStore/KeyValueDatabasetrait并内置了MemoryDatabase内存与RocksDbDatabaseRocksDB两种实现。而 linera-storage-service/README.md 开宗明义地说明了本模块的定位This module provides a shared key-value store server based on the RocksDB store and the in-memory store oflinera-views. The corresponding client implements theKeyValueStoreandKeyValueDatabasetraits.即它不创造第三种存储引擎而是把已有的内存/RocksDB 存储搬上网络。这样做带来两个直接收益多进程共享同一份数据在默认的本地网络启动流程中多个验证者进程可以连接同一个存储服务端点共享同一份链数据而不必各自维护独立的数据库文件见 linera-service/src/cli/net_up_utils.rs。进程与数据解耦存储层独立成服务后客户端进程验证者、钱包、索引器等可以随时重建或迁移只需重新连回端点即可。从仓库结构看linera-storage-service/Cargo.toml 中声明了linera-views、tonic、prost、clap等核心依赖产物为一个可执行文件linera-storage-server[[bin]]声明Cargo.toml同时以 crate 形式对外提供客户端与工具函数。架构总览四个源码模块linera-storage-service/src/lib.rs 中crate 暴露了三个模块模块文件职责key_value_storesrc/lib.rs由tonic::include_proto!生成的 gRPC 类型与服务定义对应 proto/key_value_store.protochildsrc/child.rs把存储服务作为子进程拉起、健康检查与生命周期管理clientsrc/client.rs连接远程存储服务的客户端实现KeyValueDatabase/KeyValueStoretraitcommonsrc/common.rs客户端与服务端共享的类型、常量、错误与配置此外 src/server.rs 是独立的二进制入口linera-storage-server包含 CLI 解析、后端初始化与全部 gRPC handler 实现。gRPC 协议14 个 RPC 的完整契约服务接口定义在 proto/key_value_store.protopackage key_value_store.v1在构建期通过tonic-prost-build生成 Rust 代码。协议包含 14 个 RPC可归为四类读操作RPC请求 / 响应语义ProcessReadValueRequestReadValue/ReplyReadValue按 key 读取单个 valueProcessContainsKeyRequestContainsKey/ReplyContainsKey判断单个 key 是否存在ProcessContainsKeysRequestContainsKeys/ReplyContainsKeys批量判断 key 是否存在ProcessReadMultiValuesRequestReadMultiValues/ReplyReadMultiValues批量读取多个 key 的 valueProcessFindKeysByPrefixRequestFindKeysByPrefix/ReplyFindKeysByPrefix按前缀扫描 key 列表ProcessFindKeyValuesByPrefixRequestFindKeyValuesByPrefix/ReplyFindKeyValuesByPrefix按前缀扫描 key-value 对写操作RPC请求 / 响应语义ProcessWriteBatchExtendedRequestWriteBatchExtended/Empty提交一批写语句见下文 Statement 结构大值分块传输RPC请求 / 响应语义ProcessSpecificChunkRequestSpecificChunk/ReplySpecificChunk按message_index 序号取回大响应的一个分片命名空间管理RPC请求 / 响应语义ProcessCreateNamespaceRequestCreateNamespace/Empty创建命名空间ProcessExistsNamespaceRequestExistsNamespace/ReplyExistsNamespace判断命名空间是否存在ProcessDeleteNamespaceRequestDeleteNamespace/Empty删除命名空间及其全部数据ProcessListAllEmpty/ReplyListAll列出服务上全部命名空间ProcessListRootKeysRequestListRootKeys/ReplyListRootKeys列出某命名空间下的 root keyProcessDeleteAllEmpty/Empty清空服务上所有数据批写语句Statement使用oneof表达四种操作proto/key_value_store.protodelete按 key 删除put写入KeyValueappend写入KeyValueAppend { key, value, last }用于把超大的 value 分片拼装last标记最后一片delete_prefix按前缀批量删除。服务端实现双后端与 CLI 全参数linera-storage-server的入口在 src/server.rs。main先初始化tracing日志支持RUST_LOG与RUST_LOG_SPAN_EVENTS控制 span 事件输出再解析 CLI 选项随后创建后端存储、装配StorageServer结构体最后通过tonic的Server::builder()把StorageServiceServer服务注册到指定端点。后端类型服务端内部用一个本地枚举封装两种后端src/server.rsenum LocalStore { Memory(MemoryDatabase as KeyValueDatabase::Store), #[cfg(with_rocksdb)] RocksDb(RocksDbDatabase as KeyValueDatabase::Store), }memory后端使用MemoryDatabase配置为kill_on_drop: false进程退出不销毁数据见 src/server.rsrocksdb后端使用RocksDbDatabase需要rocksdbfeature 开启Cargo.toml 中的rocksdb [linera-views/rocksdb]。从源码结构看两种后端在 src/server.rs 中通过 match 分支把read_value_bytes、contains_key、read_multi_values_bytes、find_keys_by_prefix、find_key_values_by_prefix、write_batch等底层调用映射到各自的KeyValueDatabase实现上错误统一转换为 gRPCStatus。CLI 参数CLI 由StorageServerOptions枚举定义src/server.rs子命令与参数如下memory子命令参数默认值说明--namespacelinera_storage_service存储命名空间--endpoint无必填服务监听地址如127.0.0.1:1235rocksdb子命令需编译时开启rocksdbfeature参数默认值说明--namespacelinera_storage_service存储命名空间--endpoint无必填服务监听地址--path无必填RocksDB 数据库目录路径--max-cache-size1000000010MB缓存最大字节数key value 合计--max-value-entry-size10000001MB单条 value 条目最大字节数--max-find-keys-entry-size10000001MBfind_keys 单条目最大字节数--max-find-key-values-entry-size10000001MBfind_key_values 单条目最大字节数--max-cache-entries1000缓存最大条目数--max-cache-value-size1000000010MB缓存中 value 总量上限字节--max-cache-find-keys-size1000000010MB缓存中 find_keys 结果上限字节--max-cache-find-key-values-size1000000010MB缓存中 find_key_values 结果上限字节上述缓存参数在代码中被装配成StorageCacheConfig见 src/server.rs最终与RocksDbStoreInternalConfig含spawn_mode、path_with_guard、enable_statistics等一起构成RocksDbStoreConfig。其中enable_statistics固定为false统计级别使用默认值。典型启动命令如下# 内存后端 linera-storage-server memory --endpoint 127.0.0.1:1235 # RocksDB 后端 linera-storage-server rocksdb --endpoint 127.0.0.1:1235 \ --path /data/linera/storage \ --max-cache-size 10000000 --max-cache-entries 1000客户端实现KeyValueDatabase 之上的远程存储客户端核心在 src/client.rs由两层结构组成StorageServiceDatabaseInternal一个数据库句柄持有tonic::Channel、可选的并发信号量与namespaceBCS 序列化后的字节。它实现了KeyValueDatabasetraitsrc/client.rs负责connect、open_shared、list_all、list_root_keys、delete_all、exists、create、delete。StorageServiceStoreInternal由open_shared(root_key)产生代表某命名空间 某 root key下的具体存储实现ReadableKeyValueStore与WritableKeyValueStoresrc/client.rs。命名空间与 root key 的物理编码客户端注释src/client.rs清楚地说明了数据布局。服务端用KeyPrefix枚举src/common.rs作为逻辑键的第一个字节标签标签存储内容物理 key 布局Key真正的键值对数据KeyPrefix::Key namespace keyNamespace命名空间存在性标记空 valueKeyPrefix::Namespace namespaceRootKeyroot key 存在性标记空 valueKeyPrefix::RootKey namespace root_key命名空间本身用bcs::to_bytes(namespace)编码src/client.rs之所以可以直接拼接是因为字符串的 BCS 序列化集合具有前缀自由prefix-free性质不会产生键歧义。读路径与并发控制每个读 RPC 在客户端侧先拼出完整 keystart_key KeyPrefix::Key namespace bcs(root_key)再追加用户 key然后调用 gRPC 客户端在发送前会通过acquire()获取信号量src/client.rs。该信号量由配置项max_concurrent_queries控制Some(n)时创建Semaphore::new(n)None时无限制见 src/client.rs用于限制单个客户端对数据库的并发查询数。所有调用使用connect_lazy延迟建连避免启动即阻塞。此外客户端对StorageServiceStoreInternal的root_key()直接反序列化start_key中 namespace 之后的部分得到MAX_KEY_SIZE固定为 1MBsrc/client.rs所有写/读 key 都会校验长度并返回KeyTooLong错误src/common.rs。首次写入自动登记 root keywrite_batch有一个细节通过root_key_written原子标记AtomicBool客户端在每个 store 的第一次批量写入时会自动附带一条Put把start_key以KeyPrefix::RootKey开头登记为 root key 存在性标记src/client.rs。这正是list_root_keys能枚举出命名空间下所有 root key 的原因——服务端在list_root_keys中按前缀KeyPrefix::RootKey namespace扫描后做 BCS 反序列化src/server.rs。大值处理gRPC 4MB 上限下的分块协议这是本服务最核心的工程点。src/common.rs 的注释解释了缘由gRPC尤其是tonic库对收发消息都有 4MB4194304 字节的默认上限因此模块把载荷上限定为pub const MAX_PAYLOAD_SIZE: usize 4000000;刻意比 4194304 略小为消息长度前缀等元数据留出余量。大读pending_big_reads 分片拉取服务端在process_read_value/process_read_multi_values/process_find_keys_by_prefix/process_find_key_values_by_prefix中会先统计响应字节数src/server.rs若总大小 MAX_PAYLOAD_SIZE直接内联返回否则调用insert_pending_readsrc/server.rs把结果 BCS 序列化后按MAX_PAYLOAD_SIZE切块存入pending_big_readsBTreeMapi64, BigRead分配自增的message_index响应中只携带message_index与num_chunksvalue 置空。客户端拿到分块标记后通过read_entriessrc/client.rs用futures::join_all并发发起num_chunks个ProcessSpecificChunk请求按序拼接字节流后再bcs::from_bytes还原。服务端在process_specific_chunk中按message_index取块并在取完最后一块时删除临时条目以释放内存src/server.rs。大写append 分片拼装写路径中客户端在write_batch内按批大小累计key value root key 长度当累计超过MAX_PAYLOAD_SIZE时先提交已有语句若单条 put就超过上限则把 value 切成MAX_PAYLOAD_SIZE大小的分片逐片发送Operation::Append(KeyValueAppend { key, value, last })src/client.rs。服务端收到 append 后把分片暂存到pending_big_putsRwLockBTreeMapVecu8, Vecu8中按 key 累积拼接直到收到last true的分片才落盘为一次真正的Putsrc/server.rs。测试 tests/store_test.rs 的test_storage_service_big_raw_write专门验证了这一能力它写入一个 5,000,000 字节约 5MB的随机 value——远超 gRPC 4MB 上限——并断言读写一致。测试还覆盖了随机场景读取、空库写入、有状态写入、命名空间管理、root key 管理等用例。子进程模式StorageService 与生命周期守卫在本地开发/测试场景存储服务常由上层代码自动拉起。src/child.rs 提供了StorageService与StorageServiceGuardStorageService::new(endpoint, binary)记录端点和linera-storage-server二进制路径run()的启动流程src/child.rs分三步先wait_for_absence确认端点未被占用最多等待约 9 秒再以memory --endpoint ...参数spawn_into拉起子进程kill_on_drop(true)最后轮询storage_service_check_validity做连通性验证该函数会真实发起一次read_value_bytes([42])读写探测见 src/client.rs验证成功才返回返回的StorageServiceGuard持有Child守卫被 drop 时子进程随之销毁实现了 RAII 式的生命周期管理。二进制定位由 src/common.rs 的get_service_storage_binary完成通过resolve_binary在linera-storage-server主目录与../linera-storage-servercrate 目录两个位置间兜底查找。这正是linera net up默认路径的实现当未显式指定--storage时linera-service/src/cli/net_up_utils.rs 会获取一个空闲端口、调用StorageService::new(...).run()自动拉起存储服务子进程并把InnerStorageConfig::Service { endpoint }作为默认存储配置用户显式指定存储时则走解析路径Some(storage)分支。配置集成与使用方式客户端连接配置由 src/common.rs 的StorageServiceStoreInternalConfig承载pub struct StorageServiceStoreInternalConfig { pub endpoint: String, // 服务端点如 127.0.0.1:1235 pub max_concurrent_queries: Optionusize, // 客户端最大并发查询数 }http_address()会把它规范为http://{endpoint}供 tonicEndpoint::from_shared使用。对外暴露的类型别名StorageServiceStoreConfig LruCachingConfigStorageServiceStoreInternalConfig且不带 metrics 时客户端整体为LruCachingDatabaseStorageServiceDatabaseInternal开启metricsfeature 时则嵌套MeteredDatabase包裹src/client.rs——即在远程调用之上再叠加一层 LRU 缓存兼顾网络延迟与热数据命中。在上层linera-storage-runtime/src/storage_config.rs 展示了它如何被整合进统一的StoreConfigInnerStorageConfig::Service { endpoint }会构造StorageServiceStoreInternalConfig其中max_concurrent_queries取自CommonStorageOptions::storage_max_concurrent_queries再包上views_storage_cache_config()成为StoreConfig::StorageService。也就是说最终用户只需要在 CLI 上给出一个形如service:127.0.0.1:1235的存储选择就能让验证者/钱包统一走远程存储。基准测试benches/store.rs 使用 Criterion 对远程存储客户端做了七个维度的基准contains_key、contains_keys、find_keys_by_prefix、find_key_values_by_prefix、read_value_bytes、read_multi_values_bytes、write_batch全部基于linera_views::test_utils::performance的通用性能框架可与内存/RocksDB 本地实现做横向对比。这也从侧面印证了该客户端在设计上追求与本地存储同构的 trait 接口——上层业务代码无需感知存储是本地还是远程。小结linera-storage-service的完整闭环可以概括为protocolproto 契约→ server双后端 CLI→ clienttrait 实现 缓存→ child进程管理→ config上层集成。它复用了linera-views的存储抽象与linera-storage的配置体系以约 4MB 分块协议解决了 gRPC 传输上限问题并凭借KeyValueDatabase一致性接口让整个 Linera 生态可以无缝地在本地存储与远程共享存储之间切换。若需进一步探索可重点阅读 src/server.rs、src/client.rs、proto/key_value_store.proto 三份核心文件以及 tests/store_test.rs 中的大值用例。【免费下载链接】linera-protocolMain repository for the Linera protocol项目地址: https://gitcode.com/GitHub_Trending/li/linera-protocol创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表