[mongodb]tcmalloc: large alloc out of memory, printing stack and exiting

最近Mongodb会经常突然挂掉,检查日志发现如下的错误:

tcmalloc: large alloc 2061584302080 bytes == (nil) @
Tue Nov 26 17:45:04.539 out of memory, printing stack and exiting:
0xdddd81 0x6cfb4e 0x121021d 0xafcc1f 0xaf815f 0xaf8d1d 0xaf8e0f 0xaf52ae 0xaf53c9 0xb1eb11 0x8ab6a2 0x8d78ca 0x8d951d 0x8daa72 0xa80970 0xa8523c 0x9f9079 0x9fa5a3 0x6e8b88 0xdca34e
 ./mongod(_ZN5mongo15printStackTraceERSo+0x21) [0xdddd81]
 ./mongod(_ZN5mongo14my_new_handlerEv+0x3e) [0x6cfb4e]
 ./mongod(_Znwm+0x6d) [0x121021d]
 ./mongod(_ZNSt6vectorIN5mongo18DocumentSourceSort9KeyAndDocESaIS2_EE7reserveEm+0x6f) [0xafcc1f]
 ./mongod(_ZN5mongo18DocumentSourceSort12populateTopKEv+0x6f) [0xaf815f]
 ./mongod(_ZN5mongo18DocumentSourceSort8populateEv+0x2d) [0xaf8d1d]
 ./mongod(_ZN5mongo18DocumentSourceSort3eofEv+0xf) [0xaf8e0f]
 ./mongod(_ZN5mongo18DocumentSourceSkip7skipperEv+0x6e) [0xaf52ae]
 ./mongod(_ZN5mongo18DocumentSourceSkip3eofEv+0x9) [0xaf53c9]
 ./mongod(_ZN5mongo8Pipeline3runERNS_14BSONObjBuilderERSs+0x1b1) [0xb1eb11]
 ./mongod(_ZN5mongo15PipelineCommand3runERKSsRNS_7BSONObjEiRSsRNS_14BSONObjBuilderEb+0x132) [0x8ab6a2]
 ./mongod(_ZN5mongo12_execCommandEPNS_7CommandERKSsRNS_7BSONObjEiRSsRNS_14BSONObjBuilderEb+0x3a) [0x8d78ca]
 ./mongod(_ZN5mongo7Command11execCommandEPS0_RNS_6ClientEiPKcRNS_7BSONObjERNS_14BSONObjBuilderEb+0x71d) [0x8d951d]
 ./mongod(_ZN5mongo12_runCommandsEPKcRNS_7BSONObjERNS_11_BufBuilderINS_16TrivialAllocatorEEERNS_14BSONObjBuilderEbi+0x5f2) [0x8daa72]
 ./mongod(_ZN5mongo11runCommandsEPKcRNS_7BSONObjERNS_5CurOpERNS_11_BufBuilderINS_16TrivialAllocatorEEERNS_14BSONObjBuilderEbi+0x40) [0xa80970]
 ./mongod(_ZN5mongo8runQueryERNS_7MessageERNS_12QueryMessageERNS_5CurOpES1_+0xd7c) [0xa8523c]
 ./mongod() [0x9f9079]
 ./mongod(_ZN5mongo16assembleResponseERNS_7MessageERNS_10DbResponseERKNS_11HostAndPortE+0x383) [0x9fa5a3]
 ./mongod(_ZN5mongo16MyMessageHandler7processERNS_7MessageEPNS_21AbstractMessagingPortEPNS_9LastErrorE+0x98) [0x6e8b88]
 ./mongod(_ZN5mongo17PortMessageServer17handleIncomingMsgEPv+0x42e) [0xdca34e]

内存溢出了,一开始我以为是有些排序没有加索引,后来一些地方加了索引后还是会出现,细想了下,如果是没加索引的话,是不会让整个Mongodb宕机的。

后来在Mongodb的issue上查到了这样一条提交的bug清单SERVER-10136

原来aggregation如果传递一个$skip特别大的值的时候,就会内存溢出。我看到这个bug已经被修复了,不过是在2.5.2版本,最新的稳定版是2.4.8。所以我们需要在自己的应用程序里面控制,让$skip的值不要超出总长。

再看mongodb的分组group

在之前的记一次Mongodb性能调优文章中,最后使用了aggregation来做聚合查询,今天又有一个新的需求,需要在之前按日期聚合的基础上再按照店铺来聚合(说实话,到这里已经感觉到非关系数据库这方面的弱势了)。

幸运的是aggregation的操作其实是类似于管道(pipeline),可以进行多次group,这样就方便多次处理数据,得到想要的结果。

def diary_with_shop(opts = {})
  page       = opts[:page] || 1
  per_page   = opts[:per_page] || 20
  conditions = [{'$match' => {user_id: self.id, deleted_at: nil}}]
  conditions << {'$project' => {
    'year'  => {'$year'      => '$created_at'},
    'month' => {'$month'     => '$created_at'},
    'day'   => {'$dayOfYear' => '$created_at'},
    'date'  => '$created_at', 'shop_id' => 1
  }}
  conditions << {'$group' => {
    '_id' => {
      'year' => '$year', 'month' => '$month', 'day' => '$day', 'shop_id' => '$shop_id'
    },
      'date'  => { '$min' => '$date' },
      'food_stories' => { '$push' => '$_id' }
    }}
  conditions << {'$group' => {
    '_id'   => {
      'year' => '$_id.year', 'month' => '$_id.month', 'day' => '$_id.day'
     },
    'date'  => { '$min' => '$date' },
    'shops' => { '$push' => { 'shop_id' => '$_id.shop_id', 'food_stories' => '$food_stories' } }
  }}
  conditions << {'$sort'  => {'date' => -1}}
  conditions << {'$skip'  => (page.to_i - 1)*per_page.to_i}
  conditions << {'$limit' => per_page.to_i}
  Model.collection.aggregate(conditions)
end

如果对比上次的代码,可以发现这里多了一个group操作,第一个group的`_id`带上了shop_id,带上的shop_id在第二个group中又进行了分组,最后得到想要的结果。

在这里,shop只是id,如果想返回shop的名字,可以用类似于关系型数据库的join操作吗?

答案是不行,文档型数据库只能操作一个collection。那如果想得到shop_name,难道需要再次查询吗?尽量不要这样,这样会丧失aggregation的性能,所以如果你有字段在collection中没有,那么—

最好的办法就是直接复制加上去,不用考虑数据库的冗余,这样才能发挥文档型数据库的优势。

其实每个数据库都不可能取代另一个数据库,只有把数据库自有的特点发挥出来,才能达到性能的最大化,所以如果mongodb里面有很多的外键关联,那么可以考虑是否数据库的设计偏离了mongodb的本质。

记一次Mongodb性能调优

1)关于索引

优化首先看查询是否有索引。

保证每次查询都可以走索引,可以用explain()查看,如果得到的cursor类型是`BasicCursor`,那么你要加索引了。

当一个字段属于多个索引的时候可以用`hint`来强制使用某一个索引。

谨慎使用`$in`和`$or`。

For queries that use the sort() method and use the $or operator, the query cannot use the indexes on the $or fields.

2)关于正则查找

如果Mongodb的正则查找使用 /^/ 匹配,索引可以生效,否则索引不生效,当数据量大的时候,性能非常糟糕。所以对于形如 /^/,/^a.*/,/^a.*$/,建索引吧。

对于非 /^/ 正则匹配,考虑使用全文检索吧,MongoDB 2.4 新引入了 Text Search

不过可惜的是,不支持中文,支持的语言可以查看这里,text-search-languages

所以在优化的过程中,我选择了solr来替代,solr最新的支持地理位置检索,可以很好的和MongoDB配合使用。

3)关于map/reduce

很麻烦,尽量不要使用。

4)关于aggregation

MongoDB2.2引入,基本可以满足大部分的聚合操作了,除了写起来很别扭。

Aggregation Framework Operators

今天用这个解决了一个问题,聚合还是放在数据库级别性能才足够优秀:

    def diary(opts = {})
      page       = opts[:page] || 1
      per_page   = opts[:per_page] || 20
      conditions = [{'$match' => {user_id: self.id, deleted_at: nil}}]
      conditions << {'$project' => {
        'year'  => {'$year'      => '$created_at'},
        'month' => {'$month'     => '$created_at'},
        'day'   => {'$dayOfYear' => '$created_at'},
        'date'  => '$created_at'
      }}
      conditions << {'$group' => {
        '_id' => {
          'year' => '$year', 'month' => '$month','day' => '$day'
        },
        'date' => { '$min' => '$date' },
        'food_stories' => { '$push' => '$_id' }
      }}
      conditions << {'$sort' => {'date' => -1}}
      conditions << {'$limit' => per_page}
      conditions << {'$skip'  => (page - 1)*per_page}
      Model.collection.aggregate(conditions)
    end

注意:如果你出现了,排序不正确,分页不正确,记得 $sort$limit$skip顺序有关,这几个操作要放在最后