Melhorar o desempenho das aplicações Python em Funções do Azure

Ao desenvolver Funções do Azure usando Python, precisa entender como as suas funções funcionam e como esse desempenho afeta a forma como a sua aplicação de função é dimensionada. A necessidade é mais importante na conceção de aplicações altamente performantes. Os principais fatores a ter em conta ao conceber, escrever e configurar as suas funções são a escala horizontal e configurações de desempenho de produção.

Dimensionamento horizontal

Por predefinição, Funções do Azure monitoriza automaticamente a carga da sua aplicação e cria mais casos de anfitrião para Python, conforme necessário. Funções do Azure utiliza limiares incorporados para diferentes tipos de gatilhos para decidir quando adicionar instâncias, como a idade das mensagens e o tamanho da fila para o QueueTrigger. Estes limiares não são configuráveis pelo utilizador. Para obter mais informações, consulte a escalação orientada para o evento em Funções do Azure.

Melhorar o desempenho da produção

As configurações predefinidos são adequadas para a maioria das aplicações Funções do Azure. No entanto, pode melhorar o desempenho da produção das suas aplicações utilizando configurações com base no seu perfil de carga de trabalho. O primeiro passo é compreender o tipo de carga de trabalho que está a correr.

Tipo de carga de trabalho Características da aplicação de função Exemplos
I/O-bound • A aplicação precisa de lidar com muitas invocações simultâneas.
• A aplicação processa um grande número de eventos de E/S, tais como chamadas de rede e leitura/escrita de disco.
• APIs web
Limite de CPU • A aplicação faz cálculos de longa duração, tais como o redimensionamento de imagens.
• A app faz a transformação de dados.
• Processamento de dados
• Inferência de aprendizagem automática

Como as cargas de trabalho da função real são geralmente uma mistura de I/O e CPU ligados, você deve perfilar a app sob cargas de produção realistas.

Configurações específicas do desempenho

Depois de compreender o perfil de carga de trabalho da sua aplicação de função, as seguintes são configurações que pode utilizar para melhorar o desempenho de produção das suas funções.

Async

Como Python é um tempo de execução de um único roscado, uma instância de hospedeiro para Python pode processar apenas uma invocação de função de cada vez por defeito. Para aplicações que processam um grande número de eventos de I/O e/ou está ligado a I/O, você pode melhorar o desempenho significativamente executando funções assíncroneas.

Para executar uma função assíncronea, utilize a async def declaração, que executa a função com assício diretamente:

async def main():
    await some_nonblocking_socket_io_op()

Aqui está um exemplo de uma função com trigger HTTP que usa 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 async palavra-chave é executada automaticamente numa piscina de fios 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 o pleno benefício de executar assíncroneamente, a operação/biblioteca de E/S que é utilizada no seu código também precisa de ter async implementado. A utilização de operações de E/S sincronizadas em funções definidas como assíncronos pode prejudicar o desempenho geral. Se as bibliotecas que está a utilizar não tiverem a versão async implementada, poderá ainda beneficiar de executar o seu código assíncronosamente , gerindo o loop de eventos na sua aplicação.

Aqui estão alguns exemplos de bibliotecas de clientes que implementaram padrões async:

  • aiohttp - Http cliente/servidor para assíncio
  • Streams API - Primitivos de alto nível assíncos/aguardados prontos para trabalhar com ligação à rede
  • Janus Queue - Fila segura de assício para Python
  • pyzmq - Ligações python para ZeroMQ
Compreensão de async no trabalhador python

Quando se define async na frente de uma assinatura de função, Python marca a função como uma coroutina. Ao chamar a coroutina, pode ser agendada como uma tarefa para um ciclo de eventos. Quando se convoca await uma função async, regista uma continuação no ciclo de eventos, o que permite ao ciclo do evento processar a próxima tarefa durante o tempo de espera.

No nosso Python Worker, o trabalhador partilha o ciclo do evento com a função do async cliente e é capaz de lidar com múltiplos pedidos simultaneamente. Encorajamos fortemente os nossos clientes a utilizarem bibliotecas compatíveis com assício, como aiohttp e pyzmq. Seguindo estas recomendações, aumenta a produção da sua função em comparação com as bibliotecas quando implementadas de forma sincronizada.

Nota

Se a sua função for declarada como async sem qualquer await dentro da sua implementação, o desempenho da sua função será severamente impactado, uma vez que o loop do evento será bloqueado, o que proíbe o trabalhador python de lidar com pedidos simultâneos.

Use vários processos de trabalho de língua

Por predefinição, cada instância de anfitrião de Funções tem um único processo de trabalho de língua. Pode aumentar o número de processos de trabalhador por anfitrião (até 10) utilizando a definição de FUNCTIONS_WORKER_PROCESS_COUNT aplicação. Funções do Azure tenta então distribuir invocações de função simultâneas por estes trabalhadores.

Para aplicações ligadas ao CPU, deve definir o número de trabalhadores linguísticos como ou superior ao número de núcleos disponíveis por aplicação de função. Para saber mais, consulte a instância DISPONÍVEL SKUs.

As aplicações i/O-bound também podem beneficiar do aumento do número de processos de trabalhadores para além do número de núcleos disponíveis. Tenha em mente que definir o número de trabalhadores demasiado elevado pode ter impacto no desempenho global devido ao aumento do número de interruptores de contexto necessários.

O FUNCTIONS_WORKER_PROCESS_COUNT se aplica a cada anfitrião que as Funções criam ao escalonar a sua aplicação para satisfazer a procura.

Nota

Vários trabalhadores python não são apoiados em V2 neste momento. Isto significa que permitir uma conuscência inteligente e uma regulação FUNCTIONS_WORKER_PROCESS_COUNT superior a 1 não é suportado para funções desenvolvidas utilizando o modelo V2.

Criar trabalhadores max dentro de um processo de trabalhador linguístico

Como mencionado na secção async, o trabalhador da língua python trata as funções e coroutinas de forma diferente. Uma coroutina é executada dentro do mesmo ciclo de eventos em que o trabalhador da língua funciona. Por outro lado, uma invocação de função é executada dentro de um ThreadPoolExecutor, que é mantido pelo trabalhador da língua como um fio.

Pode definir o valor dos trabalhadores máximos permitidos para executar funções de sincronização utilizando a definição de aplicação PYTHON_THREADPOOL_THREAD_COUNT . Este valor define o max_worker argumento do objeto ThreadPoolExecutor, que permite que python use uma piscina de na maioria max_worker dos fios para executar chamadas assíncroneas. O PYTHON_THREADPOOL_THREAD_COUNT se aplica a cada trabalhador que o hospedeiro de Funções cria, e python decide quando criar um novo fio ou reutilizar o fio ocioso existente. Para as versões Python mais antigas (isto é, 3.8, 3.7e 3.6- max_worker o valor está definido para 1. Para a versão 3.9 Python , max_worker está definido para None.

Para aplicações com ligação ao CPU, deve manter a definição para um número baixo, a partir de 1 e aumentando à medida que experimenta com a sua carga de trabalho. Esta sugestão é reduzir o tempo gasto em interruptores de contexto e permitir que tarefas ligadas à CPU terminem.

Para aplicações ligadas a I/O, você deve ver ganhos substanciais aumentando o número de fios 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 produção que está a ver.

Para aplicações de cargas de trabalho mistas, deve equilibrar tanto as FUNCTIONS_WORKER_PROCESS_COUNT configurações como PYTHON_THREADPOOL_THREAD_COUNT as configurações para maximizar a produção. Para entender em que as suas aplicações de função passam mais tempo, recomendamos que as perfilem e estabeleçam os valores de acordo com os seus comportamentos. Para saber mais sobre estas configurações de aplicação, consulte Utilizar vários processos de trabalhador.

Nota

Embora estas recomendações se apliquem tanto a funções de http como não-HTTP, poderá ser necessário ajustar outras configurações específicas do gatilho para funções não-HTTP ativadas para obter o desempenho esperado das aplicações da sua função. Para mais informações sobre isto, consulte este artigo.

Loop de eventos de gestão

Deve utilizar bibliotecas de terceiros compatíveis com assícios. Se nenhuma das bibliotecas de terceiros satisfaz as suas necessidades, também pode gerir os ciclos de eventos em Funções do Azure. Gerir os ciclos de eventos dá-lhe mais flexibilidade na gestão de recursos computativos, e também permite envolver bibliotecas de I/O sincronizadas em coroutinas.

Existem muitos documentos oficiais úteis python discutindo os Coroutines e Tasks e Event Loop usando a biblioteca assincio incorporada.

Tome como exemplo a biblioteca de pedidos seguintes, este código corta-códigos utiliza a biblioteca assício para envolver o requests.get() método numa coroutina, executando vários pedidos 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

Para mais unidades de processamento especialmente em operação ligada à CPU, você pode ser capaz de obter isso através da atualização para plano premium com especificações mais elevadas. Com unidades de processamento mais elevadas, pode ajustar o número de processos de trabalho de acordo com o número de núcleos disponíveis e obter um maior grau de paralelismo.

Passos seguintes

Para obter mais informações sobre Funções do Azure desenvolvimento python, consulte os seguintes recursos: