Melhorar o desempenho da taxa de transferência de aplicativos Python no Azure Functions

Ao desenvolver para o Azure Functions usando Python, você precisa entender como suas funções são executadas e como esse desempenho afeta a maneira como seu aplicativo de função é dimensionado. A necessidade é mais importante ao projetar aplicativos de alto desempenho. Os principais fatores a serem considerados ao projetar, escrever e configurar seus aplicativos de funções são o dimensionamento horizontal e as configurações de desempenho de taxa de transferência.

Dimensionamento horizontal

Por padrão, o Azure Functions monitora automaticamente a carga em seu aplicativo e cria mais instâncias de host para Python conforme necessário. O Azure Functions usa limites internos para diferentes tipos de gatilho para decidir quando adicionar instâncias, como a idade das mensagens e o tamanho da fila para o QueueTrigger. Esses limites não são configuráveis pelo usuário. Para obter mais informações, consulte Dimensionamento controlado por eventos no Azure Functions.

Melhorando o desempenho da taxa de transferência

As configurações padrão são adequadas para a maioria dos aplicativos do Azure Functions. No entanto, você pode melhorar o desempenho da taxa de transferência de seus aplicativos empregando configurações baseadas em seu perfil de carga de trabalho. A primeira etapa é entender o tipo de carga de trabalho que você está executando.

Tipo de carga de trabalho Características do aplicativo de função Exemplos
Ligação a E/S • O aplicativo precisa lidar com muitas invocações simultâneas.
• O aplicativo processa um grande número de eventos de E/S, como chamadas de rede e leitura/gravação de disco.
• APIs Web
Ligado à CPU • O aplicativo faz cálculos de longa duração, como redimensionamento de imagem.
• App faz transformação de dados.
• Tratamento de dados
• Inferência de aprendizagem automática

Como as cargas de trabalho de função do mundo real geralmente são uma mistura de E/S e CPU vinculadas, você deve criar o perfil do aplicativo sob cargas de produção realistas.

Configurações específicas de desempenho

Depois de entender o perfil de carga de trabalho do seu aplicativo de função, a seguir estão as configurações que você pode usar para melhorar o desempenho da taxa de transferência de suas funções.

Assíncrono

Como Python é um tempo de execução de thread único, uma instância de host para Python pode processar apenas uma invocação de função de cada vez por padrão. Para aplicativos que processam um grande número de eventos de E/S e/ou estão vinculados a E/S, você pode melhorar significativamente o desempenho executando funções de forma assíncrona.

Para executar uma função de forma assíncrona, use a instrução, que executa a async def função com asyncio diretamente:

async def main():
    await some_nonblocking_socket_io_op()

Aqui está um exemplo de uma função com gatilho HTTP que usa o cliente 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)

Uma função sem a palavra-chave é executada async automaticamente em um pool de threads 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()

Para obter todos os benefícios de executar funções de forma assíncrona, a operação/biblioteca de E/S usada em seu código também precisa ter um async implementado. O uso de operações de E/S síncronas em funções definidas como assíncronas pode prejudicar o desempenho geral. Se as bibliotecas que você está usando não tiverem a versão assíncrona implementada, você ainda poderá se beneficiar da execução assíncrona do código gerenciando o loop de eventos em seu aplicativo.

Aqui estão alguns exemplos de bibliotecas de cliente que implementaram padrões assíncronos:

  • aiohttp - Http cliente/servidor para asyncio
  • Streams API - Primitivos assíncronos/prontos para espera de alto nível para trabalhar com conexão de rede
  • Janus Queue - Fila com reconhecimento de assincronia segura para threads para Python
  • pyzmq - Ligações Python para ZeroMQ
Entendendo o assíncrono no Python worker

Quando você define async na frente de uma assinatura de função, o Python marca a função como uma co-rotina. Quando você chama a co-rotina, ela pode ser agendada como uma tarefa em um loop de eventos. Quando você chama await uma função assíncrona, ela registra uma continuação no loop de eventos, o que permite que o loop de eventos processe a próxima tarefa durante o tempo de espera.

Em nosso Python Worker, o trabalhador compartilha o loop de eventos com a função do async cliente e é capaz de lidar com várias solicitações simultaneamente. Encorajamos vivamente os nossos clientes a fazer uso de bibliotecas compatíveis com asyncio, tais como aiohttp e pyzmq. Seguir essas recomendações aumenta a taxa de transferência da sua função em comparação com essas bibliotecas quando implementadas de forma síncrona.

Nota

Se sua função for declarada como async sem nenhuma await dentro de sua implementação, o desempenho de sua função será severamente afetado, uma vez que o loop de eventos será bloqueado, o que proíbe o trabalhador Python de lidar com solicitações simultâneas.

Usar vários processos de trabalho linguístico

