欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-15 16:03:32
io概念 在数据库优化和存储规划过程中,总会提到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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。