angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Réduire les temps de réponse d'un Flask SQLAlchemy site web

Réduisez les émissions de dioxyde de carbone (CO2) de votre application Web en utilisant la mise en cache des résultats des requêtes et des modèles.

29 août 2019
post main image
unsplash.com/@agkdesign

Objets, c'est bien de construire une application avec eux mais cela a un très gros inconvénient : cela peut être très lent à cause des CPUcycles supplémentaires et de toute la mémoire supplémentaire utilisée. Le ralentissement est bien sûr causé en grande partie par les couches supplémentaires de mappage et les données supplémentaires.

Devriez-vous vous soucier de la performance ? Oui ! La partie admin d'une application n'a pas besoin d'être très rapide mais les pages frontend, les pages auxquelles les visiteurs peuvent accéder doivent être chargées aussi vite que possible. J'ai été choqué par l'énorme différence entre une ORM requête et une requête brute SQLAlchemy . Tous ces CPUcycles supplémentaires, toute cette mémoire supplémentaire utilisée. Et pas très respectueux de l'environnement. Je ne veux pas que mon site web soit responsable du changement climatique... ;- -)

Ensuite, il y a aussi les Jinja modèles. Je vois des temps de 30 millisecondes à construire les billets du blog pour la page d'accueil. Pourquoi ? Comment est-ce possible ? J'ai fait un peu de débogage mais je n'ai toujours pas trouvé la cause, mais sur staging et en production, les deux en utilisant Gunicorn, il semble que les Jinja délais sont beaucoup plus petits.

Ce que nous pouvons faire pour réduire le temps de génération des pages

Nous avons deux options :

1. Quitter l'objet / ORM le mode

- Transformer certaines ORM requêtes en requêtes brutes SQL
- Construire des parties de Jinja modèle dans Python

Les pros :
- Très rapide, même au premier coup

Inconvénients :
- Beaucoup de travail

2. Ajout de la mise en cache au niveau de l'application

- Mettre en cache le résultat de certaines ORM requêtes
- Mettre en cache le résultat de certaines parties du modèle

Les pros :
- Facile à ajouter, peu de changements de code

Inconvénients :
- Le premier résultat reste lent si les temps de cache sont expirés.

Quelle est l'utilité d'un cache au niveau de l'application ?

Les bases de données font aussi de la mise en cache, alors pourquoi mettre en cache de toute façon ? En utilisant la mise en cache des résultats des requêtes au niveau de l'application, nous évitons l'accès à la base de données dans un temps de mise en cache spécifié. De nombreuses pages d'un site Web sont statiques en ce sens qu'elles ne changent qu'une fois de temps en temps. S'il n'y a qu'une partie d'une page statique, nous ne pouvons mettre en cache que cette partie.

La mise en cache n'est pas pour le premier visiteur, elle sert son but lorsque de nombreux visiteurs accèdent au site Web. Ainsi, le premier visiteur obtient un temps de réponse de 200 millisecondes, mais les visiteurs suivants obtiendront un temps de réponse de 30 millisecondes, s'ils visitent les pages dans le temps de cache.

Il y a un problème ici et c'est le temps enregistré par les moteurs de recherche. Google peut noter votre site Web plus bas en raison des premières visites (pas de cache). Il n'y a pas grand-chose que nous puissions faire à ce sujet. Une option serait de pré-télécharger certains articles mais ceci une commande plus complexe.

Implémentation d'un cache

La mise en cache n'est pas vraiment difficile à coder soi-même. Nous pourrions également utiliser Flask-Caching ici. J'aime bien la fonction Caching Jinja2 Snippets :

{% cache [timeout [,[key1, [key2, ...]]]] %}
     ...
{% endcache %}

Mais comme je l'ai déjà dit, je veux faire autant que possible moi-même, c'est comme ça que j'apprends Python, Flaskdans le passé j'ai implémenté la mise en cache pour un PHP site web en utilisant des données d'un système distant qui s'est avéré très efficace, alors maintenant faisons cela en Python.

L'objet de mise en cache

