MongoDB理论浅入浅出

2015-06-09
15 min read

最近老板揽到了大数据的大项目,需要写写写写写MongoDB的设计方案,这段时间就恶补了MongoDB数据库的相关知识。本篇文章抛开以前使用MongoDB的开发细节,完完全全作为MongoDB的吹水文。前半部分主要简单讲述数据发展过程,后半部分主要讲述MongoDB这种文档型数据库的特征和功能。


##数据库发展简史

在软件工程中,数据建模是运用规范的数据建模技术,建立信息系统的数据模型的过程。数据库把信息系统中大量的数据按一定的模型结构组织起来,提供存储、维护、检索数据的功能,使信息系统可以方便、及时、准确地从数据库中获得所需的信息。在计算机诞生初期,计算机的功能是进行科学计算,数据存储在穿孔卡片上,软件和硬件都不支持存储和管理大量的计算结果的。先是外存储的出现推动软件技术的发展,随之而来的操作系统提供来专门管理数据的文件系统,文件系统设计之初被构想成包罗万象的组织范式。数据库管理系统从60年代后期开始萌发,发展阶段大致分为navigational database、relational database(SQL)、post-SQL database 3个阶段。

  1. navigational database

old database

20世纪60年代,数据库管理系统刚开始出现,这个时候出现了两种数据库的结构范式:层次模型和网格模型,相对应的数据库管理系统为层次和网状数据库管理系统,他们统称为navigational database,比较著名的数据库管理系统有IBM的IMS、CODASYL。这一阶段的数据记录的寻找使用pointer、path来定位磁带上的位置。通过这种“当前记录的下一条记录”的方式,来查找数据可想而知是非常慢的,不过在当时已经是一次性能的提升了。

Sample From Wiki

比如上图这个例子,当我们需要从Node6读取到Node1的数据,则需要通过这两个节点的连接(Node6->Node4->Node5->Node1),这个过程就是导航(navigation)嘛。如同编程语言中的goto语句,这种原始的方式被批评为无组织的意大利面条。随着时间的检验和技术的进步,第一代数据库在80年代被设计得更为合理的关系性数据库取代了,但是这种思想依然保留着,比如DOM的层次结构,并启发了图数据库这种非关系性数据库的设计。

  1. relational database

70年代,IBM的员工Edgar F. Codd发表了一篇名叫《A Relational Model of Data for Large Shared Data Banks》的论文,这篇论文提出了关系模型这一革命性的概念。关系模型是基于谓词逻辑和集合论的一种数据模型。这种模型以二维表作为数据的组织形式,也就是本科学习数据库的笛卡尔集。这种关系表以列为属性,每一列的元素为同一数据类型;以行为记录,每一行的元素不能完全相同。行列的顺序没有严格限定。

关系模型应用在关系型数据库上并成为其结构范式。关系型数据库依据此范式将应用的数据存储在数据表面,数据操作则通过SQL(结构化查询语言)查询语句来操作。这种结构化查询语言核心是对表的引用,使用其作为数据的输入与管理。随着而来的索引和日志记录功能让关系型数据库满足了当时大量的计算应用的大量数据的存储和读写。

Picture From Wiki

关系模型由关系数据结构、关系操作集合、关系完整性约束三部分组成。数据结构遵守数据库规范化来减少数据更新的开销,也就是通过满足一级级的范式(normal form)来限定数据结构。现在的数据库设计最多满足第3NF,其原因为范式越高,虽然具有对数据关系更好的约束性,但也导致数据关系表增加而令数据库IO更易繁忙。关系操作集合包括查询操作和插入、删除、修改操作两大部分。关系完整性约束是为保证数据库中数据的正确性,包括数据库完整性、实体完整性 、引用完整性。最开始两大RDBS原型有Ingres和System R(又是IBM的)。承接Ingres原型的系统有MSSQL、Sybase等,而承接System R的系统有Oracle、DB2(又是IBM的)。

RDBMS的这些复杂的约束能保证系统的可靠性(数据一致性),但缺乏可伸缩性(扩展)。数据模型的表达能力也比较差,修改成本因为结构本身的复杂增加了不少。当编程方法进入到下一个阶段(也就是下文的面向对象技术)时,关系数据库和编程语言不一致导致的问题就更加影响开发效率了,因为编程语言的存储结构是面向对象的,而关系性数据库却是关系的,使得从一个环境转换到另一个环境时需要多至30%的附加代码来进行转换。一些ORM框架能一定程度地简化这个过程,但面对查询需要高性能需求时,这些ORM从根本上来说还是很作急。

  1. post SQL database

