频道栏目
首页 > 资讯 > DB2 > 正文

MONGODBintroduction

17-07-11        来源:[db:作者]  
收藏   我要投稿

MONGODBintroduction,NOSQL最近成为了一个炙手可热的话题,大规模架构的web服务开发中对于高扩展性和半结构化数据的要求推动着数据库往NOSQL方向转变。

mongodb相对于关系型数据库的主要不同点如下:

不同与关系型数据库的数据扁平化数据格式(2维的数据格式),mongodb的表(collection)是可以嵌套的,是多维的。也就是说mongodb的表里面可以嵌套子表、数组、哈希表等。

不同于关系型数据库中存储在表中的数据必须是全部格式统一,mongodb的collection可以插入任意格式的数据。

mongodb查询没有join操作。鼓励不墨守成规的组织数据,保持数据一致性的重任交给了程序员。

mongodb没有事物(transaction)这一说。原子性只保持在了document(可以理解为一行)级别。也就是说不可能出现更新一行的时候更新不完整的情况。

mongodb没有隔离(isolation),client端读取的任何数据都有可能已经被并行的client修改过了。

通过去除掉这些传统关系型数据库支持的特性后,mongodb可以更轻量级、更灵活的完成大数据处理。

为了加快查询速度,我们需要索引。mongodb中,索引是按照B树的数据结构存储的(不用说,mongodb自然是支持范围查询的)。 如果document本身就是树结构,对应的索引也相应的变成一个路径的模式,并且会深入到document中的深层嵌套。

索引也可以创造在复合类型的属性上,比如数组。这样的话,数组的每个元素都会在BTree上创建一个独立的节点。

构建索引有两种模式:前台模式,后台模式。前台模式构建索引相对来说会更快一些,但是构建索引的时候数据库是不能被访问的。如果系统运行在relica set(下面会介绍)上,比较推荐的方式是轮流下线各个子数据库,然后采用前台模式分别构建索引。

当查询的时候有多个查询项,并且没有相对应的复合索引,mongodb会选用一个最优的单索引(相对于复合索引)查询获得一个候选集,然后线性遍历候选集来匹配其他查询项。

当一个collection有多个索引可以选用的时候,当第一次执行一类查询时,mongodb会创建一个并发执行方案(也就是并发执行所有可用的索引),直到最快的索引方案执行完毕(其它慢的也就不用再执行了,哪个方案最快已经比出来了)。最快方案的执行结果就会返回,同时系统会记住这个最快的索引。接下来,如果还有同类型的查询,系统就会自动用这个最快的索引进行查询,直到这个collection发生了比较大的改动,系统会按照同样的方法重新计算最优索引。

当一个collection索引比较少时或者只要单列索引时,查看对这个collection的常用查询条件、排序条件,然后创建更好的复合索引也是非常重要的。当然,索引也不是越多越好,保持索引也是需要耗费系统性能的。因为当更新、插入、删除记录时,首先要操作索引。为了保持一个最优的索引性能,我们需要定期评估创建或者保持高效索引,删除低效索引。

mongodb是用c++实现的。它维护了一个内存映射表(译者注:实际上是用的mmap技术,将数据文件通过mmap映射到虚拟内存,用到的时候载入物理内存)直接将硬盘上的文件映射到一个内存中的字节数组。每个collection存储在namespace文件(namespace文件在系统中的后缀是.ns,它包含一些元数据)和extent数据文件中。

这里采用了双向链表技术,每个collection的数据都组织成了一个链式extent,每个extent代表了一块连续的磁盘空间。每个extent包含了一个doc链的头尾指针。doc链中每个doc通过指针相连,每个doc中包含的数据采用了BSON格式编码。这里doc可以认为是一条记录(document)。

在数据发生改变的地方,为了防止数据改变时增加的内存空间超过了原有分配的空间,整条记录(record)将会被移到一个有额外空间的大一点的区域。这些额外空间就是用来作为一个增长缓存,这样将来数据增长的时候就不用再挪动数据了。额外空间的大小是经过对每个collection的修改统计动态调整的。另外,挪动数据后,原来的数据空间将会被释放,他们会按照大小放到一个空闲链表里面。