Por predefinição, todas as instâncias de anfitrião de Funções têm um único processo de função de trabalho de linguagem. Você pode aumentar o número de processos de trabalho por host (até 10) usando a configuração do FUNCTIONS_WORKER_PROCESS_COUNT aplicativo. Em seguida, o Azure Functions tenta distribuir uniformemente invocações de função simultâneas entre esses trabalhadores.

Para aplicativos vinculados à CPU, você deve definir o número de trabalhadores de idioma como igual ou superior ao número de núcleos disponíveis por aplicativo de função. Para saber mais, consulte SKUs de instância disponíveis.

Os aplicativos vinculados a E/S também podem se beneficiar do aumento do número de processos de trabalho além do número de núcleos disponíveis. Lembre-se de que definir o número de trabalhadores muito alto pode afetar o desempenho geral devido ao aumento do número de opções de contexto necessárias.

O FUNCTIONS_WORKER_PROCESS_COUNT aplica-se a cada host que o Azure Functions cria ao dimensionar seu aplicativo para atender à demanda.

Configurar o máximo de trabalhadores dentro de um processo de trabalhador linguístico

Como mencionado na seção assíncrona, o trabalhador da linguagem Python trata funções e co-rotinas de forma diferente. Uma co-rotina é executada dentro do mesmo loop de eventos em que o trabalhador de idiomas é executado. Por outro lado, uma invocação de função é executada dentro de um ThreadPoolExecutor, que é mantido pelo trabalhador de idioma como um thread.

Você pode definir o valor do máximo de trabalhadores permitidos para executar funções de sincronização usando a configuração do aplicativo PYTHON_THREADPOOL_THREAD_COUNT . Esse valor define o argumento do objeto ThreadPoolExecutor, que permite que o max_worker Python use um pool de no máximo max_worker threads para executar chamadas de forma assíncrona. O PYTHON_THREADPOOL_THREAD_COUNT aplica-se a cada trabalhador que o host do Functions cria, e o Python decide quando criar um novo thread ou reutilizar o thread ocioso existente. Para versões mais antigas do Python (ou seja, , , 3.83.7e 3.6), max_worker o valor é definido como 1. Para a versão 3.9 Python , max_worker está definido como None.

Para aplicativos vinculados à CPU, você deve manter a configuração em um número baixo, começando de 1 e aumentando à medida que experimenta sua carga de trabalho. Esta sugestão é reduzir o tempo gasto em comutadores de contexto e permitir que as tarefas ligadas à CPU sejam concluídas.

Para aplicativos vinculados a E/S, você deve ver ganhos substanciais aumentando o número de threads trabalhando em cada invocação. A recomendação é começar com o padrão Python (o número de núcleos) + 4 e, em seguida, ajustar com base nos valores de taxa de transferência que você está vendo.

Para aplicativos de cargas de trabalho mistas, você deve equilibrar ambas as FUNCTIONS_WORKER_PROCESS_COUNT configurações para PYTHON_THREADPOOL_THREAD_COUNT maximizar a taxa de transferência. Para entender em que seus aplicativos de função passam mais tempo, recomendamos criá-los em perfil e definir os valores de acordo com seus comportamentos. Para saber mais sobre essas configurações de aplicativo, consulte Usar vários processos de trabalho.

Nota

Embora essas recomendações se apliquem a funções acionadas HTTP e não HTTP, talvez seja necessário ajustar outras configurações específicas de gatilho para funções acionadas não HTTP para obter o desempenho esperado de seus aplicativos de função. Para obter mais informações sobre isso, consulte estas Práticas recomendadas para funções confiáveis do Azure.

Gerenciando o loop de eventos

Você deve usar bibliotecas de terceiros compatíveis com asyncio. Se nenhuma das bibliotecas de terceiros atender às suas necessidades, você também poderá gerenciar os loops de eventos no Azure Functions. O gerenciamento de loops de eventos oferece mais flexibilidade no gerenciamento de recursos de computação e também torna possível envolver bibliotecas de E/S síncronas em co-rotinas.

Há muitos documentos oficiais Python úteis discutindo as Corotinas e Tarefas e Loop de Eventos usando a biblioteca asyncio integrada.

Tome a biblioteca de solicitações a seguir como exemplo, esse trecho de código usa a biblioteca asyncio para encapsular o requests.get() método em uma co-rotina, executando várias solicitações da Web para SAMPLE_URL simultaneamente.

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')

Dimensionamento vertical

Você pode obter mais unidades de processamento, especialmente em operação vinculada à CPU, atualizando para o plano premium com especificações mais altas. Com unidades de processamento mais altas, você pode ajustar a contagem do número de processos de trabalho de acordo com o número de núcleos disponíveis e obter maior grau de paralelismo.

Próximos passos

Para obter mais informações sobre o desenvolvimento Python do Azure Functions, consulte os seguintes recursos: