如何解决MongoDB磁盘IO问题

51次阅读
没有评论

共计 3269 个字符,预计需要花费 9 分钟才能阅读完成。

丸趣 TV 小编给大家分享一下如何解决 MongoDB 磁盘 IO 问题,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

IO 概念

在数据库优化和存储规划过程中,总会提到 IO 的一些重要概念,在这里就详细记录一下,对这个概念的熟悉程度也决定了对数据库与存储优化的理解程度,以下这些概念并非权威文档,权威程度肯定就不能说了。

读 / 写 IO,最为常见说法,读 IO,就是发指令,从磁盘读取某段扇区的内容。指令一般是通知磁盘开始扇区位置,然后给出需要从这个初始扇区往后读取的连续扇区个数,同时给出动作是读,还是写。磁盘收到这条指令,就会按照指令的要求,读或者写数据。控制器发出的这种指令+数据,就是一次 IO,读或者写。

大 / 小块 IO,指控制器的指令中给出的连续读取扇区数目的多少,如果数目很大,比如 128,64 等等,就应该算是大块 IO,如果很小,比如 1,4,8 等等,就应该算是小块 IO,大块和小块之间,没有明确的界限。

连续 / 随机 IO,连续和随机,是指本次 IO 给出的初始扇区地址,和上一次 IO 的结束扇区地址,是不是完全连续的,或者相隔不多的,如果是,则本次 IO 应该算是一个连续 IO,如果相差太大,则算一次随机 IO。连续 IO,因为本次初始扇区和上次结束扇区相隔很近,则磁头几乎不用换道或换道时间极短;如果相差太大,则磁头需要很长的换道时间,如果随机 IO 很多,导致磁头不停换道,效率大大降底。

顺序 / 并发 IO,这个的意思是,磁盘控制器每一次对磁盘组发出的指令套(指完成一个事物所需要的指令或者数据),是一条还是多条。如果是一条,则控制器缓存中的 IO 队列,只能一个一个的来,此时是顺序 IO;如果控制器可以同时对磁盘组中的多块磁盘,同时发出指令套,则每次就可以执行多个 IO,此时就是并发 IO 模式。并发 IO 模式提高了效率和速度。

IO 并发几率。单盘,IO 并发几率为 0,因为一块磁盘同时只可以进行一次 IO。对于 raid0,2 块盘情况下,条带深度比较大的时候(条带太小不能并发 IO,下面会讲到),并发 2 个 IO 的几率为 1 /2。其他情况请自行运算。

IOPS。一个 IO 所用的时间=寻道时间+数据传输时间。IOPS=IO 并发系数 /(寻道时间+数据传输时间),由于寻道时间相对传输时间,大几个数量级,所以影响 IOPS 的关键因素,就是降底寻道时间,而在连续 IO 的情况下,寻道时间很短,仅在换磁道时候需要寻道。在这个前提下,传输时间越少,IOPS 就越高。

每秒 IO 吞吐量。显然,每秒 IO 吞吐量=IOPS 乘以平均 IO SIZE。Io size 越大,IOPS 越高,每秒 IO 吞吐量就越高。设磁头每秒读写数据速度为 V,V 为定值。则 IOPS=IO 并发系数 /(寻道时间+IO SIZE/V),代入,得每秒 IO 吞吐量=IO 并发系数乘 IO SIZE 乘 V /(V 乘寻道时间+IO SIZE)。我们可以看出影响每秒 IO 吞吐量的最大因素,就是 IO SIZE 和寻道时间,IO SIZE 越大,寻道时间越小,吞吐量越高。相比能显著影响 IOPS 的因素,只有一个,就是寻道时间。

MongoDB 磁盘 IO 问题的 3 种解决方法

1. 使用组合式的大文档

我们知道 MongoDB 是一个文档数据库,其每一条记录都是一个 JSON 格式的文档。比如像下面的例子,每一天会生成一条这样的统计数据:

{metric: content_count, client: 5, value: 51, date: ISODate(2012-04-01 13:00) }

{metric: content_count, client: 5, value: 49, date: ISODate(2012-04-02 13:00) }

而如果采用组合式大文档的话,就可以这样将一个月的数据全部存到一条记录里:

{metric: content_count, client: 5, month: 2012-04, 1: 51, 2: 49, …}

通过上面两种方式存储,预先一共存储大约 7GB 的数据 (机器只有 1.7GB 的内存),测试读取一年信息,这二者的读性能差别很明显:

第一种: 1.6 秒

第二种: 0.3 秒

那么问题在哪里呢?

实际上原因是组合式的存储在读取数据的时候,可以读取更少的文档数量。而读取文档如果不能完全在内存中的话,其代价主要是被花在磁盘 seek 上,第一种存储方式在获取一年数据时,需要读取的文档数更多,所以磁盘 seek 的数量也越多。所以更慢。

实际上 MongoDB 的知名使用者 foursquare 就大量采用这种方式来提升读性能。

2. 采用特殊的索引结构

我们知道,MongoDB 和传统数据库一样,都是采用 B 树作为索引的数据结构。对于树形的索引来说,保存热数据使用到的索引在存储上越集中,索引浪费掉的内存也越小。所以我们对比下面两种索引结构:

db.metrics.ensureIndex({ metric: 1, client: 1, date: 1})  与  db.metrics.ensureIndex({ date: 1, metric: 1, client: 1 })

采用这两种不同的结构,在插入性能上的差别也很明显。

当采用第一种结构时,数据量在 2 千万以下时,能够基本保持 10k/s 的插入速度,而当数据量再增大,其插入速度就会慢慢降低到 2.5k/s,当数据量再增大时,其性能可能会更低。

而采用第二种结构时,插入速度能够基本稳定在 10k/s。

其原因是第二种结构将 date 字段放在了索引的第一位,这样在构建索引时,新数据更新索引时,不是在中间去更新的,只是在索引的尾巴处进行修改。那些插入时间过早的索引在后续的插入操作中几乎不需要进行修改。而第一种情况下,由于 date 字段不在最前面,所以其索引更新经常是发生在树结构的中间,导致索引结构会经常进行大规模的变化。

3. 预留空间

与第 1 点相同,这一点同样是考虑到传统机械硬盘的主要操作时间是花在磁盘 seek 操作上。

比如还是拿第 1 点中的例子来说,我们在插入数据的时候,预先将这一年的数据需要的空间都一次性插入。这能保证我们这一年 12 个月的数据是在一条记录中,是顺序存储在磁盘上的,那么在读取的时候,我们可能只需要一次对磁盘的顺序读操作就能够读到一年的数据,相比前面的 12 次读取来说,磁盘 seek 也只有一次。

db.metrics.insert([{ metric: content_count, client: 3, date: 2012-01, 0: 0, 1: 0, 2: 0, ... }
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:
 { .................................., date:])

结果:

如果不采用预留空间的方式,读取一年的记录需要 62ms

如果采用预留空间的方式,读取一年的记录只需要 6.6ms

以上是“如何解决 MongoDB 磁盘 IO 问题”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注丸趣 TV 行业资讯频道!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-04发表,共计3269字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)