我们可以想象到,经过对数据对象的增加、删除、修改,将会产生很多数据空洞。这些碎片的存在将会使每次磁盘io读/写更少的数据。所以,我们需要一种紧凑技术(周期性的执行紧凑命令)来拷贝数据到一块连续的存储区域中。当然这种紧凑操作必须在offline的时候做。典型的做法是轮换着每次下线一个relica set(副本集),并分别对其进行紧凑。

索引是用B树实现的。每个B树节点包含若干个key,同时有个指针指向左孩子B树节点。

数据更新和事物(transaction)

下面是更新一条记录的操作:

var p1 = db.person.findone({lastname: "ho"})p1["address"] = "san jose"db.person.save(p1)#单然,也可以一句命令搞定!db.person.update({lastname:"ho"}, {$set:{address:"san jose"}}, false, true)

这种更新操作默认是不等待的(像飞出去的箭一样,管他命中不命中~)。当然,mongodb提供了等待的选项,客户端可以在更新前对这次操作配置为等待修改结果返回,比如可以设置为等待到写入硬盘,或者等待到已经确认这条修改已经扩散到各个副本server上。mongodb提供了一种非常精致的方法来搞定这一切。首先它为每个replica set 设置一个标签,这样用户就可以根据自己的需求要为每个表个性化的设置更新模式。

在关系型数据库中,可串行性(Serializability)是一个非常核心的概念。在并发执行时,多个线程对数据库的操作都好像是被按照顺序组织在了一个序列中一个一个的执行。所以每个客户端都可以认为自己是独占数据库的。底层数据库对这个功能的实现是靠锁机制或者多版本技术来提供这种隔离性的。但是这个概念在mongodb(包括很多其他NOSQL)是被无视的。

在mongodb中你读取的每条数据都只能当初他之前得一个快照,也就是说就在你对着你查询到的这条数据发呆的时候,它的本体也就是存在数据库中的样子已经被其他人修改过了。所以说,如果你想根据一条数据在数据库中本来大小的基础上修改它,当你提交修改请求的时候,你要修改的那条数据可能已经改变了。(比如你读到的是10,想加1,提交11的时候,可能它在数据库中已经被其它人改成12了)。如果对于你的应用程序来说这是不可以接受的,你不许在发送修改命令的时候再校验一次,看看是不是还是10。

在这种模式下,当你方式修改请求的时候回额外发送一个状态用来被服务端校验,从而服务端在执行你的修改请求前会根据你提交的状态进行一次校验,通过了才会执行修改。当然,服务端进行校验修改的2次操作是具有原子性的。在mongodb中,你可以通过调用“findAndModify”方法完成这个操作。

var account = db.bank.findone({id:1234})var old_bal = account['balance']var new_bal = old_bal + fund#状态校验会放到查询条件里面db.bank.findAndModify({id:1234, balance:old_bal}, {$set:{balance:new_bal}})#校验上次命令是否执行成功var success = db.runCommand({getlasterror:1,j:true})if(!success){ #重试}

复制代码

事物这个概念在mongodb中也是被无视的。mongodb会暴走对于每个document的操作是原子的(也就是说不会出现一条记录只部分更新的情况)。但是如果更新涉及到多个document,mongodb就不会保证对多条document的操作符合原子性了。

所以,对于操作多条记录并保证其原子性的重任就落到了程序开发人员身上。接下来,将描述一个设计模式用来实现这个功能。这个技术不只针对于mongodb,其它可以保证单条记录原子性的NOSQL数据库都可以用它来保证更新多条数据的原子性。

最基础的想法就是创建一个新的document(我们可以叫它事物transaction)将所有你想更新的文档存储到里面。然后在你想更改的文档里面存储transaction文档的id。然后小心的设计一个执行序列,一个个的更改各个要更改的文档和transaction文档,最后我们就可以达到更改多个文档并保持原子性的目的。

