给自己搭个量化投资系统之四——pandas的使用导致API服务停止
在我的系统里,我通过搭建API服务来访问服务器上的数据库(可参见给自己搭一个金融数据库(六)——通过API远程访问mysql),但运行一段时间后发现服务停止了。
1. 服务停止原因:内存溢出
查看日志(可参见给自己搭个量化投资系统之一——一次装饰器的应用),可以看到前运行正常,每次操作都在0.01秒级,但到最近急剧飙升,达到了100秒级,最后突然停止,没有显示错误信息。因此,初步怀疑是API服务不断占用新的内存,导致内存不足,最终出现停止。
但是API服务并没有不断占用新的内存,有可能是mysql的使用导致了内存的占用。需要再做一次尝试,并用top去监测API的运行情况。可以看到API的内存占用确实在不断增加,两个小时的运行后就增加到了20%左右。
很明显,API服务的停止是因为它在不断地占用资源,直到资源不足,引起服务停止。
2. 内存溢出排坑1:异步进程
再次审视API服务,发现里面有一个异步调用(与给自己搭一个金融数据库(六)——通过API远程访问mysql略有不同)。
class class1(RequestHandler):
executor = ThreadPoolExecutor(1)
@coroutine
def post(self):
data = yield self.dectation(json.loads(self.request.body))
if data is None:
data = pd.DataFrame()
self.write(data.to_json())
@run_on_executor
def dectation(self, data):
try:
generated_text = func(data)
except Exception as e:
generated_text = e
return generated_text
实际上这个异步调用在这里并没有用处,反而使得数据库操作出现了脏读脏写的可能,起到了画蛇添足的作用。
那么是不是这个异步操作导致了API服务对资源的占用呢?可以把它去掉后再做一次尝试。
发现经过一段时间的运行后,内存占用再次上升到了20%以上。说明导致内存持续增加的原因仍然存在。
3. 内存溢出排坑2:对象不断新建
我们用objgraph来看一下各类型的对象。
import objgraph
### 打印出对象数目最多的 50 个类型信息
objgraph.show_most_common_types(limit=50)
发现对象数量没有持续增加。
4. 内存溢出排坑3:强制垃圾回收
是否存在这么一种可能,在API调用中的垃圾没有及时回收?这个可能性实际上跟上一节有一定的冲突,按理来说,如果有垃圾没有回收,应该会造成对象的增加。不过我们也可以做一次尝试,在每次调用中加上强制垃圾回收。
gc.collect()
结果不出所料,内存占用仍然在持续增加。
5. 内存溢出排坑4:文件中的对象及所占内存
打印一下API文件中的变量,以及声明的变量所占的内存。
print(locals().keys(), sys.getsizeof(self))
print(sys.getsizeof(engine), sys.getsizeof(conn), sys.getsizeof(cur))
可以看到也不是这个原因。
6. 内存溢出排坑5:使用tracemalloc
6.1. 先看哪个模块在占用内存
先导入包
import tracemalloc
然后打印内存占用最高的模块。
shot2 = tracemalloc.take_snapshot()
shot2 = shot2.statistics('lineno')
for top_stat in shot2[:10]:
print(top_stat)
发现主要问题是pandas/core/indexes/range.py占用的内存在不断上升。
6.2. 再看哪一步操作导致内存占用
现在来看哪一步操作会导致pandas/core/indexes/range.py对内存的占用。
先申请两个全局变量。
shot1 = tracemalloc.take_snapshot()
shot2 = []
然后打印range.py内存占用的变化情况。
shot2 = tracemalloc.take_snapshot()
shot2 = shot2.compare_to(shot1, 'filename')
for top_stat in shot2:
if 'range.py' in str(top_stat):
print(json.loads(self.request.body))
print(top_stat)
break
shot1 = tracemalloc.take_snapshot()
可以看到,导致range.py内存占用持续增加的API调用参数都涉及'sql2pd'。
再来看sql2pd的代码
def sql2pd(sql):
return pd.read_sql(sql, engine)
此外还有post下的部分代码
def post(self):
global shot1, shot2
data = json.loads(self.request.body)
if len(list(data.keys())) == 2:
data = eval(data['f'])(data['p1'])
else:
data = eval(data['f'])(data['p1'], data['p2'])
if data is None:
data = pd.DataFrame()
再经过更多更密集的快照打印,并将部分代码拆开,可以看到,造成内存持续上升的代码是
pd.read_sql(sql, engine)
真正的原因应该在于pd.read_sql会创建一个对象,锁定数据库,保持联系,同时占用内存。
6.3. 问题定位
先尝试对engine进行dispose操作,即
data = pd.read_sql_query(sql, engine)
engine.dispose()
发现有一定效果,但没有从根本上解决问题,内存占用还是在增加。
尝试通过with XX as con来连接,即
with pymysql.connect(host='localhost', user='root', password='password', port=port, database='stock', charset='utf8') as con:
data = pd.read_sql_query(sql, con)
发现同样没有效果。
尝试在engine里增加连接池限制,即
engine=create_engine('mysql+pymysql://root:password@localhost:3306/stock', pool_size=1)
发现还是解决不了问题。
采用with模块也不能解决问题,说明这个问题不是关闭连接能够解决的,而是read_sql在过程中创建了什么东西并且一直没有关闭。
而改写pandas的read_sql方法非常麻烦,甚至不如弃用pandas的read_sql。
为此,我又加了个不用read_sql的方法
def curfsql(sql):
cur.execute(sql)
return pd.DataFrame(list(cur.fetchall()))
发现存在同样的内存持续占用的问题。这里的问题是不是同样出在pandas相关的方法上呢?再做进一步验证
def curfsql(sql):
global shot1, shot2
shot2 = tracemalloc.take_snapshot()
shot2 = shot2.compare_to(shot1, 'filename')
for top_stat in shot2:
if 'range.py' in str(top_stat):
print(5, top_stat)
break
shot1 = tracemalloc.take_snapshot()
cur.execute(sql)
shot2 = tracemalloc.take_snapshot()
shot2 = shot2.compare_to(shot1, 'filename')
for top_stat in shot2:
if 'range.py' in str(top_stat):
print(6, top_stat)
break
shot1 = tracemalloc.take_snapshot()
data = pd.DataFrame(list(cur.fetchall()))
shot2 = tracemalloc.take_snapshot()
shot2 = shot2.compare_to(shot1, 'filename')
for top_stat in shot2:
if 'range.py' in str(top_stat):
print(7, top_stat)
break
shot1 = tracemalloc.take_snapshot()
return data
可以看到问题确实存在于pandas上。
说明pandas用于开发高频API服务是存在问题的,那么解决的方案只是是规避Pandas的使用,直接返回二维数组。
最终,内存占用不断上升的问题终于得到了解决。
参考文献:
-
https://www.cnblogs.com/01black-white/p/15703804.html -
https://blog.csdn.net/PSpiritV/article/details/123224519 -
http://www.py.cn/faq/python/17809.html -
https://blog.csdn.net/ben1122334/article/details/106080759/ -
https://blog.csdn.net/weixin_39672572/article/details/111539781 -
https://www.cnblogs.com/dechinphy/p/mmap.html -
https://blog.csdn.net/m0_37426155/article/details/111948498 -
https://qa.1r1g.com/sf/ask/3581911861/
- 点赞
- 收藏
- 关注作者
评论(0)