【二】分布式微服务架构体系详解——数据存储

前言

微服务架构下,很适合用DDD(Domain-Drive Design)思维来设计各个微服务。使用领域驱动设计的理念,工程师们的关注点需要从CRUD思维中跳出来。更多关注通用语言的设计、实体以及值对象的设计。至于数据仓库,会有更多样化的选择。分布式系统中数据存储服务是基础,微服务的领域拆分、领域建模可以让数据存储方案的选择更具灵活性。

不一定所有的微服务都需要有一个底层的关系型数据库作为实体对象实例的存储。以一个简单的电商系统为例:“用户微服务”和“商品微服务”都分别需要关系型数据库存储结构化的关联数据。但比如有一个“关联推荐微服务“需要进行用户购买、加购物车、订单、浏览等多维度的数据整合,这个服务不能将其他所有订单,用户,商品等服务的数据冗余进来。这种场景可以考虑使用图形数据库。又比如有一个“验证码微服务”,存储手机验证码、或者一些类似各种促销活动发的活动码、口令等,这种简单的数据结构,而且读多写少,不需长期持久化的场景,可以只使用一个K-V(键值对)数据库服务。

本文会先简单介绍下适合微服务架构体系的一些分布式数据存储方案,然后深入介绍下这些存储服务的数据结构实现,知其然知其所以然。后续文章会继续介绍下分布式数据存储的复制、分区。

数据存储类型介绍

不同的数据存储引擎有着不同的特征,也适合不同的微服务。在做最初的选型时,需要先根据对整体业务范围的判断,选择尽量普适于大多数微服务的存储。例如初创型企业,需要综合考虑成本节约以及团队的知识掌握度等问题,Mysql是比较常见的选择,电商类型的微服务应用更适合InnoDB引擎(事务、外键的支持、行锁的性能),虽然InnoDB的读性能会比MyISAM差,但是读场景有很多可以优化的方案,比如搜索引擎、分布式缓存、本地缓存等。
下面会以不同场景为例,整理一部分常用的数据存储引擎。实际的企业应用中会针对不同场景、服务特征综合使用多种存储引擎。

关系型数据库

存储结构化数据,以及需要更多维度关联,需要给用户提供丰富的实时查询场景时,应该使用关系型数据库。从开源以及可部署高可用性集群的方面来看,MysqlPostgreSQL 都是不错的选择。PostgreSQL的历史更为悠久,两者都有很多大互联网公司如Twitter、Facebook、Yahoo等部署着大规模分布式存储集群。集群的复制、分区方案会在后续文章详细介绍。

Nosql

Nosql即Not only sql。其概念比关系型数据库更新。Nosql为数据的查询提供了更灵活、丰富的场景。下面简单列举了一些Nosql数据库及其应用场景。工程师不一定需要掌握所有的Nosql数据库的细节,对于不同的领域模型的设计,能有更多的灵感会更好。

KeyValue存储

KeyValue可以说是Nosql中比较简单的一族,大多数操作只有get(),put(),基础的数据格式也都是简单的Key-Value。
目前比较流行的键值存储服务有 RedisMemcached以及上篇文中提到的Dynamo。其中Redis有Redis cluster提供了支持Master选举的高可用性集群。Dynamo也有分布式高可用集群,基于Gossip协议的节点间故障检测,以及支持节点暂时、永久失效的故障恢复。这两者为了保证高可用以及性能,牺牲了强一致性的保证,但是都支持最终一致性。Memcached提供了高性能的纯基于内存的KV存储,并且提供CAS操作来支持分布式一致性。但Memcached没有官方提供的内置集群方案,需要使用一些代理中间件,如 magento 来部署集群。
在实际选择时,如果需要高速缓存的性能并且可以接受缓存不被命中的情况,以及可以接受Memcached服务实例重启后数据全部丢失,可以选择Memcached。用Memcached做二级缓存来抗住一些高QPS的请求是很适合的。比如对于一些Hot商品的信息,可以放到Memcached中,缓解DB压力。
如果既需要有数据持久化的需求,也希望有好的缓存性能,并且会有一些全局排序、数据集合并等需求,可以考虑使用Redis。Redis除了支持K-V结构的数据,还支持list,set,hash,zset等数据结构。可以使用Redis的SET key value 操作实现一些类似手机验证码的存储,对于需要按照key值排序的kv数据可以用ZADD key score member 。利用Redis的单线程以及持久化特性,还可以实现简单的分布式锁,具体可以参考笔者之前写的这篇 《基于Redis实现分布式锁实现》

