摘要:说多了都是泪,我之前排查内存泄漏的问题,超高并发的程序跑了个月后就崩溃。以前写中间件的时候,就总是把用户当,要尽量考虑各种情况避免内存泄漏。
从 Java 到 Python
本文为我和同事的共同研究成果
当跨语言的时候,有些东西在一门语言中很常见,但到了另一门语言中可能会很少见。
例如 C# 中,经常会关注拆箱装箱,但到了 Java 中却发现,根本没人关注这个。
后来才知道,原来是因为 Java 中没有真泛型,就算放到泛型集合中,一样会装箱。既然不可避免,那也就没人去关注这块的性能影响了。
而 C# 中要是写出这样的代码,那你明天不用来上班了。
同样的场景发生在了学习 Python 的过程中。
什么?数据库连接竟然没有连接池!?
完全不可理解啊,Java 中不用连接池对性能影响挺大的。
Python 程序员是因为 Python 本来就慢,然后就自暴自弃了吗?
突然想到一个笑话
问:为什么 Python 程序员很少谈论内存泄漏?
答:因为 Python 重启很快。
? 说多了都是泪,我之前排查 Java 内存泄漏的问题,超高并发的程序跑了1-2个月后就崩溃。我排查了好久,Java GC 参数也研究了很多,最后还是通过控制变量法找到了原因。
如果在 Python 中,多简单的事啊,写一个定时重启脚本,解决…
问题来源那本文的问题怎么来的呢?正是我给公司的代码加上连接池后产生的。一加连接池,就会有一定几率出现;一去掉连接池,就不会有。
db = get_connection() try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ["david"]) > 0: return None return cursor.fetchone()[0] finally: db.close()
一段很简单的代码,基本上整个项目中所有的数据库查询都是这么写的,本来没任何问题。
但当我给底层加上连接池后,问题来了。
这边后报出这样一个异常:"NoneType" object has no attribute "__getitem__"
意思就是说cursor.fetchone() 取出来的结果是None。
但是,代码在调用之前命名已经检查过affected rows了,根据文档cursor.execute()返回的就是affected rows。
文档也是这么写的:Returns long integer rows affected, if any。
解决问题第一步:网上找答案什么测试驱动开发,敏捷开发,我觉得都不对,一句话形容我们那应该是:基于 Google 的 Bug 驱动开发。?
可惜网上无任何结果,去 stackoverflow 上问也没人知道。
感觉又来到了一片无人区……
目前唯一能确认的就是和连接池相关了。
大致分析下应该是和连接复用有关,代码没写好?底层连接池并发处理的代码有 Bug?
先抓个详细的异常看看吧。
解决问题第二步:分析异常日志我们项目用了 Sentry,一个异常跟踪系统。可以把报错时的调用堆栈和临时变量都记录下来。
第一个有用的信息是,我们竟然发现cursor.execute()的返回结果在 Sentry 上记录的是18446744073709552000。
这是一个非常诡异的数字,因为它接近2^64-1 (18446744073709551615),而且还比它大了一点。
网上也找不到太多相关资料,和这个数字相关的都是 Javascript 相关的问题。
因为 Javascript 中是无法表示 2^64-1 的,相关讨论:传送门
简单的一句话解释就是:这个数字超过了 Javascript Integer 的最大范围,所以底层用 Float 来表示了,所以导致丢失了精度。
但我们的程序没用 Javascript。到了这边,我们的第一反应一定是,要么 MySQL 出了 Bug。要么 MySQL-Python 出了 Bug。
解决问题第三步:一层层看源码分析先看 MySQL-Python 源码,cursor.execute()内部调用了affected_rows()方法得到了这个数字,而affected_rows()这个方法内部使用 C 实现了。
MySQL-Python 的 C 部分源码很简单,没什么逻辑:
return PyLong_FromUnsignedLongLong(mysql_affected_rows(&(self->connection)));
看样子也没什么特别的,这里就两个地方可能有问题,PyLong_FromUnsignedLongLong()和mysql_affected_rows()。
先自己尝试写了一段代码,调用PyLong_FromUnsignedLongLong()函数,发现无论如何都不会出现18446744073709552000这个数字。
然后看 MySQL 源码,mysql_affected_rows() 返回类型是my_ulonglong,源码中其实是这么定义的:
typedef unsigned long long my_ulonglong;
也就是说,在 C 代码中,这个数字最大就是2^64-1 (18446744073709551615),不可能返回18446744073709552000的。
然后在mysql_affected_rows()的官方文档中又发现了一些有用的信息:
An integer greater than zero indicates the number of rows affected or retrieved. Zero indicates that no records were updated for an UPDATE statement, no rows matched the WHERE clause in the query or that no query has yet been executed. -1 indicates that the query returned an error or that, for a SELECT query, mysql_affected_rows() was called prior to calling mysql_store_result().
Because mysql_affected_rows() returns an unsigned value, you can check for -1 by comparing the return value to (my_ulonglong)-1 (or to (my_ulonglong)~0, which is equivalent).
好了,遇到第一个坑了,为什么 MySQL 官方文档说这里可能有-1,而 MySQL-Python 的文档中却没说?而且返回类型是无符号的,-1就变成18446744073709551615了。
那么如果我用if cursor.execute() > 0这种方式来判断命中行数时,明明出错了,我却会得到True的结果了。
很明显 MySQL-Python 写的是有问题的,同事联系了 MySQL-Python 的作者,作者承认了这里的问题,把代码修复了,下一个版本会修复。
神奇的数字但是,看源码发现的东西还是没解决我们的问题,为什么我们的到的数字是18446744073709552000,而不是18446744073709551615?
整个调用链我们都检查过了,不可能出现这个数字。
然后一个周末,我在快睡醒的时候突然想到了一个问题,这个数字是不是在 Python 报错的时候,还是18446744073709551615,而到了 Sentry 中,就变成了18446744073709552000?
因为 Sentry Web 界面用的是 ajax,而 Javascript 中转换这个数字的时候就会出错。
最后一验证,果然是 Sentry 的问题,Javascript 真的处处是坑。
好了,到了这一步,等 MySQL-Python 作者修复完后,我们的代码也就不会报错了。问题解决?
但是,MySQL 官方却没有说为什么这里会出现-1,而且为什么去掉了连接池就不会报错?
就算我们的代码不报错了,但如果这里的返回数字不符合我们预期或者说不可控的话,会导致更多隐形的数据上的问题。
Root Cause目前为止,依然没找到 Root Cause。
别动,看好了,我要用压测大法了!既然这个问题是在高并发使用连接池时出现的,那就压测看看能不能重现吧。
用了同样的代码,10个进程,没有 sleep。没想到不需要一分钟,这个问题就会立刻重现。
而且每次重现时,都会有一些 MySQL 底层的警告,说出现了错误的调用顺序。
这时,我试了一下加了一行代码:
db = get_connection() cursor = None try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ["david"]) > 0: return None return cursor.fetchone()[0] finally: if cursor: # new code cursor.close() # new code db.close()
加完后就再也没看到任何错误了。
嗯,这里我们的代码写的是不到位,我后来仔细看了官方教程,是有主动关闭cursor的代码的。(偷偷告诉你们,这里都是 CTO 以前写的 ?)
粗略看了下cursor.close()的代码,里面其实就是在把未读完的数据读完:while self.nextset(): pass。
那这里出问题的原因也就好理解了,高并发情况下复用连接池,如果上一次请求由于某些原因没有读完所有数据,后面直接复用这个连接的时候,就会出现问题了。
然后,我又奇怪了,连接池框架在关闭连接的时候不应该做清理工作吗?
Java JDBC 源码也看过不少了,Connection关闭的时候会清理Statement,Statement关闭的时候会清理ResultSet。因为单个连接只会在单线程中操作,是线程安全的,所以实现这样的自动清理是非常简单的。
以前写 Java 中间件的时候,就总是把用户当?,要尽量考虑各种情况避免内存泄漏。我们默认都是认为用户是从来不会去调用close方法的。所以常常会想方设法帮用户去自动处理。
解决问题最后要来解决问题了,代码量很大,所有调用都改一遍其实也不难,因为这里都是有规律的,正则啊脚本啊什么的齐上阵,总是能解决的。
但是,其实也可以像 JDBC 那样搞自动关闭。
class AutoCloseCursorConnection(object): cursor = None conn = None def __init__(self, conn): self.conn = conn def __getattr__(self, key): return getattr(self.conn, key) def cursor(self, *args, **kwargs): self.cursor = self.conn.cursor(*args, **kwargs) return self.cursor def close(self): if self.cursor: self.cursor.close() self.conn.close()
每次创建的连接包一下,就解决问题了。
源地址:http://www.dozer.cc/2016/07/mysql-connection-pool-in-python.html
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/38091.html
摘要:一普通连接方法使用模块普通方式连接。返回结果表示影响的行数。查询时不需要操作,插入更新删除时需要提交。模块点此下载类继承自,表示一个新的连接池。如果需要新的连接池,按照如下格式新增即可。一个连接池可同时提供多个实例对象。 一、普通 MySQL 连接方法 使用模块 MySQLdb 普通方式连接。 #!/usr/bin/env python # _*_ coding:utf-8 _*_...
摘要:另外,项目在单元测试中使用的是的内存数据库,这样开发者运行单元测试的时候不需要安装和配置复杂的数据库,只要安装好就可以了。而且,数据库是保存在内存中的,会提高单元测试的速度。是实现层的基础。项目一般会使用数据库来运行单元测试。 OpenStack中的关系型数据库应用 OpenStack中的数据库应用主要是关系型数据库,主要使用的是MySQL数据库。当然也有一些NoSQL的应用,比如Ce...
阅读 2193·2019-08-30 15:53
阅读 2371·2019-08-30 12:54
阅读 1092·2019-08-29 16:09
阅读 698·2019-08-29 12:14
阅读 717·2019-08-26 10:33
阅读 2415·2019-08-23 18:36
阅读 2903·2019-08-23 18:30
阅读 2077·2019-08-22 17:09