mongo——分片(sharding)

概念

随着应用系统规模的增长,成本会变得越来越高,而且有时候无法实现使用单台机器来处理负载压力。这种问题的一个解决方案就是汇聚大量低价、低处理能力的机器来解决问题。mongodb的分片就是为解决这种问题而设计的:把超大数据使用更小的片进行分区存储,这样就不需要在单个机器上存储所有的数据或者承担全部压力。mongodb分片对于应用系统是透明的,这意味着对于分片集群的查询与可复制或者单个mongo服务器实例的查询完全一样。

什么时候分片?

什么时候分片的问题在理论上非常简单,但是必须完全理解系统是怎么使用的。通常有两种分片情况:

1.存储分片式

1.负载分片式:

分片架构

1.分片:存储了应用程序的数据。在分片集群中,只有mongos路由器或者系统管理员可以直接连接分片服务器节点。与不分片的部署一样,每个分片可以单独作为开发和测试的节点,但是生产环境下应该是个可复制集。

2.mongos路由器(中心):缓存了集群的元数据并使用它来路由操作到正确的分片服务器。

3.配置服务器(右上):一直存储集群的元数据,包括哪个分片包含哪些数据集

分片:存储应用程序数据

分片,如图12.1左上角所示的,或者是单个mongod服务器或者是一个存储部分应用数据的可复制集。事实上,分片是分片集群中应用程序数据存储的唯一位置。用于测试,一个片可以是单个的mongod服务器,但是在生产环境中应该是可复制集,因为它会有自己的复制机制并且可以自动灾备和故障转移。可以连接到一个片,也可以连接到单个服务器节点或者可复制集。但是如果尝试在这个片上直接运行操作,则只能看到整个集群数据中的一部分。

mongos路由:路由操作

因为每个分片包含的只是总数据的一部分,所以需要某个东西来路由操作到适当的片上。这就是mongos的作用。mongos进程如上图所示,是个可以直接转发所有读、写命令到正确的分片上的路由器。mongos提供给客户端单点连接集群的方式,这使得整个集群看起来就像单个节点一样。

mongos进程是轻量级的、不持久化的。因此,它们通常部署在应用程序服务器上,以确保只有一个网络节点来转发请求,。换句话来说,应用程序连接本地的mongos,而mongos管理与每个分片服务器的连接

配置服务器:存储元数据

mongos进程是非持久化的,这意味着必须有某个东西来存储集群的元数据。这个工作就由配置服务器来做,如图右上角所示。这些元数据包含全局的集群配置信息,每个数据库,集合、特定数据的范围的位置,以及保存了跨片数据迁移历史的一个修改日志。

配置服务器保存的元数据是集群正常工作和更新维护的关键。例如,每次mongos启动,mongos都会从配置服务器获取一份元数据拷贝。没有这些数据,就没有办法完整预览整个集群。这些元数据的重要性体现在通过配置服务器来保存分片集群设计和部署的策略教程。

上图有3台配置服务器,但不是可复制集。它们需要比异步复制更强大的东西;当mongos进程写入它们时,使用的是两阶段提交协议。这样可以确保跨配置服务器的一致性。我们必须在分片集群生产环境下部署3台配置服务器,而且这些服务器必须部署在单独的冗余机器上。

在分片集群中分散数据

从最里面依次向外看,可以看到mongodb有4种不同的分片级别:文档、块、集合、数据库。这4种不同的粒度级别代表着mongodb中不同的数据单位。

1.文档:mongodb中最小的数据单元。文档表示系统中不能够再分了的单个对象。我们可以把它与关系型数据库中的行相比较。注意,我们把文档及其字段作为一个原子性单位。

2.块:根据某个字段的值包换的一组文档,块只是在集群组里存在的概念。这是根据一个或者一组字段的值进行逻辑分组,这些字段称为分片键。如上图,块就是所有username的值在‘bakkum’和‘verch’之间的文档。

3.集合:数据库中一组命名的文档集合。允许用户把数据库分组为有意义的逻辑组,mongodb提供了集合概念。这就是一组命名的文档,而且它必须被应用显示指定才能进行查询。

4.数据库:包含文档的集合。这是系统中最顶级的命名组。因为数据库包含文档的集合,所以集合也必须指定在文档上执行的操作。如上图所示,数据库名字是cloud-docs。要运行查询,集合必须指定为——spreadsheets。组合数据库名字和集合的名字提供了系统中的唯一性,这通常叫做命名空间。它通常是集合名字和数据库名字连接在一起,用圆点分割 如,cloud-docs.spreadsheets.

分片集群中的数据分散方式

mongodb分片集群更强大的形式:分片单个集合。这也是自动分片一词的本意,因为这是mongodb做出分区决定的分片形式,不需要应用参与。

为分片单个集合,mongodb定义了块的概念,它是一个基于预定义字段值或分片键的逻辑分组文档。选择分片键是用户的职责。

如果集合中的所有文档都包含这种格式,就可以选择_id和username字段作为分片键。然后mongodb就会使用每个文档中的这些信息来决定文档存储到哪个块中。

mongodb是怎么做出决定的呢?在底层核心中,mongodb的分片是基于范围的,这意味着每个块表示一个范围的键值。当mongodb查看某个文档以确定它属于哪个块时,首选它会抽取分片键的值,然后找出包含分片建所在的块。

看一下具体的例子,假设我们已经为spreadsheets集合选择了username作为分片建,而且我们有2个片,"A","B"。我们块分布可能有点类似于下表

从上表就会明白分片集群中的块的作用了。如果我们有个文档的username值为Babbage,则看一下上面的表就知道它应该属于A服务器。事实上,如果我们给大家任意包含username字段的文档,此字段是分片集群的键,你就可以使用上表确定此文档所属 的数据块,然后决定它应该被发送到哪个 分片服务器上。

 

查询路由

在图左边,可以看到查询选择器包含username的目标查询。此时,mongos路由可以使用username字段直接把查询转发到正确的分片节点上。

在图右侧,可以看到全局或分散/集中查询,在查询选择器中它不包含分片键的任意部分。此时,mongos必须广播查询到两个分片中。

目标查询的性能不能夸大。如果所有的查询都是全局的,这意味着每个分片节点都必须响应集群中的每个查询。相反如果所有的查询是目标性的,每个分片只需要处理与自己相关的请求。伸缩性的含义非常清晰。

但是定位并非是影响分片集群性能的唯一因素。正如我们下面看到的,每个在未分片部署下性能问题都会适用于分片集群,只是分散到单独的机器上。

分片集群中建立索引

无论插入定义多么完美,最终都必须运行在一个分片上。这意味着,如果分片响应查询慢,则集群也会慢。

未分片模式下,索引是优化查询性能的重要手段。

关于分片集群的索引只需要记住以下几个要点:

1.每个分片维护自己的索引。当在分片集合上声明索引时,每个分片都会为自己的集合部分定义单独的索引。例如,当mongos使用db.spreadsheets.createIndex()命令时,每个分片进程都会单独处理索引创建的命令。

2.它遵循分片集合在每个分片上应该拥有相同的索引原则。如果不是,就无法保证查询性能的一致性。

3.分片集合只允许在_id字段和分片键上建议唯一索引。禁止其他地方建立唯一索引,因为强制唯一性需要在分片之间进行通信,这是由mongodb分片集群底层工作机制决定的。

选择分片键

在我们为spreadsheet应用选择最佳分片键的过程中,会看到3个主要的陷阱:

1.热点:某些分片键会导致所有的读或者写都操作在单个数据块或单个分片上。这可能导致单个分片服务器严重不堪重负,而其他分片服务器闲置,无所事事。

2.不可分割数据块,过于粗粒度的分片建可能导致许多文档使用相同的分片建。因为分片是基于分片键值的范围,所以意味着这些文档不能被分割为多个数据块,这个最终会限制mongodb均匀分布数据的能力。

3.糟糕的定位:即使写压力可以完美分布到集群中,如果我们的分片建与某些查询没有关联,也会导致糟糕的查询性能。

非平衡写入(热点)

 

第一个可能想到的分片建是{"_id" : 1},它会在_id字段上进行分片。

咋一看,_id字段好像是最佳的候选:它肯定会出现在每个文档中,默认就有索引,在许多查询里会使用到,而且mongodb会使用bson object类型自动生成它,本质上是GUID(全局的唯一标识符)

使用object id作为分片键值有个显著的问题:它的值是严格升序的

为了简单起见,使用自增字段n来表示_id,正如图所示,我们已经插入1-1000的文档,现在将要插入1001,1002,1003三个文档。因为mongodb还没有见到过大于1000的号码,所以没有理由来分割从1000到$maxKey的数据块。这意味着新的3个文档也全部属于当前的数据块,而且所有的新文档都是这样的。这意味着每个新创建的spreadsheet文档都属于同一个数据块,也就是每个新的文档都被写入到单个片中。最重要的是,mongodb会全力以赴从过载的片上迁移数据,这样就更加剧了性能恶化。

这也大大抵消了分片的最大优势:跨机器自动分布插入负载压力。所以说,如果仍然想继续使用_id字段作为分片建,则有2个选择

1.使用自定义非自增的标识符重写_id字段,如果这样做,记住,即使在分片集群里,_id也必须唯一

2.在哈希键值上设置分片建,这样会告诉mongodb使用哈希函数的结果作为分片建,而不是直接使用分片建。哈希函数用来产生随机结果,它可以确保插入更加均匀地分布在集群中,但是也意味着范围查询需要扫描多个分片,因为虽然分片建的值相似,但是他们的哈希值完全不同。

不可分割的数据库(粗粒度)

现在我们已经知道了{_id:1}不会作为分片键,但是{username:1}会怎么样呢?这看起来是个很好的候选者,因为当我们查询或者更新spreadsheet电子表格的时候,通常我们已经知道了spreadsheet所属的用户,所以我们可以在查询里包含username字段,实现良好的目标地位。此外,在username字段上分片可以导致相对的插入均衡,因为集群中分散插入文档数据的压力应该是相对均匀的。

使用这个字段只有一个问题:它如此粗粒度,可能导致的极端情况就是数据块无线增长。假设一个用户“verch”要 插入10GB的spreadsheet电子表格数据。这会让包含username为“”verch”的文档包含超过64MB最大值的数据块。

正常情况下,当数据块变得太大时,mongodb可以把他们分割为较小的数据块,然后跨集群进行平衡迁移数据。但是,此时已经没有空间可以分割数据块了,因为它只包含了一个单独的分片键值。这会导致许多技术问题,但是最终的结果就是集群变得不平衡了,而且mongodb无法再进行高效的平衡操作。

糟糕的地位(分片键不在查询中)

在看了这些问题后,你可能会想,好吧,那我就只能选择完全随机的分片键值。它是唯一的并且不是升序的。虽然它可以解决写入的均衡问题,但是如果你打算从集群中读取数据,则它不是一个好的分片建。如果它们包含分片键值,则查询只能被路由到正确的分片上。如果分片建完全是随机的,在查询文档的时候,我们就不知道分片键值是什么意思。

理想的分片键

我们已经看过选择分片键时3种需要考虑的因素,一个是如何定位读取数据,第二个是如何更好地分布数据,最后一个是如何在应用中高效地分割和迁移数据。

此时,理想的分片键就是{username: 1, _id:1}。这个组合键可以很好地定位目标,因为username字段经常出现在读取操作中,而且可以有很好的平衡性;因为username的值具备良好的均衡性,而且包含了唯一的_id字段,所以mongodb能良好粒度地分割数据块

这里我们可以分割之前只使用username字段无法分割的数据块。现在,因为我们包含了_id字段,所以我们可以把所有从‘’verch‘’

开始的文档分为2个数据块,这在使用{username:1}作为分片键时是无法完成的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章