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

Уменьшение времени отклика на запросы на странице Flask SQLAlchemy веб-сайта

Сократите выбросы углекислого газа (CO2) в вашем веб-приложении с помощью кэширования результатов запросов и шаблонного кэширования.

29 августа 2019
post main image
unsplash.com/@agkdesign

Объекты, с ними хорошо собирать приложение, но у него есть один очень большой недостаток: он может быть очень медленным из-за дополнительных CPU-циклов и всей используемой дополнительной памяти. Конечно, замедление в значительной степени вызвано дополнительными слоями картографа и дополнительными данными.

Должны ли вы заботиться о производительности? Да! Административная часть приложения не должна быть очень быстрой, но страницы переднего плана, к которым посетители могут получить доступ, должны загружаться как можно быстрее. Я был шокирован огромной разницей между ORM запросом и грубым запросом. Все эти лишние CPUциклы, вся эта лишняя память использовалась. И не очень дружелюбны к окружающей среде. Я не хочу, чтобы мой вебсайт нес ответственность за изменение климата... ;-)

Также есть и Jinja шаблоны. Я вижу время в 30 миллисекунд создания записей в блоге для главной страницы. Почему? Как такое возможно? Я сделал некоторую отладку, но до сих пор не нашел причину, но на этапе постановки и производства, оба с использованием, кажется, Jinja задержки гораздо меньше.

Что мы можем сделать для сокращения времени создания страниц

У нас есть два варианта:

1. Оставить объект / ORM режим работы

- Преобразование некоторых запросов в сырые SQL-запросы - Постройте некоторые части Jinja шаблона следующим образом Python

Профессионалы:
- Очень быстро, также при первом попадании.

Противники:
- Очень много работы.

2. Добавить кэширование прикладного уровня

- Кэшировать результат некоторых ORM запросов - кэшировать результат некоторых частей шаблона.

Профессионалы:
- Легко добавляется, не много изменений кода.

Против:
- Первое попадание остается медленным, если истекло время кэширования.

Что такое использование кэша прикладного уровня?

Базы данных также делают кэширование, так зачем вообще кэшировать? Используя кэширование результатов запросов прикладного уровня, мы избегаем доступа к БД в течение указанного времени кэширования. Многие страницы сайта статичны в том смысле, что они меняются только время от времени. Если есть только часть страницы, то мы можем кэшировать только эту часть.

Кэширование не для первого посетителя, оно служит своей цели, когда многие посетители посещают сайт. Таким образом, первый посетитель получает время отклика страницы 200 миллисекунд, а последующие посетители получат время отклика 30 миллисекунд, если они посещают страницы в течение времени кэширования.

Здесь есть проблема, и это время регистрируется поисковыми системами. Google может понизить оценку вашего сайта из-за первого посещения (без кэширования). Мы ничего не можем поделать с этим. Вариант может заключаться в предварительной выборке некоторых позиций, но это более сложный заказ.

Реализация кэша

Кэширование не представляет особой сложности для нас самих. Мы также можем использовать Flask-Caching здесь. Мне нравится функция кэширования Jinja2 фрагментов:

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

Но, как я уже говорил ранее, я хочу сделать как можно больше сам, то есть как я учился, .. Flask. В прошлом я реализовал кэширование для PHP сайта с использованием данных из удаленной системы, которая оказалась очень эффективной, так что теперь давайте сделаем это в . Python..

Объект кэширования

Поскольку это веб-приложение работает на Linux системе, я считаю, что в нем нет необходимости Redis или , мы можем использовать файловую систему для кэширования. Файловая Linux система работает достаточно быстро. Я кэширую только некоторые результаты запросов и Jinja шаблоны. Откуда ты знаешь, что нужно кэшировать? Это просто, измерение, измерение, измерение, измерение, см. также пост о Профилировании. Я добавил таймеры на домашнюю страницу с записями в блоге, на страницу с записями в блоге. Затем я выбрал те запросы и части шаблона, которые сэкономят больше всего времени.

Реализация объекта кэширования очень похожа на реализацию Settings, см. предыдущий пост. Приятным моментом Flask является то, что мы можем создавать объекты во время создания приложений и использовать их во время работы приложений.

Ниже приведен код класса кэширования. Она имеет две основные функции: дамп, для хранения данных в кэше и загрузка, для получения данных из кэша. Оба используют cache_item_id, который идентифицирует кэшируемый элемент. Pickl используется для записи объектов в файл и чтения объектов из файла. Во время дампа мы также можем передавать другие параметры, такие как время кэширования по умолчанию и тип кэша cache_item_type. Этот тип необязателен, я использую его для выбора другого подкаталога, поэтому все результаты запроса находятся в каталоге A, а все render_template результаты - в каталоге B. Легко отлаживаются.

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

Как и в случае с предыдущим классом Settings, я инстанцирую объект в функции create_app:

    app.app_cache = AppCache(app)

Теперь мы можем использовать его в других представлениях, вызывая .app_cache.dump() и .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

Результаты кэширования ORM запросов

Мы должны ограничиться запросами, которые действительно замедляют работу системы. Хорошим кандидатом является запрос, который получает посты в блоге для главной страницы. Домашняя страница может отображаться на разных языках, а сообщения в блоге разделены на страницы, содержащие не более 10 сообщений на одной странице. Это означает, что cache_item_id должен отражать как язык, так и номер страницы:

    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 Результаты кэширования

Flask Мы используем для генерации HTML-кода страницы. Для домашней страницы у меня есть index.html
, который включает в себя петлю для печати постов блога. Для повышения производительности мы можем кэшировать именно эту часть. Для этого мы добавляем еще одну render_template функцию. Сначала мы визуализируем блог_посты, результат называется render_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)

Конечно, мы должны разделить файл шаблона index.html на два файла, я назвал их index_blog_posts.html и
index_using_rendered_blog_posts.html. Сначала мы копируем файл index.html в эти два файла, затем редактируем их. Как вы можете себе представить, файл index_blog_posts.html выглядит примерно так:

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

и индекс_using_rendered_blog_posts.html выглядит следующим образом:

{% extends "content_right_column.html" %}

{% block content %}

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

Резюме

Вышеуказанные изменения сократили время генерации домашней страницы со 150 миллисекунд до 30 миллисекунд на моем локальном ПК. Кэширование является очень эффективным способом сокращения времени отклика. На самом деле, я считаю, что большинство сайтов не могут работать без него. Изменения в коде минимальны и нам остаётся только добавить кэширование там, где это действительно важно. Если нам действительно нужна первая производительность, мы всегда можем конвертировать наши "медленные" ORM запросы в сырой SQL. До этого времени мы концентрируемся на функциональности, а не на трудоемких оптимизациях.

Ссылки / кредиты

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.