Pythonを使用してAzure Functions用に開発する場合は、関数のパフォーマンスと、そのパフォーマンスが関数アプリのスケーリング方法にどのように影響するかを理解する必要があります。 高度にパフォーマンスの高いアプリを設計する場合は、その必要性がより重要になります。 関数アプリを設計、記述、および構成する際に考慮すべき主な要因は、水平方向のスケーリングとスループットのパフォーマンス構成です。
水平スケーリング
既定では、Azure Functionsはアプリケーションの負荷を自動的に監視し、必要に応じてPythonのホスト インスタンスを作成します。 Azure Functionsでは、さまざまなトリガーの種類に対して組み込みのしきい値を使用して、QueueTrigger のメッセージの有効期間やキュー サイズなど、インスタンスを追加するタイミングを決定します。 これらのしきい値は、ユーザーが構成することはできません。 詳細については、
スループット パフォーマンスの向上
既定の構成は、ほとんどのAzure Functionsアプリケーションに適しています。 ただし、ワークロード プロファイルに基づいて構成を採用することで、アプリケーションのスループットのパフォーマンスを向上させることができます。 最初の手順では、実行しているワークロードの種類を理解します。
| ワークロードの種類 | 関数アプリの特性 | 例示 |
|---|---|---|
| I/O バウンド | •アプリは、多くの同時呼び出しを処理する必要があります。 • アプリは、ネットワーク呼び出しやディスクの読み取り/書き込みなど、多数の I/O イベントを処理します。 |
• Web API |
| CPU 制約 | •アプリは、画像のサイズ変更など、実行時間の長い計算を行います。 •アプリは、データ変換を行います。 |
• データ処理 • 機械学習の推論 |
実際の関数ワークロードは通常、I/O と CPU バインドの組み合わせなので、現実的な運用環境の負荷の下でアプリをプロファイリングする必要があります。
パフォーマンス固有の構成
関数アプリのワークロード プロファイルを理解したら、関数のスループット パフォーマンスを向上させるために使用できる構成を次に示します。
非同期
Python はシングル スレッド ランタイムであるため、Pythonのホスト インスタンスは、既定で一度に 1 つの関数呼び出しのみを処理できます。 多数の I/O イベントを処理するアプリケーションや I/O バインドされているアプリケーションでは、関数を非同期的に実行することでパフォーマンスを大幅に向上させることができます。
関数を非同期的に実行するには、async def で直接関数を実行する ステートメントを使用します。
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 クライアント/サーバー
- Streams API - ネットワーク接続を利用するための高レベルの非同期/await対応基本要素
- Janus Queue - スレッドセーフでasyncio対応のPythonキュー
- pyzmq - ZeroMQ に対するPythonバインディング
Python worker での非同期について
関数シグネチャの前に async を定義すると、Pythonは関数をコルーチンとしてマークします。 コルーチンを呼び出すときは、タスクとしてイベント ループにスケジュールできます。 非同期関数で await を呼び出すと、継続がイベント ループに登録されます。これにより、イベント ループは待機時間中に次のタスクを処理できます。
Python Worker では、worker はイベント ループを顧客の async 関数と共有し、複数の要求を同時に処理できます。
aiohttp や pyzmq などの asyncio 互換ライブラリを使用することを強くお勧めします。 これらの推奨事項に従うと、同期的に実装された場合の関数のスループットが、これらのライブラリと比較して向上します。
注
実装内で async なしで関数が await として宣言されている場合、イベント ループがブロックされ、Python ワーカーが同時要求を処理することを禁止するため、関数のパフォーマンスに重大な影響が及びます。
複数の言語ワーカー プロセスを使用する
既定では、すべての Functions ホスト インスタンスには 1 つの言語ワーカー プロセスがあります。
FUNCTIONS_WORKER_PROCESS_COUNT アプリケーション設定を使用して、ホストあたりのワーカー プロセスの数を増やすことができます (最大 10)。 Azure Functions、これらのワーカー間で同時関数呼び出しを均等に分散しようとします。
CPU にバインドされたアプリの場合は、言語ワーカーの数を、関数アプリごとに使用可能なコアの数と同じかそれ以上に設定する必要があります。 詳細については、「 使用可能なインスタンス SKU」を参照してください。
また、I/O バインド アプリは、使用可能なコア数を超えてワーカー プロセスの数を増やすことでメリットが得られる場合があります。 ワーカーの数が多すぎると、必要なコンテキスト スイッチの数が増えるので、全体的なパフォーマンスに影響する可能性があることに注意してください。
FUNCTIONS_WORKER_PROCESS_COUNTは、需要に応じてアプリケーションを拡張する際にAzure Functionsによって作成される各ホストに適用されます。
言語ワーカー プロセス内で最大ワーカー数を設定する
async section で説明したように、Python言語ワーカーは関数と coroutines を異なる方法で処理します。 コルーチンは、言語ワーカーが実行するのと同じイベント ループ内で実行されます。 一方、関数呼び出しは ThreadPoolExecutor 内で実行され、言語ワーカーによってスレッドとして維持されます。
PYTHON_THREADPOOL_THREAD_COUNT アプリケーション設定を使用して、同期関数の実行に許可される最大ワーカー数の値を設定できます。 この値は、ThreadPoolExecutor オブジェクトの max_worker 引数を設定します。これにより、Pythonは最大max_worker スレッドのプールを使用して非同期的に呼び出しを実行できます。 Functions ホストが作成する各ワーカーには PYTHON_THREADPOOL_THREAD_COUNT が適用され、新しいスレッドを作成するか既存のアイドルスレッドを再利用するかを決定するのは Python です。 以前のPython バージョン (つまり、3.8、3.7、および 3.6) の場合、max_worker 値は 1 に設定されます。 Python バージョン 3.9 の場合、max_worker は None に設定されます。
CPU にバインドされたアプリの場合は、1 から始まり、ワークロードを試す際に増やして、設定を低い数値に保つ必要があります。 この提案は、コンテキスト スイッチに費やされる時間を短縮し、CPU バインド タスクを完了できるようにすることです。
I/O バインド アプリの場合、各呼び出しで動作するスレッドの数を増やすことで、大幅な向上が得られます。 Pythonの既定値 (コア数) + 4 から始めて、表示されているスループット値に基づいて調整することをお勧めします。
ワークロードが混在するアプリの場合は、スループットを最大化するために、 FUNCTIONS_WORKER_PROCESS_COUNT 構成と PYTHON_THREADPOOL_THREAD_COUNT 構成の両方のバランスを取る必要があります。 関数アプリが最も多くの時間を費やしている内容を理解するには、それらをプロファイリングし、動作に応じて値を設定することをお勧めします。 これらのアプリケーション設定の詳細については、「 複数のワーカー プロセスを使用する」を参照してください。
注
これらの推奨事項は HTTP トリガー関数と非 HTTP トリガー関数の両方に適用されますが、関数アプリから期待されるパフォーマンスを得るために、HTTP によってトリガーされない関数の他のトリガー固有の構成を調整することが必要になる場合があります。 詳細については、この
イベント ループの管理
asyncio 互換のサード パーティ製ライブラリを使用する必要があります。 ニーズを満たすサード パーティ製ライブラリがない場合は、Azure Functionsでイベント ループを管理することもできます。 イベント ループを管理すると、コンピューティング リソース管理の柔軟性が向上し、同期 I/O ライブラリをコルーチンにラップすることもできます。
組み込みの
次の requests ライブラリを例として取ります。このコードスニペットでは asyncio ライブラリを使用して requests.get() メソッドをコルーチンにラップし、複数の Web リクエストをSAMPLE_URLに対して同時に実行します。
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')
垂直スケーリング
より高い仕様で Premium プランにアップグレードすることで、特に CPU バインド操作でより多くの処理ユニットを取得できる場合があります。 処理ユニットが多いほど、使用可能なコアの数に応じてワーカー プロセスの数を調整し、より高い並列処理を実現できます。
次のステップ
Azure Functions Python開発の詳細については、次のリソースを参照してください。