文档型数据库

面向文档的数据库可以理解成Value是一个文档类型数据的KV存储,如果领域模型是个文件类型的数据、并且结构简单,可以使用文档型数据库。比较有代表性的有MongoDBCouchDB。MongoDB 相比可用性,更关注一致性,Value存储格式是内置的BSON结构,CouchDB支持内置JSON存储,通过MVCC实现最终一致性,但保证高可用性。
如果你需要的是一个高可用的多数据中心,或者需要Master-Master,并且需要能承受数据节点下线的情况,可以考虑用CouchDB。如果你需要一个高性能的,类似存储文档类型数据的Cache层,尤其写入更新比较多的场景,那就用MongoDB吧。另外,2018年夏天可以期待下,MongoDB官方宣布即将发布的4.0版本,支持跨副本集(Replica set)的ACID事务,4.2版本将支持跨集群的事务。详情可以关注MongoDB的Beta计划

图形数据库

在现实世界中,一个图形的构成主要有“点”和“边”。在图形数据库中也是一样,只不过点和边有了抽象的概念。“点”代表着一个实体、节点,“边”代表着关系。开源的Neo4j是可以支持大规模分布式集群的图形数据库。一般被广泛用于道路交通应用、SNS应用等。Neo4j提供了独特的查询语言CypherQueryLanguage
为了直观了解Neo4j的数据结构,可以看下这个示例(在运行neo4j后,官方的内置数据示例)。图中绿色节点代表“Person”实体,中间的有向的剪头连线就是代表节点之间的关系“Knows”。

eo4j example query

通过以下CQL语句就可以查询所有“Knows” “Mike”的节点以及关系:

