改善 Azure Functions 中 Python 應用程式的輸送量效能

使用 Python 開發 Azure Functions 時,您必須了解函式的執行方式,以及該效能如何影響函式應用程式的調整方式。 在設計高效能應用程式時,需求更為重要。 在設計、撰寫和設定函數應用程式時,須考慮的主要因素為水平調整和輸送量效能設定。

水平調整規模

根據預設,Azure Functions 會自動監視應用程式上的負載,並視需要為 Python 建立其他主機執行個體。 Azure Functions 會針對不同的觸發程序類型使用內建閾值,以決定何時新增執行個體,例如訊息的存留期和 QueueTrigger 的佇列大小。 這些閾值並非使用者可設定的。 如需詳細資訊,請參閱 Azure Functions 中的事件驅動調整

改善輸送量效能

預設設定適用於大部分 Azure Functions 應用程式。 不過,您可以根據工作負載設定檔採用設定,進而改善應用程式的輸送量效能。 第一個步驟是了解您正在執行的工作負載類型。

工作負載類型 函數應用程式特性 範例
I/O 繫結 • 應用程式必須處理許多並行叫用。
• 應用程式會處理大量的 I/O 事件,例如網路呼叫和磁碟讀取/寫入。
• Web API
CPU 繫結 • 應用程式會進行長時間執行計算,例如影像調整大小。
• 應用程式會進行資料轉換。
• 資料處理
• 機器學習推斷

由於真實世界函式工作負載通常會混合 I/O 和 CPU 繫結,因此您應該在實際生產負載下分析應用程式。

效能特定設定

在了解函數應用程式的工作負載設定檔後,您可以使用下列設定來改善函式的輸送量效能。

Async

因為 Python 是單一執行緒執行階段,所以根據預設,Python 的主機執行個體一次只能處理一個函式叫用。 針對處理大量 I/O 事件和/或 I/O 繫結的應用程式,您可以透過非同步方式執行函式來大幅改善效能。

若要以非同步方式執行函式,請使用 async def 陳述式,這會搭配 asyncio 直接執行函式:

async def main():
    await some_nonblocking_socket_io_op()

以下是具有使用 aiohttp HTTP 用戶端的 HTTP 觸發程序函式範例:

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)

沒有 async 關鍵字的函式會在 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()

為了實現非同步執行函式的所有優點,程式碼中使用的 I/O 作業/程式庫也必須非同步實作。 在定義為非同步函式中使用同步 I/O 作業可能會損害整體效能。 如果您正在使用的程式庫並未實作非同步版本,您仍可藉由在應用程式中管理事件迴圈,以非同步方式執行程式碼。

下列為一些已實作非同步模式的用戶端程式庫範例:

  • aiohttp - asyncio 的 HTTP 用戶端/伺服器
  • 串流 API - 用於網路連線的高階非同步/等候就緒基本類型
  • Janus 佇列 - 適用於 Python 的執行緒安全 asyncio 感知佇列
  • pyzmq - ZeroMQ 的 Python 繫結
了解 Python 背景工作角色中的非同步

當您在函式簽章前定義 async 時,Python 會將函式標示為協同程式。 呼叫協同程式時,可以將其作為工作排程至事件迴圈。 當您在非同步函式中呼叫 await 時,其會在事件迴圈中註冊接續,其允許事件迴圈在等候期間處理下一個工作。

在我們的 Python 背景工作角色中,背景工作角色會與客戶的 async 函式共用事件迴圈,且能同時處理多個要求。 我們強烈建議客戶使用 asyncio 相容程式庫 (例如 aiohttppyzmq)。 遵循這些建議會增加函式在同步實作時,相較於這些程式庫的輸送量。

注意

如果函式於實作中宣告為 async 而並無任何 await,則函式的效能會受到嚴重影響,因為事件迴圈會遭封鎖,以禁止 Python 背景工作角色處理並行要求。

使用多個語言背景工作處理序

