在你构建第一个分片集群之前,有必要理解什么是分片以及为什么有时它能适用。为什么分片很重要?对此的说明是整个MongoDB项目的核心选择理由之一。一旦理解了为什么分片如此重要,你将欣喜地了解到组成分片集群的核心组件,还有构成MongoDB分片机制的关键概念。
9.1.1 何谓分片
到目前为止,你都是把MongoDB当做一台服务器在用,每个mongod
实例都包含应用程序数据的完整副本。就算使用了复制,每个副本也都完整克隆了其他副本的数据。对于大多数应用程序而言,在一台服务器上保存完整数据集是完全可以接受的。但随着数据量的增长,以及应用程序对读写吞吐量的要求越来越高,普通服务器渐渐显得捉襟见肘了。尤其是这些服务器可能无法分配足够的内存,或者没有足够的CPU核数来有效处理工作负荷。除此之外,随着数据量的增长,要在一块磁盘或一组RAID阵列上保存和管理备份如此大规模的数据集也变得不太现实了。如果还想继续使用普通硬件或者虚拟硬件来托管数据库,那么针对这类问题的解决方案就是将数据库分布在多台服务器上。这种方法称为分片。
为数众多的Web应用程序,知名的有Flickr和LiveJournal,都实现了手动分片,将负载分布到多台MySQL数据库上。在这些实现中,分片逻辑都寄生于应用程序之中。要明白这是如何实现的,想象一下,假设你有很多用户,需要将Users
表分布到多台数据库服务器上。你可以指定一台数据库作为元数据库。这台数据库包含每个用户ID(或者用户ID范围)到指定分片映射关系的元数据。因此,要查询一个用户实际涉及两次查询:第一次查询访问元数据库以获得用户的分片位置,第二次查询直接访问包含用户数据的分片。
对于这些Web应用程序而言,手动分片解决了负载问题,但其实现并非无懈可击。最明显的问题是迁移数据非常困难。如果单个分片负载过重,将其中的数据迁移到其他分片的过程完全是手动的。手动分片的第二个问题在于要编写可靠的应用程序代码对读写请求进行路由,并且将数据库作为一个整体进行管理,这也是非常困难的。最近出现了一些管理手动分片的框架,最著名的就是Twitter的Gizzard(详见http://mng.bz/4qvd)。
但正如那些手动分片数据库的人所说的,要把事情做好并非易事。MongoDB中有一大块工作就是为了解决该问题。因为分片是MongoDB的核心内容,所以用户无需担心在需要水平扩展时要自己设计外置分片框架。在处理困难的跨分片数据均衡问题时,这点尤为重要。这些代码并非那种大多数人能在一个周末里写出来的东西。
也许最值得一提的是MongoDB在设计时为应用程序提供了统一的接口,无论是在分片前,还是在分片后。也就是说,在数据库需要转换为分片架构时,应用程序代码几乎无需改动。
现在你应该对自动分片背后的逻辑有点感觉了。在详细描述MongoDB的分片过程前,让我们停下脚步,回答另一个摆在面前的问题:何时需要分片?
何时分片
这个问题的答案比你想的要简单得多。我们之前已经说过把索引和工作数据集放在内存里是很重要的,这也是分片的主要原因。如果应用程序的数据集持续无限增长,那么迟早有一天,内存会容纳不下这些数据。如果你正使用亚马逊的EC2,那么这个阈值是68 GB,因为这是本书编写时EC2最大的实例所能提供的内存总数。或者,你可以运行自己的硬件,并使用远高于68 GB的内存,这样便能延后一段时间再做分片。但没有哪台机器的内存是无限的,因此你早晚都会用到分片。
不可否认,还有一些其他的应对措施。举例来说,如果你有自己的硬件,而且可以将所有数据都保存在固态硬盘上(它的成本越来越能为人所接受了),那么可以增加数据内存比,而不会为性能带来负面影响。还有一种情况,工作集是总数据量中的一部分,这时可以使用相对较小的内存。另一方面,如果有特殊的写负载要求,那么可以在数据达到内存大小之前先进行适当的分片,原因是需要将负载分到多台机器上,以便能够获得想要的写吞吐量。
无论哪种情况,对现有系统进行分片的决定都要基于以下几点——磁盘活动、系统负载以及最重要的工作集大小与可用内存的比例。
9.1.2 分片的工作原理
要理解分片是如何工作的,你需要了解构成分片集群的组件,理解协调那些组件的软件进程,这就是接下来的主题。
1. 分片组件
分片集群由分片、mongos
路由器和配置服务器组成。我们所要讨论的组件如图9-1所示。
图9-1 MongoDB分片集群中的组件
- 分片
MongoDB分片集群将数据分布在一个或多个分片上。每个分片都部署成一个MongoDB副本集,该副本集保存了集群整体数据的一部分。因为每个分片都是一个副本集,所以它们拥有自己的复制机制,能够自动进行故障转移。你可以直接连接单个分片,就像连接单独的副本集那样。但是,如果连接的副本集是分片集群的一部分,那么你只能看到部分数据。
mongos
路由器
如果每个分片都包含部分集群数据,那么还需要一个接口连接整个集群,这就是mongos
。mongos
进程是一个路由器,将所有的读写请求指引到合适的分片上。如此一来,mongos
为客户端提供了一个合理的系统视图。
mongos
进程是轻量级且非持久化的。它们通常运行于与应用服务器相同的机器上,确保对任意分片的请求只经过一次网络跳转。换言之,应用程序连接本地的mongos
,而mongos
管理了指向单独分片的连接。
- 配置服务器
如果mongos
进程是非持久化的,那么必须有地方能持久保存集群的公认状态;这就是配置服务器的工作,其中持久化了分片集群的元数据,该数据包括:全局集群配置;每个数据库、集合和特定范围数据的位置;一份变更记录,保存了数据在分片之间进行迁移的历史信息。
配置服务器中保存的元数据是某些特定功能和集群维护时的重中之重。举例来说,每次有mongos
进程启动,它都会从配置服务器获取一份元数据的副本。没有这些数据,就无法获得一致的分片集群视图。该数据的重要性对配置服务器的设计和部署策略也有影响。
查看图9-1,你会看到三个配置服务器,但它们并不是以副本集的形式部署的。它们比异步复制要求更严格;mongos
进程向配置服务器写入时,会使用两阶段提交。这能保证配置服务器之间的一致性。在各种生产环境的分片部署中,必须运行三个配置服务器,这些服务器都必须部署在独立的机器上以实现冗余。1
1. 你也可以运行单个配置服务器,但这只能作为简单测试分片的一种手段。在生产环境里只用一台配置服务器就好比乘坐单引擎喷气飞机横跨大西洋:它能带你飞过去,但是一旦失去一个引擎,你就得游泳了。
现在你了解了分片集群的构成,但也许还对分片机制本身心存疑惑。数据究竟是如何分布的?接下来我会介绍一些核心分片操作,对此做出解释。
2. 核心分片操作
MongoDB分片集群在两个级别上分布数据。较粗的是以数据库为粒度的,在集群里新建数据库时,每个数据库都会被分配到不同的分片里。如果不进行什么别的设置,数据库以及其中的集合永远都会在创建它的分片里。
因为大多数应用程序都会把所有的数据保存在一个物理数据库里,因此这种分布方式带来的帮助不大。你需要更细粒度的分布方式,集合的粒度刚好能满足要求。MongoDB的分片是专门为了将单独的集合分布在多个分片里而设计的。要更好地理解这点,让我们一起想象一下在真实的应用程序里这是如何工作的。
假设你正在构建一套基于云的办公套件,用于管理电子表格,并且要将所有的数据都保存在MongoDB里。2 用户可以随心所欲地创建大量文档,每个文档都会保存为单独的MongoDB文档,放在一个spreadsheets
集合里。随着时间的流逝,假设你的应用程序发展到了拥有100万用户。现在再想想那两个主要集合:users
和spreadsheets
。users
集合还比较容易处理。就算有100万用户,每个用户文档1 KB,整个集合大概也就1 GB,一台机器就能搞定了。但spreadsheets
集合就大不一样了,假设每个用户平均拥有50张电子表格,平均大小是50 KB,那么我们所谈论的就是1 TB的spreadsheets
集合。要是这个应用程序的活跃度很高,你会希望将数据放在内存里。要将数据放在内存里并且分布读写负载,你就必须将集合分片。这时分片就该登场了。
2. 可以参考一下Google Docs之类的产品,Google Docs允许用户创建电子表格和演示幻灯片。
- 分片一个集合
MongoDB的分片是基于范围的。也就是说分片集合里的每个文档都必须落在指定键的某个值范围里。MongoDB使用所谓的分片键(shard key)让每个文档在这些范围里找到自己的位置。3从假想的电子表格管理应用程序里拿出一个示例文档,这样你能更好地理解分片键:
3. 其他的分布式数据库里可能使用分区键(partition key)或分布键(distribution key)来代替分片键这个术语。
{ _id: ObjectId(/"4d6e9b89b600c2c196442c21/") filename: /"spreadsheet-1/", updated_at: ISODate(/"2011-03-02T19:22:54.845Z/"), username: /"banks/", data: /"raw document data/"}
在对该集合进行分片时,必须将其中的一个或多个字段声明为分片键。如果选择_id
,那么文档会基于对象ID的范围进行分布。但是,出于一些原因(稍后会做说明的),你要基于username
和_id
声明一个复合分片键;因此,这些范围通常会表示为一系列用户名。
现在你需要理解块(chunk)的概念,它是位于一个分片中的一段连续的分片键范围。举例来说,可以假设docs
集合分布在两个分片A和B上,它被分成了如表9-1所示的多个块。每个块的范围都由起始值和终止值来标识。
表9-1 块与分片
粗略扫一眼表9-1,你会发现块的一个重要的、有些违反直觉的属性:虽然每个单独的块都表示一段连续范围的数据,但这些块能出现在任意分片上。
关于块,第二个要点是它们是种逻辑上的东西,而非物理上的。换言之,块并不表示磁盘上连续的文档。从一定程度上来说,如果一个从harris开始到Norris结束的块存在于分片A上,那么就认为可以在分片A的docs
集合里找到分片键落在这个范围里的文档。这和集合里那些文档的排列没有任何必然关系。
- 拆分与迁移
分片机制的重点是块的拆分(splitting)与迁移(migration)。
首先,考虑一下块拆分的思想。在初始化分片集群时,只存在一个块,这个块的范围涵盖了整个分片集合。那该如何发展到有多个块的分片集群呢?答案就是块大小达到某个阈值时会对块进行拆分。默认的块的最大块尺寸是64 MB或者100 000个文档,先达到哪个标准就以哪个为准。在向新的分片集群添加数据时,原始的块最终会达到某个阈值,触发块的拆分。这是一个简单的操作,基本就是把原来的范围一分为二,这样就有了两个块,每个块都有相同数量的文档。
请注意,块的拆分是个逻辑操作。当MongoDB进行块拆分时,它只是修改块的元数据就能让一个块变成两个。因此,拆分一个块并不影响分片集合里文档的物理顺序。也就是说拆分既简单又快捷。
你可以回想一下,设计分片系统时最大的一个困难就是保证数据始终均匀分布。MongoDB的分片集群是通过在分片中移动块来实现均衡的。我们称之为迁移,这是一个真实的物理操作。
迁移是由名为均衡器(balancer)的软件进程管理的,它的任务就是确保数据在各个分片中保持均匀分布。通过跟踪各分片上块的数量,就能实现这个功能。虽然均衡的触发会随总数据量的不同而变化,但是通常来说,当集群中拥有块最多的分片与拥有块最少的分片的块数差大于8时,均衡器就会发起一次均衡处理。在均衡过程中,块会从块较多的分片迁移到块较少的分片上,直到两个分片的块数大致相等为止。
如果现在你还不太理解,不用担心。下一节里我会通过一个示例集群来演示分片,通过实践来进一步阐述分片键和块的概念。