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

Verkürzung der Seitenreaktionszeiten einer Flask SQLAlchemy Website

Reduzieren Sie die Kohlendioxid-(CO2)-Emissionen Ihrer Webanwendung durch Query-Ergebnis-Caching und Template-Caching.

29 August 2019
post main image
unsplash.com/@agkdesign

Objekte, es ist schön, eine Anwendung mit ihnen zu erstellen, aber es hat einen sehr großen Nachteil: Es kann sehr langsam sein, wegen der zusätzlichen CPUZyklen und des gesamten zusätzlichen Speichers. Die Verlangsamung wird natürlich sehr stark durch die zusätzlichen Mapper-Layer und zusätzlichen Daten verursacht.

Solltest du dich um die Leistung kümmern? Ja! Der Admin-Teil einer Anwendung muss nicht sehr schnell sein, aber die Frontend-Seiten, auf die die Besucher zugreifen können, müssen so schnell wie möglich geladen werden. Ich war schockiert über den enormen Unterschied zwischen einer ORM Abfrage und einer Rohabfrage SQLAlchemy . All diese zusätzlichen CPUZyklen, all dieser zusätzliche Speicherverbrauch. Und auch nicht sehr umweltfreundlich. Ich möchte nicht, dass meine Website für den Klimawandel verantwortlich ist.... ;-)

Dann gibt es noch die Jinja Templatierung. Ich sehe Zeiten von 30 Millisekunden beim Aufbau der Blog-Posts für die Homepage. Warum? Wie ist das möglich? Ich habe einige Fehlersuche gemacht, aber ich habe die Ursache noch nicht gefunden, aber bei der Bereitstellung und Produktion, sowohl bei der Verwendung Gunicornvon, scheint es, dass die Jinja Verzögerungen viel kleiner sind.

Was wir tun können, um die Zeit der Seitenerstellung zu verkürzen

Wir haben zwei Möglichkeiten:

1. Verlassen des Objekts / ORM Modus

- Transformieren Sie einige ORM Abfragen in rohe SQL-Abfragen
- Erstellen Sie einige Jinja Vorlagenteile in der Datei Python

Vorteile:
- Sehr schnell, auch beim ersten Treffer

Nachteile:
- Sehr viel Arbeit

2. Hinzufügen von Caching auf Anwendungsebene

- Zwischenspeichern des Ergebnisses einiger ORM Abfragen
- Zwischenspeichern des Ergebnisses einiger Vorlagenteile

Vorteile:
- Einfach hinzuzufügen, wenig Code-Änderungen

Cons:
- Der erste Treffer bleibt langsam, wenn die Cache-Zeiten abgelaufen sind.

Wozu dient ein Cache auf Anwendungsebene?

Datenbanken zwischenspeichern auch, warum also überhaupt zwischenspeichern? Mit dem Application Level Query Result Caching vermeiden wir den Zugriff auf die Datenbank innerhalb einer bestimmten Cache-Zeit. Viele Seiten auf einer Website sind statisch in dem Sinne, dass sie sich nur ab und zu ändern. Wenn es nur einen Teil einer statischen Seite gibt, dann können wir nur diesen Teil zwischenspeichern.

Caching ist nicht für den ersten Besucher, es dient seinem Zweck, wenn viele Besucher auf die Website zugreifen. So erhält der erste erste Besucher eine Seitenreaktionszeit von 200 Millisekunden, aber die nachfolgenden Besucher erhalten eine Reaktionszeit von 30 Millisekunden, wenn sie die Seiten innerhalb der Cachezeit besuchen.

Es gibt hier ein Problem, und das ist die Zeit, die von Suchmaschinen erfasst wird. Google kann Ihre Website aufgrund von erstmaligen (nicht zwischengespeicherten) Besuchen niedriger bewerten. Es gibt nicht wirklich viel, was wir darüber wissen. Eine Option wäre, bestimmte Elemente vorab abzurufen, aber dies ist eine komplexere Bestellung.

Implementierung eines Caches

Caching ist nicht wirklich schwierig, sich selbst zu kodieren. Wir könnten hier auch -Caching verwenden Flask. Ich mag die Funktion Caching Jinja2 Snippets:

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

Aber wie gesagt, ich möchte so viel wie möglich selbst tun, so lerne Pythonich, Flask.... In der Vergangenheit habe ich das Caching für eine PHP Website mit Daten aus einem entfernten System implementiert, das sich als sehr effektiv erwiesen hat, also lassen Sie uns das jetzt in Python.....

Das Cachingobjekt

