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

Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web

Reduzca las emisiones de dióxido de carbono (CO2) de su aplicación web utilizando el almacenamiento en caché de los resultados de la consulta y el almacenamiento en caché de plantillas.

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

Objetos, es bueno construir una aplicación con ellos pero tiene una gran desventaja: puede ser muy lenta debido a los CPUciclos extra y a toda la memoria extra que se usa. La ralentización, por supuesto, se debe en gran medida a las capas de mapeo adicionales y a los datos adicionales.

¿Debería preocuparte el rendimiento? Si! La parte administrativa de una aplicación no tiene que ser muy rápida, pero las páginas frontales, las páginas a las que los visitantes pueden acceder deben cargarse lo más rápido posible. Me sorprendió la enorme diferencia entre una ORM consulta y una consulta en bruto SQLAlchemy . Todos estos CPUciclos extra, toda esta memoria extra usada. Y no muy respetuoso con el medio ambiente también. No quiero que mi página web sea responsable del cambio climático .... ;-)

Luego también hay Jinja plantillas. Veo tiempos de 30 milisegundos construyendo las entradas del blog para la página de inicio. ¿Por qué? ¿Cómo es posible? Hice un poco de depuración pero aún no encontré la causa, pero en la puesta en escena y la producción, ambos usando Gunicorn, parece que los Jinja retrasos son mucho menores.

Qué podemos hacer para reducir el tiempo de generación de páginas

Tenemos dos opciones:

1. Abandonar el objeto / ORM modo

- Transformar algunas ORM consultas en consultas SQL sin procesar
- Construir algunas partes de la Jinja plantilla en Python

Pros:
- Muy rápido, también en el primer golpe

Contras:
- Mucho trabajo

2. Agregar caché a nivel de aplicación

- Cachear el resultado de algunas ORM consultas
- Cachear el resultado de algunas partes de la plantilla

Pros:
- Fácil de añadir, no hay muchos cambios de código

Contras:
- El primer golpe sigue siendo lento si los tiempos de caché han expirado

¿Para qué sirve una caché de nivel de aplicación?

Las bases de datos también hacen cache, así que ¿por qué cachear de todos modos? Utilizando la caché de resultados de consultas a nivel de aplicación evitamos el acceso a la base de datos dentro de un tiempo de caché especificado. Muchas páginas de un sitio web son estáticas en el sentido de que sólo cambian de vez en cuando. Si sólo hay una parte de la estática de una página, entonces podemos cachear sólo esa parte.

El almacenamiento en caché no es para el primer visitante, sirve su propósito cuando muchos visitantes acceden al sitio web. Así, el primer visitante obtiene un tiempo de respuesta de página de 200 milisegundos, pero los visitantes siguientes obtendrán un tiempo de respuesta de 30 milisegundos, si visitan las páginas dentro del tiempo de caché.

Hay un problema aquí y es el tiempo registrado por los motores de búsqueda. Google puede calificar su sitio web más bajo debido a las visitas por primera vez (sin caché). No hay mucho que podamos hacer al respecto. Una opción sería buscar previamente ciertos artículos, pero este es un pedido más complejo.

Implementación de una caché

El almacenamiento en caché no es realmente difícil de codificarnos a nosotros mismos. También podríamos usar Flask-Caching aquí. Me gusta la función de Caching Jinja2 Snippets:

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

Pero como he dicho antes, quiero hacer todo lo posible por mí mismo, así es como aprendí. Flask.. En el pasado implementé el caching para un PHP sitio web utilizando datos de un sistema remoto que resultó ser muy efectivo, así que ahora hagámoslo en Python...

El objeto de caché

Como esta aplicación web se ejecuta en un Linux sistema, creo que no hay necesidad de Redis o Memcachedpodemos usar el sistema de archivos para el almacenamiento en caché. El Linux sistema de archivos es lo suficientemente rápido. Sólo guardo en caché algunos resultados de consultas y partes de Jinja plantillas. ¿Cómo sabe qué debe almacenar en caché? Esto es simple, medir, medir, medir, medir, ver también el post sobre Perfiles. He añadido temporizadores a la página de inicio con entradas de blog, a la página de entradas de blog. Luego seleccioné las consultas y las partes de la plantilla que ahorrarían más tiempo.

La implementación del objeto de almacenamiento en caché se parece mucho a la implementación de Configuración, ver un post anterior. Lo bueno de Flask esto es que podemos crear un objeto durante el tiempo de creación de la aplicación, y usarlo durante el tiempo de ejecución de la aplicación.

A continuación se muestra el código de la clase de caché. Tiene dos funciones principales: volcar, para almacenar datos en la caché, y cargar, para obtener datos de la caché. Ambos utilizan un cache_item_id que identifica el elemento en caché. Pickl Se utilizan objetos de escritura en un archivo y objetos de lectura de un archivo. En el momento de la descarga también podemos pasar otros parámetros como el tiempo de caché no predeterminado y un tipo cache_item_type. Este tipo no es necesario, lo uso para seleccionar un subdirectorio diferente para que todos los resultados de la consulta estén en el directorio A y todos los render_template resultados en el directorio B. Fácil de depurar.

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

Al igual que con la clase Settings, instancio el objeto en la función create_app:

    app.app_cache = AppCache(app)

Ahora podemos usarlo en otras vistas llamando current_appa .app_cache.dump() y 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

Resultados de la consulta en caché

Deberíamos limitarnos a las consultas que realmente ralentizan el sistema. Un buen candidato es la consulta que recibe las entradas del blog para la página de inicio. La página de inicio se puede mostrar en diferentes idiomas y las entradas del blog se dividen en páginas que tienen un máximo de 10 entradas en una página. Esto significa que el cache_item_id debe reflejar tanto el idioma como el número de página:

    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)

Resultados del almacenamiento en caché

En Flask usamos render_template para generar el HTML para una página. Para la página de inicio tengo un index.html
que incluye un for-loop para imprimir las entradas del blog. Para mejorar el rendimiento podemos cachear exactamente esta parte. Para ello añadimos otra render_template función. Primero renderizamos los blog_posts, el resultado se llama rendered_blog_posts. En la segunda render_template usamos las entradas de blog renderizadas.

    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)

Por supuesto que debemos dividir el archivo de plantilla index.html en dos archivos, los llamé index_blog_posts.html e
index_using_rendered_blog_posts.html. Primero copiamos index.html a estos dos archivos, luego los editamos. Como puedes imaginar, el index_blog_posts.html se parece a esto:

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

y el index_using_rendered_blog_posts.html tiene el siguiente aspecto:

{% extends "content_right_column.html" %}

{% block content %}

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

Resumen

Los cambios anteriores redujeron el tiempo de generación de la página de inicio de 150 milisegundos a 30 milisegundos en mi PC local. El almacenamiento en caché es una forma muy eficaz de reducir los tiempos de respuesta. De hecho, creo que la mayoría Flask SQLAlchemy de los sitios web no pueden funcionar sin él. Los cambios en el código son mínimos y sólo tenemos que añadir caché donde realmente importa. Si realmente necesitamos el rendimiento del primer golpe, siempre podemos convertir nuestras ORM consultas"lentas" a SQL sin procesar. Hasta ese momento, nos centramos en la funcionalidad, no en las optimizaciones que requieren mucho tiempo.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.