Aryaman Agrawal
Aryaman Agrawal

Reputation: 11

How to Enable Memory Sharing Between Workers in Flask-based GAE App (Python/Gunicorn) — Copy-on-Write Issues

In Gunicorn, each worker by default loads the entire application code. This causes increased memory usage and occasional OOM (out of memory errors).

I am looking to enable the --preload option of gunicorn so that workers refer to memory of master process, thus saving memory used and avoiding OOM errors as well.

I started by adding the --preload flag, however on measuring the RSS and shared memory (using psutil) of individual workers, I found there to be no difference as compared to when I deploy without --preload.

I discovered a concept called Copy-on-Read in Instagram's disabling garbage collector article (https://medium.com/binu-22feet/dismissing-python-garbage-collection-at-instagram-37faf66828ad) indicating how CPython's reference counting and garbage collection causes a Copy-on-Write even on a read.

Following the article, I did they same thing that they did, however even that did not had any difference for my app when compared to deploying without --preload.

How do I get the workers to share memory?


Sample Code:

app.yaml:

runtime: python311 
instance_class: F2 
entrypoint: gunicorn -b :$PORT -w 4 --preload --config gunicorn.conf.py app_main:app

automatic_scaling: 
  min_idle_instances: 1

app_main.py

> import os
> import gc
> import atexit
> from flask import Flask
> 
> gc.set_threshold(0)
> atexit.register(os._exit, 0)
> 
> app = Flask(__name__)
> 
> @app.route('/')
> def hello():
>   return "Hello, World!"
> 
> if __name__ == '__main__':
>   app.run()

gunicorn.conf.py

> import psutil
> import logging
> from google.cloud import logging as cloud_logging
> 
> def post_fork(server, worker):
>   # Disable GC inside worker processes as well 
>   gc.set_threshold(0)
>   atexit.register(os._exit, 0) # from IG's analysis
> 
>   try:
>     client = cloud_logging.Client()
>     client.setup_logging()
>   except Exception as e:
>     logging.basicConfig(level=logging.INFO)
> 
>   process = psutil.Process()
>   pid = process.pid
> 
>   mem = process.memory_info().rss / (1024 ** 2)
>   shared = process.memory_info().shared / (1024 ** 2)
> 
>   logging.info(f"Process {pid} memory: {mem:.2f} MB")
>   logging.info(f"Process {pid} shared memory: {shared:.2f} MB")
> 
>   \# Log memory of master process
>   mpid = psutil.Process(pid).ppid()
>   mp = psutil.Process(mpid)
> 
>   mmem = mp.memory_info().rss / (1024 ** 2)
>   mshared = mp.memory_info().shared / (1024 ** 2)
> 
>   logging.info(f"Master {mpid} memory: {mmem:.2f} MB")
>   logging.info(f"Master {mpid} shared memory: {mshared:.2f} MB")

In all the experiments I have run so far, the shared memory is always 0 for all workers and even master process.

Upvotes: 0

Views: 51

Answers (0)

Related Questions