简单描述需求,当前我们的分析型数据都是不可变的,且每次的分析都是要将整体数据都加载到计算节点进行分析计算,所以基础的存储和缓存都是面向文件的,并不支持对某一行的修改,如果需要Update某些行或者插入新的记录,需要将增量修改与原数据源联合进行复杂的合并操作,对于经常需要修改的数据源尤其是更新某些行的属性值不那么方便,如果只是Append还好,并且还有对这个数据源的实时查询需求,用户希望能够在页面上进行交互式查询,要求响应速度亚秒级别。
看起来这个需求很像是一个数据库所擅长的,但是从另外的角度看,这并不是典型的数据库的应用场景,我们平时使用数据库都是作为某个成熟的业务场景的数据保存,这个数据一般是提前定义好的结构,数量可以很大但是数一个或者一组数据库实例服务组合在一起的这个集群,其中的表格种类一般是有限的几十最多几百个,而在我们的产品中,这种可变数据源不属于产品的结构化数据,而是用户所自定义的个人数据,属于数据里边的数据,格式多种多样,作为一个个独立的数据源,且使用的频率非常低,有可能存在很大量的这种数据源,每个的结构完全不同且属于不同的用户,有可能一天也用不上一次,使用数据库来管理这种低频数据对资源有些浪费,大概可以采用的方案有以下三种:
1. 分布式数据库:
也就是上边提到的,数据不再以文件的形式存在于分布式存储中,而是直接写入到支持索引和复杂查询的数据库中,这个数据库可以支持各种存储结构,文档、图、key-value最好都支持,最好是支持很方便横向的扩展,能够无限制的新建很多的数据库和表,并且可以控制将表加载到内存以及释放内存,以减少资源的占用。从NOSQL Databases这个网站看了比较了很多的数据库,目前看来支持以上要求的数据库有RethinkDB和ArangoDB(ArangoDB on Github),Mongodb由于有明确的命名空间数量限制,所以创建表有数量限制,暂时不考虑,RethinkDB理念不错且支持对表的加载释放,API和文档非常友好,然而这家公司已经被收购,产品未来前景不明朗,而ArangoDB相对来说很小众,支持的数据模型和索引种类很多,使用起来也相对比较灵活,运行效率也不错,可以作为首选考虑。
优势就是使用起来简单,数据采用传统的数据库增删改语句写入到数据库中,查询也就直接使用索引,执行效率较高,使用数据库的引擎可以避免我们自己去处理各种原始数据和增量数据的合并,以Write-Ahead-Log(WAL)系统为例,其实所有的修改操作都是直接写入日志,由数据库引擎去寻找对应的数据同步或者异步的将操作反应到底层的数据库存储中,可能是某种自定义的文件结构,也可能是某种更小巧的嵌入式数据库。最终数据的存在形式一般是一个方便插入的树型结构,常见的有B+树,LSM树。
缺点也比较明显,一是资源的占用,用户的数据作为低频使用数据使用数据库来做托管相对比较昂贵,如果支持从内存中释放还可以减少数据库自身的缓存处理压力,如果表的数据很大数量很多则压力会更大,二是写入速度受限于数据库集群的处理能力,比如有大量的插入时需要路由节点的运行效率足够高,与Alluxio这种直接写本地缓存的速度有较大的差距,另外插入的过程中需要建立大量的连接,否则单连接的循环写入速度会非常慢,三是跟Spark等分布式处理框架的结合,目前数据的输入输出都是类Hadoop文件的,如果直接读取或者写入数据库,需要自己开发,目前这方便比较少见,大家的分析型数据要么是直接从某系统导出要么就是直接生成的日志,很少直接使用分布式计算引擎去读取已经结构化的带索引的数据库,这样也会加大当前支持业务产品服务的数据库的压力。四是分析型数据的使用,假如这个数据源也会经常的跟其他数据联合或者独立的进行复杂的统计分析,这时典型的场景会通过Spark将数据都加载到内存中,相当于一次将数据库全表导出的过程,比直接读一个几百M的文件要慢很多。
2. 采用嵌入式数据库
嵌入式数据库相对分布式数据库更灵活,只需要在数据需要进行读写的时候启动一个实例供调用,不用的时候数据以文件的形式存在于系统中,对资源的消耗低,同时具有数据库读写的各种优势。缺点是文件只能存在于本地,如果我们需要以统一的存储来作为嵌入式数据的来源,每次修改都需要去远程分布式文件系统去比较数据是否有更新,如果有需要加锁并下载数据到本地,启动实例进行读写,如果这时候有其他用户想修改则只能等待这个写操作释放,如果是读操作则不影响直接下载使用。在修改完成之后将数据写入分布式存储某地址,并标记新数据的地址为该数据源地址,控制起来比较复杂,由于没有统一的服务实例地址,本地操作之间互不知晓,所以不支持并行写入。同样也有分布式数据库的问题,需要自己开发Spark到嵌入式数据结构的转换代码,如果有直接支持远程分布式存储的嵌入式数据库就比较完美了,当然这种定义本身就比较矛盾,不是嵌入式数据库的使用场景。有个比较有趣的数据库是CouchBase,他家的嵌入式数据库可以在联网的时候将修改同步到远程数据库,适合网络环境不稳定的移动端,如果要在我们的产品中使用也存在数据同步问题,因为数据不是在固定的某个“移动端”,随着计算资源分配的不同,用户的可变数据源可能是在任意一台机器的任意的一个容器。另外就是嵌入式数据库支持的数据量普遍规模较小。
3. 采用文件存储+OLAP解决方案
Parquet+Druid解决方案,目前我们采用Alluxio作为文件还存,HDFS或者S3作为底层的文件存储,具体的存储格式采用了利于分析的Parquet格式,且经过了压缩。但是不管是Alluxio还是Partqut,都不支持对原来数据的修改,只适合于不可变的分析型数据源,假如需要对原来的数据进行修改,需要在Spark内部进行数据的联合,之后写入新的数据源,这个操作消耗较大,读写成本高。而且直接使用SparkSQL做数据分析实时性较差,即使对DataSet做了Cache也难以在秒内返回结果,所以需要借助于额外的索引服务,这里考虑了Druid,Pinot,Kylin这三种OLAP方案,其中麒麟纯粹的以绝对的空间换取时间,建立索引的时间也很长,使用不灵活,不考虑,前两者区别不大,成熟度上Druid更高,LinkIn 所开发的Pinot对非BitMap索引支持的较好,可能未来会比Druid好,但暂时不考虑。
Druid做的事情比较简单,就是根据预先定义的数据格式(包括timestamp, dimension, metric 列的属性)将批量的和实时的数据经过一个Indexing service来生成目标的segment数据,生成的数据经过了压缩,针对不同的统计列和分析列来生成对应的segment数据,以自己开发的列式存储的形式将这些中间索引数据保存下来,用户提交的查询经过broker节点会根据预先保存在metadata server里边的数据找到对应的historical节点或者realtime节点去进行索引数据的二次查询分析,不需要查询的节点不会收到请求,最终结果汇总到broker返回给客户端。作为一个额外的索引服务,其数据来源可以是Hadoop文件系统或者兼容的协议,索引数据也可以保存到他所定义的deep storage里边,也就是hdfs或者s3,保证数据也是分布式存储的,这个额外的服务除了占用系统计算资源不对现在的存储结构造成大的影响,也可以方便的迁移或者替换,如果我们采用了数据库,这样如果要切换一种数据库,所需要的迁移工作是巨大的。
4. ElasticSearch解决方案
原始数据依然通过文件备份,同时发送给ES做索引服务,支持增量更新以及一些简单的聚合运算,优势在于查询速度快,对于需要小规模结果返回的查询来说优势很大,索引同时携带原始数据,可以作为数据库使用,但其核心引擎又是列式存储,利用率很高。
ES还可以跟Spark直接连接,读取或者写入都有成熟的connector可用,结合ES的索引能力和Spark的分析能力,可以满足大部分的需求。
目前看来,选择哪种方案还是看具体的需求,能满足产品的设计。
是否需要提供用户交互界面修改数据,或者每次批量的更新规模非常小,这种情况适合通过数据库来托管关系型数据源。
是否经常需要重新分析这个数据源,还是只是用来做实时查询展示用,如果需要分析,就会涉及倒全量数据的读取,适合采用文件。