“MySQL server has gone away” in django ThreadPoolExecutor


MySQL server has gone away报错

最近碰到MySQL server has gone away的报错,报错出现的现象是:

项目基本情况:基于python3.5+django1.8,数据库mysql,生产和测试环境都是通过nginx+uwsgi部署

分析原因

谷歌了一下MySQL server has gone away问题可能的原因:
1. MySQL服务宕了
2. 连接超时
3. 进程在server端被主动kill
4. SQL语句太长

再结合项目实际情况逐条分析:

mysql> show global status like 'uptime';
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Uptime        | 13881221 |
+---------------+----------+
1 row in set (0.00 sec)

报错日志中并没有服务重启的信息,同时uptime值很大,表示已经运行很长时间。因此第一条原因可以排除

mysql> show global status like 'com_kill';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Com_kill      | 12    |
+---------------+-------+
1 row in set (0.00 sec)

Com_kill居然有这么多-_-//,但也不确定出错的查询语句是否是慢查询。找了一下报错代码的查询语句,属于索引查询,而且查询时间不超过100ms,因此,这一条也可以排除。

mysql> show global variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 86400 |
+---------------+-------+
1 row in set (0.00 sec)

目前设置的最大超时时间是24小时,也就是在这24小时内有数据库连接超时,超时连接后面又被用到,导致报错。

之前在django数据库连接中分析了django的数据库连接是基于线程(thread.local)创建的全局变量,即线程本地变量,下面简称为线程变量。

由于没有像常规请求一样的在开始和结束之前检查数据库连接是否可用的机制,线程池中的线程保存的数据库连接也许是不可用的,就导致下次被调起执行数据库操作时出现“MySQL server has gone away”的报错。

再结合出错现象分析一下:

看一下ThreadPoolExecutor中创建线程的逻辑:

def _adjust_thread_count(self):
    # When the executor gets lost, the weakref callback will wake up
    # the worker threads.
    def weakref_cb(_, q=self._work_queue):
        q.put(None)
    # TODO(bquinlan): Should avoid creating new threads if there are more
    # idle threads than items in the work queue.
    if len(self._threads) < self._max_workers:
        t = threading.Thread(target=_worker,
                             args=(weakref.ref(self, weakref_cb),
                                   self._work_queue))
        # 线程被设为守护线程
        t.daemon = True
        t.start()
        self._threads.add(t)
        _threads_queues[t] = self._work_queue

线程池中创建的线程属于守护线程,当主线程退出,子线程也会跟着退出。而子线程是在调用submit方法提交异步任务时,若线程池中实际线程数量小于指定数量,便会创建。因此主线程是请求线程。

在用uWSGI部署的django项目中,请求线程是由uWSGI分配的。uWSGI会根据配置文件中的process, threads参数决定开多少工作进程和子线程,同时还有max-requests参数,表示为每个工作进程设置的请求数上限。
当该工作进程请求数达到这个值,就会被回收重用(重启),其子线程也会重启。所以上面的报错现象中,其实是工作进程重启了,请求子线程也会重建,导致线程池中的守护线程也会被kill了,报错就停止了。

总结一下原因

解决方案

要解决这个问题,最直接的办法是在线程池的所有异步任务中,在执行数据库操作之前,检查数据库连接是否可用,然后关掉不可用连接。可以通过装饰器实现,装饰涉及到数据库操作的异步任务。

def close_old_connections(func):
    # 获取当前线程中的数据库连接
    from django.db import connections
    @wraps(func)
    def wrapper(*args, **kwargs)
        try:
            for connection in connections.all():
                # 检查连接可用性,并关闭不可用连接
                conn.close_if_unusable_or_obsolete()
            return func(*args, **kwargs)
        finally:
            for connection in connections.all():
                # 检查连接可用性,并关闭不可用连接
                conn.close_if_unusable_or_obsolete()
    return wrapper

或者改写一下django获取和保存数据库连接的机制,可以创建一个全局的数据库连接池,不管是常规请求还是异步任务,都从连接池获取数据库连接,由连接池保证数据库连接的数量和可用性。


参考阅读
MySQL server has gone away报错原因分析
django数据库连接
WSGI & uwsgi