مشاركة عبر


تحسين أداء معدل النقل لتطبيقات Python في Azure Functions

عند التطوير ل Azure Functions باستخدام Python، تحتاج إلى فهم كيفية أداء وظائفك وكيف يؤثر هذا الأداء على كيفية تكبير تطبيق الوظائف الخاص بك. الحاجة تكون أكثر أهمية عند تصميم تطبيقات عالية الأداء. العوامل الرئيسية التي يجب أخذها في الاعتبار عند تصميم وكتابة وتكوين تطبيقات الوظائف هي التكبير الأفقي وتهيئة أداء معدل النقل.

تحجيم أفقي

افتراضيا، يراقب Azure Functions تلقائيا الحمل على تطبيقك وينشئ المزيد من مثيلات المضيف ل Python حسب الحاجة. يستخدم Azure Functions عتبات مدمجة لأنواع المشغلات المختلفة لتحديد متى تضيف مثلات، مثل عمر الرسائل وحجم الطابور في QueueTrigger. هذه العتبات غير قابلة للتخصيص من قبل المستخدم. لمزيد من المعلومات، راجع التحجيم المدفوع بالأحداث في Azure Functions.

تحسين أداء معدل النقل

التكوينات الافتراضية مناسبة لمعظم تطبيقات Azure Functions. ومع ذلك، يمكنك تحسين أداء معدل نقل التطبيقات باستخدام إعدادات تعتمد على ملف عبء العمل الخاص بك. الخطوة الأولى هي فهم نوع عبء العمل الذي تعمله.

نوع حِمل العمل خصائص تطبيق الوظائف امثله
مقيدة بالإدخال/الخروج • يحتاج التطبيق إلى التعامل مع العديد من الاستدعاءات المتزامنة.
• يعالج التطبيق عددا كبيرا من أحداث الإدخال/الإخراج، مثل استدعاءات الشبكة وعمليات القراءة/الكتابة على الأقراص.
• واجهات برمجة التطبيقات على الويب
المرتبط بوحدة المعالجة المركزية • يقوم التطبيق بعمليات حسابية طويلة الأمد، مثل تغيير حجم الصورة.
• التطبيق يقوم بتحويل البيانات.
• معالجة البيانات
• استنتاج التعلم الآلي

وبما أن أعباء الوظائف الواقعية عادة ما تكون مزيجا من الإدخال/الإخراج والمعالج، يجب أن تضع التطبيق تحت أعمال الإنتاج الواقعية.

تكوينات خاصة بالأداء

بعد أن تفهم ملف عبء العمل في تطبيق الوظائف الخاص بك، إليك تكوينات يمكنك استخدامها لتحسين أداء إنتاجية وظائفك.

غير متزامنة

نظرا لأن Python هو وقت تشغيل أحادي الخيط، يمكن لمثيل مضيف ل Python معالجة استدعاء دالة واحدة فقط في كل مرة بشكل افتراضي. بالنسبة للتطبيقات التي تعالج عددا كبيرا من أحداث الإدخال/الإخراج و/أو مرتبطة بالإدخال/الإخراج، يمكنك تحسين الأداء بشكل كبير من خلال تشغيل الدوال بشكل غير متزامن.

لتشغيل دالة بشكل غير متزامن، استخدم العبارة async def التي تشغل الدالة مباشرة مع asyncio :

async def main():
    await some_nonblocking_socket_io_op()

إليك مثال على دالة تستخدم مشغل HTTP وتستخدم عميل aiohttp 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()

لتحقيق الفائدة الكاملة من تشغيل الدوال بشكل غير متزامن، يجب أن يكون لعملية الإدخال/المكتبة/المكتبة المستخدمة في كودك تنفيذ غير متزامن أيضا. استخدام عمليات الإدخال/الإخراج المتزامنة في الدوال المعرفة بأنها غير متزامنة قد يضر بالأداء العام. إذا لم تكن المكتبات التي تستخدمها تحتوي على نسخة غير متزامنة منفذة، فقد تستفيد من تشغيل الكود بشكل غير متزامن من خلال إدارة حلقة الأحداث في تطبيقك.

إليك بعض الأمثلة على مكتبات العملاء التي نفذت أنماطا غير متزامنة:

  • aiohttp - عميل/خادم HTTP ل asyncio
  • واجهة برمجة تطبيقات التدفقات - بدائيات عالية المستوى غير متزامنة وجاهزة للانتظار للعمل مع الاتصال بالشبكة
  • Janus Queue - قائمة انتظار آمنة للخيط والوعي بعدم التزامن ل Python
  • pyzmq - Python الروابط ل ZeroMQ
فهم عدم التزامن في Python worker

عندما تعرف async أمام توقيع الدالة، Python تضع علامة على الدالة كروتين. عندما تستدعي الروتين المشترك، يمكن جدولتها كمهمة في حلقة حدث. عندما تستدعي await دالة غير متزامنة، تسجل استمرارية في حلقة الحدث، مما يسمح لحلقة الحدث بمعالجة المهمة التالية خلال فترة الانتظار.

في Python Worker لدينا، يشارك العامل حلقة الأحداث مع وظيفة async الخاصة بالعميل وهي قادرة على التعامل مع عدة طلبات في نفس الوقت. نشجع عملاءنا بشدة على استخدام مكتبات متوافقة مع asyncio، مثل aiohttpوpyzmq. اتباع هذه التوصيات يزيد من معدل نقل الدالة مقارنة بتلك المكتبات عند تنفيذها بشكل متزامن.

‏‫ملاحظة‬

إذا تم إعلان وظيفتك ك async بدون أي await داخل تنفيذها، فإن أداء وظيفتك سيتأثر بشدة لأن حلقة الأحداث ستمنع عامل Python من التعامل مع الطلبات المتزامنة.

استخدم عدة عمليات لعامل اللغة

افتراضيا، كل مثيل مضيف للوظائف يحتوي على عملية عامل لغة واحدة. يمكنك زيادة عدد عمليات العمال لكل مضيف (حتى 10) باستخدام FUNCTIONS_WORKER_PROCESS_COUNT إعدادات التطبيق. يحاول Azure Functions بعد ذلك توزيع استدعاءات الوظائف المتزامنة بالتساوي بين هؤلاء الموظفين.

بالنسبة للتطبيقات المرتبطة بالمعالج، يجب أن تضبط عدد العاملين في اللغة ليكون مساويا أو أعلى من عدد الأنوية المتاحة لكل تطبيق وظيفة. لمعرفة المزيد، راجع وحدات SKUs المتوفرة للمثيل.

قد تستفيد التطبيقات المرتبطة بالإدخال/التشغيل أيضا من زيادة عدد عمليات العمال إلى ما يتجاوز عدد الأنوية المتاحة. ضع في اعتبارك أن تعيين عدد العمال مرتفعا جدا قد يؤثر على الأداء العام بسبب زيادة عدد المفاتيح السياقية المطلوبة.

ينطبق FUNCTIONS_WORKER_PROCESS_COUNT على كل مضيف ينشئه Azure Functions عند توسيع تطبيقك لتلبية الطلب.

إعداد الحد الأقصى من العمال ضمن عملية عامل اللغة

كما ذكر في القسم غير المتزامن section، عامل اللغة Python يتعامل مع الوظائف وcoroutines بشكل مختلف. يتم تشغيل روتين مصور ضمن نفس حلقة الأحداث التي يعمل عليها عامل اللغة. من ناحية أخرى، يتم تشغيل استدعاء الدالة داخل ThreadPoolExecutor، والذي يحافظ عليه عامل اللغة كخيل.

يمكنك تحديد الحد الأقصى للعاملين المسموح به لتشغيل دوال المزامنة باستخدام إعداد تطبيق PYTHON_THREADPOOL_THREAD_COUNT . تحدد هذه القيمة وسيط max_worker لكائن ThreadPoolExecutor، مما يسمح Python باستخدام مجموعة من خيوط max_worker على الأكثر لتنفيذ الاستدعاءات بشكل غير متزامن. ينطبق PYTHON_THREADPOOL_THREAD_COUNT على كل عامل ينشئه مضيف الوظائف، ويقرر Python متى ينشئ خيطا جديدا أو يعيد استخدام الخيط الموجود في الخمول. بالنسبة للإصدارات الأقدم Python (أي 3.8، 3.7، و 3.6)، يتم تعيين قيمة max_worker على 1. بالنسبة للإصدار Python 3.9، max_worker مضبوط على None.

بالنسبة للتطبيقات المرتبطة بالمعالج، يجب أن تبقي الإعداد منخفضا، يبدأ من 1 ويزداد مع تجربتك في عبء العمل. هذا الاقتراح هو تقليل الوقت المستغرق في تبديل السياق والسماح بإكمال المهام المرتبطة بالمعالج.

بالنسبة للتطبيقات المرتبطة بالإدخال/الإخراج، يجب أن تلاحظ مكاسب كبيرة من خلال زيادة عدد الخيوط التي تعمل على كل استدعاء. التوصية هي أن تبدأ بالإعداد الافتراضي ل Python (عدد الأنوية) + 4 ثم تعدل بناء على قيم معدل النقل التي تراها.

بالنسبة لتطبيقات الأحمال المختلطة، يجب أن توازن بين الاثنين FUNCTIONS_WORKER_PROCESS_COUNT والتكوينات PYTHON_THREADPOOL_THREAD_COUNT لتعظيم معدل الإنتاجية. لفهم ما تقضي فيه تطبيقات الوظائف معظم وقتها، نوصي بتحليل الصفات وضبط القيم وفقا لسلوكياتها. للتعرف على إعدادات التطبيقات هذه، راجع استخدام عمليات عامل متعددة.

‏‫ملاحظة‬

على الرغم من أن هذه التوصيات تنطبق على كل من الدوال التي يتم تفعيلها بتقنية HTTP وغير HTTP، قد تحتاج إلى تعديل إعدادات أخرى خاصة بالمشغلات للوظائف غير المفعلة بتقنية HTTP للحصول على الأداء المتوقع من تطبيقات الوظائف الخاصة بك. لمزيد من المعلومات حول هذا الموضوع، يرجى الرجوع إلى هذا أفضل الممارسات للحصول على Azure Functions موثوقة.

إدارة حلقة الأحداث

يجب عليك استخدام مكتبات طرف ثالث متوافقة مع asyncio. إذا لم تلبي أي من مكتبات الطرف الثالث احتياجاتك، يمكنك أيضا إدارة حلقات الأحداث في Azure Functions. إدارة حلقات الأحداث تمنحك مرونة أكبر في إدارة موارد الحوسبة، كما تتيح لك تغليف مكتبات الإدخال/الإخراج المتزامنة في روتينات متزامنة.

هناك العديد من الوثائق الرسمية Python المفيدة التي تناقش ><الروتينات المشتركة والمهام وEvent Loop باستخدام مكتبة asyncio المدمجة.

خذ مكتبة الطلبات التالية كمثال، حيث يستخدم هذا المقتطف الكود مكتبة asyncio لتغليف requests.get() الطريقة في روتين موحد، مع تشغيل عدة طلبات ويب 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')

تحجيم عمودي

قد تتمكن من الحصول على وحدات معالجة أكثر، خاصة في التشغيل المرتبط بالمعالج، عن طريق الترقية إلى خطة بريميوم بمواصفات أعلى. مع وحدات معالجة أعلى، يمكنك تعديل عدد عمليات العمال حسب عدد الأنوية المتاحة وتحقيق درجة أعلى من التوازي.

الخطوات التالية

لمزيد من المعلومات حول تطوير Azure Functions Python، راجع الموارد التالية: