项目连接多MongoDB库的问题

2015-08-26 Li Shuai 更多博文 » 博客 » GitHub »

mongoengine MongoDB 项目

原文链接 https://cyrusin.github.io/2015/08/26/mongo20150826/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


项目中用到了MongoDB和mongoengine, 由于平时开发都是正式线、测试线两条线, 所以正式线连接生产环境下的数据库, 而测试线连接测试线的数据库, 两个库的物理服务器配置不同, 也就是IP和端口不一致。

之前他们一般把配置文件放在一个叫settings.py的文件里, 同时把本地配置放在local_settings.py里, settings.py里会在末尾执行一下import local_settings的动作, 这样, 在测试环境下, 一些线上配置可以通过在local_settings.py里重写从而被覆盖。

随着项目的演进, 开始出现一些测试线的服务必须调正式线的情况, 这样的话, 纯覆盖配置就不好使了, 有些不同的模块会连不同的库, 所以一般都是单独配置, 比如有一些服务用的了mongo_a我们就在settings里配:

mongo_a_config = {
    'db': 'mongo_a',
    'host': 0.0.0.0, #正式线
    ...
    }

假如还有一个mongo_b, 在settings.py里配:

mongo_b_config = {
   'db': 'mongo_b',
   'host': 0.0.0.0, #正式线
   ....
   }

当你想在测试线让mongo_a连正式线的库, mongo_b连测试线的库的时候, 只需在local_settings.py里覆盖mongo_b的配置就好了:

mongo_b_config = {
   'db': 'mongo_b',
   'host': 1.1.1.1, #测试线
   ....
   }

看似很简单, 但在真正改完开测的时候, 测试线发生了比较诡异的现象, 明明在local_settings.py里覆盖了正式线的配置, 可连的时候总是连到正式线的库里去, 代码里用mongoengine的connect函数去获取连接, 逻辑也不复杂, 到底是啥原因?

又到了哥该出手的时候了, 非常好的学习机会。 一个牛逼的工程师, 不应该只是写代码牛逼, 其debug功力也应该灰常犀利, 于代码之中往来穿梭, 调用关系一清二楚, 逻辑推理清晰严谨。这正是一个好好解决问题的机会。

从项目代码来看, mongo_b的测试线配置已经覆盖正式线配置无疑, 由于mongo_amongo_b的正式线的配置的host和port都是一致, 再看代码, mongo_a的连接在前, mongo_b的连接在后, 很明显, 测试线的mongo_b用的连接是正式线的mongo_a的连接。

所以问题来了, 就在mongoengine的connect上, 后来的连接都复用了第一次的连接。虽然我们写过数据库的封装, 但有一些基本的概念还是有的, 对于数据库而言, 维护连接是比较耗资源的, 很多都用进程池或线程池搞。所以作为数据库封装的作者, 肯定不会你调一个connect, 我就重连一次, 这是比较二的方法。一般连接都是全局的, 除非你想显式获取新的连接, 这个时候这种情况一般是通过为连接设置一种区分的方法, 比如: 别名(alias), 来区分不同的连接。

我猜测是由于代码里用mongo_a的时候已经先调了mongoengine的connect, 所以用后续的mongo_b的时候, 即使你调了connect, mongoengine也不会傻到重新连一个, 而是直接复用了上次的连接实例。正式线没问题是因为正式线的mongo_amongo_b其实是位于同一个host:port下, 所以正式线的项目里每个进程也是只有一个连接实例。为了印证一下, 去看源码吧。

mongoengine的connect:

def connect(db, alias=DEFAULT_CONNECTION_NAME, **kwargs):
    global _connections
    if alias not in _connections:
            register_connection(alias, db, **kwargs)
    return get_connection(alias)

connect函数的参数里有一个alias, 取了默认值, 也就是意味着当你的项目里有不止一次调用connect的时候, 他们都有共同的连接别名, 并且_connections变量是global的, 也就是全局共享的。connect的时候先判断这个alias是不是在我们的全局的_connections里, 没有的话才会register_connection(alias), 和哥的猜测一致。我们的测试线里连mongo_a的时候已经connect过了, 并且由于我们没有为mongo_b设置alias, 所以连mongo_b的时候直接return get_connection(alias)了, 就是直接返回了我们connect(**mongo_a_config)时候的连接, 当然就是线上的库了。

所以解决方案很简单, 在local_settings.py里加个alias就好了:

mongo_b_config = {
   'db': 'mongo_b',
   'host': 1.1.1.1, #测试线
   'alias': "", #连接别名
   ...
   }