分布式存储架构选型年度复盘: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(level=logging.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_ok=True)
wal_dir = os.path.join(self.db_path, "wal")
os.makedirs(wal_dir, exist_ok=True)
self._default_config["wal_dir"] = wal_dir
opts = rocksdb.Options(**self._default_config)
self.db = rocksdb.DB(self.db_path, opts)
logger.info(f"RocksDB opened at {self.db_path}")
return True
except rocksdb.errors.RocksIOError as e:
logger.error(f"Attempt {attempt + 1}: RocksDB IO error: {e}")
if attempt < max_retries - 1:
import time
time.sleep(2 ** attempt)
except Exception as e:
logger.error(f"Unexpected 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(f"Batch write failed: {e}")
raise
except Exception as e:
logger.error(f"Unexpected 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(f"Failed 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(f"Compaction 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(f"Error closing RocksDB: {e}")
finally:
self.db = None
# 使用示例
if __name__ == "__main__":
db = OptimizedRocksDB("/data/rocksdb/timeseries")
try:
if db.open():
data = [(f"metric:cpu:{i}", f"{{'value': {i * 0.1}}}") for i in range(10000)]
count = db.batch_write(data)
print(f"Written {count} records")
# 检查Compaction状态
stats = db.get_property("rocksdb.stats")
if stats:
print(f"Stats: {stats[:200]}...")
finally:
db.close()
四、决策的边界:哪种场景该选哪种引擎
选RocksDB(单机嵌入式)当:
- 数据量在TB级别,不需要分布式扩展
- 写入量远大于读取量(W>R)
- 团队有C++能力进行深度调优
- 场景是时序数据、日志存储、消息队列持久化
选TiKV(分布式强一致)当:
- 需要跨多节点的强一致性保证
- 业务有分布式事务需求
- 可以接受额外的网络延迟开销(P99 < 10ms通常可行)
- 运维团队能驾驭Raft集群
考虑自研引擎当:
- 业务场景极度特殊,通用引擎无法满足
- 团队规模在50人以上,有专职存储引擎团队
- 预期生命周期在5年以上
- 已经有成功的自研经验积累
五、总结
存储引擎选型没有银弹。过去一年的三次决策教会了一个核心原则:不要为未来的需求提前买单,但要为可预见的扩展留出设计空间。RocksDB是单机场景下最务实的选择,TiKV是分布式强一致性场景的优选,而自研引擎需要极其审慎的ROI评估。三季度的工作重点是继续深入RocksDB的Compaction策略优化,以及在TiKV的热点调度方面做定向改进。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/guoyizhongxing/article/details/163232902