Comme cette application de site web fonctionne sur un Linux système, je crois qu'il n'y a pas besoin de Redis ou Memcached, nous pouvons utiliser le système de fichiers pour la mise en cache. Le Linux système de fichiers est assez rapide. Je cache seulement quelques résultats de requête et des parties de Jinja modèle. Comment savez-vous ce que vous devez mettre en cache ? C'est simple, mesurer, mesurer, mesurer, mesurer, voir aussi l'article sur le profilage. J'ai ajouté des minuteries à la page d'accueil avec des billets de blog, à la page de billets de blog. Ensuite, j'ai sélectionné les requêtes et les parties du modèle qui me feraient gagner le plus de temps.

L'implémentation de l'objet cache ressemble beaucoup à l'implémentation Settings, voir un message précédent. L'avantage Flask est que nous pouvons créer un objet pendant la création de l'application, et l'utiliser pendant l'exécution de l'application.

Ci-dessous se trouve le code de la classe de mise en cache. Il a deux fonctions principales : dump, pour stocker les données dans le cache, et load, pour récupérer les données du cache. Les deux utilisent un cache_item_id qui identifie l'élément mis en cache. Pickl est utilisé pour écrire des objets dans un fichier et lire les objets d'un fichier. Au moment du dump, nous pouvons aussi passer d'autres paramètres comme le temps de cache non par défaut et un cache_item_type. Ce type n'est pas nécessaire, je l'utilise pour sélectionner un sous-répertoire différent afin que tous les résultats de la requête soient dans le répertoire A et tous les render_template résultats dans le répertoire B. Facile à déboguer.

class AppCache:

    def __init__(self, app=None):
        self.app = app
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        self.cache_item_id2cache_details = {}

    def get_subdir_and_seconds(self, cache_item_type, cache_item_seconds):
        ...
        return cache_subdir, cache_seconds

    def dump(self, cache_item_id, cache_item_data, cache_item_type=None , cache_item_seconds=None):

        cache_subdir, cache_seconds = self.get_subdir_and_seconds(cache_item_type, cache_item_seconds)

        cache_file = os.path.join('cache', cache_subdir, cache_item_id + '.pickle')

        temp_name = next(tempfile._get_candidate_names())
        cache_file_tmp = os.path.join('cache', cache_subdir, cache_item_id + '_' + temp_name + '.pickle')

        try:
            fh = open(cache_file_tmp, 'wb')
            pickle.dump(cache_item_data, fh)
            fh.close()
            # rename is atomic
            os.rename(cache_file_tmp, cache_file)
        except:
            return False

        # no errors so store
        if cache_item_id not in self.cache_item_id2cache_details:
            self.cache_item_id2cache_details[cache_item_id] = { 'cache_file': cache_file, 'cache_seconds': cache_seconds } 
        else:
            self.cache_item_id2cache_details[cache_item_id]['cache_file'] = cache_file
            self.cache_item_id2cache_details[cache_item_id]['cache_seconds'] = cache_seconds
        return True

    def load(self, cache_item_id):
        if cache_item_id in self.cache_item_id2cache_details:
            cache_file = self.cache_item_id2cache_details[cache_item_id]['cache_file']
            cache_seconds = self.cache_item_id2cache_details[cache_item_id]['cache_seconds']

            try:
                if os.path.isfile(cache_file):
                    mtime = os.path.getmtime(cache_file)
                    if (mtime + cache_seconds) > time.time():
                        fh = open(cache_file, 'rb')
                        return True, pickle.load(fh) 
            except:
                pass

        return False, None

Comme pour la classe Settings, j'instancie l'objet dans la fonction create_app :

    app.app_cache = AppCache(app)

Maintenant nous pouvons l'utiliser dans d'autres vues en appelant current_app.app_cache.dump() et current_app.app_cache.load() :

    cache_item_id = 'some_cache_item_id'
    hit, data = current_app.app_cache.load(cache_item_id)
    if not hit:
        # get the data somewhere
        ...
        # cache it
        current_app.app_cache.dump(cache_item_id, data)
    ...
    # use data

Mise en cache ORM des résultats de la requête

Nous devrions nous limiter aux requêtes qui ralentissent vraiment le système. Un bon candidat est la requête qui obtient les billets de blog pour la page d'accueil. La page d'accueil peut être affichée dans différentes langues et les billets de blog sont divisés en pages ayant un maximum de 10 billets de blog sur une page. Cela signifie que le cache_item_id doit refléter à la fois la langue et le numéro de page :

    cache_item_id = 'home_page_blog_posts_' + lang_code + '_p' + str(page_number)
    hit, home_page_blog_posts = current_app.app_cache.load(cache_item_id)
    if not hit:
        home_page_blog_posts = get_home_page_blog_posts(lang_code, page_number)
        # cache it
        current_app.app_cache.dump(cache_item_id, home_page_blog_posts)

Mise en cache Jinja render_template des résultats

Dans Flask nous utilisons render_template pour générer le HTML d'une page. Pour la page d'accueil, j'ai un index.html
qui inclut une boucle for pour imprimer les billets du blog. Pour améliorer les performances, nous pouvons mettre en cache exactement cette partie. Pour ce faire, nous ajoutons une autre render_template fonction. Tout d'abord nous rendons le blog_posts, le résultat est appelé rendered_blog_posts. Dans la seconde render_template , nous utilisons les billets de blog rendus.

    cache_item_id = 'homepage_blog_posts_' + lang_code + '_p' + str(page_number)
    hit, rendered_blog_posts = current_app.app_cache.load(cache_item_id)
    if not hit:
        rendered_blog_posts = render_template(
            'pages/index_blog_posts.html', 
            page_title=page_title,
            ...
            home_page_blog_posts=home_page_blog_posts)

        # cache it
        current_app.app_cache.dump(cache_item_id, rendered_blog_posts)

    ...
    return render_template(
        'pages/index_using_rendered_blog_posts.html', 
        page_title=page_title,
        rendered_blog_posts=rendered_blog_posts)

Bien sûr, nous devons diviser le fichier modèle index.html en deux fichiers, je les ai appelés index_blog_posts.html et
index_using_rendered_blog_posts.html. Nous copions d'abord index.html dans ces deux fichiers, puis nous éditons les fichiers. Comme vous pouvez l'imaginer, le fichier index_blog_posts.html ressemble à ceci :

    {% if blog_posts %}
        {% for blog_post in blog_posts %}
            <div class="row">
                <div class="col-12 p-0">

                    <h1><a href="{{ url_for('pages.blog_post_view', slug=blog_post.slug) }}">{{ blog_post.title }}</a></h1>

                </div>
            </div>
            ...
        {% endfor %}

        {{ list_page_pagination(pagination) if pagination }}

    {% endif %}

et l'index_utilisant_rendu_blog_posts.html ressemble à ceci :

{% extends "content_right_column.html" %}

{% block content %}

    {% if rendered_blog_posts %}
        {{ rendered_blog_posts|safe }}
    {% endif %}
                
{% endblock %}

Résumé

Les changements ci-dessus ont réduit le temps de génération de la page d'accueil de 150 millisecondes à 30 millisecondes sur mon PC local. La mise en cache est un moyen très efficace de réduire les temps de réponse. En fait, je crois que la plupart Flask SQLAlchemy des sites Web ne peuvent pas fonctionner sans elle. Les changements de code sont minimes et nous n'avons qu'à ajouter la mise en cache là où c'est vraiment important. Si nous avons vraiment besoin de performances de premier coup, nous pouvons toujours convertir nos ORM requêtes'lentes' en requêtes brutes SQL. D'ici là, nous nous concentrons sur la fonctionnalité et non sur des optimisations qui prennent beaucoup de temps.

Liens / crédits

Flask Extension Development
https://flask.palletsprojects.com/en/1.1.x/extensiondev/

Flask-Caching
https://pythonhosted.org/Flask-Caching/

pickle — Python object serialization
https://docs.python.org/3/library/pickle.html

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.