Problems with Mongodb deployment

· Read in about 8 min · (3543 Words)
mongodb Tech

背景

大约一个月前,我们的开发向我们提出他们一直在用的mongodb集群插入速度极其缓慢,希望我们能够登录到机器查看一下机器的状态信息. 我上去mongodb集群(这个原先不是我维护的)一看,发现他们创建的索引过多,索引的大小基本把整个集群所拥有的内存占完了.当然,同时 集群机器的负载,磁盘io一切正常.对于这样的现象,我首先要求他们将多余的索引删除,比如说是用于专门删除数据的ttl索引.对于这类索引 在数据量比较大的情况下,其实占用的内存量还是比较大的.但删除部分索引只能解放一部分索引,在综合考虑业务逻辑后,我向他们提出按时间对数据库进行分库. 为什么不进行分表呢?主要有以下几个原因

  1. mongodb删除collection并不会释放硬盘空间,mongodb认为数据库申请硬盘空间是个比较费时的操作.
  2. mongodb的写锁是数据库级别的,也即是对某个collection的写操作会block掉对其他collection的操作,这个是不太能容忍的事情.
  3. 我们的拆分的依据是时间,这样分库的话,对删除数据非常有好处,直接将超过一定时间的数据库drop掉就ok了. 补充: 伴随着上述插入缓慢的一个现象是mongod和一个shard机器上有非常大的网络流速,这个其实是后续根源问题的一个表象,但当时 太年轻,没能据此抓住问题的核心.

第一个问题

确定好了分库的方案后,数据的导出和导入的工作是交由我来完成.由于接手的另一个mongodb集群的经验:在使用hash字段做shard键的时候,balancer 还是不能跑匀新增数据的不平衡.上面的经验让我做出了关闭balancer的决定.这直接导致了

  1. 导入速度的缓慢,要导入数据的集群没有使用hash字段来做分区键,新建立的数据库默认只在primary上,导入数据的时候所有写只在一个磁盘上
  2. 由于balancer关闭,数据保持不动,也即本来应该是均匀分散到各个shard的数据只呆在了一个shard上,这个完全违背了做sharding集群的初衷

解决

最初的解决方案,便是将balancer打开,慢慢等待balancer将数据均匀地分散到各个shard上,但由于mongodb本身对balancer做了限制,每次只能移动 一个chunk,加之数据量比较大,有较多的chunk,在默默等待一天无太大进展后,直接采取了重导数据的方案.具体步骤如下:

  1. 停掉balancer
  2. 设置shard collection的分区键
  3. 根据分区键的范围,将默认的一个chunk比较均匀地划分和移动到各个shard上.
  4. 开始导数据
  5. 重新开启balancer,等待之前导入的可能不平衡的数据均衡.

其实这个方案10gen官方在faq已经有提到过,可惜只是匆匆扫过的表示强调的不够.具体见http://docs.mongodb.org/manual/faq/sharding/###can-i-change-the-shard-key-after-sharding-a-collection

第二个问题

在完成数据库的分库后,我们预想中的速度提升并没有出现.在导入数据进行到一定时间后,导入数据的速度极度缓慢,具体的表现是mongos和某个shard上的mongod 有巨大的网络流量,同时该mongod的cpu占用达到了100%,但磁盘io基本为0,cpu是在计算而不是在等待io.在mongod上运行db.currentOp()命令发现有多条ns记录为空的 记录.

问题分析

网络问题的分析

有大量的网络流量,必然是数据在传输,但是我们的mongos传输出去的网络数据和接受回来的网络数据相差了一个数量级,而且这只在一个mongod和mongos发生.对于网络传输的问题, 我们的第一判断便是client在做文档更新的时候也把文档返回回来了.经过和开发沟通后,发现开发使用的是findAndModify,他在返回field上设置了None,认为这个是不返回数据.但实际上 JAVA的mongodb client api默认就是None,也即返回更新前的数据.

网络问题的解决

确定了findAndModify的问题后,我们的开发在仔细比较了一下findAndModify和update后,使用了update来代替findAndModify.这样解决了在更新的时候出现的网络流量暴涨的问题. 但实际上,这样的解决问题的方式,掩盖了为什么只有一个mongod和mongos会出现网络暴涨而其他mongod正常的问题.当时我们对于这个问题的解释是可能有太多的写操作同时打到了 那个出问题的shard上了,解决方法便是将前端导入程序的多线程数限制在一定shard总数的一定倍数上,这样既可以发挥多线程的高效,也不至于让后端mongod被插入请求打死. 实际上,这样的解决方案是错误的,因为问题的根源不是有太多的读写操作,具体的就引出了下文的问题.

