Datomic架构的一点理解

一年多以前Rich Hickey刚刚发布Datomic的时候就看过它的架构图,当时印象深刻的是以Datalog作为查询语言,架构却理解不深。这一年多对分布式系统有不少心得,晚上无意间发现一篇《unofficial-guide-to-datomic-internals》,读完发现理解起来并不费劲,然后又找出Datomic的文档读了一通,做了一点简单记录,又翻看了Hickey的视频《Using Datomic with Riak》。夜深人静,写点粗浅理解。

应该说函数式数据库从架构上非常不同于我们平时会接触到的分布式系统,主流的分布式NoSQL(Cassandra, Mongodb, Riak, Aerospike, Couchbase)也好,分布式MQ(Kafka,ActiveMQ)也好,架构设计上除了考虑如何处理Replication和Partitioning,再就是考虑一致性和可用性之间的TradeOff,对于数据引擎,就考虑是读优化还是写优化,基本上思维都围绕这些打转。所以遇到像datomic这样的东西,架构上分成Peer、Transactor和Datastore,然后交互访问上又从来不提我们熟知的这些概念,就会觉得很奇怪。我们会想这个Peer是指什么,看起来又不是Gossip协议中的Peer,更奇怪的是它是一个CP系统,却构建在Riak,Cassandra这类Dynamo系的AP系统之上。这是怎么回事。

Datomic架构图

传统上,我们都将DB作为一个全局状态来看待,应用层进行更新、读取,整个系统行为上与一个有共享状态的多线程程序无异,要考虑复杂的数据竞争问题。而对于纯函数式环境,所有东西都是immutable的,修改即创建,不再需要考虑竞争问题了。函数式DB也同样,对数据库的修改,实际是增加新的内容,而旧的数据仍然存在,每条数据都有关联的Version和时间戳,这样有一个好处是,我们可以查看此前任意时间的Snapshot,这对做分析或者审计都非常有用,也可用来恢复由于程序bug引起的事故。

Datomic架构上还是比较简单的,数据存储使用第三方的成熟方案,冗余和分片就不需要操心了。Transactor作为独立的协调者用于处理一般写入和事务,Peer作为客户端的Lib处理读请求。可以看到,写和读是两条独立的路径,这与《turning-the-database-inside-out》的思路很相似,只不过datomic需要处理事务,要满足ACID。这个保证逻辑并不复杂(我们暂时忽略处理事务本身的细节,只关心整体流程):每个Transaction都会经由单点的Transactor来执行,Transactor将请求发给Backend如Riak,当写入都成功后,Transactor主动向所有连接着的Peer推送更新通知,Peer这方更新本地索引,新数据可见,Transactor返回给客户端。又由于Transactor是个单点,并且事务处理不并发,所以Isolation Level是Serializable,一致性是满足Linearizability的。如果理解没有错误,Datomic里面的索引(Covering Index)实际已经包含了数据,按理说更新索引也就更新了数据,不过考虑到如果这样做,Transactor可能Fan-Out太大,网卡未必受得了,所以可能仅仅通知的是Segment的UUID。系统配置上,需要配置保证写入的Factor,如Riak,N=3,W=2,这里R=1就足够了,因为不需要做Read-Repair,依靠自身的最终一致就好了。对于Peer来说,最近更新的数据已经记录在其内存索引里(细节不清楚,猜测记录的是UUID,Segment需要重新从Backend读取),如果读后端Miss,就表明数据同步尚未结束,选择其他节点或者重试就好,牺牲可用性保证一致性。这个逻辑很有意思,语义上是强一致的,但是实际的写入是最终一致。这其实就是Datomic在AP系统上实现CP的奥秘。

Datomic还有一个优势就是Caching,一般我们使用MySQL+Cache模式时,如果DB做了修改,需要失效Cache。这个过程是需要业务来实现,如果涉及到从库,从库前面也有一层Cache,这其中就有一个一致性问题。比如说,更新了主库,然后失效主库前的Cache,再失效从库的Cache,但是如果Cache失效发生于数据同步到从库之前,就很可能造成Cache又重新回填了旧数据,造成从库这边Cache出现脏数据。一种比较好的方法是搜集从库的Binlog来删Cache,但是这可能需要修改协议和MySQL实现;还有一种方法是做延迟删除,等几秒再进行一次Cache删除。这是我们遇到的实际问题,但是Datomic根本没有这类问题。数据是Immutable的,只要索引正确,数据就是正确的,而且由于是Immutable的,我们也就不需要担心数据在使用过程中被篡改,所以就可以尽可能的进行Caching,而让大部分Query都在客户端进行,避免了网络交互。客户端查询有很多优势,比如业务需要查询一系列连续的数据,如果使用MySQL,写业务时不太可能会Cache一个Range的数据,于是就需要多次Round-Trip,而对于Datomic,这些数据(Datom)很可能在同一个Segment里。

另外,Datomic的缺点似乎也很明显:Transactor是个单点,即便官方Pro版提供HA,应该也仅仅是提供个主备,实际工作的只有一个Primary,所有的写入都走这一个点,必然容易出现瓶颈。还有就是索引,Transactor为了写入效率,应该会维护一个完整的索引,假如这样,单机容量也是瓶颈(好像最初GFS那个单点Master)。不过,这个问题可能并不严重,对于一个业务系统,有ACID要求的数据一般并不太多,Datomic的一个特点就是它是工作在其他成熟的分布式NoSQL之上的,所以用户大可以直接使用下层的NoSQL来存储对一致性要求不高的数据,而使用Datomic存储重要或需要复杂查询支持的数据。Transactor虽说是个单点,但是它只处理写入,听Hickey描述,写入逻辑跟LevelDB差不多,基本都是顺序写,定期建Index,总体的性能应该还OK。另外一点担心是,Transactor到Peer是个广播,如果Peer节点特别多,假如出现慢节点,很可能拖垮整个Transactor,导致写入不可用。这些关键的细节问题Datomic如何处理就不得而知了。

除了上面提到的这些点,还有诸如Datomic五种类型索引的设计、索引根信息的维护(需要强一致存储)、Datalog查询优化等等有趣但与架构关系不大的内容,没太仔细考虑,有时间再仔细玩味。