80年代,面向对象的方法和技术的出现,给面临新挑战的数据库技术带来了新的机遇和希望。数据库研究人员借鉴和吸收了面向对象的方法和技术,在RDBMS上进行不同层次的扩充,提出了面向对象的数据库管理系统(OODBMS)。向对象的数据库模型能有效地表达客观世界和有效地查询信息。OODBMS还解决了一个关系数据库运行中的典型问题:应用程序语言与数据库管理系统对数据类型支持的不一致问题,这一问题通常称之为阻抗不匹配问题。早期的产品有Gemstone,200后,出现了db4o的开源产品。但正如面向对象技术一样,需要的开发培训是比较长的。再加上本身技术、理论的不完善,关系数据库系统基本适合商业事务处理的前提下,对工程中的应用用面向对象数据库来补充不足的问题。

2000年以后,出现了NoSQL数据库和NewSQL数据库。NoSQL数据库取的是Not Only SQL的意思,而NewSQL则是追求NoSQL适应网络高扩展需求的新一代RDBMS,这两种数据库都是为了解决关系型数据库的痛点。单说NoSQL,NoSQL这一术语在90年代末就提出来了,一位名叫Carlo Strozzi的人开发的数据库名称,这种数据库不使用SQL来存取数据而是直接用shell脚本存取文件。但真正意义上的NoSQL技术是在09年一场尾随BigTable和Dynamo产品的会议上提出的。发展至今,NoSQL数据库大约有150多种。依据结构描述的不同,大致有4大类。

第一类是key-value型数据库。这类数据库的数据模型就是采用key-value这种形式的键值对,操作时以keys来读取values。这样简单的结构对于存读非常高效,用来当缓存处理大量数据的高访问负载不错,并容易扩展,不过数据的无结构会导致数据的完整性,通常只被当作字符串或二进制数据的临时数据存储,完整性和容错控制交给应用。Redis数据库就属于这种。

第二类是Graph型数据库。它的存储结构为图这种以节点为基础的数据结构。当应用的数据模型能映射到图这种结构时,图的操作能提高联合查找的速度。比如IngoGrid,在社交网络和推荐系统都用不错的应用。但这种结构不太好做分布式集群方案。

第三类是column型数据库。这类数据库数据以列的方式存放在一起,数据表是列的集合。由于同列数据的类型是一样的,能很好地实现数据压缩。在查询速度有保证的情况下,还便于廉价的分布式扩展,这样也导致column型比较局限于分布式文件系统。对于事务操作的不支持加大来操作的繁杂。这类数据库比较典型的就是HBaese了。

第四类是Document型数据库。它的主要特定就是将半结构化的数据存在文档中。这种结构支持灵活的查询语句,也方便水平扩展。这种数据库比如我最近在用的MongoDB特别适合Web应用。

这四类数据库之所以叫NoSQL主要是反规范化,也就是不依照RDBMS的范式做。这样虽然数据量大了,但是读取速度却因为数据存储的集中进而加快了,也降低了查询复杂度。通过上面的介绍,能看到所有的NoSQL数据库都提供灵活的Schema。

还有一点值得提及的是关系型与NoSQL数据库对于事务处理的要求是不一样的。关系型数据库为保障事务的正确可靠,必须具备ACID要求,这这个特征分别是原子性、一致性、隔离性、持久性。这4个特性是Jim Grey上世纪90年代提出来的,经后人完善总结出特性的具体含义:

  • 原子性(Atomicity):一个事务的所有操作要么全部完成,要么全部不完成。以银行转账举例,从原账户扣除金额与向目标账户添加金额这两个操作就为一个事务。原子性需保障事务在执行过程中发生错误就需要回滚到事务未执行前的状态。
  • 一致性(Consistency):事务必须满足预设规则,数据库的完整性没有被破坏。再拿银行举例就是银行对于所有用户的金额有个总和完整性保护。当执行转账这一事务时,银行的总金额是不能被改变的,否则这一事务就破坏了一致性。
  • 隔离性(Isolation):当多个事务并发执行时,不同事务之间的相互影响应该受到控制。比如上例中若同时对同一账户进行操作,应该将账户的金额扣除操作串行化处理以免数据结果错误;而查询同一账户的事务则能同时进行。
  • 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久地保存在数据库之中,并且是完全的。当系统故障时,也能通过备份和恢复来保障持久性。