第三个问题

修改了findAndModify和限制线程数后,我们觉得应该解决问题了,结果,第二个问题除了网络流量问题之外的问题又出现了.数据导入到一定时间后,导入速度缓慢的问题再一次出现,和 上面第二个问题表现一致!!其实这个还是第二个问题,我们还没解决. What the fuck!

插入缓慢问题分析

在经过几天毫无头绪,以头撞墙的绝望后,我们终于开始注意到刚开始导入数据是正常的,但导入数据到晚上的时间后就开始变得极度缓慢,直接从8k+records/s到200+records/s,同时ns 为空的记录里看到了多条指向同一个文档的记录.该文档的记录是某个特定用户的文档,我们抱着试一试的态度去看了一下那个用户的记录.砰!如瀑布般的好几百屏的数据奔腾而出!这对 我们简直是当头棒喝!原来如此,是该用户产生了巨量的数据导致了整个导入速度的缓慢.

数据存储说明

这里有必要提一下啊我们是如何在mongodb存储数据的.简单来说,我们有出现问题的数据是用户的登录数据.考虑到数据量的问题,我们将用户一个小时之内的数据进行了聚合,也就是将该小时 的所有登录数据都放到了数组里面.也即是将一个小时内的所有数据都放在了一个文档上面. 示例的数据是下面这个样子的.

{k1 : v1, k2 : v2, k3 : v3, lt : [t1,t2,t3,t4,...], ip : [ip1, ip2, ip3, ip4, ...]}

分析继续

回到我们之前看到的那个特殊用户,对于一般用户而言,一个小时的登录次数是20-30次,而这个特殊用户在晚上7,8,9这几个小时的登录次数是每小时7w+,最高的一个小时的登录次数超过了10W次. 那为什么这样的数据会导致问题,登录10w次就10w好了.按照我们之前1s中8K+,每个shard有1k+r/s的速度,一个小时能插入的记录为1k * 3600 = 360w条.问题的根源就在于我们使用了数组. 如果说我们的特殊用户一小时内登录了10w,对于那个小时的记录文档而言,我们需要对那个数组进行10w次的数组更新操作,但这个10w次的操作不是常数级别,而是随着数组的长度的增长而线性增长的, 那对于完成所有更新操作的时间复杂性便是O(n^2)的时间复杂度.除了更新操作,由于我们不断地增加数组的长度,导致了mongodb要不断地移动document以适应数据大小的增加,这个比之前的操作更加 灾难,频繁的移动数据导致内存被强制刷新到硬盘上.

问题解决

确定了问题的根源后,我们提出了下面的几个解决方案

  1. 不使用数组,而使用nested tree的方案来存储方案.具体见
  2. 对数据进行抽样,前端采集数据的时候1s内只采集一次登录信息.这个方案对于一小时内只有几十次,但某个1s内有特定的几次登录的用户会莫名中枪
  3. 固定数组长度,比如说数组长度最多只有3600.这样会丢失一部分数据.
  4. 按更细粒度的时间来划分,比如说每5分钟聚合数据.但这个方案不仅要大改现有方案,还大大增加了存储空间.
  5. 继续使用数组,但限定数组长度为3600.如果记录上超过了3600,则新增一条记录.对于这个方案,最开始是由开发来决定是否新增记录.不过这个对于编程处理上还是有一定难度的,最后我提出了在现有 的文档上添加n字段,记录目前数组长度的大小,插入的程序改用findAndModify来只返回更新前n的取值.这样对新增记录的判断就变得很简单了.同时这样新增字段的方案对于程序上的改动基本是最少的.

最终使用了第5个方案,应用后插入速度不在出现因为这样的特殊用户而发生波动.

事情总结

  1. 开发发现插入缓慢,当时是index不能放进索引,同时也有上面提到的巨型数组的问题.这样就可以解释网路流量暴涨的问题:每次插入这个大型速度都会返回.
  2. 分库后,内存占用问题解决.但插入速度仍然缓慢
  3. 经过一个星期的纠结,最终定位问题,提出解决方案.速度不再是问题.

mongodb总结

mongodb的坑还是有点多的.array的实现上应该还是有缺陷的.在能不使用mongodb的情况下还是别使用mongodb了.最近貌似postgresel貌似出来hstore,据说可以替代mongodb了.

Comments