给自己搭个量化投资系统之四——pandas的使用导致API服务停止

举报
darkpard 发表于 2022/05/18 20:44:29 2022/05/18
【摘要】 在我的系统里,我通过搭建API服务来访问服务器上的数据库(可参见给自己搭一个金融数据库(六)——通过API远程访问mysql),但运行一段时间后发现服务停止了。1. 服务停止原因:内存溢出查看日志(可参见给自己搭个量化投资系统之一——一次装饰器的应用),可以看到前运行正常,每次操作都在0.01秒级,但到最近急剧飙升,达到了100秒级,最后突然停止,没有显示错误信息。因此,初步怀疑是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的使用,直接返回二维数组。

最终,内存占用不断上升的问题终于得到了解决。

图片

参考文献:

  1. https://www.cnblogs.com/01black-white/p/15703804.html
  2. https://blog.csdn.net/PSpiritV/article/details/123224519
  3. http://www.py.cn/faq/python/17809.html
  4. https://blog.csdn.net/ben1122334/article/details/106080759/
  5. https://blog.csdn.net/weixin_39672572/article/details/111539781
  6. https://www.cnblogs.com/dechinphy/p/mmap.html
  7. https://blog.csdn.net/m0_37426155/article/details/111948498
  8. https://qa.1r1g.com/sf/ask/3581911861/
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。