作为一名软件开发者,我们非常看重那些抽象化的东西。API越简单,对我们越有吸引力。辩证地讲,MongoDB最大的优势就是“优雅”的API和它的敏捷性,这让开发者的编码过程变得异常的简单。
但是,当MongoDB涉及到大数据可扩展性问题时,开发者还是需要了解一下它的底层,弄明白那些潜在的问题,然后才能快速地进行解决。如果不理解,最终可能会选择一个低效的解决方案,而且浪费了时间和金钱。本文重点介绍了,如何为大数据的扩展性问题找个一个高效的解决方案。
定义问题
首先,我们要确定应用的上下文,本文主要讨论的是MongoDB的应用程序。这意味着,我们将研究一个分布式文档存储数据库,而且它还支持二级索引和分片集群。如果是针对其他的NoSQL产品,像Riak或者Cassandra,我们可能会讨论I/O瓶颈问题,而本文主要关注MongoDB的一些特性。
其次,这些应用能够做什么?是做联机事务处理( OLTP)还是做联机分析处理( OLAP)?本文主要讨论的是OLTP,因为对MongoDB而言,OLAP还是一个不小的挑战,或者说基本不能够进行处理。
第三,大数据是什么?通过大数据,我们能够处理和使用更多的数据,不再局限于单机RAM中的那些部分。这样的话,有些数据保留在服务器上,而更多的数据则是存放在磁盘中,这就需要I/O的访问。但是请注意,我们不是在讨论数据库够不够大,而是关注那些经常被存取和使用的数据(有时称之为“工作集”)是不是很小。比如说,磁盘上虽然存储了好几年的数据,但是应用可能经常访问的只有最后一天的数据。
第四,OLTP应用的限制性因素有哪些?简而言之,就是I/O。硬盘驱动每秒钟只可以启动上百次的I/O,而另一方面,RAM每秒可以实现数百万次的存取,这个限制性因素就是导致大数据应用I/O瓶颈的原因所在。
最后,我们应该如何解决I/O瓶颈?通过分析思维,公式和直接指令给我们提供了很多种方式,但是一个持久性的解决方案就需要“理解”。用户必须着眼于应用程序的I/O特性,然后才能做出最好的设计决策。
开销模型
未来解决I/O瓶颈,第一步需要掌握哪些数据库操作会包括I/O。 无论MongoDB,还是其他的数据库类型,都有三种基本的操作:
Point Query:查找一个独立的文件。在一个给定的位置的文件夹(磁盘或者内存上),检索该文档。对于大数据来说,该文件可能不在内存中。此操作可能会导致一次I/O。
Range Query:在索引中,查找大量的连续性文件,对比Point Query而言,它是一个更高效的查询操作。这是因为我们查找的这些数据都是打包存放在磁盘上,可以通过极少的I/O操作来直接读入内存。Range Query一般检索100个文件才会启动一次I/O,相对比,100个Point Query检索100个文件可能就需要100次I/O操作。
Write:写文件到数据库中。类似MongoDB这样的数据库,都会产生I/O。而对那些“写优化”数据结构的数据库而言,比如 TokuMX,仅仅需要很少的I/O。不像MongDB,“写优化”的数据结构能够通过执行多次插入来分摊I/O。
在了解三个基本操作对I/O的影响之后,还需要理解MongoDB数据库语句对I/O的影响。MongoDB包含了这三个基本操作,同时还构建了四个用户级别的操作:
插入:将一个新文件写到数据库中。
查询:在集合上使用索引,这样做一个Range Queries和Point Query的整合。如果该索引是一个覆盖索引或者是集群索引,那么接下来基本上只需要做范围查询。否则的话,整合的范围查询和点查询就会被启用。
修改和删除:这是一个查询和写操作的整合。查询操作用于发现那些需要更新和删除的文件,然后写操作再对这些文件进行修改或者是删除。
现在,我们理解了开销模型。不过为了解决I/O的瓶颈问题,用户还需要知道哪些应用启动了I/O操作。这就需要我们了解数据库的行为。I/O启动是源于查询操作吗?如果是这样的话,查询行为是如何影响I/O的?还是源于修改操作?如果是因为修改导致的影响,那么是因为修改过程中的查询操作还是插入操作?一旦用户掌握了哪些因素会影响 I/O,接下来就可以逐步来解决瓶颈的问题了。
假设我们明白了某个应用的I/O特性,我们就可以探讨几种途径来解决这一问题。我最喜欢的方式是这样的:首先尝试使用软件来解决该问题,如果不能完美的解决,那么再考虑硬件。毕竟软件的成本更低且易于维护。
可行的软件解决方案
一个可行的软件解决方案,就是需要减少软件或者应用的I/O启动次数。针对不同的瓶颈,下面列举了对应可行的解决方案:
问题:插入操作导致过多的I/O
可行方案:使用一个写优化的数据库,比如说TokuMX,使用TokuMX的一项优势就是它能够大幅度减少对写操作的I/O请求,而索引方式使用的是Fractal Trees( TokuDB实际上还对Fractal Trees做了改进,将插入的IO数量级从log(N)提高到了logB(N))
问题:查询操作导致过多的I/O
可行方案:使用一个更好的索引,减少点查询,尽量使用范围查询替代。在我看来,还是需要“理解索引”。我解释了索引能够减少查询操作的I/O请求。当然这不是用一两段话就可以说得清的,但是要点如下:减少应用的I/O请求首先要避免独立的点查询来检索每个文件夹,为了实现这点,我们要使用覆盖或者集群索引,这样可以智能地过滤掉在查询过程中已经分析过的文件,然后再使用范围查询报告结果。
诚然,一个好的索引方式还不足够,如果是一个OLTP应用,那么所有的查询从本质上讲都是点查询(因为他们只是检索极少的文件),即使使用一个完全适合的索引,用户还是会出现I/O的瓶颈问题。在这种情况下,硬件的解决方案就是必要的。
当然,增加索引也意味着增加插入的成本,因为每个插入操作都要保证索引的及时更新,但是写优化的数据库可以减少一定的成本。这也是我一直强调我们需要理解应用的原因所在,对于某些应用来说,一个可行的解决方案,并不代表也适用于其他的应用。
问题:修改或者删除导致过多的I/O
解决方案:整合以上所有的解决方案
修改和删除之所以复杂是因为这是查询和插入的一个整合。提升它们的性能要求对操作开销有一个很好的理解。哪部分操作会涉及到I/O?是查询?如果这样,我们就提升索引。是写操作?还是兼而有之?简单来讲,就是哪部分操作牵涉到I/O问题,我们就应用对应的解决方案。
一个很常见的错误就是:当我们使用一个写优化的数据库(像TokuMX),在不改变任何索引的情况下,就希望它能够消除修改或者删除的I/O瓶颈问题。毕竟,写优化的数据库还是不足够的,在修改/删除过程中的隐含的查询也必须进行处理。
可行的硬件解决方案
就像上文提到的那样,当软件解决方案不能够解决问题的时候,我们就需要寻求硬件的解决方案。我们先分析一下硬件的优势和劣势:
购买尽可能多的内存,即使难以做到,也要把工作集放到内存中
使用SSD来增加IOPS
购买更多的服务器,并使用一个可扩展的解决方案,其中包括
通过副本进行读扩展
分片
购买更多的内存
RAM是很昂贵的,而且在一台机器上可扩展的内存也是有限的。如果数据太大,都保存在RAM中也不是一个可行的选择。这种解决方案可能适于用大多数的应用,但我们更关注那些这种方案解决不了的应用。
使用SSD存储
不可否认,如果存储设备使用SSD提升吞吐量,这绝对是一个很实用的解决方案。如果I/O成为应用的限制性因素,那么增加IOPS(每秒的I/O操作)理所当然地能够增加吞吐量,简单且使用。
然而,SSD的成本和硬盘的成本也不一样,SSD绝对可以显著地提升I/O的吞吐量,虽然成本也不便宜,但是更轻、容量更大,速率也更快。那么为了降低成本,数据压缩就是一个关键。 其实,虽然硬件成本增加了,但这并不意味着管理成本也会增加:
通过副本进行读扩展
当查询操作成为应用的瓶颈,那么通过副本进行读扩展就是一个非常有效的解决方案。想法如下:
使用备份功能,将多个数据拷贝到相互独立的机器上
分布地在不同的机器上进行读取,这样将会提升读的吞吐量
如果在单一的机器上进行读操作而且传输出来,这样会出现很大的瓶颈。如果有多个副本的话,应用将有更多的资源可用,因此读写方面都将有很大的提升。
如果是插入、修改或者是删除过程存在瓶颈,那么副本可能就不会那么的高效。这是因为写操作需要将数据备份到所有的服务器的副本集上,机器在数据的输入过程中也面临同样的瓶颈问题。
分片
基于shard key,分片片将数据切分到不同的副本集,集群中不同副本集负责相应范围的值。所以,可以通过将写操作分配给集群中不同的副本集来提升应用程序的写吞吐量。对于写负载高的应用,分片可以非常有效的。
在shard key空间中将数据按范围划分,使用shard key横跨多个副本做范围查询可以非常有效。如果将shard key哈希,那么所有范围查询必须运行在集群中的所有分片,但是在shard key上的点查询只运行在一个分片上。shard key哈希,据结构模型并且不支持join,分片用起来非常的高效且简单。如果上述的方案都不足以解决应用的瓶颈问,那么你可以在分片上多下工夫了。
无论如何,分片都是一个重量级的解决方案,而且成本非常之高。对初入者来说,你的硬件投入预算需要增加好几倍。不仅仅需要为分片设置增加服务器,还需要增加一整套副本集。你还需要增加和管理配置服务器,考虑到成本问题,用户需要慎重考虑分片是不是很必要。通常来讲,上面所有方案都比这个节省成本。
另外一个很大的挑战就是选择一个shard key,一个好的shard key有以下特点:
大多数(如果不是全部)的查询都使用shard key,任何查询(没有使用shard key)必须运行在所有的分片上。这点可能比较麻烦。
shard key应该非常有利于集群不同副本及上的分布写操作,如果所有的写操作都对应到集群上同一个副本集,那么该副本集对于写操作来讲,就会成为一个瓶颈,就好像处在一个非分片的设置中,这样的话,这种情况糟糕到就像使用时间戳作为shard key。
这些要求并不是很容易实现。有时,一个好的shard key并不存在,这让分片变得极不高效。
总结
很多解决方案都行得通,但是没有一个能百分百得到保障的,甚至包括分片。这也是作者强调的:理解应用的特性是至关重要的。工具可以解决问题,但是如何更好地使用工具,这就取决于用户了。