MATCH p=()-[r:KNOWS]->(g) where g.name ='Mike' RETURN p LIMIT 25 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wiYOQG0N-1595902316405)(http://images.gitbook.cn/d98b38b0-794e-11e8-8985-b17fa96df47a)]

以上只是单个点和单维度关系的例子,在实际中"Person"实体间可能还存在“Follow”,“Like"等关系,如果想找到"Knows"并且"Like” Mike,同时又被 “Jim” "Follow"的Person。在没有图形数据库的情况下,用关系型数据库虽然也可以查询各种关联数据,但这需要各种表join、union,性能差而且需要写很多sql代码。用CQL只要一行即可。
在Springboot工程中,使用Springboot-data项目,可以很简单地和Neo4j进行集成,官方示例可以直接checkout查看java-spring-data-neo4j
文档数据库一般都是很少有数据间的关联的,图形数据库就是为了让你快速查询一切你想要的关联。如果想更进一步了解Neo4j,可以直接下载Neo4j桌面客户端,一键启动、然后在浏览器输入*http://localhost:7474/browser/*就可以用起来了。

列族数据库

列族数据库一般都拥有大规模的分布式集群,可以用来做灵活的数据分析、处理数据报表,尤其适合写多读少的场景。列族和关系型数据库的差别,从应用角度来看,主要是列族没有Schema的概念,不像关系型数据库,需要建表的时候定义好每个列的字段名、字段类型、字段大小等。
列族数据库中目前比较广泛应用的有Hbase,Hbase是基于Google BigTable 设计思想的开源版。BigTable虽然没开源,但是其论文Bigtable: A Distributed Storage System for Structured Data提供了很多设布式列族DB的实现逻辑。另外Facebook Cassandra 也是一个写性能很好的列族数据库,其参考了Dynamo的分布式设计以及BigTable的数据存储结构,支持最终一致性,适合跨地域的多数据中心的分布式存储。不过Cassandra中文社区相对薄弱,国内还是Hbase的集群更为广泛被部署。

存储服务的数据结构

在了解了一些分布式数据存储的产品之后,为了能更深地理解,下文会对分布式存储引擎的一些常用数据结构做进一步介绍。一台计算机,可以作为数据存储的地方就是内存、磁盘。分布式的数据存储就是将各个计算机(Node)的内存和磁盘结合起来。不同类型的存储服务使用的核心数据结构也会不同。

哈希表

哈希表是一种比较简单K-V存储结构,通过哈希函数将key散列开,Key哈希值相同的Value一般会以单链表结构存储。哈希表查找效率很高,常用于内存型存储服务如Memcached,Redis。Redis除了哈希表,因为其支持的操作的数据类型很多,所以还有像Skiplist、SDS、链表等存储结构,并且Redis的哈希表结构可以通过自动再哈希进行扩容。

哈希表一般存储在内存中,随着哈希表数据增多,会影响查询效率,并且内存结构也没法像磁盘那样可以持久化以及进行数据恢复。Redis默认提供了RDB持久化方案,定时持久化数据到RDB。用RDB来做数据恢复、备份是很合适的方案,但是因为其定期执行,所以无法保证恢复数据的一致性、完整性。Redis还支持另一种持久化方案——基于**AOF(Append only file)**方式,对每一次写操作进行持久化,AOF默认不启用,可以通过修改redis.conf启用,AOF增加了IO负荷,比较影响写性能,适合需要保证一致性的场景。

SSTable

在我们平常在Linux上分析日志文件的时候,比如用grep,cat,tail等命令,其实可以想象成在Query一个持久化在磁盘的log文件。我们可以用命令轻松查询以及分析磁盘文件,查询一个记录的时间复杂度是O(n)的话(因为要遍历文件),查询两个记录就是2*O(n),并且如果文件很大,我们没法把文件load到内存进行解析,也没法进行范围查询。
SSTable(Sorted String Table)就解决了排序和范围查询的问题,SSTable将文件分成一个一个Segment(段),不同的Segment File可能有相同的值,但每个Segement File内部是按照顺序存储的。不过虽然只是将文件分段,并且按照内容顺序(sorted string)存储可以解决排序,但是查询磁盘文件的效率是很低的。
为了能快速查询文件数据,可以在内存中附加一个KV结构的索引:(key-offset)。key值是索引的值并且也是有序的,offset指向segment file的实际存储位置(地址偏移)。
如下图简单画了一个有内存kv存储的SSTable数据结构:

memtable sstable exm

这个k-v结构的存储结构又叫“Memtable”,因为Memtable的key也是有序的,所以为了实现内存快速检索,Memtable本身可以使用红黑树、平衡二叉树、skip list等数据结构来实现。Ps:B-Tree、B+Tree的结构适合做大于内存的数据的索引存储(如Mysql使用B+树实现索引文件的存储),所以其更适合磁盘文件系统,一般不会用来实现Memtable。

SSTable也是有些局限性的,内存的空间是有限的,随着文件数越来越多,查询效率会逐渐降低。为了优化查询,可以将Segment file进行合并,减少磁盘IO,并且一定程度持久化Memtable(提高内存查询效率) —— 这就是LSM-tree (Log-structured Merge-Tree)。LSM-tree最初由Google发布的Bigtable的设计论文 提出,目前已经被广泛用于列族数据库如HBase,Cassandra。并且Google的LevelDB 也是用LMS-tree实现,LevelDB的Memtable使用的是skip list数据结构。

这种提供SSTable合并、压缩以及定期flush Memtable到磁盘的优化,使LMS-tree的写入吞吐量高,适合高写场景。下面以Cassandra为例介绍下LMS-tree的典型数据流:

  1. Cassandra LMS-tree 写
    1.1 数据先写到Commit Log文件中(Commit Log用WAL实现)WAL保证了故障时,可以恢复内存中Memtable的数据。
    1.2 数据顺序写入Memtable中。
    1.3 随着Memtable size达到一定阀值或者时间达到阀值时,会flush到SSTable中进行持久化。并且在Memtable数据持久化到SSTable之后,SSTables都是不可再改变的
    1.4 后台进程会进行SSTable之间的压缩、合并,Cassendra支持两种合并策略:对于多写的数据可以使用SizeTiered合并策略(小的、新的SSTable合并到大的、旧的SSTable中),对于多读的数据可以使用Leveled合并策略(因为分层压缩的IO比较多,写多的话会消耗IO),详情可以参考when-to-use-leveled-compaction
  2. Cassandra LMS-tree 读
    2.1 先从Memtable中查询数据。
    2.2 从Bloom Filter中读取SStable中数据标记,Bloom Filter可以简单理解为一个内存的set结构,存储着被“删除”的数据,因为刚才介绍到SSTable不能改变,所以一些删除之后的数据放到这个set中,读请求需要从这个标记着抛弃对象们的集合中读取“不存在”的对象,并在结果中过滤。对于SSTables中一些过期的,会在合并时被清除掉。
    2.3 从多个SSTables中读取数据 。
    2.4 合并结果集,返回。另外,对于“更新”操作,是直接更新在Memtable中的,所以结果集会优先返回Memtable中的数据。

BTree、B+Tree

BTree和B+Tree比较适合磁盘文件的检索,一般用于关系型数据库的索引数据的存储。如Mysql-InnoDB,PostgreSQL。为了提高可用性,一般DB中都会有一个append-only的WAL(一般也叫redo-log)用来恢复数据,比如Mysql InnoDB中用binlog记录所有写操作。binlog还可以用于数据同步、复制。
使用Btree、B+Tree的索引需要每个数据都写两次,一次写入redo-log,一次将数据写入Tree中对应的数据页(Page)里。LMS-tree结构其实需要写入很多次,因为会有多次的数据合并(后台进程),因为都是顺序写入,所以写入的吞吐量更好,并且磁盘空间利用率更高。而B树会有一些空的Page没有数据写入,空间利用率较低。读取的效率来说,Btree更高,同样的key在Btree中存储在一个固定的Page中,但是LSM-tree可能会有多个Segment file中存储着同个Key对应的值。

小结

本篇介绍了很多分布式存储服务,在实际的开发中,需要结合领域服务的特点选择。有的微服务可能只需要一个Neo4j,有的微服务只需要Redis。微服务的架构应该可以让领域服务的存储更加灵活和丰富,在选择时可以更加契合领域模型以及服务边界。
文章后半部分介绍了部分存储服务的数据结构。了解了实现的数据结构可以让我们更深刻理解存储引擎本身。从最简单的append-only的文件存储,再到哈希表、SSTable、BTree,基本涵盖了目前流行的存储服务的主流数据结构。如果想深入理解LSM-tree,可以读一下BigTable的那篇经典论文。
除了数据库服务,像Lucene提供了全文索引的搜索引擎服务,也使用了类似SSTable的结构。对于用Docker部署Elasticsearch集群的实践可以参考下之前写的Elasticsearch实践(一)用Docker搭建Elasticsearch集群Elasticsearch实践(二)用Docker搭建Elasticsearch集群

资料

Bigtable: A Distributed Storage System for Structured Data
基于Redis实现分布式锁-Redisson使用及源码分析
Elasticsearch实践(一)用Docker搭建Elasticsearch集群
Elasticsearch实践(二)用Docker搭建Elasticsearch集群
Upgrade Elasticsearch 5

猜你喜欢

转载自blog.csdn.net/lijingyao8206/article/details/107629990