发现mgo的一个坑,以及解决过程

碰到的问题:
在使用”https://github.com/go-mgo/mgo“过程中发现,当大量多次交互之后,也就是服务运行大概一到两个小时之后,会出现cpu占用率极速上升的情况,并且是必现的。

所以我用pprof的火焰图来分析了一下,到底是哪里占用了cpu:
这里写图片描述
从火焰图能很明显的看出来,是 SaltPassword() 占用了大部分资源,即


func (s *Session) acquireSocket(slaveOk bool) (*mongoSocket, error) {
    ...
    if err = s.socketLogin(sock); err != nil {
        sock.Release()
        return nil, err
    }
    ...
    }

再根据火焰图,一层层往里找,找到了这个函数:

func (c *Client) saltPassword(salt []byte, iterCount int) {
    mac := hmac.New(c.newHash, []byte(c.pass))
    mac.Write(salt)
    mac.Write([]byte{0, 0, 0, 1})
    ui := mac.Sum(nil)
    hi := make([]byte, len(ui))
    copy(hi, ui)
    for i := 1; i < iterCount; i++ {
        mac.Reset()
        mac.Write(ui)
        mac.Sum(ui[:0])
        for j, b := range ui {
            hi[j] ^= b
        }
    }
    c.saltedPass = hi
}

这是个对密码进行加盐的方法,确实会对cpu产生极大的开销,但是按理来说,我每次都是从空闲连接池拿的空闲连接,内部应该是带着之前的认证信息的,不需要再次去和db进行校验,这让我非常头疼,仔细看了源码,也没找到问题所在。

没办法,只好去github里面看看有没有相关的issue,果不其然,有个朋友和我碰到了相同的问题。

相关issue地址:
https://github.com/go-mgo/mgo/issues/403
https://github.com/go-mgo/mgo/issues/254

看完后我发现,这确实是mgo的一个坑,并且mgo已经挺久没有维护了,作者还说了这么段话:
这里写图片描述
于是去”https://github.com/globalsign/mgo“一看,果然:
这里写图片描述
修复了那个问题。
那么问题到底是什么呢?

I'm seeing an issue where, after running for some time in production, the mgo client begins calling
authenticate for every query. This happens after ~ 2 hours at ~ 700RPS. Once the client enters this
state, it never recovers.

When the process first starts, each query is sent using the credentials that were previously
authenticated on the socket. The sequence looks like this: authenticate, query, logout, authenticate,
query, ... The authentication optimization in auth.go causes the (logout, authenticates) to cancel out
producing a sequence of queries: authenticate, query, query, query, ...

At some point, the client begins to send 'ismaster' before every request. This breaks that above
sequence which now looks like: authenticate, query, logout, ismaster, authenticate, query, logout.

issue的笔者其实上面已经写得挺清楚了,是出在了 isMaster 这个方法的问题上。

那我们再接下去看看这个isMaster:
在 s.socketLogin(sock) 之前,会先调用 s.cluster().AcquireSocket(),这个方法里面有这么一段代码:

if abended && !slaveOk {
            var result isMasterResult
            err := cluster.isMaster(s, &result)
            if err != nil || !result.IsMaster {
                logf("Cannot confirm server %s as master (%v)", server.Addr, err)
                s.Release()
                cluster.syncServers()
                time.Sleep(100 * time.Millisecond)
                continue
            } 
        }

这个IsMaster方法,是去和db校验的,校验完之后,会调用 session.Close() ,而session.Close()会调用下面这段代码:

func (socket *mongoSocket) LogoutAll() {
    socket.Lock()
    if l := len(socket.creds); l > 0 {
        debugf("Socket %p to %s: logout all (flagged %d)", socket, socket.addr, l)
        socket.logout = append(socket.logout, socket.creds...)
        socket.creds = socket.creds[0:0]
    }
    socket.Unlock()
}

socket.creds = socket.creds[0:0],把连接的认证信息全部删掉。
目前看确实没什么问题,但是,回到上上段代码,if abended && !slaveOk {,abended一直是true,没有代码将其置为false,按理来说调用完IsMaster之后,应该将abended置为false的,但是并没有,估计是作者忘了吧,这就导致每次都会去调用IsMaster,每次都会把连接的cred清空,那么自然而然每次都要重新认证了。

修复很简单,我们可以看一下”https://github.com/globalsign/mgo“的修复:

if abended && !slaveOk {
            var result isMasterResult
            err := cluster.isMaster(s, &result)
            if err != nil || !result.IsMaster {
                logf("Cannot confirm server %s as master (%v)", server.Addr, err)
                s.Release()
                cluster.syncServers()
                time.Sleep(100 * time.Millisecond)
                continue
            } else {
                // We've managed to successfully reconnect to the master, we are no longer abnormaly ended
                server.Lock()
                server.abended = false
                server.Unlock()
            }
        }

if err != nil || !result.IsMaster {,在这个判断下加个else,把abended置为false,就好了。

至此问题顺利解决,重新发到测试环境,那个问题也再也没有出现,正常的火焰图如下:
这里写图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值