CAP理论

RDBMS的事务特性保障了数据的一致性,但对于细小的操作,一些开销只是为了满足这些约束条件,这在刚开始没成为很大的问题。当网络时代的数据爆发带来分布式系统和大数据,Eric Brewer和他提出来经过后人证明的CAP定理就应运而生,这套定理指出对于一个分布式计算系统来说,不可能同时满足以下三点(不是最初定义,而是在工程时间中的理解):

  • 一致性(Consistency):读操作这是能读取到客户端之前完成的写操作的结果。从任意节点任意时刻读取的数据都是一样的,所谓的强一致性。弱一致性则采用异步延时同步方案。
  • 可用性(Availability):读写操作在单点故障的情况下依然能够正常执行。
  • 分区容忍性(Partition Tolerance):容忍是说系统中的数据分布对系统性能的影响程度。分区可容忍性也就是机器故障、网络故障等异常情况下依然能够满足一致性和可用性。

NoSQL一定程度就是基于这个理论提出来的,因为传统的SQL数据库(关系型数据库)都是都是具有ACID属性,对一致性(C)要求很高,因此降低了分区容忍性(P),因此,为了提高系统性能和可扩展性,必须牺牲一致性(C),推翻关系型数据库中ACID这一套。

NoSQL通过弱化了事务和多表关联的功能,牺牲一致性,增强分区的扩展性,也就是水平扩展。传统数据库追求的是CA。NoSQL在设计策略上就选择了AP,牺牲了C。这一理论就是BASE(基本可用性、软状态与最终一致性)的含义——“NoSQL数据库设计可以通过牺牲一定的数据一致性和容错性来换取高性能”。

  • 基本可用性(Basically Available):这一条保证CAP中的可用性,不管何种请求,系统都会给出响应。哪怕给出的响应可能是错误代码。
  • 软状态(Soft state):server端会以client的名义维护状态,但是仅仅维持一小段时间,过了这段时间,server就会将这些状态信息丢弃掉。
  • 最终一致性(Eventual Consistency):最终整个系统的数据是一致的。

ACID是关系型数据库强一致性的四个要求,而BASE是NoSQL数据库通常对可用性及一致性的弱要求原则。比如在Amazon购物,单个用户看到的库存数是不一样的,但最终买完后,系统的库存数是同步的。这种只需要整个系统经过一定时间后最终达到是一致性就是最终一致性。


##MongoDB

MongoDB是一种文档型数据库,这里的文档其实就是一个数据记录,能够对包含的数据内容和类型进行自我描述。文档型数据能存储多样化的数据模型和复杂的数据建模,很方便地在快速迭代的Web开发上替换原有的关系型数据库。可以从下图看出文档型数据库与关系型数据库在结构上的对应关系。

MongoDB采用的文档格式是JSON这种流行的Web程序交互的数据对象,而在硬盘上以BSON格式存储二进制的JSON。其CURD操作在以前也有所提及(使用mongoose中间件调用MongoDB),这一部分功能的使用能有效降低开发难度,也展现出了MongoDB的灵活。而MongoDB还有扩展性相关的分片和可用性相关的复本备份。

  1. 高可用性——replica set

可用性通过自动故障转移和数据冗余来支持,MongoDB通过replica set(复本集)来实现。replica set是一组存储相同数据的mongod进程。当一个节点的数据丢失时,数据可以从其他复本集恢复过来。当然replication技术还不止这些,当数据请求很大时,复本还能提高读数据库的能力。

首先,Replica set的成员分为三种:Primary、Secondary和Arbiter。

  • Primary:一个复本集中有且仅有一个Primary,它主要接收写操作。
  • Secondary: 为Primary的复本,接收读操作。当Primary出现故障时能成为Primary。其又可细分为优先级为0的Secondary和隐藏的Secondary,优先级为0就表示其不能成为Primary,隐藏的复本则只是充当备份和记录,不被应用直接使用。
  • Arbiter: Arbiter不用来存储数据,而是通过参与到选举来决定哪个Secondary成为Primary。

当有数据需要存入数据库时,MongoDB首先将写操作分配给Primary并将操作记录到Primary的oplog(operations log)集合。Secondary则将日志复制并执行操作,这过程以异步的方式进行来保证系统的响应速度。通过覆写 write concern 参数来强制写操作的处理对象个数。比如我们可以设置 writeConcern: { w: 2 } 来决定设置写操作到达2个复本集完成才返回响应。

当需要读取数据给上层时,默认地读取Primary的数据,这是因为Primary上的数据时最新的,而Secondaries则提供实时性不是太高的数据。我们也通过设置读偏好模式来决定数据读取到哪个复本。MongoDB的驱动总共支持五种偏好:只读取Primary、以Primary为主、只读取Secondary、以Secondary为主以及基于地理位置最近的方式来处理读操作。

当Primary出现故障时,会通过一个选举机制来决定哪个Secondary提升为Primary。当新复本启动、Secondary失去Primary的Heartbests响应或者Primary关闭时触发选举事件。能参与投票的复本默认是可以选择投一次票,复本成员会默认投给优先级高的复本。故障转移除了这一套选举机制,还有回滚机制。当数据 数据回滚在故障转移后进行。

  1. 水平扩展——sharding

当存储到数据库的数据越来越多,一台机器无法满足良好的服务。MongoDB的分片(Sharding)功能以水平扩展地方式来解决这个问题。一台服务器的CPU不足以支持大量的查询请求,最终导致超过内存容量的数据量拖垮磁盘的IO。我们可以通过垂直扩展,直接增加机器的CPU、RAM和存储资源,抛开理论最大数量限制不说,高昂的大型机费用也是不相称的。与垂直扩展不同,分片采用的是水平扩展,将数据分布在不同的机器(分片)上。每个分片运行独立的数据库,逻辑上,分片以一个整体给上层应用提高数据服务。每个分片只存储集群的一部分数据并只接收这一部以及新存入的数据的相关请求,处理这一小部分的操作能减少单机的压力并整体分担服务。这种方式解决吞吐量和容量需求的问题是比较合理的。

首先,分片集群的成员分为三种:Shards、Query Routers和Config Servers。

  • Shards:存储实际集群数据子集的节点,每一个分片运行一个mongod实例。每个数据库都有一个primary分片用来存储不进行分片的数据集合。
  • Query Routers:与外部应用进行通信,并将数据操作分配给各个分片。路由服务器运行的是mongos实例。
  • Config Servers:存储集群的元数据,元数据包括集群数据到分片的映射。配置服务器运行的是也mongos实例。

当我们搭建完一个最小的分片集群(至少有一个Shard、Router和Config)后,来探索一下集群的一下行为方式。集群通过Shard key记录了分片中集合文档的位置,当数据第一次插入后生成Shard key。使用单调增加的数字充当shard key能提高集群写的能力,当Shard key使用类似于_id的数字,所有的写操作都会将数据插入同一个块,并存储到同一个分片。这样的写操作是高效的。当路由节点收到应用的查询操作请求,通过配置节点得知目标分片,shard key在这个过程中就充当来定位的作用。当然查询还牵扯到隔离性的问题,当查询语句没有描述shard key时,路由节点会向所有的分片查询,并等待返回。这种方式会消耗相当长的时间。

今次就写这么多,这些MongoDB的特性都是摘自MongoDB的官网,其是否能应对实际应用的需求还需要检验。并且,MongoDB的最大的问题还是稳定性,目前,作为开发者的解决方法是通过集群来提高可用性。但对于单机就能满足需求场景(比如决策树),还是存在比较大的宕机风险。不过,开发最重要的还是人员的经验能力,我想,能灵活使用工具的特性的开发者才是一个开发团队的保障。以后在实际开发中遇到问题,再好好补充。


###Reference:

  1. A Brief History of Databases

  2. 数据库技术发展历史

  3. World of the NoSQL databases

  4. 关系数据库的第一第二第三范式

  5. PDF 架构设计基础

  6. ACID VS BASE

  7. 非关系型数据库NoSQL理论基础之CAP理论

  8. 如何正确理解CAP理论

  9. 当下NoSQL类型、适用场景及使用公司

  10. 视觉中国的MongoDB技术交流

  11. 盛拓的MongoDB技术交流

  12. NoSQL 数据建模技术

  13. MongoDB官方文档

  14. 大规模分布式存储系统:原理解析与架构实战

以及其他


Changelogs

16-11-19:更新CAP理论。

comments powered by Disqus