Zwiększanie wydajności przepływności aplikacji języka Python w usłudze Azure Functions

Podczas tworzenia aplikacji dla usługi Azure Functions przy użyciu języka Python musisz zrozumieć, jak działają funkcje i jak ta wydajność wpływa na sposób skalowania aplikacji funkcji. Potrzeba jest ważniejsza podczas projektowania wysoce wydajnych aplikacji. Głównymi czynnikami, które należy wziąć pod uwagę podczas projektowania, pisania i konfigurowania aplikacji funkcji, są konfiguracje wydajności skalowania w poziomie i przepływności.

Skalowanie w poziomie

Domyślnie usługa Azure Functions automatycznie monitoruje obciążenie aplikacji i tworzy więcej wystąpień hosta dla języka Python zgodnie z potrzebami. Usługa Azure Functions używa wbudowanych progów dla różnych typów wyzwalaczy, aby zdecydować, kiedy dodać wystąpienia, takie jak wiek komunikatów i rozmiar kolejki dla klasy QueueTrigger. Te progi nie są konfigurowalne przez użytkownika. Aby uzyskać więcej informacji, zobacz Skalowanie oparte na zdarzeniach w usłudze Azure Functions.

Zwiększanie wydajności przepływności

Domyślne konfiguracje są odpowiednie dla większości aplikacji usługi Azure Functions. Można jednak zwiększyć wydajność przepływności aplikacji, stosując konfiguracje na podstawie profilu obciążenia. Pierwszym krokiem jest zrozumienie typu uruchomionego obciążenia.

Typ obciążenia Charakterystyka aplikacji funkcji Przykłady
Wiązanie we/wy • Aplikacja musi obsługiwać wiele współbieżnych wywołań.
• Aplikacja przetwarza dużą liczbę zdarzeń we/wy, takich jak wywołania sieciowe i odczyt/zapisy dysku.
• Internetowe interfejsy API
Powiązana z procesorem CPU • Aplikacja wykonuje długotrwałe obliczenia, takie jak zmiana rozmiaru obrazu.
• Aplikacja wykonuje transformację danych.
• Przetwarzanie danych
• Wnioskowanie uczenia maszynowego

Ponieważ obciążenia funkcji rzeczywistych są zwykle kombinacją operacji we/wy i powiązanego procesora CPU, należy profilować aplikację w realistycznych obciążeniach produkcyjnych.

Konfiguracje specyficzne dla wydajności

Po zapoznaniu się z profilem obciążenia aplikacji funkcji poniżej przedstawiono konfiguracje, których można użyć do poprawy wydajności przepływności funkcji.

Async

Ponieważ język Python to jednowątkowe środowisko uruchomieniowe, wystąpienie hosta dla języka Python może przetwarzać tylko jedną wywołanie funkcji w danym momencie domyślnie. W przypadku aplikacji, które przetwarzają dużą liczbę zdarzeń we/wy i/lub jest powiązana we/wy, można znacznie zwiększyć wydajność, uruchamiając funkcje asynchronicznie.

Aby uruchomić funkcję asynchronicznie, użyj async def instrukcji , która uruchamia funkcję bezpośrednio z asyncio :

async def main():
    await some_nonblocking_socket_io_op()

Oto przykład funkcji z wyzwalaczem HTTP, który używa klienta 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)

Funkcja bez słowa kluczowego async jest uruchamiana automatycznie w puli wątków 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()

Aby osiągnąć pełną korzyść z uruchamiania funkcji asynchronicznie, operacja we/wy/biblioteka używana w kodzie musi być również zaimplementowana asynchronicznie. Użycie synchronicznych operacji we/wy w funkcjach zdefiniowanych jako asynchroniczne może zaszkodzić ogólnej wydajności. Jeśli biblioteki, których używasz, nie mają zaimplementowanej wersji asynchronicznej, nadal możesz skorzystać z asynchronicznego uruchamiania kodu, zarządzając pętlą zdarzeń w aplikacji.

Oto kilka przykładów bibliotek klienckich, które zaimplementowały wzorce asynchroniczne:

  • aiohttp — klient/serwer Http dla asynchronicznego
  • interfejs API Strumienie — asynchroniczne/gotowe do oczekiwania typy pierwotne wysokiego poziomu do pracy z połączeniem sieciowym
  • Kolejka Janus — kolejka z obsługą asynchronicznych wątków dla języka Python
  • pyzmq — powiązania języka Python dla zeroMQ
Opis asynchronicznego procesu roboczego języka Python

Podczas definiowania async przed podpisem funkcji język Python oznacza funkcję jako kohroutynę. Podczas wywoływania kohroutyny można ją zaplanować jako zadanie w pętli zdarzeń. Wywołanie await funkcji asynchronicznych powoduje zarejestrowanie kontynuacji w pętli zdarzeń, dzięki czemu pętla zdarzeń będzie przetwarzać następne zadanie w czasie oczekiwania.

W naszym środowisku roboczym języka Python proces roboczy współużytkuje pętlę zdarzeń z funkcją klienta async i może obsługiwać wiele żądań jednocześnie. Zdecydowanie zachęcamy naszych klientów do korzystania z bibliotek zgodnych z asyncio, takich jak aiohttp i pyzmq. Wykonanie tych zaleceń zwiększa przepływność funkcji w porównaniu z tymi bibliotekami po zaimplementowaniu synchronicznie.

Uwaga

Jeśli funkcja zostanie zadeklarowana jako async bez żadnej await wewnątrz jej implementacji, wydajność funkcji będzie poważnie wpływać, ponieważ pętla zdarzeń zostanie zablokowana, co uniemożliwia procesowi roboczemu języka Python obsługę współbieżnych żądań.

Używanie wielu procesów roboczych języka

Domyślnie każde wystąpienie hosta rozwiązania Functions ma pojedynczy proces roboczy języka. Przy użyciu FUNCTIONS_WORKER_PROCESS_COUNT ustawienia aplikacji można zwiększyć liczbę procesów roboczych na hosta (do 10). Następnie usługa Azure Functions próbuje równomiernie dystrybuować równoczesne wywołania funkcji między tymi procesami roboczymi.

W przypadku aplikacji powiązanych z procesorem CPU należy ustawić liczbę procesów roboczych języka na taką samą jak lub wyższą niż liczba rdzeni, które są dostępne dla aplikacji funkcji. Aby dowiedzieć się więcej, zobacz Dostępne jednostki SKU wystąpienia.

Aplikacje związane z we/wy mogą również korzystać z zwiększenia liczby procesów roboczych poza liczbą dostępnych rdzeni. Należy pamiętać, że ustawienie zbyt dużej liczby procesów roboczych może mieć wpływ na ogólną wydajność ze względu na zwiększoną liczbę wymaganych przełączników kontekstowych.

Dotyczy FUNCTIONS_WORKER_PROCESS_COUNT każdego hosta tworzonego przez usługę Azure Functions podczas skalowania aplikacji w celu spełnienia wymagań.

Konfigurowanie maksymalnych procesów roboczych w procesie roboczym języka

Jak wspomniano w sekcji asynchroniczne, proces roboczy języka Python traktuje funkcje i koroutines inaczej. Coroutine jest uruchamiana w ramach tej samej pętli zdarzeń, na których działa proces roboczy języka. Z drugiej strony wywołanie funkcji jest uruchamiane w ramach klasy ThreadPoolExecutor, która jest utrzymywana przez proces roboczy języka jako wątek.

Można ustawić wartość maksymalnej liczby procesów roboczych dozwolonych do uruchamiania funkcji synchronizacji przy użyciu ustawienia aplikacji PYTHON_THREADPOOL_THREAD_COUNT . Ta wartość ustawia max_worker argument obiektu ThreadPoolExecutor, który umożliwia językowi Python używanie puli w większości max_worker wątków do wykonywania wywołań asynchronicznie. Dotyczy PYTHON_THREADPOOL_THREAD_COUNT każdego procesu roboczego tworzonego przez hosta usługi Functions, a język Python decyduje, kiedy utworzyć nowy wątek lub ponownie użyć istniejącego bezczynnego wątku. W przypadku starszych wersji języka Python (czyli , 3.83.7, i 3.6), max_worker wartość jest ustawiona na 1. W przypadku wersji 3.9max_worker języka Python jest ustawiona wartość None.

W przypadku aplikacji powiązanych z procesorem CPU należy zachować ustawienie na małą liczbę, począwszy od 1 i zwiększając się w miarę eksperymentowania z obciążeniem. Ta sugestia polega na skróceniu czasu spędzonego na przełącznikach kontekstowych i umożliwieniu ukończenia zadań powiązanych z procesorem CPU.

W przypadku aplikacji związanych z we/wy powinno być widoczne znaczne zyski, zwiększając liczbę wątków pracujących nad poszczególnymi wywołaniami. Zaleceniem jest rozpoczęcie od domyślnego języka Python (liczby rdzeni) + 4, a następnie dostosowania na podstawie wyświetlanych wartości przepływności.

W przypadku aplikacji dla obciążeń mieszanych należy równoważyć zarówno FUNCTIONS_WORKER_PROCESS_COUNT konfiguracje, jak i PYTHON_THREADPOOL_THREAD_COUNT w celu zmaksymalizowania przepływności. Aby zrozumieć, co aplikacje funkcji spędzają najwięcej czasu, zalecamy profilowanie ich i ustawianie wartości zgodnie z ich zachowaniami. Aby dowiedzieć się więcej o tych ustawieniach aplikacji, zobacz Używanie wielu procesów roboczych.

Uwaga

Chociaż te zalecenia dotyczą zarówno funkcji wyzwalanych przez protokół HTTP, jak i innych niż HTTP, może być konieczne dostosowanie innych konfiguracji specyficznych dla funkcji wyzwalanych przez protokół HTTP, aby uzyskać oczekiwaną wydajność z aplikacji funkcji. Aby uzyskać więcej informacji na ten temat, zapoznaj się z tym artykułem Najlepsze rozwiązania dotyczące niezawodnej usługi Azure Functions.

Zarządzanie pętlą zdarzeń

Należy użyć bibliotek innych firm zgodnych z asyncio. Jeśli żadna z bibliotek innych firm nie spełnia Twoich potrzeb, możesz również zarządzać pętlami zdarzeń w usłudze Azure Functions. Zarządzanie pętlami zdarzeń zapewnia większą elastyczność w zarządzaniu zasobami obliczeniowymi, a także umożliwia zawijanie synchronicznych bibliotek we/wy do kohroutyn.

Istnieje wiele przydatnych oficjalnych dokumentów języka Python omawiających kohroutines i tasks and Event Loop przy użyciu wbudowanej biblioteki asyncio .

Weźmy na przykład następującą bibliotekę żądań , w tym fragmencie kodu użyto biblioteki asyncio do opakowania requests.get() metody do coroutine, uruchamiając wiele żądań internetowych w celu SAMPLE_URL jednocześnie.

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

Skalowanie w pionie

Może być możliwe uzyskanie większej liczby jednostek przetwarzania, zwłaszcza w przypadku operacji powiązanej z procesorem CPU, przez uaktualnienie do planu w warstwie Premium z wyższymi specyfikacjami. Dzięki wyższym jednostkom przetwarzania można dostosować liczbę procesów roboczych zgodnie z liczbą dostępnych rdzeni i osiągnąć wyższy stopień równoległości.

Następne kroki

Aby uzyskać więcej informacji na temat programowania w języku Python w usłudze Azure Functions, zobacz następujące zasoby: