NoSQL学习——文档数据库MongoDB(五)集群读写与分片复制

11/21/2021 NoSQLMongoDB

# MongoDB集群

MongoDB集群可以使用两种方式组织集群,即分片(Sharding)和复制(Replication)。

复制模式主要通过冗余的存储实现在集群上的扩展,通过复制,一个集群能够提供更强的持久性(当一个节点错误时,仍可以通过其他节点读写数据)、可用性(通过多个复制集分散读操作,但默认情况下不适用),根据CAP原理可见,该模式存在一致性的挑战,这种挑战主要来源于主节点和多个次级节点之间的写延迟,以及部分节点失效后的最终一致性判断。

分片模式则是通过数据的某些属性(如id)将数据分散存储到不同的节点上。分片模式同样提供了一定的可用性(通过分片机制分散写操作和缓解单节点上的读操作负载)。

另外,关于一致性需要说明的是单文档的读写始终具有一致性,而多文档的一致性是不能保证的。因此无论是否使用了集群,涉及多个文档的操作都应当在数据库的用户程序中控制这类操作的一致性。

# 复制模式

首先了解MongoDB复制模式的集群组织方式。在MongoDB集群中,复制模式采用了主从模式,包括一个主节点和多个次要节点,这些次要节点又被称为复制集(replica set);另外,集群也可以包含一个仲裁节点(arbiter),不存储数据,仅用于对集群存在矛盾的数据记录进行判断。

默认的情况下,所有的读写操作都直接发送到主节点。主节点在处理数据时,还会将操作记录到oplog上,并将操作发送到从节点,使其更新自己的数据。主节点会根据用户设置的模式选择是在收到多数从节点的确认后响应客户端或是在自身完成操作后响应,以及是否可以把读操作分散到次级节点上(由于写延迟的存在,次级节点响应读操作在默认情况下不适用)。

所有的节点会通过心跳确认彼此之间的连接,如果经过了一段时间没有得到回复,就会将对应的节点标记为不可达。这种现象即分布式系统中的网络分区,会导致不同的节点被划分到集群的子群中。在分区现象发生时,主节点也可能会被分到较小的那个子群中。当这种情况发生时,主节点会退化为次级节点。

当主节点被认为不可达后,集群中认为其不可达的节点就会发起主节点的选举。如果发起选举的节点能够与大多数节点(包括自己在内的节点数量超过半数,the majority)通信,则该节点可能通过选举成为新的主节点。在选举结束前,复制集不会处理写操作。

# 读写一致性

MongoDB针对读写提供了多种一致性控制的选项,使得使用它的客户程序在一致性与可用性的平衡上较为灵活,可以处理需要高可用性的应用,如日志等的流式分析;或需要严格一致性的应用,如关键事务处理。

在读操作上,MongoDB提供了名为Read Preference和Read Concern的模式供调用者自行设计一致性和可用性平衡。Read Preference即指明客户程序从哪里读取数据,包括primarysecondarynearest,即只从主节点、可从次级节点或从最近节点读取。Read Concern则指明客户端期望的一致性,包括读取单节点和读取集群表现出的一致性。

在写操作上,Write Concern被用于指示写操作如何被承认为有效。该选项对于单一实例、复制模式和分片模式都有效。由于数据库的机制,写操作通常需要内存内更新数据、添加操作日志和数据文件更新三步。这就使得MongoDB需要通过Write Concern控制写入操作在哪一步(j)被认为是完成的,以及如果一直等待在最长的等待时间(wtimeout)是多少。另一方面,由于集群的存在,Write Concern还需要控制写入多少个节点(w)可以认为写操作成功。

在实际查询中,指明期望的读写一致性表现可以使用两种方式:数据库设置或查询设置。以下对读写分别进行说明。

# 读操作

读操作相关的第一项Read Preference指明了从哪里读取数据。相关的数值包括:

  • primary:默认采用该项。仅从主节点读取。
  • primaryPrefered:在主要从主节点读取的基础上,如果主节点不可读,从次级节点读取。
  • secondary:仅从次级节点读取。
  • secondaryPrefered:与primaryPrefered正好相反。
  • nearest:从网络延迟最低的节点读取,无论节点是何类型。

第二项是Read Concern,指明读取操作如何返回数据,主要包含以下几种级别:

  • local:仅从直接访问的节点读取数据,不保证读取的数据被多数节点接受,因此可能出现读到未提交数据的情况(Read uncommitted)。默认用于preference为primary或secondary的设置。
  • available:读取可用节点上的数据,默认用于secondary设置,需要保证读取没有出现在因果连续的session中。因为不保证读取的数据是大多数节点认可的,故前后读取的数据可能存在不一致。
  • majority:读取的数据需要得到大多数节点的认可,由于该特性,读到的数据是被提交到大多数节点的,因此能够实现read committed。
  • linearizable:线性读取,即去除与读操作并行的写操作,因而需要等待。
  • snapshot:从存储的快照中读取。同样不适用于具有前后连续读写的session,因为数据修改并不出现在快照上。该项与MongoDB的多版本并发控制(MVCC)相关。 Read Concern使用一个紧跟find()readConcern(<level>)方法进行指定,其默认值如前述的一样与read preference挂钩。

# 写操作

写操作的一致性描述在查询上通常使用一个object表示。在上文中,我们已经描述了Write Concern的关键参数表示的意义,下面对每一个参数分别进行详细解释:

  • j:表示是否在数据库日志记录操作后认为写操作成功。采用bool值记录,true表示日志记录成功后返回,false表示仅在内存记录后即可返回,若未指明则采用数据库设置或与w挂钩。由于日志记录需要写入到磁盘,因此该项可能会影响写操作的持久性和延迟。
  • w:表示写操作成功需要的节点数量。采用整型数或字符串记录,0表示无需认可,1表示主节点认可,>1的值表示需要对应数量的节点认可,"majority"表示需要集群中的大部分节点认可。默认情况下,采用"majority"(5.0版本后)。
  • wtimeout:表示超时秒数。只在w>1时使用,超过指定时间后会返回写错误,但并不一定说明没有得到写入,因此需要仔细设计错误发生时该做什么,而不是简单地回滚。如果没有指明wtimeout,可能导致无限的阻塞。

# 分片模式

分片模式通过将不同的数据分布到不同的机器上实现集群上的可用性扩展。MongoDB采用水平分片,即将每个collection内的文档分布到不同的节点上。在MongoDB中,分片实际存储在一个名为shard的复制集上,通过协调服务mongos进行路由处理,即mongos会将客户端对数据的查询解析到分片数据的位置,此外由config server管理分片的元数据和设置。

在数据库中时,一个collection可以被分片到多个shard上,也可能不分片而只出现在一个shard上。被分片的数据通过固定大小的chunk存储(Mongo默认设置为64MB,可以类比HDFS的chunk)。一个chunk的内容只属于一个collection。chunk的分布由分片的键和策略决定,但需要保证每个分片得到的chunk基本平衡,否则部分节点就会因负载过重带来性能损失。

# 分片方法

在分片开始时,服务需要创建初始的chunk数据块。初始chunk可以从有数据的collection或空collection中创建。有数据的collection会被一小组chunk记录所有分片键值,无数据的chunk可以直接创建并分布到不同的分片服务器上。

当一个chunk超过了设置的大小时,Mongo会将尝试其拆分为多个更小的chunk并重新分布到多个分片服务器上。如果一个chunk不能被分割,例如collection内只有一个文档,而该文档的内容超过了设置的大小,这个chunk就会成为被称为jumbo chunk的大型数据块。chunk不仅会在被拆分后重新分布,当一个collection的数据没有均匀分布时,迁移同样会发生。

将分片分布的过程会使用不同的分片策略,其中主要有两类,范围分片和哈希分片。范围分片会根据分片的键的范围创建,每个记录分片数据的chunk都有一个对应的分片键范围(可以从minKey到中间值再到maxKey),这种方式比较适合具有连续性质的键,例如某通过字典序标识的文档。哈希分片则是通过键的哈希值进行分片,不同键的数据可能通过哈希分散到不同的chunk中,这种方式比较适合需要根据哈希函数均匀化某些稠密范围的存储,例如存储某一时刻产生的大量数据。

除了分片策略,分片的过程同样需要键的参与,因而分片使用的键会极大地影响分片的效果。理想情况下,分片的键能够将数据均匀地分布到所有分片服务器上,反之则会导致部分分片服务器负载过高。例如,使用与数据关联较弱的键(如user中的gender)会造成chunk过大的问题,使得拆分和迁移频繁发生,降低分片的性能;又或者,使用导致数据倾斜的键(如user的vipLevel)会导致部分节点的访问显著地频繁于其他节点;另外,使用时间戳时,短期内的负载过高可能导致按范围分片的部分节点出现压力负载。因此,对分片键的选择同样重要,应当仔细设计。此外,分片键还需要支持索引。

分片模式下,MongoDB不提供对分片collection的全局索引结构,只对每一个chunk建立相应的本地索引,这些索引在服务器之间不进行交换。另外,索引只能提供对分片键的单一性保证,而对其他键不提供保证。

# 配置服务器(Config Server)

配置服务器是一个独立的复制集节点,其中只保存管理和分片的元数据。在这个服务器中,通常只维持两个数据库,adminconfig。前者存储与认证和授权等系统内信息相关的数据,后者主要存储chunk的元数据,如其存储的位置和描述符等数据。例如,user collection使用了范围分片,则配置服务器会保存每个分片的minKey和maxKey以及对应的位置。

配置服务器虽然可以向用户提供数据,但用户不应读写其中的collection,而是把这些工作交给Mongo的各项服务。例如,由mongos将发生迁移和分片的chunk的元数据进行修改。配置服务器的读写考量(Read/Write Concern)都使用majority级别,从而在一定程度上保证集群元数据的一致性。在可用性上,当一个配置服务器与整个集群的大多数节点失去联系后,元数据会变为只读形式,即只允许对分片内的数据进行读写,而该服务器不会允许chunk的拆分和迁移,直到新的主节点出现。当所有节点都不可用时,集群就不可用了。

# 分片路由

mongos是处理客户请求的前端服务,通过读取配置服务器上的元数据和解析查询来实现对请求的分片并合并查询的结果。由于该服务不具有持久性状态,mongos相互之间可以独立运行,且没有运行数量的限制。

mongos会根据元数据中的分片键范围限制查询的分片服务器,例如查询是一个等式筛选时,mongos会将查询仅导航到对应的分片服务器上。而在进行全局查询,如不指定过滤范围的findMany()和创建索引时,mongos会将查询广播到所有分片服务器上执行。

# 总结

本章我们简述了MongoDB的集群组织模式,分片和复制,以及相应的一些读写行为。至此,MongoDB的基本概念我们就已经大致覆盖了。下面几章中,我们将关注MongoDB的一些额外功能——面向空间信息的查询、存储和索引。