使用 Google Cloud Datastore Python 库时应如何调查内存泄漏?

Dav*_*vid 6 python memory-leaks google-cloud-datastore

我有一个 Web 应用程序,它使用 Google 的数据存储区,并且在收到足够多的请求后内存不足。

我已将其范围缩小到数据存储查询。下面提供了一个最小的 PoC,一个稍长的版本,包括内存测量在 Github 上。

from google.cloud import datastore
from google.oauth2 import service_account

def test_datastore(entity_type: str) -> list:
    creds = service_account.Credentials.from_service_account_file("/path/to/creds")
    client = datastore.Client(credentials=creds, project="my-project")
    query = client.query(kind=entity_type, namespace="my-namespace")
    query.keys_only()
    for result in query.fetch(1):
        print(f"[+] Got a result: {result}")

for n in range(0,100):
    test_datastore("my-entity-type")
Run Code Online (Sandbox Code Playgroud)

分析流程 RSS 显示每次迭代大约增长 1 MiB。即使没有返回结果,也会发生这种情况。以下是我的 Github 要点的输出:

[+] Iteration 0, memory usage 38.9 MiB bytes
[+] Iteration 1, memory usage 45.9 MiB bytes
[+] Iteration 2, memory usage 46.8 MiB bytes
[+] Iteration 3, memory usage 47.6 MiB bytes
..
[+] Iteration 98, memory usage 136.3 MiB bytes
[+] Iteration 99, memory usage 137.1 MiB bytes
Run Code Online (Sandbox Code Playgroud)

但与此同时,Python 的mprof显示了一个平面图(运行如下mprof run python datastore_test.py):

100 次数据存储提取的 mprof 输出

问题

我调用 Datastore 的方式是否有问题,或者这可能是库的潜在问题?

环境是 Windows 10 上的 Python 3.7.4(也在 Docker 中的 Debian 上的 3.8 上进行了测试)google-cloud-datastore==1.11.0grpcio==1.28.1.

编辑 1

澄清这不是典型的 Python 分配器行为,它从操作系统请求内存,但不会立即从内部领域/池中释放它。下面是来自 Kubernetes 的图表,我的受影响的应用程序在其中运行:

来自 Kubernetes 的内存使用图,显示了进程运行时内存使用量的线性增加

由此可见:

  • 内存线性增长,直到大约 2GiB,应用程序实际上因为内存不足而崩溃(从技术上讲,Kubernetes 驱逐了 pod,但这在这里无关紧要)。
  • 运行相同的 Web 应用程序,但不与 GCP 存储或数据存储交互。
  • 仅添加 GCP 存储的交互(随着时间的推移非常轻微的增长,可能是正常的)。
  • 仅添加了 GCP 数据存储的交互(更大的内存增长,每小时大约 512MiB)。Datastore 查询与本文中的 PoC 代码完全相同。

编辑 2

为了绝对确定 Python 的内存使用情况,我使用gc检查了垃圾收集器的状态。退出前,程序报告:

gc: done, 15966 unreachable, 0 uncollectable, 0.0156s elapsed
Run Code Online (Sandbox Code Playgroud)

我还在gc.collect()循环的每次迭代期间手动使用强制垃圾收集,这没有区别。

由于没有不可收集的对象,内存泄漏似乎不太可能来自使用 Python 的内部内存管理分配的对象。因此,外部 C 库更有可能泄漏内存。

可能相关

有一个开放的 grpc 问题,我不确定是否相关,但与我的问题有许多相似之处。

Dav*_*vid 4

我已将内存泄漏范围缩小到datastore.Client对象的创建。

对于以下概念验证代码,内存使用量不会增加:

from google.cloud import datastore
from google.oauth2 import service_account

def test_datastore(client, entity_type: str) -> list:
    query = client.query(kind=entity_type, namespace="my-namespace")
    query.keys_only()
    for result in query.fetch(1):
        print(f"[+] Got a result: {result}")

creds = service_account.Credentials.from_service_account_file("/path/to/creds")
client = datastore.Client(credentials=creds, project="my-project")

for n in range(0,100):
    test_datastore(client, "my-entity-type")
Run Code Online (Sandbox Code Playgroud)

这对于小脚本来说是有意义的,其中client对象可以创建一次并在请求之间安全地共享。

在许多其他应用程序中,安全地传递客户端对象更困难(或不可能)。我希望当客户端超出范围时库会释放内存,否则任何长时间运行的程序都可能会出现此问题。

编辑1

我已经将范围缩小到 grpc。GOOGLE_CLOUD_DISABLE_GRPC可以设置环境变量(为任意值)以禁用 grpc。

设置完成后,我在 Kubernetes 中的应用程序如下所示:

来自 Kubernetes 的内存使用图,在禁用 grpc 后显示一条平坦的线(没有内存增加)

对 valgrind 的进一步调查表明,它可能与 grpc 中的 OpenSSL 使用有关,我在错误跟踪器的票证中记录了这一点。