上一篇说了OLTP和OLAP,这篇文章从OLTP说起。

    在一个大型的在线业务里,数据量一旦很大的时候,就需要考虑如何加入更多的服务器来存储。
    在互联网的发展过程中,当开始碰到数据库访问瓶颈时,仅仅是单张数据表到了一定的容量时,比如某互联网公司有了千万级别的用户时,查询操作就开始变慢。之后某位牛X的技术人员想出了一个很好的解决方案,就是分表,按照用户的ID把用户表分拆成好几个,这就是分表。分表还仅仅是一台机器上的事情,当表记录到了数亿级别,一台机器放不下了,某位牛X的程序员又想起分库。总之通过分治的策略,可以很好的将数据分散,利用Hash或者类似的方式来找到应用需要的数据介质来获取数据。由此产生了一个最原始而有效的概念就是拆分。
    垂直拆分:是指按功能模块拆分,比如可以将群组相关表和照片相关表存放在不同的数据库中,这种方式多个数据库之间的表结构不同。比如在设计大型论坛时,我们可以把不同版面的数据存储到不同的数据库表或者不同的数据库中。
    水平拆分:而水平拆分是将同一个表的数据进行分块保存到不同的数据库中,这些数据库中的表结构完全相同。刚才说到的千万级的用户表所用到的办法就是水平拆分。
    垂直拆分和水平拆分是互联网公司的一般做法,没有什么太大的技术亮点,说出来简单的吓人。但是随着数据量的几何指数增长,原来几百MB的数据,已经增长到了几百PB,但是分治的思想和实现方法也在这个过程中,发生了更加精彩的蜕变。
    首先是划分方式,基本的水平划分方式是很随机很原始的,即使是用上一致性hash等技术,仍然存在扩容困难的问题,但是好处也很明显,不需要存储集群的拓扑结构,不需要存分布信息,读写操作效率也高。
 

    衍生的分片方式需要先对数据进行一定的处理,比如要先排序或者对主键进行Range操作。划分之后,再将数据划分成若干个虚拟页,一般来说虚拟页要比实际物理的存储节点要多,比如我们预先将某数据划分成60000页(下面称为Page),再投影到200台机器(下面叫ChunkNode)中。相当于每台机器存储了300多页的数据。当然,为了冗余和备份,实际存储的数据页应该是原数据的三倍以上大小(参考hadoop),即每台机器至少需要存放900页。这里就包含了两层映射,即原始数据-虚拟页-存储节点,这样就需要增加一个存储数据位置的节点(下面叫MasterNode)来管理数据的存储(hbase里叫hmaster)。这种两段映射的好处是便于做扩容、容错和查询优化:
  1. 便于做服务扩展,是指增加ChunkNode时,只需要将部分页拷贝到新的机器,再把源来存储节点的数据删除,然后再去MasterNode修改一下虚拟分页数据的实际存储位置即可,且扩容时源数据还可以支持读写操作,不需要停服。
  2. 较好的容错性:MasterNode里包含了所有集群的拓扑结构,只需要将每个Page复制三份放到不同的ChunkNode就进行完善的冗余备份。
  3. 能支持更好的查询优化:每个Page里的数据可以计算一些基本Count、Max、Min等字段来便于检索,一旦发现不符合查询条件,就可以忽略这个Page里的数据,大幅度增加查询速度。
    在实际项目里,也可以使用动态分片来增加虚拟页的个数。

 
    很容易发现,所谓的Page只是数据的一个单位,在实现上很随意,可以是mysql的一个表,或者自己实现的SSTable,也可以是一个文本文件,反正只是数据载体,能够支持查询就好了。说到查询,一个较好的OLTP系统应该能够支持SQL查询,不支持的话上面的应用开发者就苦逼了,他们甚至希望最好能够兼容Mysql客户端连接,这就需要对SQL客户端协议进行一定的研究了。但是好的开源工具基本都能做到。
     说完分片储存,我们再研究下分层,拿百度的网页搜索来说,其查询业务背后,就是海量的Keyword对应文章URL的倒排表。这个数据量虽然我不大清楚,能想象到肯定很大,都是离线计算算出来的。要支持数据实时搜索到,就必须分层:
 

    分层主要是为了划分数据存储介质,比如最实时的数据,会放到内存里,不太实时的数据,比如小时库和日库,就放到SSD上。再大一点且不怎么更新的就放到海量硬盘,这些个海量的硬盘甚至不需要电源,直接找个机架垒起来连上就好了。这个方式能很好的处理实时数据和非实时数据,节省了不必要的机器开销。
    再拿淘宝开源的OceanBase来说,OceanBase提到自己要支持ACID,那么在分布式基础上怎么支持事务呢,答案就是不怎么行。不过呢,OceanBase找了变通的法子,它要求系统必须有个大内存的服务器作为实时数据和事务的处理机器(RealtimeNode),RN负责实时的更新操作,并分成Add和Del操作,在一台机器上来支持事务就简单多了,效率也很高。
 

    唯一的麻烦是需要定期合并。RN因为是纯内存的方式,所以它必须要定期将数据flush到ChunkNode上。在合并过程中,RN中的数据会被冻结一份供合并操作,再另开一份新的内存供新的数据进来。这时MergeNode需要将新数据、冻结数据和CN中的数据合并起来提供查询结果,这里有个问题就是MN怎么才能知道哪些数据合并过,所以需要在数据块里记一个版本信息。通过检查RN和CN的版本号是否连续,比如RN是3,CN是2,说明数据已经合并到CN上。而CN如果是1的话,说明冻结数据里还有版本为2的数据,需要去冻结数据块里查询。