对比常用文档型数据库,如ElasticSearch、MongoDB,图数据库,如JanusGraph、Neo4J等。

ElasticSearch

数据模型:文档型数据库

doc-db

概念

  • document(文档)

document由一个或者多个Field(域)组成,每个Field由一个名称和一个或者多个值(有多个值的叫做multi-valued)组成。在ES中,每个document都可能会有不同的Field集合,也就是说document没有固定的模式和统一的结构。document之间保持着相似性即可。从客户端的角度来看,document就是一个JSON对象。

Mapping(映射)是定义document以及其包含的字段的存储和索引方式的过程。每个索引都有一种映射类型,用于确定文档的索引方式。 在6.0.0中已弃用映射类型(即只有一个固定的类型名,不能自定义)。7.x之后每个索引只有一个type,名字为_doc。Mapping可以显式的指定,也可以动态映射。

  1. 显式映射(对比数据库中定义的表结构) 可以在创建索引时创建字段映射,也可以使用PUT映射API将字段添加到现有索引。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
       curl -X PUT "localhost:9200/my_index" -H 'Content-Type: application/json' -d'
       {
        "mappings": {  
            "_doc": {     //映射类型为 _doc
            "properties": {    //定义字段或者properties
                "title":    { "type": "text"  },  //定义字段title的字段类型为text
                "name":     { "type": "text"  }, 
                "age":      { "type": "integer" },  
                "created":  {
                "type":   "date", 
                "format": "strict_date_optional_time||epoch_millis"
                }
             }
            }
          }
       }
       '
    
  2. 动态映射(根据字段的值自动推断字段的类型) 在使用之前不定义字段和映射类型,ES只需要索引文档就能自动添加新的字段名称。同一个字段第一次出现的时候就确定了字段类型,后续此字段必须符合这个字段类型。

ES中的mapping一旦创建之后,就无法修改,只能追加。如果需要修改,就需要删除掉整个文档,进行重建。

  • index(索引)

ES会把数据存放到一个或者多个index(索引)中,索引存放和读取基本单元是document(文档)。在ES中,被视为单独的一个index,在Lucene中可能不止一个,这是因为在分布式系统中,ES会用到分片(Shards)和备份(replicas)机制将一个index存储多份。

  • type(文档类型)

每个文档在ES中都必须设定它的类型,文档类型使得同一个索引中存储结构不同的文档时,只需要根据文档类型就可以找到对应的参数映射(Mapping)信息,方便文档的存取。

ES6时,官方就提到了ES7会删除type,并且在ES6时已经规定每个index只能有一个type,在ES7中使用默认的_doc作为type,官方说在8.x版本将彻底移除type。API请求方式也发生了变化,对索引的文档进行操作的时候,默认使用的Type是_doc。获得某个索引的某个ID的文档:GET {index}/_doc/{id}。

  • Node(节点)

一个ES服务器的实例就是一个节点。考虑到容错和数据过载,一般会配置多节点的ES集群。

  • Cluster(集群)

集群是多个ES节点的集合。多节点构成的集群可以应对单节点无法处理的搜索需求和数据存储需求。也能够应对由于部分机器(节点)运行中断或者升级导致无法提供服务的问题。

  • Shard(分片索引)

集群能够存储超过单台机器容量的信息,为了实现这种需求,ES把数据分发到多个存储Lucene索引的物理机上。这些Luncene索引称为分片索引,这个分发的过程称为索引分片(Sharding)。在ES集群中,Sharding是自动完成的,而且所有的Shard是作为一个整体呈现给用户的。尽管Sharding过程是自动的,但是应用中需要事前配置好参数,因为集群中分片的数量需要在索引创建之前配置好,且服务启动后是无法修改的。

协调节点(coordinating node)通过文档id的hash值(murmur3散列函数)和主分片数量(Primary shard)取模,确定文档应被索引到哪个分片。 shard = hash(document_id) % (num_of_primary_shards)

  • Replica(索引副本)

通过Sharding(索引分片)机制,可以向ES集群中导入超过单机容量的数据,客户端操作任意一个节点即可以实现对集群数据的读写操作。当集群负载增长,用户搜索请求阻塞在某个节点时,通过索引副本(Replica)就可以解决这个问题。索引副本可以动态的添加或者删除,用户可以再需要的时候动态调整其数量。

es-replica

primary shard的默认数量是5,replica默认是1,默认有10个shard,5个primary shard,5个replica shard。

  • Gateway

Gateway是ES用来存储索引的文件系统,支持多种类型。包括本地文件系统(默认),HDFS,S3等,Gateway代表ES的持久化存储方式,包含索引信息,ClusterState(集群信息),mapping,索引碎片信息,以及transaction log等。

架构

  • 整体架构图

es-arch

  • 集群节点、分片、副本

es-node-replica

  • 数据逻辑结构图

es-index-doc-term

ES的打分机制

打分机制

优点(性能、稳定性、易用性、扩展性、文档完备性)

  • 开箱即用。安装好ElasticSearch后,所有参数的默认值都自动进行了比较合理的设置,基本不需要额外的调整。包括内置的发现机制(比如Field类型的自动匹配)和自动化参数配置。

  • 天生集群。ElasticSearch默认工作在集群模式下。节点都将视为集群的一部分,而且在启动的过程中自动连接到集群中。

  • 自动容错。ElasticSearch通过P2P网络进行通信,这种工作方式消除了单点故障。节点自动连接到集群中的其它机器,自动进行数据交换及以节点之间相互监控。

  • 扩展性强。无论是处理能力和数据容量上都可以通过一种简单的方式实现扩展,即增添新的节点。

  • 近实时搜索和版本控制。由于ElasticSearch天生支持分布式,所以延迟和不同节点上数据的短暂性不一致无可避免。ElasticSearch通过版本控制(versioning)的机制尽量减少问题的出现。

缺点

  • 不支持事务
  • 资源消耗高
  • 写入默认有1s延迟

ES读写过程

ES写数据过程

  • ES写数据的大概流程
  1. 客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)

  2. coordinating node对document进行路由,将请求转发给对应的node(primary shard)

  3. node上的primary shard处理请求,然后将数据同步到replica node

  4. coordinating node,如果发现primary node和replica node都搞定之后,就返回响应结果给客户端

  • ES写数据的底层原理

1.先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件

2.如果buffer快满了,或者每隔一秒钟,就会将buffer数据refresh到一个新的segment file中并清空buffer,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。当数据进入os cache后,就代表该数据可以被检索到了。因此说es是准实时的,这个过程就是refresh。

3.只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了

4.重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发commit操作。

commit操作(也叫flush操作,默认每隔30分钟执行一次):执行refresh操作 -> 写commit point -> 将os cache数据fsync强刷到磁盘上去 -> 清空translog日志文件

commit操作保证了在机器宕机时,buffer和os cache中未同步到segment file中的数据还可以在重启之后恢复到内存buffer和os cache中去,

5.translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。

6.如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了

7.如果是更新操作,就是将原来的doc标识为deleted状态,然后新写入一条数据

8.buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge,当segment多到一定的程度时,自动触发merge操作

9.每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file。

es里的写流程中主要的4个底层核心概念,refresh、flush、translog、merge

ES读数据过程

  • ES读取数据的大概流程

在写入ES时,ES会给每个document分配一个全局唯一的id(如果未指定id的话),读取的时候也是对这个doc id进行hash,路由到对应的primary shard上去的。过程如下:

1.客户端发送请求到任意一个node,该node称为coordinating node

2.coordinating node对document进行路由,建请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其replica中随机选择一个,进行读请求的负载均衡

3.将查询到的对应的的document返回给coordinating node

4.coordinating node 返回 document给客户端

  • ES搜索数据的过程

1.客户端发送请求到coordinating node

2.coordinating node 将搜索请求转发到所有的shard对应的primary shard或者replica shard上

3.query phase: 每个shard将自己的搜索结果(实际是doc id)返回给coordinating shard, 由coordinating shard进行数据的合并、排序、分页等操作,并产出最终结果

4.fetch phase: coordinating node根据doc id去各个节点拉取document数据,并最终返回给客户端。

MongoDB

数据模型:文档型数据库

doc-db

概念

MongoDB是面向文档的NoSQL数据库,用于大量数据的存储。

MongoDB的逻辑结构是一种层次结构,主要由:文档(document)、集合(collection)和数据库(database)这三部分组成。三部分的层次结构如图所示:

mongo-arch

  • Replica set (RS) 复制集合(Primary、Secondary、Leader),主从关系,由多个Mongodb单机组成的高可用单元,起到数据的备份作用,避免单点故障。

    • 包含奇数个MongoDB节点(2N+1, 最少3个)
    • 读的时候默认是读主节点
    • 主节点宕机后,replica set会重新选举,客户端会把数据写到新选出的primary中
    • 如果RS中超过一半机器宕机,则无法写入,只能读取
  • Sharding 由多个Replica set组成的扩展集群,如图所示。

mongo-shard

  1. 每一个ReplicaSet称为一个shard,多个分片组成shards,即sharding架构

  2. 客户端请求会通过mongos router(也可以是多个router,可以理解为网关代理),通过路由层可以把数据路由到具体的shard上,在这个过程中会存储许多的元信息 meta,简单理解元信息就是索引,存储的是哪个key存在了哪个shard上。同时元信息服务器【config servers】本身也是个replica set,本身也是主从复制的,提供高可用。

  • Collection(类似MySQL中的table)分片

    • 当查询某个collection数据的时候,router(mongos)会路由到具体的shard(Replication set)中,根据shard规则可能数据都在一个shard中,也可能存在多个

    • collection会自动分层多个chunk,如下图collection1的白色的框框,每个chunk会被自动负载均衡到不同的shard(Replication set),即实际保证的是chunk在不同shard的高可用(根据设置的副本的数量)

    mongo-collection

    Collection切分chunk的规则:

    1. 按照范围(range)切分(存在热点问题) mongo-shard-chunk01

    2. 按Hash切分chunk
      mongo-shard-chunk02

优点

  • Schema free,基于二进制的Json存储文档,即binary json。一个Json就是一个document,支持嵌套。

  • 弱一致性(最终一致),能保证用户的访问速度

  • 支持分布式扩容

  • 聚合统计,支持类似ES的全文检索

  • 支持地理位置坐标检索

  • 支持table join(use $lookup),支持group by(use aggregation pipeline)

缺点

  • 不支持事务操作

  • 占用空间过大 当MongoDB的空间不足时它就会申请生成一大块硬盘空间,而且申请的量都是有64M、128M、256M来增加直到2G为单个文件的较大体积,并且随着数量叠增,可以在数据目录下看到整块生成而且不断递增的文件。

  • MongoDB没有类似MySQL那样成熟的运维工具

  • 有自己的DSL语言,有一定的学习成本

HBase

数据模型:列式数据存储

column-db

架构

HBase采用Master/Slave架构搭建集群,由以下类型的节点组成:HMaster节点、HRegionServer节点、ZooKeeper,操作HBase的Client模块。底层将数据存储在HDFS上,涉及到HDFS的NameNode、DataNode等。

hbase-arch

  • Client

用户主要通过Client操作HBase,Client是整个HBase集群的访问入口,Client使用HBase RPC机制与HMaster和HRegionServer进行通信。

  1. 与HMaster进行通信进行管理类操作

  2. 与HRegionServer进行数据读写类操作

Client包含访问HBase的接口并维护cache(缓存)来加快对HBase的访问。

  • ROOT表和META表

HBase的所有Region元数据被存储在.META.表中,随着Region的增多,.META.表中的数据也会增大,并分裂成多个新的Region。为了定位.META.表中各个Region的位置,把.META.表中所有Region的元数据保存在-ROOT-表中,最后由Zookeeper记录-ROOT-表的位置信息。所有客户端访问用户数据前,需要首先访问Zookeeper获得-ROOT-的位置,然后访问-ROOT-表获得.META.表的位置,最后根据.META.表中的信息确定用户数据存放的位置,如下图所示。

hbase-root-meta

-ROOT-表永远不会被分割,它只有一个Region,这样可以保证最多只需要三次跳转就可以定位任意一个Region。为了加快访问速度,.META.表的所有Region全部保存在内存中。客户端会将查询过的位置信息缓存起来,且缓存不会主动失效。如果客户端根据缓存信息还访问不到数据,则询问相关.META.表的Region服务器,试图获取数据的位置,如果还是失败,则询问-ROOT-表相关的.META.表在哪里。最后,如果前面的信息全部失效,则通过ZooKeeper重新定位Region的信息。所以如果客户端上的缓存全部是失效,则需要进行6次网络来回,才能定位到正确的Region。

  • Zookeeper

zk保证只有一个alive的master,并对RegionServer做心跳检测,存储Region的寻址入口,存储HBase的scheme和table元数据。需要注意的是要保证良好的一致性及顺利的Master选举,集群中的服务器数目必须是奇数。例如三台或五台。

hbase-zk

  • HMaster

HMaster为RegionServer分配Region,负责RegionServer的负载均衡,将失效的RegionServer(由zk通知)中的StoreFile转移到其他的RegionServer中,管理table的增删改操作。

hbase-master

  • HRegionServer

RegionServer维护Region,处理Region的I/O请求,负责切分在运行过程中变大的Region,尽量避免过多的小文件和数据倾斜。

hbase-region-server

  • Region

HBase自动把表水平划分成多个区域(Region),每个Region会保存一个表里面某段连续的数据(row key位于region的起始键值和结束键值之间的行)。每个表一开始只有一个Region,随着数据不断插入表,Region不断增大,当增大到一个阀值的时候,Region就会等分会两个新的Region(裂变)。

