分布式存储架构选型年度复盘:RocksDB、TiKV与自研引擎的决策经验

分布式存储架构选型年度复盘:RocksDB、TiKV与自研引擎的决策经验 分布式存储架构选型年度复盘RocksDB、TiKV与自研引擎的决策经验过去一年团队经历了三次重大的存储引擎选型决策一次是为时序数据平台选择LSM引擎、一次是为核心交易系统评估分布式KV存储、还有一次是决定是否启动自研引擎项目。三次决策的最终结果各不相同但过程中的取舍逻辑值得复盘总结。一、三次决策、三种路径一个存储架构师的真实选型笔记第一次决策发生在去年Q3。业务方提出需求一个日增50TB的监控时序数据平台需要支持按标签的多维聚合查询写入吞吐要求达到200万点/秒。当时的选择面很窄——要么用InfluxDB的TSM引擎要么基于RocksDB自建。最终选择了RocksDB原因有两个一是团队对RocksDB有较深的调优经验二是时序场景下LSM Tree的顺序写优势与RocksDB的设计天然契合。第二次决策在去年Q4。核心交易系统需要替换老化的自研存储层。要求是强一致性、支持分布式事务、P99延迟低于5ms。候选方案包括TiKV、etcd和自研。最终选择了TiKV因为它的Raft实现经过了大规模验证且Percolator事务模型与业务需求高度匹配。第三次决策在今年Q2。随着业务复杂度增长通用引擎在一些极端场景下暴露出了性能瓶颈——比如大Value场景下RocksDB的写放大问题以及TiKV在热点Key场景下的调度延迟。团队内部出现了自研引擎的声音。经过两个月的技术预研和成本评估最终决定暂不自研而是在现有引擎上做定向优化。二、三大引擎的核心差异从写入路径到一致性模型RocksDB的核心优势在于LSM Tree的写入优化。它将随机写转换为顺序写通过MemTable缓冲、WAL保障持久性、Compaction整理数据。但在读多写少的场景下多层SST文件带来的读放大问题会严重拖累性能。实际测试中当LSM层数超过5层时点查的P99延迟从2ms飙升到了50ms以上。TiKV的本质是RocksDB Raft MVCC。它在RocksDB之上构建了分布式一致性层和事务层。这使得它天然适合需要强一致性的场景但代价是额外的Raft日志复制延迟和两阶段提交开销。在我们的测试中TiKV的写入延迟比裸RocksDB高约2-3倍但换来的是跨节点的强一致性保证。自研引擎的吸引力在于可以针对业务场景做极致优化。但同时需要清醒认识到一个生产级存储引擎的开发周期通常在18-24个月且需要投入至少3-5名资深工程师全职开发。维护成本同样不可忽视——RocksDB社区有数百名贡献者任何Bug都能快速修复而自研引擎需要独自承担所有维护负担。三、实践指南RocksDB性能调优的核心配置以下配置经过了多个生产集群的验证适用于写入密集型时序数据场景import rocksdb import os import logging from typing import Optional, Dict, Any logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class OptimizedRocksDB: 写入优化的RocksDB封装含完整异常处理 def __init__(self, db_path: str, config: Optional[Dict[str, Any]] None): self.db_path db_path self.db None self._default_config { # 写入优化 create_if_missing: True, max_background_jobs: 8, # 后台Compaction线程 max_subcompactions: 4, # 子Compaction并发 write_buffer_size: 256 * 1024 * 1024, # 256MB MemTable max_write_buffer_number: 4, # 最多4个MemTable min_write_buffer_number_to_merge: 2, # Compaction策略 level_compaction_dynamic_level_bytes: True, target_file_size_base: 64 * 1024 * 1024, # 64MB SST max_bytes_for_level_base: 512 * 1024 * 1024, # 512MB L1 # 读优化 bloom_locality: 1, bloom_bits_per_key: 10, # 布隆过滤器 block_cache_size: 2 * 1024 * 1024 * 1024, # 2GB Block Cache # 压缩 compression: rocksdb.CompressionType.lz4_compression, bottommost_compression: rocksdb.CompressionType.zstd_compression, # WAL wal_dir: os.path.join(db_path, wal), wal_ttl_seconds: 3600, # 限流 rate_limiter: rocksdb.RateLimiter(200 * 1024 * 1024), # 200MB/s } if config: self._default_config.update(config) def open(self) - bool: 打开数据库含重试机制 max_retries 3 for attempt in range(max_retries): try: os.makedirs(self.db_path, exist_okTrue) wal_dir os.path.join(self.db_path, wal) os.makedirs(wal_dir, exist_okTrue) self._default_config[wal_dir] wal_dir opts rocksdb.Options(**self._default_config) self.db rocksdb.DB(self.db_path, opts) logger.info(fRocksDB opened at {self.db_path}) return True except rocksdb.errors.RocksIOError as e: logger.error(fAttempt {attempt 1}: RocksDB IO error: {e}) if attempt max_retries - 1: import time time.sleep(2 ** attempt) except Exception as e: logger.error(fUnexpected error opening DB: {e}) return False return False def batch_write(self, kv_pairs: list) - int: 批量写入返回成功写入数量 if not self.db: raise RuntimeError(Database not opened) batch rocksdb.WriteBatch() success_count 0 try: for key, value in kv_pairs: batch.put(key.encode() if isinstance(key, str) else key, value.encode() if isinstance(value, str) else value) success_count 1 write_opts rocksdb.WriteOptions() write_opts.sync False # 异步写WAL提升吞吐 self.db.write(write_opts, batch) return success_count except rocksdb.errors.RocksIOError as e: logger.error(fBatch write failed: {e}) raise except Exception as e: logger.error(fUnexpected write error: {e}) raise def get_property(self, property_name: str) - Optional[str]: 获取RocksDB内部属性 if not self.db: return None try: return self.db.get_property(property_name.encode()) except Exception as e: logger.error(fFailed to get property {property_name}: {e}) return None def compact_range(self, start: bytes None, end: bytes None): 手动触发Compaction if not self.db: raise RuntimeError(Database not opened) try: self.db.compact_range(start, end) logger.info(Manual compaction completed) except Exception as e: logger.error(fCompaction failed: {e}) raise def close(self): 安全关闭数据库 if self.db: try: # 等待Compaction完成 self.db.compact_range() self.db.close() logger.info(RocksDB closed safely) except Exception as e: logger.error(fError closing RocksDB: {e}) finally: self.db None # 使用示例 if __name__ __main__: db OptimizedRocksDB(/data/rocksdb/timeseries) try: if db.open(): data [(fmetric:cpu:{i}, f{{value: {i * 0.1}}}) for i in range(10000)] count db.batch_write(data) print(fWritten {count} records) # 检查Compaction状态 stats db.get_property(rocksdb.stats) if stats: print(fStats: {stats[:200]}...) finally: db.close()四、决策的边界哪种场景该选哪种引擎选RocksDB单机嵌入式当数据量在TB级别不需要分布式扩展写入量远大于读取量WR团队有C能力进行深度调优场景是时序数据、日志存储、消息队列持久化选TiKV分布式强一致当需要跨多节点的强一致性保证业务有分布式事务需求可以接受额外的网络延迟开销P99 10ms通常可行运维团队能驾驭Raft集群考虑自研引擎当业务场景极度特殊通用引擎无法满足团队规模在50人以上有专职存储引擎团队预期生命周期在5年以上已经有成功的自研经验积累五、总结存储引擎选型没有银弹。过去一年的三次决策教会了一个核心原则不要为未来的需求提前买单但要为可预见的扩展留出设计空间。RocksDB是单机场景下最务实的选择TiKV是分布式强一致性场景的优选而自研引擎需要极其审慎的ROI评估。三季度的工作重点是继续深入RocksDB的Compaction策略优化以及在TiKV的热点调度方面做定向改进。