根據預設,每個 Functions 主機執行個體都有單一語言背景工作處理序。 您可以使用 FUNCTIONS_WORKER_PROCESS_COUNT 應用程式設定,來增加每個主機的背景工作處理序數目 (最多 10 個)。 Azure Functions 接著會嘗試在這些背景工作中平均散發同時函式叫用。

針對 CPU 繫結應用程式,您應該將語言背景工作角色數目設定為與每個函數應用程式可用的核心數目相同或更高。 若要深入了解,請參閱可用的執行個體 SKU

I/O 繫結應用程式也可能受益於將背景工作處理序數目增加至超過可用的核心數目。 請記住,將背景工作角色數目設的太高可能會影響整體效能,因為所需的環境切換數目增加。

FUNCTIONS_WORKER_PROCESS_COUNT 適用於 Azure Functions 在擴增應用程式以符合需求時所建立的每個主機。

在語言背景工作處理序內,設定最大背景工作角色

如<非同步>一節中所述,Python 語言背景工作角色會以不同的方式處理函式和協同程式。 協同程式會在語言背景工作角色所執行的相同事件迴圈內執行。 另一方面,函式叫用是在由語言背景工作角色所維護的 ThreadPoolExecutor 內,作為執行緒執行。

您可以使用 PYTHON_THREADPOOL_THREAD_COUNT 應用程式設定,設定允許執行同步處理函式的最大背景工作角色值。 這個值會設定 ThreadPoolExecutor 物件的 max_worker 引數,其可讓 Python 使用最多 max_worker 執行緒的集區來以非同步方式執行呼叫。 PYTHON_THREADPOOL_THREAD_COUNT 適用於 Functions 主機所建立的每個背景工作角色,而 Python 決定何時要建立新的執行緒,或重複使用現有的閒置執行緒。 針對舊版 Python (也就是 3.83.73.6),max_worker 值會設定為 1。 針對 Python 版本 3.9max_worker 會設定為 None

針對 CPU 繫結應用程式,您應該將設定維持於較低數目,從 1 開始,並隨著您對工作負載的實驗而增加。 這項建議乃為減少在環境切換上耗費的時間,並允許 CPU 繫結工作完成。

針對 I/O 繫結的應用程式,您應能藉由增加在每個叫用上運行的執行緒數目,進而獲得大幅增益。 建議從 Python 預設值開始 (核心數目) + 4,然後根據您看到的輸送量值進行調整。

針對混合工作負載應用程式,您應該平衡 FUNCTIONS_WORKER_PROCESS_COUNTPYTHON_THREADPOOL_THREAD_COUNT 設定,以將輸送量最大化。 若要了解函數應用程式於何處花費最多時間,建議您加以分析並根據行為來設定值。 若要了解這些應用程式設定,請參閱使用多個背景工作處理序

注意

雖然這些建議同時適用於 HTTP 和非 HTTP 觸發程序函數,但您可能需要調整非 HTTP 觸發程序函數的其他觸發程序特定設定,才能從函式應用程式獲得預期效能。 如需詳細資訊,請參閱此可靠 Azure Functions 的最佳做法

管理事件迴圈

您應該使用與 asyncio 相容的第三方程式庫。 若無任何第三方程式庫符合您的需求,您也可以在 Azure Functions 中管理事件迴圈。 管理事件迴圈可在計算資源管理方面提供更大的靈活性,亦能將同步 I/O 程式庫包裝為協同程式。

有許多實用的 Python 官方文件會使用內建的 asyncio 程式庫來討論協同程式和工作以及事件迴圈

以下列要求程式庫為例,此程式碼片段使用 asyncio 程式庫將 requests.get() 方法包裝為協同程式,同時執行多個對 SAMPLE_URL 的 Web 要求。

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

垂直調整規模

您可以透過升級至更高規格的進階方案,來取得更多處理單位,尤其是在 CPU 繫結作業中。 使用較高的處理單位,您可以根據可用的核心數目來調整背景工作處理序數目,並達到更高的平行處理原則程度。

下一步

如需 Azure Functions Python 開發的詳細資訊,請參閱下列資源: