اللاخوادم والعمليات المدفوعة بالأحداث

تشغيل المعماريات المدفوعة بالأحداث

18 دقيقة الدرس 5 من 28

تشغيل المعماريات المدفوعة بالأحداث

تنقل المعمارية المدفوعة بالأحداث (EDA) عقد الموثوقية من الاستجابة المتزامنة للطلبات إلى التسليم غير المتزامن للرسائل. في السياق بلا خوادم هذا يعني أن Lambda ليست الخادم بعد الآن — بل وسيط الأحداث هو الخادم. لهذا التمييز تبعات تشغيلية عميقة. استدعاء HTTP المتزامن يفشل فوراً وبشكل واضح؛ حدث فاشل يمكن أن يجلس صامتاً في قائمة الانتظار، أو يعيد المحاولة خفية، أو يُكرَّر، أو يصل بترتيب خاطئ. فهم كيفية تشغيل هذه الأنظمة بانضباط إنتاجي — قوائم الرسائل الميتة، وسياسات إعادة المحاولة، وعقود الاستبدادية، وضمانات الترتيب — هو الفارق بين EDA تتوسع بأناقة وأخرى تفسد البيانات تحت الحمل.

قوائم الرسائل الميتة (DLQ): شبكة الأمان التشغيلية

قائمة الرسائل الميتة (DLQ) هي وجهة الأحداث التي استنفدت ميزانية إعادة محاولتها دون معالجة ناجحة. ليست سجل أخطاء — بل قائمة انتظار قابلة للاسترداد من العمل غير المعالج. على نطاق Amazon، DLQs غير قابلة للتفاوض: كل مصدر أحداث Lambda غير متزامن (SQS، SNS، EventBridge، Kinesis، تدفقات DynamoDB) يجب أن يكون له DLQ مُعدَّة ومُراقَبة. DLQ خالية من التنبيهات تمتلئ بصمت تعني أن الطلبات لا تُنفَّذ، والمدفوعات لا تُسجَّل، وأعداد المخزون تنحرف.

اضبط DLQ على تعيين مصدر أحداث SQS وأنشئ تنبيهاً في كتلة Terraform واحدة:

# event_source_mapping.tf — SQS trigger with DLQ and alarm resource "aws_sqs_queue" "orders_dlq" { name = "orders-processor-dlq" message_retention_seconds = 1209600 # 14 days — enough time for on-call to triage kms_master_key_id = aws_kms_key.sqs.arn } resource "aws_sqs_queue" "orders" { name = "orders-processor" visibility_timeout_seconds = 300 # must be >= Lambda timeout redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn maxReceiveCount = 3 # move to DLQ after 3 failed receives }) } resource "aws_lambda_event_source_mapping" "orders" { event_source_arn = aws_sqs_queue.orders.arn function_name = aws_lambda_function.order_processor.arn batch_size = 10 maximum_batching_window_in_seconds = 5 function_response_types = ["ReportBatchItemFailures"] } # Alert when anything lands in the DLQ resource "aws_cloudwatch_metric_alarm" "dlq_not_empty" { alarm_name = "orders-dlq-has-messages" namespace = "AWS/SQS" metric_name = "ApproximateNumberOfMessagesVisible" dimensions = { QueueName = aws_sqs_queue.orders_dlq.name } comparison_operator = "GreaterThanThreshold" threshold = 0 evaluation_periods = 1 period = 60 statistic = "Sum" alarm_actions = [aws_sns_topic.pagerduty.arn] }
DLQs للكتابة مرة واحدة؛ إعادة تشغيلها يتطلب أدوات. زر "redrive" في وحدة تحكم AWS على SQS (متاح منذ 2021) يعيد إرسال رسائل DLQ إلى قائمة الانتظار المصدر بتزامن قابل للتحكم. لـ Kinesis وتدفقات DynamoDB لا يوجد إعادة تشغيل أصلية — يجب كتابة Lambda للإعادة تقرأ DLQ وتعيد نشر الأحداث. ابنِ هذه الأدوات واختبرها قبل أن تحتاجها الساعة الثانية صباحاً.

