一,背景介绍
Nacos如何满足分布式CAP理论中的一致性“C”,主要是依赖SOFARaft。SOFARaft(Java 实现的分布式一致性协议)是Raft共识算法(见前文《Raft协议介绍》)的一种优秀实现。SOFARaft使用RocksDB 作为存储引擎,可以显著提升其日志和状态的持久化性能和稳定性。RocksDB可以提供高性能和可靠的持久化支持。在 JRaft 中使用 RocksDB,主要涉及到配置和初始化,将其用作日志和状态机的持久化存储。
二,RocksDB介绍
RocksDB 是一个高性能的键值存储引擎,由 Facebook 开发并开源,起源于 Google 的 LevelDB。它是一个基于磁盘的键值存储库,使用 LSM-Tree(Log-Structured Merge Tree)数据结构。
2.1 RocksDB的特性:
- 高性能:RocksDB 是为快速读写操作优化的,特别适用于需要低延迟和高吞吐量的应用场景,如实时分析、在线事务处理等。
- 可嵌入:它是一个嵌入式数据库,这意味着它可以直接嵌入到应用程序中,而不需要运行一个独立的服务器进程。这使得它非常适合于需要紧密集成数据库存储的应用程序。
- 灵活的配置:RocksDB 提供了多种配置选项,允许开发者根据具体需求调整读写性能、内存使用、磁盘使用等。
- 支持多种存储引擎:除了默认的磁盘存储引擎外,RocksDB 还支持基于内存的存储引擎,以满足不同的性能需求。
- 丰富的功能:包括快照、事务、列族、压缩、合并操作等,这些功能使得 RocksDB 能够应对各种复杂的应用场景。
- 横向扩展:虽然 RocksDB 本身是一个嵌入式数据库,但它可以通过集成到分布式系统(如 Apache Cassandra、TiDB 等)中实现横向扩展。
2.2 RocksDB的应用场景:
- 缓存存储:由于其快速的读写性能,RocksDB 经常被用作缓存存储解决方案。
- 实时数据分析:适用于需要低延迟数据写入和查询的实时分析系统。
- 嵌入式设备:因其嵌入式特性,RocksDB 常用于需要本地数据存储的嵌入式设备中。
- 分布式系统:作为存储引擎集成到分布式数据库和文件系统中。
2.2.1 . 日志存储和处理
在日志存储和处理系统中,RocksDB 可以处理高频率的写入操作,并提供高效的数据持久化和查询能力。
特点和优势:
- 高效写入:RocksDB 的 LSM-Tree 结构和写缓冲机制可以处理高频率的写入操作,减少写入放大。
- 持久化:通过 WAL 日志和快照机制,确保数据在系统崩溃时也能恢复。
- 压缩:支持多种压缩算法,减少存储空间消耗。
示例:
- Kafka 使用 RocksDB 作为内部的状态存储引擎,用于存储日志和偏移量信息。
2.2.2 缓存系统
RocksDB 可以作为持久化缓存层,结合内存缓存和磁盘存储的优势。
特点和优势:
- 持久化缓存:数据可以永久存储在磁盘上,减少数据丢失的风险。
- 内存管理:灵活的内存管理策略,可以根据内存大小调整缓存策略。
- 高性能读取:通过内存中的 memtable 和 block cache 提供高效的读取性能。
示例:
- Facebook 的 McDipper:一个分布式缓存系统,使用 RocksDB 作为持久化存储。
2.2.3. 大数据和分布式系统
在大数据处理和分布式系统中,RocksDB 可以作为高效的存储引擎,支持高并发读写操作。
特点和优势:
- 多线程支持:充分利用多核处理器的优势,提升并发性能。
- 灵活配置:提供大量的配置选项,可以根据具体场景进行优化。
- 事务支持:基本的事务支持,确保数据一致性。
示例:
- Apache Flink:一个流处理框架,使用 RocksDB 作为状态存储引擎。
- CockroachDB:一个分布式 SQL 数据库,使用 RocksDB 作为底层存储引擎。
2.2.4. 物联网和边缘计算
在资源受限的物联网设备和边缘计算环境中,RocksDB 提供高效的数据存储和处理能力。
特点和优势:
- 小内存占用:适合内存有限的设备。
- 高效写入和读取:适合频繁的传感器数据收集和查询。
- 嵌入式:无需独立服务,可以直接嵌入到应用程序中。
示例:
物联网网关设备:收集传感器数据并进行本地存储和处理。
2.2.5. NoSQL 数据库
许多 NoSQL 数据库使用 RocksDB 作为存储引擎,提供高效的键值存储和查询能力。
特点和优势:
- 高并发支持:多线程写入和读取,提升并发性能。
- 灵活的键值存储:支持多种数据类型和复杂查询。
- 持久化和压缩:确保数据安全并优化存储空间。
示例:
- TiDB:一个分布式 SQL 数据库,使用 RocksDB 作为底层存储。
- ScyllaDB:一个高性能的 NoSQL 数据库,也使用 RocksDB 作为存储引擎。
三,SOFARaft对RocksDB的使用
3.1 RocksDB的使用方式
SOFARaft工程中,pom文件包含如下依赖,SOFARaft通过依赖rocksdbjni这个Java类库,以API的方式间接调用RocksDB
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>${rocksdb.version}</version>
</dependency>
打开这个依赖库可以发现,包含很多系统库文件,而这些系统库包含直接操作RocksDB的函数,可以看到这个依赖库提供了RocksDB在不同平台的实现;
从SOFARaft的源码中也能看到,目前默认的日志存储是使用的RocksDB
@SPI
public class DefaultJRaftServiceFactory implements JRaftServiceFactory {
public static DefaultJRaftServiceFactory newInstance() {
return new DefaultJRaftServiceFactory();
}
@Override
public LogStorage createLogStorage(final String uri, final RaftOptions raftOptions) {
Requires.requireTrue(StringUtils.isNotBlank(uri), "Blank log storage uri.");
return new RocksDBLogStorage(uri, raftOptions);
}
@Override
public SnapshotStorage createSnapshotStorage(final String uri, final RaftOptions raftOptions) {
Requires.requireTrue(!StringUtils.isBlank(uri), "Blank snapshot storage uri.");
return new LocalSnapshotStorage(uri, raftOptions);
}
@Override
public RaftMetaStorage createRaftMetaStorage(final String uri, final RaftOptions raftOptions) {
Requires.requireTrue(!StringUtils.isBlank(uri), "Blank raft meta storage uri.");
return new LocalRaftMetaStorage(uri, raftOptions);
}
@Override
public LogEntryCodecFactory createLogEntryCodecFactory() {
return LogEntryV2CodecFactory.getInstance();
}
}
那么使用RocksDB存储的内容具体是什么,有什么作用?
3.2 SOFAJRaft Log存储模块
SOFARaft存储主要分为以下三类:
- Log存储: Raft 配置变更和用户提交任务日志;
- Meta存储:即元信息存储记录 Raft 实现的内部状态;
- Snapshot存储:用于存放用户的状态机 Snapshot 及元信息;
重点了解一下Log存储这一块,Log存储模块记录 Raft 配置变更和用户提交任务的日志,把日志从 Leader 复制到其他节点上面。以下为日志存储模块几个关键内部组件,其分工如下:
- LogStorage 是日志存储实现,默认实现基于 RocksDB 存储,通过 LogStorage 接口扩展自定义日志存储实现;
- LogManager 负责调用底层日志存储 LogStorage,针对日志存储调用进行缓存、批量提交、必要的检查和优化;
- StateMachine:状态机接口,处理应用到状态机的日志条目;
LogStorage 日志存储实现,定义 Raft 分组节点 Node 的 Log 存储模块核心 API 接口包括:
- 返回日志里的首/末个日志索引;
- 按照日志索引获取 Log Entry 及其任期;
- 把单个/批量 Log Entry 添加到日志存储;
- 从 Log 存储头部/末尾删除日志;
- 删除所有现有日志,重置下一任日志索引。
Log Index 提交到 Raft Group 中的任务序列化为日志存储LogEntry,每条日志一个编号,在整个 Raft Group 内单调递增并复制到每个 Raft 节点。
LogStorage(日志存储) 的默认实现 RocksDBLogStorage 是基于 RocksDB 的日志存储。
LogManager(日志管理器) 负责调用 Log 日志存储 LogStorage,对 LogStorage 调用进行缓存管理、批量提交、检查优化。
LogEntry数据模型如下
public class LogEntry implements Checksum {
public static final ByteBuffer EMPTY_DATA = ByteBuffer.wrap(new byte[0]);
/** entry type */
private EnumOutter.EntryType type;
/** log id with index/term */
private LogId id = new LogId(0, 0);
/** log entry current peers */
private List<PeerId> peers;
/** log entry old peers */
private List<PeerId> oldPeers;
/** log entry current learners */
private List<PeerId> learners;
/** log entry old learners */
private List<PeerId> oldLearners;
/** entry data */
private ByteBuffer data = EMPTY_DATA;
/** checksum for log entry*/
private long checksum;
/** true when the log has checksum **/
private boolean hasChecksum;
}
3.3 RocksDB存储文件
以Nacos的关键RaftGroup(nacos_config)为例,其日志存储位于$NACOS_HOME/data/protocol/raft/nacos_config/log
通过使用RocdsDB的Iterator API遍历数据,进行反序列化后,可以看到其存储的具体日志信息如下。每条RocksDB记录的Key为日志Index,Value为LogEntry
Index:[307]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=307, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_configO1717144977040-nacos_config-127.0.0.1:8648-8f73232707a79f6950e0792f1f09b309�yC09com.alibaba.nacos.config.server.service.sql.ModifyRequest� executeNosqlargs`�0�DELETE FROM his_config_info WHERE id IN( SELECT id FROM his_config_info WHERE gmt_modified < ? OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY)r[objectCjava.sql.Timestamp�valueaJ �Mh��"java.util.ArrayList]]
Index:[308]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=308, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_configO1717145577049-nacos_config-127.0.0.1:8648-c7ead124d9ce7d2938196b564ab67e11�yC09com.alibaba.nacos.config.server.service.sql.ModifyRequest� executeNosqlargs`�0�DELETE FROM his_config_info WHERE id IN( SELECT id FROM his_config_info WHERE gmt_modified < ? OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY)r[objectCjava.sql.Timestamp�valueaJ �M*(��"java.util.ArrayList]]
Index:[309]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=309, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0]SELECT * FROM custom_properties_info WHERE name LIKE 'nacos.core.auth.enable.userAgentAuth%'0acom.alibaba.nacos.plugin.auth.impl.persistence.AuthRowMapperManager.CustomPropertiesInfoRowMapperp[object]]
Index:[310]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=310, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0& SELECT count(*) FROM config_info_tag java.lang.IntegerN]]
Index:[311]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=311, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0]SELECT * FROM custom_properties_info WHERE name LIKE 'nacos.core.auth.enable.userAgentAuth%'0acom.alibaba.nacos.plugin.auth.impl.persistence.AuthRowMapperManager.CustomPropertiesInfoRowMapperp[object]]
Index:[312]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=312, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0& SELECT count(*) FROM config_info_tag java.lang.IntegerN]]
Index:[313]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=313, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�06SELECT count(*) FROM ctyun_access_security WHERE 1=1 java.lang.Integerp[object]]
Index:[314]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=314, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�SELECT max(id) FROM config_infojava.lang.LongN]]
Index:[315]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=315, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�06SELECT count(*) FROM ctyun_access_security WHERE 1=1 java.lang.Integerp[object]]
Index:[316]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=316, term=18], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0]SELECT * FROM custom_properties_info WHERE name LIKE 'nacos.core.auth.enable.userAgentAuth%'0acom.alibaba.nacos.plugin.auth.impl.persistence.AuthRowMapperManager.CustomPropertiesInfoRowMapperp[object]]
2024-06-19 15:56:16 [main] INFO JRaftServiceLoader:275 - SPI service [com.alipay.sofa.jraft.util.timer.RaftTimerFactory - com.alipay.sofa.jraft.util.timer.DefaultRaftTimerFactory] loading.
Index:[317]<--->[LogEntry [type=ENTRY_TYPE_CONFIGURATION, id=LogId [index=317, term=19], peers=[127.0.0.1:7648, 127.0.0.1:7748, 127.0.0.1:7848], oldPeers=null, learners=null, oldLearners=null, data=]]
Index:[318]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=318, term=19], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_configO1717148548763-nacos_config-127.0.0.1.106:8748-70b9b7695438b87b6bf2484f4e9513bc�yC09com.alibaba.nacos.config.server.service.sql.ModifyRequest� executeNosqlargs`�0�DELETE FROM his_config_info WHERE id IN( SELECT id FROM his_config_info WHERE gmt_modified < ? OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY)r[objectCjava.sql.Timestamp�valueaJ �MH���"java.util.ArrayList]]
Index:[319]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=319, term=19], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0&SELECT count(*) FROM roles WHERE 1=1 java.lang.Integerp[object]]
Index:[320]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=320, term=19], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0&SELECT count(*) FROM roles WHERE 1=1 java.lang.Integerp[object]]
Index:[321]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=321, term=19], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0&SELECT count(*) FROM roles WHERE 1=1 java.lang.Integerp[object]]
Index:[322]<--->[LogEntry [type=ENTRY_TYPE_DATA, id=LogId [index=322, term=19], peers=null, oldPeers=null, learners=null, oldLearners=null, data=8
nacos_config�C09com.alibaba.nacos.config.server.service.sql.SelectRequest� queryTypesql classNameargs`�0&SELECT count(*) FROM roles WHERE 1=1 java.lang.Integerp[object]]
四,总结
Nacos因为其所以依赖的分布式一致性算法需要频繁的日志写入和状态存储操作,而RocksDB作为一个高性能的嵌入式键值存储引擎,能够提供快速的读写操作,其高性能特性使其非常适合Nacos的存储场景。RocksDB 提供了丰富的特性,如压缩、缓存、写入批处理等,可以显著提升存储和检索的效率。这些特性有助于优化 SofaRaft 在处理大量数据时的性能。
凡事有利就有弊,从另外一个角度来看,RocksDB是一个使用C++语言编写的存储引擎,天然存在跨平台问题。而Nacos作为一款Java语言编写的分布式协调组件,绑定使用RocksDB作为其存储模块,势必会导致其丧失“一处编译,处处运行”的优势。因此Nacos社区也在新版本中添加其他类型的日志存储方案并移除RockDB依赖。我们后续也会聊聊新的存储方案,敬请关注!