Da diese Website-App auf einem Linux System läuft, glaube ich, dass es keine Notwendigkeit gibt Redis oder Memcached, wir können das Dateisystem für das Caching verwenden. Das Linux Dateisystem ist schnell genug. Ich speichere nur einige Abfrageergebnisse und Jinja Vorlagenteile im Cache. Woher weißt du, was du zwischenspeichern sollst? Dies ist einfach, messen, messen, messen, messen, messen, siehe auch den Beitrag über Profiling. Ich habe Timer auf der Startseite mit Blog-Posts und auf der Blog-Post-Seite hinzugefügt. Dann wählte ich die Abfragen und Vorlagenteile aus, die am meisten Zeit sparen würden.

Die Implementierung des Cachingobjekts sieht sehr ähnlich aus wie die Implementierung von Einstellungen, siehe vorherigen Beitrag. Schöne Sache Flask ist, dass wir ein Objekt während der Erstellungszeit der App erstellen und es während der Laufzeit der App verwenden können.

Nachfolgend finden Sie den Code der Caching-Klasse. Es hat zwei Hauptfunktionen: Dump, um Daten im Cache zu speichern, und Load, um Daten aus dem Cache zu holen. Beide verwenden eine cache_item_id, die das zwischengespeicherte Element identifiziert. Pickl Sie verwenden Schreibobjekte in eine Datei und lesen Objekte aus einer Datei. Zur Speicherauszugszeit können wir auch andere Parameter wie die nicht standardmäßige Cache-Zeit und einen cache_item_type übergeben. Dieser Typ ist nicht notwendig, ich benutze ihn, um ein anderes Unterverzeichnis auszuwählen, so dass alle Abfrageergebnisse im Verzeichnis A und alle render_template Ergebnisse im Verzeichnis B sind. Leicht zu debuggen.

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

Wie bei der vorherigen Klasse Settings instanziiere ich das Objekt in der Funktion create_app:

    app.app_cache = AppCache(app)

Jetzt können wir es in anderen Ansichten verwenden, indem wir current_app.app_cache.dump() und current_app.app_cache.load() aufrufen:

    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

Zwischenspeichern ORM von Abfrageergebnissen

Wir sollten uns auf Anfragen beschränken, die das System wirklich verlangsamen. Ein guter Kandidat ist die Anfrage, die die Blog-Posts für die Homepage erhält. Die Startseite kann in verschiedenen Sprachen angezeigt werden und die Blogbeiträge sind in Seiten mit maximal 10 Blogbeiträgen auf einer Seite aufgeteilt. Das bedeutet, dass die cache_item_id sowohl die Sprache als auch die Seitenzahl enthalten muss:

    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)

Jinja render_template Caching-Ergebnisse

In Flask verwenden render_template wir, um das HTML für eine Seite zu generieren. Für die Homepage habe ich eine index.html
, die eine for-Schleife zum Drucken der Blog-Einträge enthält. Um die Leistung zu verbessern, können wir genau diesen Teil zwischenspeichern. Dazu wird eine weitere render_template Funktion hinzugefügt. Zuerst rendern wir die blog_posts, das Ergebnis heißt rendered_blog_posts. In der zweiten render_template verwenden wir die gerenderten Blog-Posts.

    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)

Natürlich müssen wir die Vorlagendatei index.html in zwei Dateien aufteilen, ich nannte sie index_blog_posts.html und
index_using_rendered_blog_posts.html. Zuerst kopieren wir index.html in diese beiden Dateien, dann bearbeiten wir die Dateien. Wie du dir vorstellen kannst, sieht die index_blog_posts.html in etwa so aus:

    {% 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 %}

und die index_using_rendered_blog_posts.html sieht so aus:

{% extends "content_right_column.html" %}

{% block content %}

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

Zusammenfassung

Die obigen Änderungen reduzierten die Erstellungszeit der Homepage von 150 Millisekunden auf 30 Millisekunden auf meinem lokalen PC. Das Caching ist eine sehr effektive Möglichkeit, die Reaktionszeiten zu verkürzen. Tatsächlich glaube ich, dass die meisten Flask SQLAlchemy Websites ohne sie nicht funktionieren können. Die Code-Änderungen sind minimal und wir müssen nur dort Caching hinzufügen, wo es wirklich wichtig ist. Wenn wir wirklich die Leistung des ersten Treffers benötigen, können wir unsere langsamen ORM Anfragen jederzeit in Roh-SQL umwandeln. Bis dahin konzentrieren wir uns auf die Funktionalität, nicht auf zeitraubende Optimierungen.

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.