flaskgoogle-cloud-platformgoogle-app-enginegoogle-cloud-datastore

Google App Engine slow cold boot time (Flask app)


I have a problem with Google App Engine that hosts my Flask app.

The Flask app's size iz 60 MB.

The requirements.txt for the app is:

python-bidi>=0.4.2,<0.5.0
Flask
mock
google-auth
pytest
google-cloud-ndb
bcrypt
google-cloud-tasks
google-cloud-storage
bleach
openpyxl
pandas
xlrd
protobuf<4.0.0dev,>=3.15.0
xmltodict
stripe
cryptography
pycountry
openai
PyPDF2
deep_translator
reportlab
xhtml2pdf
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
oauth2client
werkzeug>=2
requests>=2,<3
identity>=0.5.1,<0.6

The app is used for converting files from CSV to XML, and also it's using some API endpoints to retrieve data and covert it to XML and PDF.

So anyways, the problem is that the cold boot start of an instance on Google App Engine is taking 40 seconds and sometimes even more. The instance class for GAE is: F4_1G

The scaling of instances is automatic in app.yaml Warmup requests are enabled. App is using Python 3.9

Logging example of a cold boot start: Logging example of a cold boot start

Logging after the request: Logging after the request

Stack trace: Stack trace of the request

I don't see anything useful from the stack trace, it just displays that the loading took 40 seconds. After the cold start is done, then the responses are fast (average 40ms).

Any help would be appreciated, I have tried numerous fixes but nothing worked (warmup requests, reducing required libraries, I can't select a smaller instance class because then the memory is too low).

I can also provide more screenshots/information if needed.


Solution

  • I solved this issue using Flask Lazy loading views. Rather than importing everything in your main application .py file, just use lazy loading. This got my Flask app cold boot loading from 37 seconds down to 6-8 seconds. This doc helped me solve it: https://flask.palletsprojects.com/en/stable/patterns/lazyloading/

    This is an example. So this is how the code used to be in main.py (main Flask app file where the routes are set and the app.run() is located):

    from handlers.profile import profile_main
    
    app.add_url_rule(rule="/profile", view_func=profile_main, methods=["GET"])
    

    And this is the new way, using lazy loading:

    from utils.lazy_loading_handlers import LazyView
    
    app.add_url_rule(rule="/profile", view_func=LazyView('handlers.profile.profile_main'), methods=["GET"])
    

    The LazyView function looks like this:

    from werkzeug.utils import import_string, cached_property
    
    
    class LazyView(object):
        def __init__(self, import_name):
            self.__module__, self.__name__ = import_name.rsplit('.', 1)
            self.import_name = import_name
    
        @cached_property
        def view(self):
            return import_string(self.import_name)
    
        def __call__(self, *args, **kwargs):
            return self.view(*args, **kwargs)
    

    So essentially what we are doing is, importing handlers after they are needed (ie. when the route is called) and not when the app is starting. This also saves on memory consumption. Before, the app was using 1600MB of memory, now when it starts up, only 460MB of memory. This is because not all libraries are loaded. They are loaded only when needed.

    The only downside to this is: The first time you open a route after the app started from cold boot, the response time could be around 2-4 seconds. This is because, before we were already initializing everything when the app started, but now it will only start initializing a handler once needed. But either way, I think this is the only normal way to run a Flask app on Google App Engine... You can't have customers wait for 37 seconds for the app to fully load, that is just ridiculous.