You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on May 6, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on May 6, 2026. It is now read-only.
Datastore: (using NDB) Linear memory usage consumption when using fetch_page #752
We were recently working on large scale migration batches (on ~550.000 entities) using NDB on App Engine. As we are limited to 2048MB of ram, memory consumption is a big subject when doing this kind of things.
When trying to lower it, I noticed something that I found an unexpected (in my opinion) behaviour of fetch_page.
I would have expected the memory consumption to be close to a plateau, as the new entities will weight approx. the same as the previous ones. I found that it instead grows linearly until it ends, even without processing the results at all.
Environment details
Specify the API: Datastore using NDB 1.11.1
OS type and version: Ubuntu 20.04.3 LTS x86_64
Python version and virtual environment information: 3.8.10
google-cloud- version: 1.11.1
Code example
importosimportpsutil# https://pypi.org/project/psutil/fromgoogle.cloudimportndbfromapp.modelsimportEnterpriseprint(ndb.__version__)
defmemory_usage():
pid=os.getpid()
process=psutil.Process(pid)
returnprocess.memory_info().rss/10**6withndb.Client().context():
more=Truecursor=Nonestandard_usage=memory_usage()
whilemore:
# 'Enterprise' is rather a big entity in our datastore, which makes the memory consumption increase more visibleresult, cursor, more=Enterprise.query().fetch_page(200, start_cursor=cursor, use_cache=False, use_global_cache=False)
print(memory_usage() -standard_usage)
Output
I truncated the output, as it loops ~400 times. Using the best App Engine servers (B8) we would not be able to finish that script.
This sounds alarming; we use fetch_page(use_cache=False) a lot. It's not clear if you actually tried this or just were worried it wouldn't work, because of the output of memory_info(), and if you confirmed this same behavior didn't occur on python 2 / legacy ndb. It seems possible that this could just mean that garbage collection wasn't triggered during your test, and that in a real situation a collection would be triggered rather than just using up all available memory. More investigation into GC stats inside your test script might have shed some light on this, but instead this issue is still just sitting here mostly untouched for years.
Does the type: bug label mean this was confirmed by the team to be a real issue? If so, isn't this kind of important since GCP developer support is still urging developers to go ahead and use this library? If not, shouldn't the issue be closed with an explanation showing it's safe to use this library in production? Without knowing the answer to either of those questions, the responsible thing is to assume the worst - which is kind of worse than just having a known/confirmed bug like this.
Hi, I'll try to address some of your questions to the best of my knowledge !
It's not clear if you actually tried this or just were worried it wouldn't work,
We actually experienced this bug on our productions servers; we use "B8" instances; which hold 2048MB of memory. We initially thought our code was simply not optimized enough, but with this snippet above we do reproduce the "soft memory exceeded" error.
Our application usually runs such long loops as Cloud Tasks. In that configuration, we managed to get around this bug by using ndb cursors. The function used in Cloud Tasks only processes a few loops at a time; and triggers another execution of itself with an updated cursor to let the instance clean its memory, and start again fresh.
and if you confirmed this same behavior didn't occur on python 2 / legacy ndb
I could'nt confirm how python 2 / legacy ndb behaves; we're currently using python 3.10 and google-cloud-ndb v1.11.1; which isn't legacy ndb.
It seems possible that this could just mean that garbage collection wasn't triggered during your test, and that in a real situation a collection would be triggered rather than just using up all available memory.
As I mentionned, this issue occured on real B8 servers; so we did experienced soft memory being exceeded in that sort of cases.
I have tried to include your suggestion of investigating more into the GC stats. I must admit I have very little experience with GC; let alone GC Stats.
In an effort to clear-up all doubts; I've included both the stats and a forced gc.collect into the loop of my original script:
importgcimportosimportpsutil# https://pypi.org/project/psutil/fromgoogle.cloudimportndbfromrsa.primeimportgcdfromapp.modelsimportEnterpriseprint(ndb.__version__)
defmemory_usage():
pid=os.getpid()
process=psutil.Process(pid)
returnprocess.memory_info().rss/10**6withndb.Client().context():
more=Truecursor=Nonestandard_usage=memory_usage()
whilemore:
# 'Enterprise' is rather a big entity in our datastore, which makes the memory consumption increase more visibleresult, cursor, more=Enterprise.query().fetch_page(200, start_cursor=cursor, use_cache=False, use_global_cache=False)
print("memory: ", memory_usage() -standard_usage)
print(f"{gc.get_stats()}")
gc.collect()
And the output of the program is attached . bug_fetch_page.txt.
I've interrupted the script as letting it run would take a while. The conclusion remains that the 2048Mb are still exceeded without any actual computations.
I'll let you investigate the GC stats and hopefully draw some conclusions. Without prior knowledge, nothing is uncollectable but the number of collections never decreases despite the regular gc.collect.
Hope this clarifies some your interogations; and makes it easier to investigate that issue for maintainers.
We do things like this all the time in tasks running on f1-micro instances and it works fine, memory gets freed up along the way because nothing is stored in instance cache. So if you're having memory issues fetching pages with 2GB of memory available, it seems like a pretty major regression.
Re. Python 2 ndb, if I'm not mistaken this library was made mainly to coax developers off the old appengine standard runtime. At some point, messages started appearing in issues about how development was stopping on this and no further support, Google started recommending against using it for new projects, and finally seemed to give in and just let developers continue using (for now, anyway) most of the legacy appengine services, including the ndb library, on Python 3 using this: https://github.com/GoogleCloudPlatform/appengine-python-standard
I'd be curious to know if the ndb support using the legacy bundled services sdk exhibits this issue for you. As it is, and without any comment from the team on it, it's kind of a red flag for us since we know our instances would just run out of memory right away if this is bug exists. If legacy bundled services support goes away at some point, using this instead wouldn't really be an option.
Great news ! This could also fix issue googleapis/google-cloud-python#15859, where erroneous use of cache might play a role. Actually maybe not, since googleapis/google-cloud-python#15859 is a case of accessing same entity from another instance, it would have to relate to global cache, not local cache.
Giving some more thoughts to this cache management issue, it seems to me that taking the use_cache into account is only part of the story. Indeed it will provide a way to alleviate the memory limit issue by adding use_cache=False to many query.fetch(), but there is another side to this: a proper local cache management should have some sort of memory management so that it does not use more than a pre-defined amount of memory, it should not take memory until the instance crashes.
The local cache as it is today is just a dict object, to which entities are added on Key.get() and Model.put(). I don't see where things get cleared. Could it be a good idea to use cachetools.LRUCache(), with a max_size that could be set as a context variable ?
Activity
pnico commentedon Feb 3, 2023
This sounds alarming; we use
fetch_page(use_cache=False)a lot. It's not clear if you actually tried this or just were worried it wouldn't work, because of the output ofmemory_info(), and if you confirmed this same behavior didn't occur on python 2 / legacy ndb. It seems possible that this could just mean that garbage collection wasn't triggered during your test, and that in a real situation a collection would be triggered rather than just using up all available memory. More investigation into GC stats inside your test script might have shed some light on this, but instead this issue is still just sitting here mostly untouched for years.Does the
type: buglabel mean this was confirmed by the team to be a real issue? If so, isn't this kind of important since GCP developer support is still urging developers to go ahead and use this library? If not, shouldn't the issue be closed with an explanation showing it's safe to use this library in production? Without knowing the answer to either of those questions, the responsible thing is to assume the worst - which is kind of worse than just having a known/confirmed bug like this.Nathanmalnoury commentedon Feb 8, 2023
Hi, I'll try to address some of your questions to the best of my knowledge !
We actually experienced this bug on our productions servers; we use "B8" instances; which hold 2048MB of memory. We initially thought our code was simply not optimized enough, but with this snippet above we do reproduce the "soft memory exceeded" error.
Our application usually runs such long loops as Cloud Tasks. In that configuration, we managed to get around this bug by using ndb cursors. The function used in Cloud Tasks only processes a few loops at a time; and triggers another execution of itself with an updated cursor to let the instance clean its memory, and start again fresh.
I could'nt confirm how python 2 / legacy ndb behaves; we're currently using python 3.10 and google-cloud-ndb v1.11.1; which isn't legacy ndb.
As I mentionned, this issue occured on real B8 servers; so we did experienced soft memory being exceeded in that sort of cases.
I have tried to include your suggestion of investigating more into the GC stats. I must admit I have very little experience with GC; let alone GC Stats.
In an effort to clear-up all doubts; I've included both the stats and a forced
gc.collectinto the loop of my original script:And the output of the program is attached .
bug_fetch_page.txt.
I've interrupted the script as letting it run would take a while. The conclusion remains that the 2048Mb are still exceeded without any actual computations.
I'll let you investigate the GC stats and hopefully draw some conclusions. Without prior knowledge, nothing is
uncollectablebut the number ofcollectionsnever decreases despite the regulargc.collect.Hope this clarifies some your interogations; and makes it easier to investigate that issue for maintainers.
pnico commentedon Feb 16, 2023
Thanks for responding!
We do things like this all the time in tasks running on f1-micro instances and it works fine, memory gets freed up along the way because nothing is stored in instance cache. So if you're having memory issues fetching pages with 2GB of memory available, it seems like a pretty major regression.
Re. Python 2 ndb, if I'm not mistaken this library was made mainly to coax developers off the old appengine standard runtime. At some point, messages started appearing in issues about how development was stopping on this and no further support, Google started recommending against using it for new projects, and finally seemed to give in and just let developers continue using (for now, anyway) most of the legacy appengine services, including the ndb library, on Python 3 using this: https://github.com/GoogleCloudPlatform/appengine-python-standard
I'd be curious to know if the ndb support using the legacy bundled services sdk exhibits this issue for you. As it is, and without any comment from the team on it, it's kind of a red flag for us since we know our instances would just run out of memory right away if this is bug exists. If legacy bundled services support goes away at some point, using this instead wouldn't really be an option.
fix: Query options were not respecting use_cache
fix: Query options were not respecting use_cache
bw-yuhogg commentedon Feb 22, 2023
Hi, I'm working on a PR to fix this issue. Thanks for bringing it up.
In the meantime, you can try the following workaround of explicitly setting a never-cache policy:
Instead of
you can do:
fix: Query options were not respecting use_cache
patb2 commentedon Feb 23, 2023
Great news ! This could also fix issue googleapis/google-cloud-python#15859, where erroneous use of cache might play a role. Actually maybe not, since googleapis/google-cloud-python#15859 is a case of accessing same entity from another instance, it would have to relate to global cache, not local cache.
patb2 commentedon Feb 23, 2023
Giving some more thoughts to this cache management issue, it seems to me that taking the
use_cacheinto account is only part of the story. Indeed it will provide a way to alleviate the memory limit issue by addinguse_cache=Falseto manyquery.fetch(), but there is another side to this: a proper local cache management should have some sort of memory management so that it does not use more than a pre-defined amount of memory, it should not take memory until the instance crashes.The local cache as it is today is just a dict object, to which entities are added on
Key.get()andModel.put(). I don't see where things get cleared. Could it be a good idea to usecachetools.LRUCache(), with amax_sizethat could be set as a context variable ?If it is a good idea, I could give it a try.
Nathanmalnoury commentedon Feb 24, 2023
Hi @rwhogg,
Thank you for your response and your work on this issue.
Just to let you know that your suggested fix :
does the trick for me ! The maximum memory usage reached is 72 Mo, which is expected in our case.
fix: Query options were not respecting use_cache (#873)