Поделиться через


Повышение производительности Python приложений в Azure Functions

При разработке Azure Functions с помощью Python необходимо понять, как выполняются функции и как эта производительность влияет на масштаб приложения-функции. При разработке высокопроизводительных приложений необходимость в этом становится особенно важной. Основными факторами, которые следует учитывать при разработке, написании и настройке функциональных приложений, являются конфигурации горизонтального масштабирования и производительности пропускной способности.

Горизонтальное масштабирование

По умолчанию Azure Functions автоматически отслеживает нагрузку в приложении и создает дополнительные экземпляры узлов для Python по мере необходимости. Azure Functions использует встроенные пороговые значения для различных типов триггеров, чтобы решить, когда следует добавлять экземпляры, например возраст сообщений и размер очереди для QueueTrigger. Эти пороговые значения не настраивается пользователем. Дополнительные сведения см. в статье Масштабирование на основе событий в Azure Functions.

Повышение производительности пропускной способности

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

Тип рабочей нагрузки Характеристики функционального приложения Примеры
ограничение по вводу-выводу • Приложению необходимо обрабатывать множество одновременных вызовов.
• Приложение обрабатывает большое количество событий ввода-вывода, таких как сетевые вызовы и операции чтения и записи диска.
• Веб-API
с привязкой к ЦП • Приложение выполняет длительные вычисления, такие как изменение размера изображения.
• Приложение выполняет преобразование данных.
• Обработка данных
• Вывод машинного обучения

Так как рабочие нагрузки функций реального мира обычно представляют собой сочетание привязанных операций ввода-вывода и ЦП, следует профилировать приложение под реалистичными рабочими нагрузками.

Конфигурации, относящиеся к производительности

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

Асинхронный режим

Так как Python — это однопоточная среда выполнения, экземпляр узла для Python может обрабатывать вызов только одной функции по умолчанию. Для приложений, которые обрабатывают большое количество событий ввода-вывода и /или привязаны к ввода-выводам, можно значительно повысить производительность, выполняя функции асинхронно.

Чтобы выполнять функцию асинхронно, используйте async def инструкцию, которая запускает функцию с asyncio напрямую:

async def main():
    await some_nonblocking_socket_io_op()

Ниже приведен пример функции с триггером HTTP, использующим http-клиент aiohttp :

import aiohttp

import azure.functions as func

async def main(req: func.HttpRequest) -> func.HttpResponse:
    async with aiohttp.ClientSession() as client:
        async with client.get("PUT_YOUR_URL_HERE") as response:
            return func.HttpResponse(await response.text())

    return func.HttpResponse(body='NotFound', status_code=404)

Функция без ключевого async слова выполняется автоматически в пуле потоков ThreadPoolExecutor:

# Runs in a ThreadPoolExecutor threadpool. Number of threads is defined by PYTHON_THREADPOOL_THREAD_COUNT. 
# The example is intended to show how default synchronous functions are handled.

def main():
    some_blocking_socket_io()

Чтобы в полной мере воспользоваться преимуществами асинхронного выполнения функций, операции ввода-вывода или библиотека, используемая в вашем коде, также должна поддерживать асинхронность. Использование синхронных операций ввода-вывода в функциях, определенных как асинхронные , может повредить общую производительность. Если в библиотеках, которые вы используете, не реализована асинхронная версия, вы по-прежнему можете воспользоваться запуском кода асинхронно, управляя циклом событий в приложении.

Ниже приведены несколько примеров клиентских библиотек, которые реализовали асинхронные шаблоны:

  • aiohttp — http client/server для asyncio
  • API потоков — высокоуровневые примитивы, готовые к асинхронному (async/await) взаимодействию с сетевым подключением.
  • Janus Queue — потокобезопасная очередь с поддержкой asyncio для Python
  • pyzmq — привязки Python для ZeroMQ
Понимание асинхронности в рабочем процессе Python

При определении async перед сигнатурой функции Python помечает функцию как корутин. При вызове корутины её можно запланировать как задачу в цикле событий. При вызове await асинхронной функции он регистрирует продолжение в цикле событий, что позволяет циклу событий обрабатывать следующую задачу во время ожидания.

В нашем работчике Python цикл событий разделяется с функцией async клиента, и он способен обрабатывать несколько запросов одновременно. Мы настоятельно рекомендуем нашим клиентам использовать совместимые с asyncio библиотеки, такие как aiohttp и pyzmq. После выполнения этих рекомендаций увеличивается пропускная способность функции по сравнению с этими библиотеками при синхронном выполнении.

Замечание

Если функция объявлена как async без каких-либо await внутри своей реализации, производительность функции серьезно пострадает, так как цикл событий будет заблокирован, что мешает Python рабочему процессу обрабатывать одновременные запросы.

Используйте несколько языковых рабочих процессов

По умолчанию каждый экземпляр узла Функций имеет единый процесс обработки языка. Вы можете увеличить количество рабочих процессов на узле (до 10) с помощью FUNCTIONS_WORKER_PROCESS_COUNT параметра настройки приложения. Затем Azure Functions пытается равномерно распределить одновременные вызовы функций между этими рабочими.

Для приложений, ограниченных производительностью ЦП, необходимо задать число языковых рабочих процессов равным или большее, чем количество ядер, доступных на каждую функцию. Дополнительные сведения см. в разделе "Доступные номера SKU экземпляра".

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

FUNCTIONS_WORKER_PROCESS_COUNT применяется к каждому узлу, который Azure Functions создает при масштабировании приложения в соответствии с требованиями.

Настройка максимального количества рабочих процессов в среде языкового рабочего процесса

Как упоминалось в асинхронном разделе, рабочая роль языка Python обрабатывает функции и корутины по-другому. Корутин выполняется в том же цикле событий, в котором работает интерпретатор. С другой стороны, вызов функции выполняется в ThreadPoolExecutor, который поддерживается обработчиком языка в виде потока.

Можно задать значение максимального числа рабочих ролей, разрешенных для выполнения функций синхронизации, с помощью параметра приложения PYTHON_THREADPOOL_THREAD_COUNT . Это значение задает аргумент max_worker объекта ThreadPoolExecutor, который позволяет Python использовать пул потоков не более max_worker для выполнения вызовов асинхронно. PYTHON_THREADPOOL_THREAD_COUNT применяется к каждому потоку, который создает хост Функций, и Python решает, когда создать новый поток или повторно использовать простаивающий поток. Для старых версий Python(то есть 3.8, 3.7 и 3.6), значение max_worker имеет значение 1. Для Python версии 3.9 для max_worker задано значение None.

Для приложений, ограниченных производительностью ЦП, следует установить параметр на низкое значение, начиная с 1 и увеличивая его по мере экспериментов с рабочей нагрузкой. Предложение заключается в сокращении времени, затрачиваемого на переключения контекста, и обеспечении завершения задач, интенсивно использующих ЦП.

Для приложений, связанных с вводом-выводом, можно ожидать существенных преимуществ, увеличив количество потоков, задействованных в каждом вызове. Рекомендуется начать с Python по умолчанию (количество ядер) + 4, а затем настроить на основе отображаемых значений пропускной способности.

Для приложений смешанных рабочих нагрузок следует сбалансировать и FUNCTIONS_WORKER_PROCESS_COUNT, и PYTHON_THREADPOOL_THREAD_COUNT конфигурации, чтобы максимально увеличить пропускную способность. Чтобы понять, какие приложения-функции тратят больше всего времени, рекомендуется профилировать их и задавать значения в соответствии с их поведением. Дополнительные сведения об этих параметрах приложения см. в разделе "Использование нескольких рабочих процессов".

Замечание

Несмотря на то что эти рекомендации применяются как к HTTP-триггерам, так и к триггерам, не относящимся к HTTP, может потребоваться настроить другие специфические конфигурации триггеров для функций, не связанных с HTTP, чтобы достичь ожидаемой производительности ваших функционных приложений. Дополнительные сведения об этом см. в разделе Лучшие практики по обеспечению надежности функций Azure.

Управление циклом событий

Следует использовать совместимые с asyncio сторонние библиотеки. Если ни одна из сторонних библиотек не соответствует вашим потребностям, вы также можете управлять циклами событий в Azure Functions. Управление циклами событий обеспечивает большую гибкость в управлении вычислительными ресурсами, а также позволяет упаковывать синхронные библиотеки ввода-вывода в корутины.

Существует множество полезных официальных документов Python, обсуждающих Coroutines и Tasks и Event Loop с помощью встроенной библиотеки asyncio.

Рассмотрим следующую библиотеку requests в качестве примера, этот фрагмент кода использует библиотеку asyncio для преобразования метода requests.get() в корутину, выполняя несколько веб-запросов к SAMPLE_URL одновременно.

import asyncio
import json
import logging

import azure.functions as func
from time import time
from requests import get, Response


async def invoke_get_request(eventloop: asyncio.AbstractEventLoop) -> Response:
    # Wrap requests.get function into a coroutine
    single_result = await eventloop.run_in_executor(
        None,  # using the default executor
        get,  # each task call invoke_get_request
        'SAMPLE_URL'  # the url to be passed into the requests.get function
    )
    return single_result

async def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    eventloop = asyncio.get_event_loop()

    # Create 10 tasks for requests.get synchronous call
    tasks = [
        asyncio.create_task(
            invoke_get_request(eventloop)
        ) for _ in range(10)
    ]

    done_tasks, _ = await asyncio.wait(tasks)
    status_codes = [d.result().status_code for d in done_tasks]

    return func.HttpResponse(body=json.dumps(status_codes),
                             mimetype='application/json')

Вертикальное масштабирование

Вы можете получить больше единиц обработки, особенно в операции, связанной с ЦП, путем обновления до плана premium с более высокими спецификациями. Благодаря более высоким единицам обработки можно настроить количество рабочих процессов в соответствии с количеством доступных ядер и достичь более высокой степени параллелизма.

Дальнейшие шаги

Дополнительные сведения о разработке Azure Functions Python см. в следующих ресурсах: