Migliorare le prestazioni della velocità effettiva delle app Python in Funzioni di Azure

Quando si sviluppa per Funzioni di Azure usando Python, è necessario comprendere come le funzioni e come queste prestazioni influiscono sul modo in cui l'app per le funzioni viene ridimensionata. La necessità è più importante quando si progettano app ad alte prestazioni. I fattori principali da considerare durante la progettazione, la scrittura e la configurazione delle app per le funzioni sono configurazioni di scalabilità orizzontale e prestazioni della velocità effettiva.

Scalabilità orizzontale

Per impostazione predefinita, Funzioni di Azure monitora automaticamente il carico nell'applicazione e crea più istanze host per Python in base alle esigenze. Funzioni di Azure usa soglie predefinite per diversi tipi di trigger per decidere quando aggiungere istanze, ad esempio l'età dei messaggi e le dimensioni della coda per QueueTrigger. Queste soglie non sono configurabili dall'utente. Per altre informazioni, vedere Ridimensionamento basato su eventi in Funzioni di Azure.

Miglioramento delle prestazioni della velocità effettiva

Le configurazioni predefinite sono adatte per la maggior parte delle applicazioni Funzioni di Azure. Tuttavia, è possibile migliorare le prestazioni della velocità effettiva delle applicazioni usando configurazioni basate sul profilo del carico di lavoro. Il primo passaggio consiste nel comprendere il tipo di carico di lavoro in esecuzione.

Tipo di carico di lavoro Caratteristiche dell'app per le funzioni Esempi
Associazione di I/O • L'app deve gestire molte chiamate simultanee.
• L'app elabora un numero elevato di eventi di I/O, ad esempio chiamate di rete e operazioni di lettura/scrittura su disco.
• API Web
Associato alla CPU • L'app esegue calcoli a esecuzione prolungata, ad esempio il ridimensionamento delle immagini.
• L'app esegue la trasformazione dei dati.
• Elaborazione dei dati
• Inferenza di Machine Learning

Poiché i carichi di lavoro delle funzioni reali sono in genere una combinazione di operazioni di I/O e CPU associate, è consigliabile profilare l'app in carichi di produzione realistici.

Configurazioni specifiche delle prestazioni

Dopo aver compreso il profilo del carico di lavoro dell'app per le funzioni, di seguito sono riportate le configurazioni che è possibile usare per migliorare le prestazioni della velocità effettiva delle funzioni.

Async

Poiché Python è un runtime a thread singolo, un'istanza host per Python può elaborare una sola chiamata di funzione alla volta per impostazione predefinita. Per le applicazioni che elaborano un numero elevato di eventi di I/O e/o è associato i/O, è possibile migliorare significativamente le prestazioni eseguendo funzioni in modo asincrono.

Per eseguire una funzione in modo asincrono, usare l'istruzione async def, che esegue la funzione direttamente con asyncio:

async def main():
    await some_nonblocking_socket_io_op()

Di seguito è riportato un esempio di funzione con trigger HTTP che usa il client 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)

Una funzione senza la async parola chiave viene eseguita automaticamente in un pool di thread 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()

Per ottenere il massimo vantaggio dell'esecuzione di funzioni in modo asincrono, anche l'operazione/libreria di I/O usata nel codice deve essere implementata in modo asincrono. L'uso di operazioni di I/O sincrone nelle funzioni definite come asincrone può compromettere le prestazioni complessive. Se le librerie in uso non hanno una versione asincrona implementata, puoi comunque trarre vantaggio dall'esecuzione del codice in modo asincrono gestendo il ciclo di eventi nell'app.

Ecco alcuni esempi di librerie client che hanno implementato modelli asincroni:

  • aiohttp - Client/server HTTP per asyncio
  • API Flussi : primitive asincrone/await-ready di alto livello per l'uso con la connessione di rete
  • Coda Janus - Coda asyncio-aware thread-safe per Python
  • pyzmq - Associazioni Python per ZeroMQ
Informazioni sull'asincrona nel ruolo di lavoro Python

Quando si definisce async davanti a una firma di funzione, Python contrassegna la funzione come coroutine. Quando si chiama la coroutine, può essere pianificata come attività in un ciclo di eventi. Quando si chiama await in una funzione asincrona, registra una continuazione nel ciclo di eventi, che consente al ciclo di eventi di elaborare l'attività successiva durante il tempo di attesa.

Nel ruolo di lavoro Python il ruolo di lavoro condivide il ciclo di eventi con la funzione del async cliente ed è in grado di gestire più richieste contemporaneamente. Si consiglia vivamente ai clienti di usare librerie compatibili con asyncio, ad esempio aiohttp e pyzmq. Seguendo questi consigli, la velocità effettiva della funzione aumenta rispetto a quelle librerie quando implementate in modo sincrono.

Nota

Se la funzione viene dichiarata come async senza alcuna await implementazione all'interno dell'implementazione, le prestazioni della funzione saranno fortemente influenzate perché il ciclo di eventi verrà bloccato, che impedisce al ruolo di lavoro Python di gestire le richieste simultanee.

Usare più processi di lavoro del linguaggio

