Повышение производительности приложений Python в Функциях Azure

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

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

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

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

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

Тип рабочей нагрузки Характеристики приложения функции Примеры
Ограниченность ресурсов ввода-вывода • Приложение должно обрабатывать множество одновременных вызовов.
• Приложение обрабатывает большое количество событий ввода-вывода, таких как сетевые вызовы и дисковые операции чтения и записи.
• Веб-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 — клиент-серверный протокол для asyncio
  • API потоков — основные асинхронные и ждущие примитивы для работы с сетевым подключением высокого уровня
  • Очередь Janus — потокобезопасная очередь с поддержкой asyncio под Python
  • pyzmq — Python для ZeroMQ
Основные сведения об Async в рабочей процессе Python

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

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

Примечание.

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

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

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

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

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

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

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

Как упоминалось в разделеasync, рабочий процесс языка 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_COUNTPYTHON_THREADPOOL_THREAD_COUNT конфигурации, чтобы максимально увеличить пропускную способность. Чтобы понять, какие приложения-функции тратят больше всего времени, рекомендуется профилировать их и задавать значения в соответствии с их поведением. Дополнительные сведения об этих параметрах приложения см. в разделе "Использование нескольких рабочих процессов".

Примечание.

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

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

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

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

Возьмем в качестве примера библиотеку запросов. В приводимом фрагменте кода используется библиотека 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 с более высокими спецификациями. Благодаря более высоким единицам обработки можно настроить количество рабочих процессов в соответствии с количеством доступных ядер и достичь более высокой степени параллелизма.

Следующие шаги

Дополнительные сведения о разработках под Python в рамках службы "Функции Azure" см. в статьях ниже.