Verbessern der Durchsatzleistung von Python-Apps in Azure Functions

Beim Entwickeln von Apps für Azure Functions mithilfe von Python müssen Sie wissen, wie leistungsfähig Ihre Funktionen sind und wie sich die Leistung auf die Skalierung Ihrer Funktions-App auswirkt. Beim Entwerfen von hochleistungsfähigen Apps ist dieser Aspekt besonders wichtig. Die wichtigsten Faktoren, die Sie beim Entwerfen, Schreiben und Konfigurieren Ihrer Funktions-Apps bedenken müssen, sind die Konfigurationen für die Aufskalierung und die Durchsatzleistung.

Horizontale Skalierung

Standardmäßig überwacht Azure Functions automatisch die Auslastung Ihrer Anwendung und erstellt bei Bedarf weitere Hostinstanzen für Python. Um zu entscheiden, wann Instanzen hinzugefügt werden sollen, verwendet Azure Functions integrierte Schwellenwerte für verschiedene Triggertypen, beispielsweise in Bezug auf das Alter von Nachrichten und die Warteschlangengröße für QueueTrigger. Diese Schwellenwerte sind nicht vom Benutzer konfigurierbar. Weitere Informationen finden Sie unter Ereignisgesteuerte Skalierung in Azure Functions.

Verbessern der Durchsatzleistung

Die Standardkonfigurationen sind für die meisten Azure Functions-Anwendungen geeignet. Allerdings können Sie die Leistung des Durchsatzes Ihrer Anwendungen verbessern, indem Sie Konfigurationen einsetzen, die zu Ihrem Workloadprofil passen. Daher besteht der erste Schritt darin, zu verstehen, welcher Typ von Workload ausgeführt wird.

Workloadtyp Merkmale der Funktions-App Beispiele
E/A-Bindung • Die App muss viele gleichzeitige Aufrufe verarbeiten.
• Die App verarbeitet eine große Anzahl von E/A-Ereignissen, wie z. B. Netzwerkaufrufe und Lese-/Schreibvorgänge auf Datenträgern.
• Web-APIs
CPU-Bindung • Die App führt Langzeitberechnungen durch, wie z. B. die Größenänderung von Bildern.
• Die App führt Datentransformationen durch.
• Datenverarbeitung
• Machine Learning-Rückschluss

Da Funktionsworkloads in der Praxis üblicherweise eine Mischung aus E/A- und CPU-gebundenen Workloads sind, sollten Sie das Profil der App unter realistischen Produktionslasten erstellen.

Leistungsspezifische Konfigurationen

Nachdem Sie das Workloadprofil ihrer Funktions-App jetzt kennen, können Sie die folgenden Konfigurationen zum Verbessern der Durchsatzleistung ihrer Funktionen einsetzen.

Async

Da Python eine Single-Threading-Runtime ist, kann eine Hostinstanz für Python standardmäßig jeweils nur einen Funktionsaufruf gleichzeitig verarbeiten. Bei Anwendungen, die eine große Anzahl von E/A-Ereignissen verarbeiten und/oder E/A-gebunden sind, können Sie die Leistung erheblich verbessern, indem Sie Funktionen asynchron ausführen.

Um eine Funktion asynchron auszuführen, verwenden Sie die async def-Anweisung, die die Funktion mit asyncio direkt ausführt:

async def main():
    await some_nonblocking_socket_io_op()

Im Folgenden finden Sie ein Beispiel für eine Funktion mit einem HTTP-Trigger, die den aiohttp-HTTP-Client verwendet:

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)

Eine Funktion ohne das Schlüsselwort async wird automatisch in einem ThreadPoolExecutor-Threadpool ausgeführt:

# 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()

Um den vollen Nutzen aus der asynchronen Ausführung von Funktionen zu ziehen, muss der E/A-Vorgang bzw. die Bibliothek, die in Ihrem Code verwendet wird, ebenfalls asynchron implementiert sein. Die Verwendung synchroner E/A-Vorgänge in Funktionen, die als asynchron definiert sind, kann die Gesamtleistung beeinträchtigen. Auch wenn für die von Ihnen verwendeten Bibliotheken keine asynchrone Version implementiert ist, können Sie möglicherweise von der asynchronen Ausführung Ihres Codes profitieren, indem Sie in Ihrer App Ereignisschleifen verwalten.

Hier sind einige Beispiele von Clientbibliotheken, die asynchrone Muster implementiert haben:

  • aiohttp: HTTP-Client/-Server für asyncio
  • Streams API: Allgemeine async/await-fähige Primitiven für die Arbeit mit Netzwerkverbindungen
  • Janus Queue: Threadsichere asyncio-fähige Warteschlange für Python
  • pyzmq: Python-Bindungen für ZeroMQ
Verstehen asynchroner Verarbeitung in Python-Workern

Wenn Sie async vor einer Funktionssignatur definieren, markiert Python die Funktion als Coroutine. Wenn Sie die Coroutine aufrufen, kann sie beim Aufruf als Task in einer Ereignisschleife geplant werden. Wenn Sie await in einer asynchronen Funktion aufrufen, wird eine Fortsetzung in der Ereignisschleife registriert, und die Ereignisschleife kann den nächsten Task während der Wartezeit verarbeiten.

Unser Python-Worker nutzt die Ereignisschleife gemeinsam mit der async-Funktion des Kunden und kann mehrere Anforderungen gleichzeitig verarbeiten. Wir empfehlen unseren Kunden dringend die Verwendung von Bibliotheken, die mit asyncio kompatibel sind (z. B. aiohttp oder pyzmq). Wenn Sie diese Empfehlungen befolgen, erhöht sich der Durchsatz Ihrer Funktion im Vergleich zu diesen Bibliotheken, wenn sie synchron implementiert werden.

Hinweis

Wenn Ihre Funktion als async ohne await in der Implementierung deklariert ist, ist die Leistung der Funktion wesentlich beeinträchtigt, da die Ereignisschleife blockiert wird. Dadurch wiederum wird der Python-Worker daran gehindert, gleichzeitige Anforderungen zu verarbeiten.

Verwenden mehrerer Sprachworkerprozesse

Standardmäßig verfügt jede Functions-Hostinstanz über einen einzigen Sprachworkerprozess. Sie können die Anzahl der Workerprozesse pro Host erhöhen (bis zu 10), indem Sie die Anwendungseinstellung FUNCTIONS_WORKER_PROCESS_COUNT verwenden. Azure Functions versucht dann, gleichzeitige Funktionsaufrufe gleichmäßig auf diese Worker zu verteilen.

Für CPU-gebundene Apps sollten Sie die Anzahl der Sprachworker so festlegen, dass sie gleich oder höher ist als die Anzahl der Kerne, die pro Funktions-App zur Verfügung stehen. Weitere Informationen finden Sie unter Verfügbare Instanz-SKUs.

E/A-gebundene Apps können auch davon profitieren, eine höhere Anzahl von Workerprozessen im Vergleich zu den verfügbaren Kerne festzulegen. Denken Sie daran, dass eine zu hohe Anzahl von Workern die Gesamtleistung aufgrund der höheren Anzahl der erforderlichen Kontextwechsel beeinträchtigen kann.

FUNCTIONS_WORKER_PROCESS_COUNT gilt für jeden Host, der von Azure Functions erstellt wird, wenn Ihre Anwendung horizontal skaliert wird.

Festlegen der maximalen Workeranzahl in einem Sprachworkerprozess

Wie bereits im Abschnitt zur asynchronen Verarbeitung erwähnt, behandelt der Python-Sprachworker Funktionen und Coroutinen unterschiedlich. Eine Coroutine wird in derselben Ereignisschleife ausgeführt, in der auch der Sprachworker ausgeführt wird. Ein Funktionsaufruf dagegen wird als Thread in einem ThreadPoolExecutor ausgeführt, der vom Sprachworker verwaltet wird.

Sie können den Wert für die maximale Anzahl von Workern, die für die Ausführung von synchronen Funktionen zulässig sind, mithilfe der Anwendungseinstellung PYTHON_THREADPOOL_THREAD_COUNT festlegen. Dieser Wert legt das max_worker-Argument des ThreadPoolExecutor-Objekts fest, sodass Python zur asynchronen Ausführung von Aufrufen einen Pool mit höchstens so vielen Threads verwenden kann, wie durch max_worker festgelegt. Die Einstellung PYTHON_THREADPOOL_THREAD_COUNT gilt für jeden Worker, den der Functions-Host erstellt, und Python entscheidet, wann ein neuer Thread erstellt und wann der vorhandene, im Leerlauf befindliche Thread wiederverwendet werden soll. In älteren Python-Versionen (also 3.8, 3.7 und 3.6), ist der max_worker-Wert auf 1 festgelegt. In der Python-Version 3.9 ist max_worker auf None festgelegt.

Bei CPU-gebundenen Apps sollten Sie mit einem niedrigen Wert (1) beginnen und den Wert erhöhen, wenn Sie mit der Workload experimentieren. Dieser Vorschlag dient dazu, die benötigte Zeit für Kontextwechsel zu reduzieren und den Abschluss von CPU-gebundenen Tasks zu ermöglichen.

Bei E/A-gebundenen Apps sollten Sie beträchtliche Vorteile erzielen können, indem Sie die Anzahl von Threads für die Verarbeitung jedes Aufrufs erhöhen. Hier empfiehlt es sich, mit dem Python-Standardwert (Anzahl von Kernen) plus 4 zu beginnen und die Einstellung entsprechend den erzielten Durchsatzwerten anzupassen.

Bei Apps mit gemischten Workloads sollten Sie die Konfigurationen für FUNCTIONS_WORKER_PROCESS_COUNT und PYTHON_THREADPOOL_THREAD_COUNT ausgleichen, um den Durchsatz zu maximieren. Um herauszufinden, wofür Ihre Funktions-Apps am meisten Zeit benötigen, empfiehlt es sich, Profile für die Apps zu erstellen und die Werte entsprechend dem Verhalten festzulegen. Informationen zu diesen Anwendungseinstellungen finden Sie unter Verwenden mehrerer Workerprozesse.

Hinweis

Obwohl diese Empfehlungen sowohl für über HTTP ausgelöste als auch für nicht über HTTP ausgelöste Funktionen gelten, müssen Sie möglicherweise weitere triggerspezifische Konfigurationen für nicht über HTTP ausgelöste Funktionen anpassen, um die erwartete Leistung Ihrer Funktions-Apps zu erzielen. Weitere Informationen hierzu finden Sie unter Bewährte Methoden für zuverlässigen Azure Functions-Code.

Verwalten der Ereignisschleife

Sie sollten asyncio-kompatible Drittanbieterbibliotheken verwenden. Wenn keine Drittanbieterbibliothek Ihre Anforderungen erfüllt, können Sie die Ereignisschleifen auch in Azure Functions verwalten. Durch Ereignisschleifen erhalten Sie mehr Flexibilität bei der Verwaltung von Computeressourcen, und Sie können synchrone E/A-Bibliotheken mit Coroutinen umschließen.

Es gibt eine Vielzahl nützlicher offizieller Python-Dokumente zu Coroutinen und Tasks sowie zu Ereignisschleifen unter Verwendung der integrierten asyncio-Bibliothek.

Ein Beispiel ist die folgende requests-Bibliothek: Der folgende Codeausschnitt verwendet die asyncio-Bibliothek, um die requests.get()-Methode mit einer Coroutine zu umschließen und mehrere Webanforderungen an SAMPLE_URL gleichzeitig ausführen zu können.

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

Vertikale Skalierung

Sie können möglicherweise weitere Verarbeitungseinheiten – insbesondere in CPU-gebundenen Vorgängen – durch ein Upgrade auf einen Premium-Plan mit höheren Spezifikationen erhalten. Wenn Ihnen mehr Verarbeitungseinheiten zur Verfügung stehen, können Sie die Anzahl von Workerprozessen an die Anzahl verfügbarer Kerne anpassen und auf diese Weise ein höheres Maß an Parallelität erzielen.

Nächste Schritte

Weitere Informationen zur Python-Entwicklung für Azure Functions finden Sie in den folgenden Ressourcen: