要想了解NoSQL,必须先了解现有的这些工具,去理解那些引导它们开拓出新的存储领域的设计思路。
NoSQL 其名
在给 NoSQL 下定义之前,我们先来试着从它的名字上做一下解读。顾名思义,NoSQL 系统的数据操作接口应该是非 SQL 类型的。但在 NoSQL 社区,NoSQL 被赋予了更具有包容性的含义,其意为 Not Only SQL,即 NoSQL 提供了一种与传统关系型数据库不同的存储模式,这为开发者提供了关系型数据库之外的另一种选择。
NoSQL 的启示
NoSQL 运动受到了很多相关研究论文的启示,在所有资料中,最核心的有两个:Google 的 BigTable 论文和 Amazon 的 Dynamo 论文。
特性概述
NoSQL 系统舍弃了一些 SQL 标准中的功能,取而代之的是一些简单灵活的功能。NoSQL 的构建思想就是尽量简化数据操作,尽量让操作的执行效率可预估。当你去考查一个 NoSQL 系统时,下面的几点是值得注意的。
数据模型及操作模型:你的应用层数据模型是行、对象还是文档型的呢?这个系统是否能支持你进行一些统计工作呢?
可靠性:当你更新数据时,新的数据是否立刻写到持久化存储中去了?新的数据是否同步到多台机器上了?
扩展性:你的数据量有多大,单机是否能容下?你的读写量需求单机是否能支持?
分区策略:考虑到对扩展性、可用性或者持久性的要求,你是否需要一份数据被存在多台机器上?你是否需要知道或者说你能否知道数据在哪台机器上?
一致性:你的数据是否被复制到了多台机器上?这些不同节点的数据如何保证一致性?
事务机制:业务是否需要 ACID 事务机制?
单机性能:如果你打算持久化的将数据存在磁盘上,哪种数据结构能满足你的需求(你的需求是读多还是写多)?写操作是否会成为磁盘瓶颈?
负载可评估:对于一个读多写少的应用,诸如响应用户请求的网络应用,我们总会花很多精力来关注负载情况。你可能需要进行数据规模的监控,对多个用户的数据进行汇总统计。你的应用场景是否需要这样的功能呢?
NoSQL 数据模型及操作模型
数据库的数据模型指的是数据在数据库中的组织方式,数据库的操作模型指的是存取这些数据的方式。通常数据模型包括关系模型、键值模型以及各种图 结构模型。操作语言可能包括 SQL、键值查询及 MapReduce 等。NoSQL 通常结合了多种数据模型和操作模型,提供不一样的架构方式。
基于Key值存储的NoSQL数据模型
在键值型系统中,复杂的联合查询以及满足多个条件的数据查询操作就不那么容易实现了,需要换一种思维来建立和使用键名。比如要获取部门号为 20 的所有员工的信息,应用层可以先获取 Key 为 employee_departments:20的这个列表,然后再循环地拿这个列表中的 ID 通过获取 employee:ID 得到所有员工的信息。
Key-Value 存储
Key-Value 存储可以说是最简单的 NoSQL 存储,每个 Key 值对应一个任意的数据值。对 NoSQL 系统来说,这个任意的数据值是什么,它并不关心。比如在员工信念数据库里,employee:30这个 Key 对应的可能就是一段包含员工所有信息的二进制数据。这个二进制的格式可能是 Protocol Buffer、Thrift 或者 Avro 都无所谓。
Key-结构化数据存储
Key-结构化数据存储的典型代表是 Redis,Redis 将 Key-Value 存储的 Value 变成了结构化的数据类型。Value 的类型包括数字、字符串、列表、集合以及有序集合。除了 set/get/delete 操作以为,Redis 还提供了很多针对以上数据类型的特殊操作,比如针对数字可以执行增、减操作,对 list 可以执行 push/pop 操作,通过提供这种针对单个 Value 进行的特定类型的操作,Redis 可以说实现了功能与性能的平衡。
Key-文档存储
Key-文档存储的代表有 CouchDB、MongoDB 和 Riak。这种存储结构下 Key-Value 的 Value 是结构化的文档,通常这些文档是被转换成 JSON 或者类似于 JSON 的结构进行存储。文档可以存储列表,键值对以及层次结构复杂的文档。
BigTable 的列簇式存储
HBase 和 Cassandra 的数据模型都借鉴自 Google 的 BigTable。这种数据模型的特点是列式存储,每一行数据的各项被存储在不同的列中(这些列的集合称作列簇)。而每一列中每一个数据都包含一个时间戳 属性,这样列中的同一个数据项的多个版本都能保存下来。
列式存储可以这样理解:将行 ID、列簇号,列号以及时间戳一起,组成一个 Key,然后将 Value 按 Key 的顺序进行存储。Key 值的结构化使这种数据结构能够实现一些特别的功能,最常用的就是将一个数据的多个版本存成时间戳不同的几个值,这样就能方便地保存历史数据。这种结构也能 天然地进行高效的松散列数据(在很多行中并没有某列的数据)存储。当然,对于那些很少有某一行有 NULL 值的列,由于每一个数据必须包含列标识,这又会造成空间的浪费。
图结构存储
图结构存储是 NoSQL 的另一种存储实现。其指导思想是:数据并非对等的,关系型的存储或者键值对的存储,可能都不是***的存储方式。图结构是计算机科学的基础结构之一,Neo4j 和 HyperGraphDB 是当前***的图结构数据库。
复杂查询
在 NoSQL 存储系统中,有很多比键值查找更复杂的操作。比如 MongoDB 可以在任意数据行上建立索引,可以使用 Javascript 语法设定复杂的查询条件。BigTable 型的系统通常支持对单独某一行的数据进行遍历,允许对单列的数据进行按特定条件的筛选。CouchDB 允许你创建同一份数据的多个视图,通过运行 MapReduce 任务来实现一些更为复杂的查询或者更新操作。很多 NoSQL 系统都支持与 Hadoop 或者其他 MapReduce 框架结合来进行一些大规模数据分析工作。
事务机制
与关系型数据库不同的是,NoSQL 系统通常注重性能和扩展性,而非事务机制。传统的 SQL 数据库的事务通常都是支持 ACID 的强事务机制。ACID 的支持使得应用者能够很清楚他们当前的数据状态。对很多 NoSQL 系统来说,对性能的考虑远在 ACID 的保证之上。通常 NoSQL 系统仅提供行级别的原子性保证,也就是说同时对同一个 Key 下的数据进行的两个操作,在实际执行时是会串行的,保证了每一个 Key-Value 对不会被破坏。
Schema-free 的存储
还有一个很多 NoSQL 的共同点,就是它通常并没有强制的数据结构约束。即使是在文档型存储或者列式存储上,也不会要求某一个数据列在每一行数据上都必须存在。
数据可靠性
最理想的状态是,数据库会把所有写操作立刻写到持久化存储的设备,同时复制多个副本到不同地理位置的不同节点上,以防止数据丢失。但这种对数据安全性的要求对性能是有影响的,所以不同的 NoSQL 系统在自身性能的考虑下,在数据安全上采取了不太一样的策略。
单机可靠性
单机可靠性理解起来非常简单,它的定义是写操作不会由于机器重启或者断电而丢失。通常单机可靠性的保证是通过把数据写到磁盘来完成的,而这通常会造成磁盘I/O成为整个系统的瓶颈。下面我们谈谈一些在单机可靠性的保证下提高性能的方法。
控制fsync的调用频率
Redis 提供了几种对 fsync 调用频率的控制方法。应用开发者可以配置 Redis 在每次更新操作后都执行一次 fsync,这样会比较安全,当然也就比较慢。Redis 也可以设置成N秒种调用一次 fsync,这样性能会更好一点。但这样的后果就是一旦出现故障,最多可能导致N秒内的数据丢失。而对一些可靠性要求不太高的场合(比如仅仅把 Redis 当 Cache 用的时候),应用开发者甚至可以直接关掉 fsync 的调用:让操作系统来决定什么时候需要把数据 flush 到磁盘(译者注:这只是 Redis append only file 的机制,Redis 是可以关闭 aof 日志的,另外,Redis 本身支持将内存中数据 dump 成 rdb 文件的机制,和上面说的不是一回事)。
使用日志型的数据结构
Cassandra、HBase、Redis 和 Riak 都会把写操作顺序的写入到一个日志文件中。相对于存储系统中的其他数据结构,上面说到的日志文件可以频繁地进行 fsync 操作,这样就把对磁盘的随机写变成顺序写了。
通过合并写操作提高吞吐性能
Cassandra 有一个机制,它会把一小段时间内的几个并发的写操作放在一起进行一次 fsync 调用,这种做法叫 group commit。
多机可靠性
由于硬件层面有时会造成无法恢复的损坏,单机可靠性的保证在这时就鞭长莫及了。对于一些重要数据,跨机器做备份保存是必备的安全措施。一些 NoSQL 系统提供了多机可靠性的支持。
Redis 采用传统的主从数据同步的方式。
MongoDB 提供了一种叫 Replica Sets 高可用架构。
Riak、Cassandra 和 Voldemort 提供了一些更灵活的可配置策略,并提供一个可配置的参数N,代表每一个数据会被备份的份数。为了应对整个数据中心出现故障的情况,需要实现跨数据中心的多机备份功能。
横向扩展带来性能提升
横向扩展的目标是达到线性的效果,即如果你增加一倍的机器,那么负载能力应该也能相应的增加一倍。其主要需要解决的问题是如何让数据在多台机器间分布,这里面涉及到分片技术。
分片的意思,就是没有任何一台机器可以处理所有写请求,也没有任何一台机器可以处理对所有数据的读请求。下面我们将会对 hash 分片和范围分片两种分片方式进行描述。
如非必要,请勿分片
分片会导致系统复杂程度大增,所以,如果没有必要,请不要使用分片。普通情况下,我们可以使用读写分离和构建缓存的方式来缓解我们的数据读压力。但如果写操作达到单点无法承担的程度,那我们可能就真的需要进行分片了。
通过协调器进行数据分片
一种分片策略是通过引入一个中间代理层来实现,该代理层记录数据在各个节点的分布状况,所有读写请求都通过代理层来做路由。比如与 CouchDB 的两个项目:Lounge 和 BigCouch。类似的,Twitter 自己也实现了一个叫 Gizzard 的协调器,可以实现数据分片和备份功能。
一致性 hash 环算法
一致性 hash 是一种被广泛应用的技术,其最早在一个叫 distributed hash tables(DHTs)的系统中进行使用。那些类 Dynamo 的应用,比如 Cassandra、Voldemort 和 Riak,基本上都使用了一致性 hash 环算法。
如图 1 所示,一致性 hash 环算法有一个 hash 函数H,所有存储数据的节点和数据本身都可以通过这个函数算出一个 hash 值,作为自己在下面环上的位置。然后每个节点会负责存储其 hash 值到下一个节点间的所有数据的存储。这样使得即使节点数变化了,大部分数据并不需要进行迁移。
图 1 一致性 hash 环算法的 hash 函数
连续范围分区
使用连续范围分区的方法进行数据分片,需要我们保存一份映射关系表,标明哪一段 Key 值对应存在哪台机器上。与一致性 hash 类似,连续范围分区会把 Key 值按连续的范围分段,每段数据会被指定保存在某个节点上,然后会被冗余备份到其他节点。
BigTable 的处理方式
Google BigTable 论文中描述了一种范围分区方式,它将数据切分成一个个的 tablet 数据块。每个 tablet 保存一定数量的键值对。然后存储在 Tablet 服务器上。tablet 块的大小会保持在一定范围,太大的块会分裂成两个,太小的块又会合并成一个。BigTable 通过一个叫 Chubby 的模块来实现节点状态检测。类似的在 Hadoop 中有一个叫 ZooKeeper 的工具实现此功能。
一致性
上面讲到了通过将数据冗余存储到不同的节点来保证数据安全和减轻负载,下面我们来看看这样做引发的一个问题:保证数据在多个节点间的一致性是非常困难的。在多个点间保持数据的一致性的问题,也就是本章的主题。下面我们首先来看一下在著名的 CAP 理论。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性(P):集群中的某些节点在无法联系后,集群整体是否还能继续进行服务。
而 CAP 理论就是说在分布式存储系统中,最多只能实现上面的两点。再加之当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。结果就是我们只能在一致性和可用性之间进行权衡,没有 NoSQL 系统能同时保证这三点。
对一致性的保证,通常有强一致性和弱一致性的选择,而在弱一致性里,又以最终一致性的实现较为普遍。
如果我们采用 NRW 的设定,N为数据需要备份的份数,R为读操作需要读到的不同节点上的数据份数,W为写操作需要成功写到不同节点的数据份数,那么当R+W>N时,既 是强一致性的保证,当R+W
写在***的话
目前 NoSQL 系统来处在它的萌芽期,我们上面讨论到的很多 NoSQL 系统,它们的架构、设计和接口可能都会改变。本章的目的,不在于让你了解这些 NoSQL 系统目前是如何工作的,而在于让你理解这些系统之所以这样实现的原因。NoSQL 系统把更多的设计工作留给了应用开发工作者来做。理解上面这些组件的架构,不仅能让你写出下一个 NoSQL 系统,更让你对现有系统应用得更好。
原文:http://www.programmer.com.cn/9276/
【编辑推荐】