hbase-region

  • Store、MemStore和StoreFile

一个Region由多个store组成,一个Store对应一个CF(列族),表中有几个列族,就有几个Store。

Store包括位于内存中的Memstore和位于磁盘的Storefile,写操作先写入Memstore,当Memstore中的数据达到某个阈值,HRegionServer会启动flashcache进程写入StoreFile,每次写入形成单独的一个StoreFile。

当StoreFile文件的数量增长到一定阈值后,系统会进行合并(minor、major compaction),在合并过程中会进行版本合并和删除,形成更大的StoreFile。

当一个Region所有StoreFile的大小和数量超过一定阈值后,会把当前的Region分割为两个,并由HMaster分配到相应的RegionServer服务器,实现负载均衡。

客户端检索数据,先在MemStore找,找不到再找StoreFile。 StoreFile底层是以HFile的格式保存。HBase通过HdfsClient来存储数据存储到datenode上。

hbase-hfile

  • HLog

HLog(WAL log):WAL意为write ahead log(预写日志),用来做灾难恢复使用,HLog记录数据的变更,包括序列号和实际数据,所以一旦region server 宕机,就可以从log中回滚还没有持久化的数据。

优点

  • 海量存储:单表可存储百亿量级数据,不用担心读取性能下降

  • 面向列:数据在表中是按照列数据聚集存储,数据即索引,只访问查询涉及的列,大大降低系统I/O

  • 稀疏性: 传统行数据存储存在大量NULL的列,需要占用存储空间,造成存储空间的浪费,而HBase为空的列并不占用空间,因此表可以设计的很稀疏

  • 扩展性:HBase底层基于HDFS,支持扩展,并且可以随时添加或者减少节点

  • 高可靠:基于zookeeper的协调服务,能够保证服务的高可用行。HBase使用WAL和replication机制,前者保证数据写入时不会因为集群异常而导致写入数据的丢失,后者保证集群出现严重问题时,数据不会发生丢失和损坏。

  • 高性能:底层的LSM数据结构,使得HBase具备非常高的写入性能。RowKey有序排列、主键索引和缓存机制使得HBase具备一定的随机读写性能。

缺点

  • 单一RowKey固有的局限性决定了它不可能有效地支持多条件查询

  • 不适合于大范围扫描查询

  • HBase只支持行级事务: HBase的数据首先会写入WAL,再写入Memstore。写入Memstore异常的话很容易实现回滚,因此只要保证WAL的原子性即可,每个事务只会产生一个WAL单元,这样就可以保证其原子性。

  • WAL回放较慢

  • 异常恢复复杂且低效

  • 需要进行占用大量资源和大量I/O操作的Major compaction

HBase 读写过程

hbase-read-write

写操作流程

  • Client通过Zookeeper的调度,向RegionServer发出写数据请求,在Region中写数据

  • 数据被写入Region的MemStore,直到MemStore达到预设阈值

  • MemStore中的数据被Flush成一个StoreFile

  • 随着StoreFile文件的不断增多,当其数量增长到一定阈值后,触发Compact合并操作,将多个StoreFile合并成一个StoreFile,同时进行版本合并和数据删除

  • StoreFiles通过不断的Compact合并操作,逐步形成越来越大的StoreFile

  • 单个StoreFile大小超过一定阈值后,触发Split操作,把当前Region Split成2个新的Region。父Region会下线,新Split出的2个子Region会被HMaster分配到相应的RegionServer上,使得原先1个Region的压力得以分流到2个Region上

可以看出HBase只有增添数据,所有的更新和删除操作都是在后续的Compact过程进行,使得用户的写操作只要进入内存就可以立刻返回,实现了HBase I/O的高性能。

读操作流程

  • Client访问Zookeeper,查找-ROOT-表,获取.META.表信息

  • 从.META.表查找,获取存放目标数据的Region信息,从而找到对应的RegionServer

  • 通过RegionServer获取需要查找的数据

  • Regionserver的内存分为MemStore和BlockCache两部分,MemStore主要用于写数据,BlockCache主要用于读数据。读请求先到MemStore中查数据,查不到就到BlockCache中查,再查不到就会到StoreFile上读,并把读的结果放入BlockCache

寻址过程:client–>Zookeeper–>-ROOT-表–>.META.表–>RegionServer–>Region–>client

JanusGraph

数据模型:图数据库

graph-db

优点

缺点

Neo4J

数据模型:图数据库

graph-db

优点

缺点

参考