Per impostazione predefinita, ogni istanza host di Funzioni include un singolo processo di lavoro del linguaggio. È possibile aumentare il numero di processi di lavoro per host (fino a 10) usando l'impostazione dell'applicazione FUNCTIONS_WORKER_PROCESS_COUNT . Funzioni di Azure prova quindi a distribuire uniformemente le chiamate di funzioni simultanee tra questi processi di lavoro.

Per le app associate alla CPU, è necessario impostare il numero di ruoli di lavoro linguistici uguali o superiori al numero di core disponibili per ogni app per le funzioni. Per altre informazioni, vedere SKU di istanza disponibili.

Le app associate a I/O possono anche trarre vantaggio dall'aumento del numero di processi di lavoro oltre il numero di core disponibili. Tenere presente che l'impostazione del numero di ruoli di lavoro troppo elevati può influire sulle prestazioni complessive a causa dell'aumento del numero di commutatori di contesto necessari.

Si FUNCTIONS_WORKER_PROCESS_COUNT applica a ogni host che Funzioni di Azure crea quando si aumenta la scalabilità orizzontale dell'applicazione per soddisfare la domanda.

Configurare il numero massimo di ruoli di lavoro all'interno di un processo di lavoro linguistico

Come accennato nella sezione asincrona, il ruolo di lavoro del linguaggio Python tratta funzioni e coroutine in modo diverso. Una coroutine viene eseguita all'interno dello stesso ciclo di eventi in cui viene eseguito il ruolo di lavoro del linguaggio. D'altra parte, una chiamata di funzione viene eseguita all'interno di un ThreadPoolExecutor, gestito dal ruolo di lavoro del linguaggio come thread.

È possibile impostare il valore massimo di ruoli di lavoro consentiti per l'esecuzione di funzioni di sincronizzazione usando l'impostazione dell'applicazione PYTHON_THREADPOOL_THREAD_COUNT . Questo valore imposta l'argomento dell'oggetto max_worker ThreadPoolExecutor, che consente a Python di usare un pool di al massimo max_worker thread per eseguire le chiamate in modo asincrono. Si PYTHON_THREADPOOL_THREAD_COUNT applica a ogni ruolo di lavoro creato dall'host di Funzioni e Python decide quando creare un nuovo thread o riutilizzare il thread inattio esistente. Per le versioni precedenti di Python, ovvero , 3.83.7e 3.6, max_worker il valore è impostato su 1. Per python versione 3.9 , max_worker è impostato su None.

Per le app associate alla CPU, è consigliabile mantenere l'impostazione su un numero basso, a partire da 1 e aumentando durante l'esperimento con il carico di lavoro. Questo suggerimento consiste nel ridurre il tempo impiegato per i commutatori di contesto e consentire il completamento delle attività associate alla CPU.

Per le app associate a I/O, si noterà un notevole miglioramento aumentando il numero di thread che lavorano su ogni chiamata. È consigliabile iniziare con l'impostazione predefinita di Python (il numero di core) + 4 e quindi modificare in base ai valori di velocità effettiva visualizzati.

Per le app con carichi di lavoro misti, è consigliabile bilanciare le FUNCTIONS_WORKER_PROCESS_COUNT configurazioni e PYTHON_THREADPOOL_THREAD_COUNT per ottimizzare la velocità effettiva. Per comprendere in che modo le app per le funzioni impiegano più tempo, è consigliabile profilarle e impostare i valori in base ai comportamenti. Per informazioni su queste impostazioni dell'applicazione, vedere Usare più processi di lavoro.

Nota

Anche se queste raccomandazioni si applicano alle funzioni attivate sia HTTP che non HTTP, potrebbe essere necessario modificare altre configurazioni specifiche del trigger per le funzioni attivate non HTTP per ottenere le prestazioni previste dalle app per le funzioni. Per altre informazioni, vedere questa procedura consigliata per l'Funzioni di Azure affidabile.

Gestione del ciclo di eventi

È consigliabile usare librerie di terze parti compatibili con asyncio. Se nessuna delle librerie di terze parti soddisfa le proprie esigenze, è anche possibile gestire i cicli di eventi in Funzioni di Azure. La gestione dei cicli di eventi offre maggiore flessibilità nella gestione delle risorse di calcolo e consente anche di eseguire il wrapping delle librerie di I/O sincrone in coroutine.

Sono disponibili molti documenti ufficiali di Python utili che illustrano le coroutine e le attività e il ciclo di eventi usando la libreria asincrona predefinita.

Accettare la libreria delle richieste seguente come esempio, questo frammento di codice usa la libreria asyncio per eseguire il wrapping del requests.get() metodo in una coroutine, eseguendo più richieste Web per 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')

Scalabilità verticale

Potrebbe essere possibile ottenere più unità di elaborazione, in particolare nell'operazione associata alla CPU, eseguendo l'aggiornamento al piano Premium con specifiche più elevate. Con unità di elaborazione più elevate, è possibile modificare il numero di processi di lavoro in base al numero di core disponibili e ottenere un grado di parallelismo superiore.

Passaggi successivi

Per altre informazioni sullo sviluppo di Funzioni di Azure Python, vedere le risorse seguenti: