总述
在 MongoDB Version 3.3.4 之前,用 findAndModify()
加上 sort
排序,并发地批量更新一批数据时(每次只更新一条数据),会出现返回 not found
的错误。
详情阐述
事件起因
客户通过其他模块负责人报来 bug 说,用自动化营销功能去给用户发券时,出现大量失败的情况,报错 Coupon code not found
。
背景解释
我们的自动化营销功能可以将数据量巨大的一批客户加入到营销进程流中,然后用 go routine 对进入流程中的每个环节进行处理。比如发优惠券、发短信、发微信等等。
这里是去发优惠券。在这个 db 操作中是找一批 status: unissued
的数据,将它 set 成 status: unused
,以代表此券码被用了,findAndModify()
会返回找到的 document,获取这个的券码信息与客户对应储存起来。
这里用 sort := []string{"+_id"}
来保证发券码是有顺序的,先创建的码先发出去。
func (self *CouponCode) FindAndIssueOne(ctx context.Context, couponId bson.ObjectId) error {
query := bson.M{
"couponId": couponId,
"status": COUPON_CODE_STATUS_UNISSUED,
}
sort := []string{"+_id"}
change := mgo.Change{
Update: bson.M{"$set": bson.M{"status": COUPON_CODE_STATUS_UNUSED}},
Upsert: false,
}
_, err := Common.FindAndApply(ctx, query, sort, change, C_COUPON_CODE, self)
return err
}
调查
经调查,报的错只可能在一个方法 findAndApply()
处返回。这个操作底层就是用的 mongo 的 findAndModify()
函数,它是个原子操作。所以我们首先怀疑的是 couponCode 真的不够用了,但在 staging 环境上真实测试后就排除了此原因。
令人疑惑的是,我在本地 db 中批量创建 40w 个 couponCode,写了一段代码用 30 个 goroutine 分别调用 findAndModify()
去更新一定数量的码,并没有重现此问题。
线上与本地的不同,我想到两点:
- 一是索引,我本地没有索引,线上有。所以我怀疑,可能是线上的索引不能精确命中导致的。
- 二是分布式数据库。但这个操作是 update 操作,所以应该是在主库上进行的,从库上不可能更新成功。这个原因随即否定了。
所以我在线上创建了一个认为更精确索引,直接在 staging 上跑了一个自动化营销,结果令我失望。所以这个问题我没有思路了,就在 issue 中汇报给大 Leader 转头去做其他事情了。
突来的灵感
当天晚上在家里忽然想到,我在本地与线上索引差异的问题上走错了方向。本地没索引应该创建个索引去试,而不是在 staging 环境上更精化索引。
于是第二天来到办公室,就在本地测试了这个想法:本地环境加上索引,然后依然用之前的代码并发测试。终于,在本地重现了这个问题。
新方向
与此同时,大 leader 回复了昨天的问题,说没看懂我写了一大堆的描述。只说看看报出来的原始错误是什么。
把这个发现和 devLead 汇报了后,devLead 也同样指出先看一下它 mongo 的原始错误是什么,我在 go 库中打了一些日志,发现其返回的是 null
,就是 not found
的错了。由此我们再进一步地来看, mongo 命令行中有此问题会是什么错误。
验证思路:在 mongo 命令行中写一个脚本去消费券码,但这只是一个进程的操作,想模拟并发的话,在此同时让服务去并发地去消费。
这个方法验证了此问题,mongodb 在做 findAndModify 操作时,规定如果成功的话就应该返回查到的 document 信息,但 mongo shell 中也返回了 null,即证明了此行为是 mongodb 的行为,与 go 库无关。
但到这里也是卡住了,这是 mongodb 的问题,我的思维受到了限制,觉得不是自己的问题就无法再进行了。于是,将这个问题的调查状态反馈给大 Leader 结束了当天的调查。
还能再进一步
次日,大 Leader 回复了,简单一句:“可能是 bug,翻一下官方的 issue list”。
翻 issue list 不得其路,我找了好久,最终找到了它官网的 issue list,输入了几次,终于命中了关键字 findAndModify concurrency
查到了曾有人报过的 bug。找到了以下信息:
issue list 中有报过此 bug : https://jira.mongodb.org/browse/SERVER-31288
在 mongo v3.3.4 中修复了: https://jira.mongodb.org/browse/SERVER-22178
经过对 issue 的仔细阅读,看到了这么一句话:
This is undesirable during a sorted findAndModify, because if we are doing an in-memory sort, it will have a limit of 1, so skipping will effectively mean returning null (even if there may have been other matches).
这句话中解释了其出现的原因,它提到了 a sorted findAndModify
,所以来了灵感:既然这里说用了 sorted
的操作会有问题,我们这里也是正好用了 sort
,那是不是去掉它就可以成功了呢?
难结终开
本地就可以试,我传入一个字段来控制其是否填入 sort
字段,发现确实在不使用 sort
时便不再有问题了。并且性能高出了很多,数据量很大时也没有一个慢查询(之前上点数据量就有了很高的慢查询),消费券码也全部成功。
再次汇报了之后, devLeader 让我在 staging 上去掉排序跑一下自动化营销大量发券功能。经测试,确实无一失败,无一慢查询,经 sre 反馈,数据库 cpu 也没有很大的压力。
案结收获
OK,此案终于了结了。
回顾一下,感觉到问题根源的关键点不是那么难,但自己当时就觉得已经走到了尽头,只能求助于人了。
我的思维有以下问题:
一是总有不落于实地的瞎猜。比如猜测是索引不够精确导致的,做了尝试之后才否定了这个猜测,之后才闪念想到正路应该要想是“有索引”造成的。还有验证了索引后,又开始猜是因为索引更新不及时,导致两个并发操作同时查到了,但更新先后不一造成了这个问题。这些猜测都不落地,因此如果方向错了的话,一股脑地去网上找证据证明自己的猜想可能永远都找不到,最终走进死胡同。
二是自己认为已经没有门路了。一方面是超出了自己所做的业务逻辑,比如如果确定是 mongodb 的 bug 的话,很可能就此放手了,这里是幸而仔细看了人家对此问题的解释才找到了突破点。另一方面是自己还是经验少,不知道下一个该做的方向是什么,想不到应该去正规的 mongo 官网上的 issue list 中去调查,而不是在网上查关键字。
谨以此为文相勉。
附:
网友评论