下面解释下这个图表。

第一步:

创建一个新文档Tran1,并将要更新的文档存到Tran1中。Tran1的status设置为init。这一步操作完成后如果失败了(这里的失败译者认为不是操作数据库失败,应该是应用程序出现了异常,如果是操作数据失败可以用getlasterror获得,然后重新操作一次数据库就可以了。而下面的恢复操作应该是在catch代码块执行的操作)恢复一致性的时候:扫描所有状态为init的transaction走第二步。(这一步创建Tran1其实是一个原子操作,它只是新插入了一条记录)。

第二步:

遍历transaction中的所有doc,添加一个指向transaction的属性。如果这一步完成后应用程序失败,继续扫描状态为init的transaction,然后将改transaction中的doc遍历添加指向该transaction的属性。

第三步:

将transaction的状态标记为pending。如果这一步完成后应用程序失败,扫描所有状态为pending的transaction,查询是否有指向该transaction的doc,如果没有了,说明第四步已经完成,就更改状态为commited。如果还有,就继续执行第四步。

对transaction的doc分别执行更新操作(当然这里每个doc的更新是能保证原子性的)。如果这一步执行的时候出现失败的情况,就扫描状态为pending的transaction,如果还有doc执行该transaction,就继续执行第四步,如果没有了(就是说都修改完成了)就删除掉这个transaction。译者认为也可以将transaction的状态标记为committed。

第五步:

当所有doc指向该transaction的时候(就是说都修改完成了),修改transaction的状态为committed。这个时候这个transaction已经执行完毕,并且保证了修改多条的原子性。

mongodb的官方网站上也有一个类似的技术可以解决mongodb没有transaction的问题,这两个的思想概念基本一样,实现方法有略微不同。

副本集模型(ReplicationModel)

mongodb通过replica set 来实现其高可用性。replica set是一种在多个物理机器上通过数据冗余备份的方式来实现高可用性的。通常包括一个主库多个从库。为了保证数据一致性,所有的修改操作(包括插入、更新、删除)都操作主库,然后这些更新会异步更新到其它从库。

在replica set(也就是所有的主从数据库)中,成员之间相互关联,并且相互监听别人的心跳。如果有一个从库挂了,其它库就会感应不到该库的心跳信号,从而就会从副本集中删掉它。当这个从库将来恢复的时候,他可以重新加入集群,并且从主库那里同步它挂掉这段时间的数据更新。当然,如果它挂掉的时间太长了,主库的更新log已经不能完全记录这段时间发生的数据修改,就只能重新加载所有的主库数据到这个从库了,就好像这个从库是新加入的一样。(译者注:主库会维护一个oplog来记录数据修改操作,但是这个log的大小是有限的,如果写满了就会覆盖掉最旧的log,我们这边的dba叫做写圈)。

上面说的是从库挂掉,如果主库挂掉,情况就会更糟糕一些。为了防止主库挂掉后的尴尬,一个主库选举协议会一直在各个成员之间运行着,当主库挂掉,会最快的根据这个协议选举一个新的主库。各个成员选举投票的时候考虑的因素挺多,比如节点更新时间,网络等等。当一个成员获得了大多数投票,一个新的主库就产生了。需要注意的是如果主库挂掉的时候还在进行异步复制主库的操作,新选举的主库将不会获得到这些更新的信息,也就是说,会丢失掉这些信息。

mongodb客户端lib会提供访问mongodb的api。在最开始,客户端会连接副本集中的一些成员(根据一个种子列表),通过向这些成员询问你是不是主库?(ismaster?)最后客户端会生成一个谁是主库谁是从库的快照。这样,当客户端执行修改操作的时候就连接主库,执行查询操作的时候就随机连个从库。同时客户端会周期性的运行ismaster命令来探测是否有新的库加入。当有已经存在的库挂掉后,所有连接它的客户端都会断掉跟它的连接,同时强制更新快照。

这里有一个特殊的从库需要介绍下,他就是延时从库(slave delay)。它会让数据从主库扩散到它的时间有一个固定的延时(比如2个小时)。它的作用是当出现误删除时,可以用它的数据进行恢复。(译者注:上面讲的oplog只会记录删除的id,不会记录删除的具体内容。刚参加工作那会儿,误删除过一些数据,求救dba,dba发来oplog一看只有删除的记录,没有删除的内容,都是泪..)

对于数据更新操作,客户端会将请求发到主库,默认情况下,当主库写入成功后就会返回。但是,mongodb提供了请求参数来指定当数据扩散到多少个,或者哪个从库的时候返回。具体见上面的一个表。这样客户端可以确认一条更改操作至少扩散到了多少个从库。当然,这个操作要在效率和实时性之间做权衡。

对于查询请求,默认情况下,客户端可以从主库那里拿到最新的数据。客户端也可以选择指定自己读哪个从库,这个时候必须能容忍从库返回的数据有可能已经过期。查询请求读从库为mongodb提供了负载均衡的功能。需要注意的是,在读从库这种模式下,写完立刻读可能会看不到修改。

对于大部分请求都是查询的应用,从库读可以极大的改善系统性能。客户端驱动会周期性的扫描各个从库,选择最快延迟最低的的从库来执行查询操作。这里需要注意的是读请求只能访问一个从库,mongodb没有群读或者多个节点读这个说法。

relica set的作用是提供数据冗余和读负载均衡。但是对于写请求来说,没有提供负载均衡,所有的写都落到主库上。

relica set的另外一个好处就是成员可以轮流下线做一些耗费性能的操作,比如紧凑操作,索引建立,数据备份等,不会影响线上服务。

分片模型

为了让写请求均衡,我们可以采用mongodb的分片。分片首先要把一个表根据partition key分成多个chunk(一个chunk包括一个key区间的数据),然后将各个chunk分别分发到各个分片(每个分片都是一个replica set-副本集)。

mongodb的分片让表可以扩充到无限大,这对于大数据场景非常重要。

重申下,在分片模型中,每个collection都要定义一个partition key。key的空间被分成多个存储在各个shard上的chunk,每个chunk都是一段连续的key空间。

#创建一个partition keydb.runcommand({shardcollection: "testdb.person", key: {firstname:1, lastname:1}})

在分片的设计中,客户端lib连接一个无状态的路由server(mongos), mongos的行为跟mongod类似。mongos作为一个路由server,在根据客户端请求的特征重定向到相应的分片server时起到异常重要的作用。

对于包含partition key的插入、删除、更新请求,根据partition key找到包含这个key的chunk,根据chunk和shard的对应关系,mongos(也就是路由server,下面统一称mongos)将此请求路由到相应shard的主库上。其中shard和chunk的对应关系维护在一个config server上,当然每个server本地也会保留一个备份(并且当config发生变化,本地备份会去config server上拉取最新的配置)。partition key和chunk是m:1的关系,也就是说partition key对应唯一的一个chunk,一个chunk包含很多partition key。chunk和库也是m:1的关系。从而可以唯一确定这个chunk所在的库。

对于查询请求来说,mongos会先考察partition key是selectioncriteria(译者注:实在不知道该怎么翻译这个东西)的一部分,mongos会把这种请求只定向到相应的shard上,由该shard上的主库或者从库服务。否则,mongos会先分发请求到各个shard上,在各个shard上分别完成查询,各个shard的计算结果将会汇总到mognos,最后返回给client。当请求需要对结果排序,并且排序的字段是partition key, mongos将会顺序的请求各个shard,client分别拿到结果就行了。如果排序的字段不是partition key,mongos将会把请求分发到各个shard上执行局部排序,然后mongos在局部排序结果的基础上进行再次合并排序。

当数据插入到chunk时,如果chunk负载已经接近满负荷,我们需要分割chunk。mongs根据请求这个chunk的请求数量,对比其他shard上chunk的请求数量统计,来发现这种需要分割chunk的情况。接下来mongos会向存储这个chunk的shard主库发送分割chunk命令。这个shard的主机将会执行分割chunk,同时将这个分割点汇报给配置服务器。这里可以看到,到目前为止,chunk分割并没有发生shard之间的数据移动,所有的数据还都在原来的shard上。

