Druid架构分析(二)
17 Dec 2016如果用几个关键词来概括Druid的设计的,可能用下面几点是合适的。
- 折中,融合
- 高效,快速
- 做少而不是做多
Druid并不是严格意义上的数据库系统,更像是一个 基于Lambda架构的工程解决方案 ,它再设计上做了许多取舍,上篇文章中提到了分布式存储系统的几个基本问题,其中数据数据完整性方面,几乎所有的数据存储系统都用WAL Log来保障数据的持久性,拿MySQL的InnoDB存储引擎来说,Redo Log实现了ACID中的D,Undo Log用来做事务的rollback及实现MVCC,所以Redo Log基本上都是顺序写,在数据库运行的期间没有对Redo Log的读操作。Undo Log则需要随机的读写。然而Druid并没有WAL Log,牺牲了数据完整性。带来的好处是——数据写入十分高效(这是废话)。对比HBase在HLog打开和关闭的时数据写入性能差距在10倍不止。
为了保障分布式存储系统的可靠行及可用性,副本几乎是必须存在的,数据在系统中存在多个副本,节点的副本出现故障时候,系统会将服务自动切换到其他副本来自动容错。在有中心节点的系统中,同一份数据的多个副本往往有角色之分,其中一个副本为主,其他的副本通过数据复制协议来做数据同步,数据的一致性主要是靠Paxos或Raft协议来保障。那么!!Druid有副本么?答案是有的。但Druid数据摄入的方式与其他系统有别,所以并不存在Primary Replica各副本之间是对等的,其副本存在的意义更多是为了应对查询负载当然也包括可靠性方面的考虑。
Lambda架构的特点之一就是区别对待实时数据与离线数据,对于实时数据的写入典型的是做法是消费Kafka及其他类型的MQ。离线数据通常的做法是以批量导入为主。Druid的设计也体现了上述原则,在Druid内部Real-time Node主要处理实时的数据写入。也会提供查询功能。Historical Node主要是对”历史”数据做查询,这种设计也让Druid能够对读,写操作分别优化——Real-time以写操作居多,Historical则是纯粹的读操作,所以巧妙的避开了读写竞争。
实时数据写入过程
对于时序数据大规模激进的写入,LSM tree是非常适合的。对于一个写操作,只有一次磁盘的顺序写入(写WAL Log)和一个内存操作(写Memtable)。Druid的做法也是类似的。
对于实时的数据流Druid提供了不同的Adapter(在Druid中叫Firehouse)来接入各种数据源,比如Kafka及RabbitMQ,本质上这是一种Pull模式。拉取过来的数据首先会进入Real-time Node的内存Buffer中(JVM Heap),Real-time Node会周期性的将其持久化到磁盘上。这么做也能够避免JVM堆内存溢出。同时当内存中的数据达到最大行限制的时候也会触发写操作。在落磁盘的时候Druid会将其转换成column oriented的格式存储。数据持久化之后便不可修改了。此时Real-time Node依然会将其加载到off-heap memory中以供查询。跟LSM-tree模型类似Druid会启动一个后台的Task来merge这些写入的小文件。merge后的数据文件被称作Segment,Segment是Druid存取数据的基本单位。生成后的Segment会被Real-time Node upload 到DeepStorage,把DeepStorage想象成一个备份系统就好了。通常是些分布式的文件系统比如HDFS,S3。upload 完成后Coordinator会通知Historical Node将其下载到本地。加载成功后Historical Node会通知Coordinator自己能够为当前的Segment提供查询服务了。实时节点收到此通知后也就会对外宣布不再提供该时段数据的查询了。
可以看得出来Druid虽然不是严格按照LSM tree的架构但是思想都是类似的。可见万变不离其宗,技术都是相同的。