نوع الاستجابة ReportBatchItemFailures حاسم على نطاق الإنتاج. بدونه، إذا عُولجت دفعة من 10 رسائل SQS وفشلت الرسالة السابعة، تُبلّغ Lambda بفشل الدفعة كلها وتصبح جميع الرسائل العشر مرئية مجدداً. مع الإبلاغ عن فشل العناصر الجزئية، تُعاد الرسالة السابعة فقط إلى القائمة:

# Python Lambda handler with partial batch failure reporting import json def handler(event, context): failures = [] for record in event["Records"]: try: body = json.loads(record["body"]) process_order(body) except Exception as exc: # Report only the failed item; the rest are deleted failures.append({"itemIdentifier": record["messageId"]}) print(f"Failed to process {record['messageId']}: {exc}") return {"batchItemFailures": failures}
أوقات مهلة الرؤية المُعدَّة بشكل خاطئ هي أكثر الأخطاء شيوعاً لملء DLQ. إذا كانت مهلة Lambda 60 ثانية لكن مهلة رؤية القائمة 30 ثانية، تصبح الرسالة مرئية مجدداً بينما Lambda لا تزال تعالجها — يلتقطها مستهلك ثانٍ، يفشل كلاهما، ويتضاعف عدد الاستلام في كل استدعاء. يجب أن تكون مهلة رؤية SQS ضعف مهلة Lambda 6 مرات على الأقل لاستيعاب إعادة المحاولات والبدايات الباردة وزمن استجابة امتداد Lambda.

سياسات إعادة المحاولة: التصميم للفشل الحتمي

كل استدعاء حدث غير متزامن في AWS له سياسة إعادة محاولة قابلة للتهيئة. الإعدادات الافتراضية مصممة للصحة وليس التكلفة — فهمها ضروري قبل أن تُفعَّل في الإنتاج.

  • استدعاءات Lambda غير المتزامنة (SNS، S3، EventBridge): تعيد AWS المحاولة مرتين مع تراجع أسي قبل الإرسال إلى وجهة Lambda أو DLQ. نافذة إعادة المحاولة الكاملة تصل إلى 6 ساعات. استخدم aws lambda put-function-event-invoke-config لضبط هذا.
  • SQS: يتحكم فيها maxReceiveCount للقائمة (سياسة إعادة التوجيه). كل استلام فاشل يزيد العداد. رسالة عالقة في المعالجة لأطول من مهلة الرؤية تزيد العداد دون أن تُبلّغ Lambda بالفشل — احتراق هادئ لإعادة المحاولة.
  • Kinesis / تدفقات DynamoDB: تُعاد المحاولة حتى النجاح أو انتهاء صلاحية البيانات (24 ساعة افتراضياً لكلاهما). سجل مسموم يفشل دائماً سيحجب الـ shard طوال فترة الاحتفاظ كاملة. اضبط bisectBatchOnFunctionError وdestinationConfig.onFailure لعزل الأخطاء وتوجيهها.
  • EventBridge Pipes: محاولات إعادة المحاولة قابلة للتهيئة (0–185) وحد أقصى للعمر (60 ثانية–24 ساعة). أخطاء الإثراء لا تُعاد محاولتها؛ حالات عدم التطابق في الفلاتر تُسقَط بصمت.
aws lambda put-function-event-invoke-config \ --function-name order-processor \ --maximum-retry-attempts 2 \ --maximum-event-age-in-seconds 3600 \ --destination-config \ '{"OnFailure":{"Destination":"arn:aws:sqs:us-east-1:123456789:order-processor-dlq"}, "OnSuccess":{"Destination":"arn:aws:sqs:us-east-1:123456789:order-audit"}}'
التراجع الأسي مع التشتيت هو المعيار لفترات إعادة المحاولة. تطبقه AWS تلقائياً للاستدعاءات غير المتزامنة، لكن إذا كنت تبني حلقة إعادة محاولة مخصصة (مثلاً إعادة النشر من DLQ)، استخدم random.uniform(0, min(cap, base * 2 ** attempt)). بدون تشتيت، تُعيد موجة من إعادة المحاولات بعد انقطاع في المنبع تشبع الخدمة المتعافية في وقت واحد. هذا نمط فشل موثَّق جيداً في Netflix وAmazon وGoogle.

الاستبدادية: العقد الذي يجعل إعادة المحاولة آمنة

إعادة المحاولة آمنة فقط إذا كانت معالجاتك استبدادية: معالجة نفس الحدث مرتين تنتج نفس الآثار الجانبية كمعالجته مرة واحدة. على نطاق الإنتاج هذا ليس اختيارياً — توثّق AWS نفسها "التسليم مرة واحدة على الأقل" لكل مصدر أحداث غير متزامن. السؤال ليس إن ستستقبل معالجتك مكرراً؛ بل متى.

ثلاثة أنماط استبدادية معيارية تُستخدم في أنظمة EDA الإنتاجية:

  1. مفتاح الاستبدادية في مخزن البيانات: اكتب معرف الحدث في جدول DynamoDB مع كتابة مشروطة. إذا كان العنصر موجوداً بالفعل، تجاوز المعالجة. هذا النمط الأكثر موثوقية ويصمد عبر إعادات تشغيل Lambda.
  2. استبدادية Lambda Powertools: مزخرف مدعوم بـ DynamoDB يتولى الكتابة المشروطة وقفل التنفيذ وانتهاء الصلاحية بثلاثة أسطر من الكود.
  3. عمليات استبدادية في الهدف: الـ upserts (مثل INSERT ... ON CONFLICT DO UPDATE في PostgreSQL؛ أو UpdateItem مع تعبير مشروط في DynamoDB) استبدادية بطبيعتها لتحديث السجل، رغم أن الآثار الجانبية (إرسال بريد إلكتروني، نشر حدث منبع) تحتاج نمط المفتاح.
# Lambda Powertools idempotency — DynamoDB-backed (Python) # pip install aws-lambda-powertools from aws_lambda_powertools.utilities.idempotency import ( idempotent_function, IdempotencyConfig, DynamoDBPersistenceLayer, ) persistence_store = DynamoDBPersistenceLayer(table_name="idempotency-store") config = IdempotencyConfig( event_key_jmespath="body", # use the SQS message body as the key expires_after_seconds=3600, # deduplicate within 1 hour raise_on_no_idempotency_key=True, # fail fast if key field is missing ) @idempotent_function(data_keyword_argument="event", config=config, persistence_store=persistence_store) def handler(event, context): order = json.loads(event["Records"][0]["body"]) charge_card(order["payment_token"], order["amount_cents"]) fulfill_inventory(order["sku"], order["quantity"]) return {"status": "fulfilled", "order_id": order["id"]}

جدول DynamoDB لمخزن الاستبدادية يحتاج خاصية TTL ومفتاح hash بقيمة id (SHA-256 لمفتاح الحدث). وفّره بسعة on-demand — موجات من إعادات المحاولة تسبب كتابات متقطعة، والسعة المُوفَّرة مسبقاً ستُقيَّد وتُفشل الغرض.

الترتيب: حين يهم التسلسل

أنظمة EDA لديها طيف من ضمانات الترتيب. اختيار مصدر الحدث الخاطئ لحالة استخدام تتطلب الترتيب هو خلل تصميم لا يظهر إلا تحت الحمل.

Event ordering guarantees by AWS service Event Source Ordering Guarantees Service Ordering Delivery Use case SQS Standard Best-effort only At-least-once Tasks, fan-out, async work SQS FIFO Per message group Exactly-once (dedup) Financial tx, state machines Kinesis Data Streams Strict within shard At-least-once Streams, CDC, analytics EventBridge No guarantee At-least-once Cross-service events, fan-out MSK / Kafka Strict within partition Configurable (acks) Event sourcing, log replay
تتباين ضمانات الترتيب والتسليم على نطاق واسع عبر مصادر أحداث AWS. اختر المصدر بناءً على ما إذا كان حِملك يتطلب ترتيباً صارماً، وصمّم معالجتك لدلالات التسليم (التسليم مرة واحدة على الأقل يتطلب دائماً الاستبدادية).

في SQS FIFO، معرف مجموعة الرسائل هو وحدة الترتيب — كل الرسائل التي لها نفس معرف المجموعة تُعالَج بترتيب FIFO صارم من خلال فتحة تزامن Lambda واحدة. هذا يعني أن رسالة بطيئة أو فاشلة في مجموعة واحدة لا تحجب المجموعات الأخرى، لكن التزامن داخل المجموعة دائماً 1. لنظام إدارة الطلبات هذا صحيح تماماً: معرف الطلب كمعرف مجموعة يضمن معالجة جميع انتقالات الحالة لطلب معين (وُضع → دُفع → نُفِّذ → شُحن) بشكل تسلسلي دون حجب الطلبات غير ذات الصلة.

# Publishing to SQS FIFO with message group ID (Python / boto3) import boto3, uuid, json sqs = boto3.client("sqs") def publish_order_event(order_id: str, event_type: str, payload: dict): sqs.send_message( QueueUrl="https://sqs.us-east-1.amazonaws.com/123456789/orders.fifo", MessageGroupId=order_id, # strict ordering per order MessageDeduplicationId=str(uuid.uuid4()), # idempotency key (content-based dedup is alternative) MessageBody=json.dumps({ "event_type": event_type, "order_id": order_id, "payload": payload, "schema_version": "1.2", }), )
تصميم مفتاح الـ partition في Kinesis يحدد الإنتاجية والترتيب في آنٍ معاً. مفتاح بكثافة낮ة (مثل رمز البلد) يوجه كثيراً من الأحداث إلى عدد قليل من الـ shards، ويخلق shards ساخنة عند حد 1 MB/ثانية و1,000 سجل/ثانية. مفتاح بكثافة عالية (مثل UUID لكل حدث) يوزع الحمل بالتساوي لكن يدمر الترتيب للكيان ذاته. النمط الصحيح لأحمال CDC وevent-sourcing: استخدم معرف الكيان (معرف المستخدم، الطلب، الحساب) كمفتاح الـ partition — يوزع بشكل معقول ويحافظ على الترتيب لكل كيان داخل الـ shard.

أنماط الفشل الإنتاجية في تشغيل EDA

ثلاثة أنماط فشل تظهر على النطاق لا تطفو في بيئات التجريب:

  1. حدث مسموم على Kinesis: سجل مشوه يرمي استثناءً دائماً سيحجب shard إلى أجل غير مسمى حتى تنتهي فترة الاحتفاظ (24 ساعة–365 يوماً). اضبط bisectBatchOnFunctionError: true وdestinationConfig.onFailure (SQS DLQ) على تعيين مصدر أحداث Kinesis. خيار القطع ينصف الدفعة الفاشلة بشكل متكرر حتى يُعزل السجل المسموم ويُوجَّه إلى DLQ — ويستأنف معالجة الـ shard لبقية التدفق.
  2. أخطاء الترتيب بسبب انحراف الساعة: الأحداث المنشورة من منتجين متعددين بطوابع زمنية لساعة الحائط لا تُرتَّب بشكل موثوق بالطابع الزمني في SQS Standard أو EventBridge لأن انحراف الساعة بين المنتجين يمكن أن يكون 100 ملي ثانية أو أكثر. استخدم رقم تسلسل متصاعد من قاعدة البيانات المصدر (مثل xmin في Postgres أو رقم تسلسل تدفق DynamoDB) لا طابعاً زمنياً يُنشئه العميل للأحداث التي يهم فيها الترتيب.
  3. جدول الاستبدادية يصبح قسماً ساخناً: إذا تعيّنت جميع أحداث كيان ذي حركة مرور عالية على نفس مفتاح قسم DynamoDB (مثل جدول استبدادية مسطح بمفتاح hash واحد)، ستصطدم بحد 1,000 كتابة/ثانية لكل قسم. شتّت جدول الاستبدادية بإضافة بادئة رقم عشوائي (0–9) للمفتاح، واستخدم فهرساً ثانوياً شاملاً إذا كنت بحاجة لبحث نقطي.

تشغيل أنظمة serverless المدفوعة بالأحداث على نطاق واسع يتطلب نموذجاً ذهنياً مختلفاً عن تشغيل APIs المتزامنة. الأخطاء مؤجلة وصامتة؛ إعادات المحاولة تلقائية وخفية؛ المكررات متوقعة وليست استثناءً. المهندسون الذين يديرون أنظمة EDA بإتقان يعاملون DLQ ليس كإنذار بل كسطح تشغيلي من الدرجة الأولى — لديهم دفاتر تشغيل لكل DLQ، ومقاييس لمعدلات إعادة المحاولة ومعدلات ضربات ذاكرة التخزين المؤقت للاستبدادية، ويتدربون على إعادة تشغيل DLQ في أيام اللعب قبل أن يحتاجوها في حادثة.