另外,还有一种均衡机制(运行在某个mongos上),这种机制的作用是保证每个shard上有差不多相同数量的chunk。当不均衡状态被监控到时,均衡器会向繁忙的shard发送移动chunk的命令。chunk转移将会在online进行。原始shard会联系目的方shard来初始化数据传输,接着就会进行从原始shard到目的shard数据复制。这个过程可能会持续一段时间,时间取决于数据量的大小。数据复制的时候可能会出现数据还在更新的情况,这些更新的数据会存在原始shard上,并且会被原始shard跟踪记录下来,当复制结束时,这些更新会同步到目的shard。经过多轮迭代,chunk迁移将进入最后阶段:原始shard停止对该chunk 的请求的服务。经过最后一轮更新同步,目的shard将会更新配置服务器,同时通知原始shard返回StaleConfigException给mongos,mongos从配置服务器上读取最新的配置信息,重新发送原来发给原始shard的请求。在未来的某个时间点,原始shard上的这些已经成功迁移的chunk将会物理性的永久删掉。

这里也存在高频度更新的情况。chunk数据从原始shard到目的shard 复制的时候发生了大量更新操作到原始shard,并且数据更新的速度快于数据复制的速度。这种情况下,对于这个chunk的迁移将会退出。然后, mongos会接着处理其它需要迁移的chunk。

Map/Reduce 的执行

mongodb提供了一套map/reduce框架来提供并行数据处理。mongodbmap/reduce的概念跟hadoop类似。只有下面一些略微的区别:

1. mongodb 输入数据来自collection而不是hdfs目录

2. mongodb 输出可以追加到一个已经存在的collection中,而不是hdfs目录

mongodb的map/reduce工作流程大概如下:

1. 客户端定义map函数,reduce函数,输入数据的查询条件,输出数据的目的collection

2. 客户端向mongos发送请求

3. mongos定向这个请求到相应的shard(发送到单个shard还是多个shard根据查询条件中是否包括partition key)。这里要注意的是mongos会从从各个shard中选取一个server来服务,现在的版本基本都是选择主库。

4. 每个shard的主库会执行查询命令,然后将结果通过管道的形式发送给客户端定义的map函数,map函数会返回一串串键值对放到内存buffer中。当buffer满后,客户端定义的reduce函数将会执行,它会将buffer中的键值对进行归类,最后把结果存到本地collection中。注意这里reduce的只是buffer中的数据并不是最终结果。

5. 当第4步结束后,reduce函数将会在上次reduce的结果上再次reduce,合并出这个shard最终结果。

6. 当第5步执行结束后,mongos会通知相关用来存储结果collection 的shard servers(如果输出collection不是存储在多个shard上,就只通知一个就好了,如果是多个就通知多个)。

7. 存储结果collection

shard的主库将会分别请求各个shard的前面步骤reduce的结果。当然,它只会请求它要保存的key区间。

8. 主库用从各个shard上请求的初步reduce结果再次运行reduce函数。接着本地存储这些最终结果。如果用户还定义了finalize函数,接下来就会运行。

面有一个简单的示例。该示例用来根据文档话题进行倒排。

db.book.insert({title:"NOSQL", about:["software", "db"]})db.book.insert({title:"Java programming", about:["software", "program"]})db.book.insert({title:"Mongo", about:["db", "technology"]})db.book.insert({title:"Oracle", about:["db", "software"]})db.book.find()m = function() {for (var i in this.about) { emit(this.about[i], this.title)}}r = function(k, vals) {return({topic:k, title:vals})}db.book.mapReduce(m, r, {query:{}, out:{replace:"mroutput"}})

20. db.mroutput.find()

相关TAG标签
上一篇:MYSQL高级查询
下一篇:Juniper策略路由配置
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站