当 Tornado 连接 MySQL 报错:Too many connections


事情的经过是这样的:

我们对 .NET 程序进行移植,它在 MySQL 中所使用的数据库是很多个,所以请求来了之后其实是动态连接不同的数据库,而不是常见的只有一个库。

我们使用的数据库连接工具是 SQLAlchemy ,它连接数据库是使用 Engine,事务处理使用的是 Session,每一个请求进来,都会先获取到对应数据库的 engine,然后向 engine 申请一个 session 来实现数据库操作。

由于之前的错误理解,把获取 engine 这步写在了 BaseHandler 下面,在编码阶段其实是看不到问题的,毕竟一个人手动又能开几个数据库连接呢。

但是无意间在一个 k8s 的群里留意到有人提及压测工具,ab 是基于命令的,JMeter 则是使用 Java 开发的有界面工具,Windows 和 Mac 都可以运行,想着可以试试看,刚好可以看看咱自己的这套代码的运行能力如何。

从 10 到 100,然后增加到 1000 就发现了错误出现:Too many connections,这不是该有的结果。

这个问题曾经尝试压测的时候也遇到过,但是没彻底弄明白。

再次通过 Google 进行寻找,让人很失望:清一色的修改 MySQL 的 max_connections。

为啥说很失望呢,你想想,连接超了你就修改最大连接数限制,可是总不能无限增加吧?

但是用户的 Request 是无限增加的呀,直觉上我就认为这并不是解决这个问题的正确方向。

同时也思考是不是可以设置 pool_size=0,这样就是不限连接数,但是一想,肯定也不对。

再次寻找一番,其实并没有明确答案,但是思考一番,engine 其实就是连接代理,按目前获取 engine 的方法,如果每来一个请求就开一个新的 engine,每增加一个 engine 就是增加了一个连接,自然就是无限增加了数据库连接。

如果,engine 不是新开的呢?而是全局使用呢?如何做到全局使用,那就是在 app 启动时候就有了所有的 engine,然后再在 Request 处动态获取到已经事先准备好的 engine,这样不就实现了 engine 的复用了?

上代码:

# app.py

class Application(tornado.web.Application):
    def __init__(self):
        self.Session = sessionmaker()
        # engine不能在request上创建,而是应该在app全局创建提供给request使用
        # 否则在面对高并发时mysql会出现"Too many connections"错误,而简单修改max_connections并不是正确的做法
        self.engines = {n: db.make_engine(n) for n in DBNAMES}
        super(Application, self).__init__(handlers, **settings)
        
if __name__ == '__main__':
    application = Application()
    options.define('port', default=80, type=int)
    options.parse_command_line()
    application.listen(options.options.port)
    ioloop.IOLoop.instance().start()
# base.py

class BaseHandler(tornado.web.RequestHandler):
    session = None

    def get_engine(self):
        dbname = self.get_dbname()
        engine = self.application.engines.get(dbname)
        return engine

    def get_session(self):
        if self.session is None:
            engine = self.get_engine()
            self.application.Session.configure(bind=engine)
            self.session = self.application.Session()
        return self.session
    
    @property
    def db(self):
        return self.get_session()

JMeter 线程开到 10000,压测通过,未再报错。

max_connections 要不要改,可能要改,由于默认 pool_size=5,当数据库很多的时候,连接数=pool_size*数据库数量,这时候就非常容易超出最大连接数了,就必须要调整 pool_size 与 max_connections 来防止 “Too many connections” 错误的出现。

有时候虽然有些方法看似可以立即解决当前的问题,但是,方向却不一定对。

现在,解决了多数据库动态连接的问题,也解决了并发请求导致数据库连接暴增的问题,如果你也有同样类似的需求,或正在进行技术选型却心中没底,或问题已经出现很久却悬而未决,那么你应该知道怎么做了。