البناء للأنظمة الكبيرة والمهام الخلفية طويلة المدى.
الائتمان: إلياس شيبي على Unsplashقبل أشهر، توليت دورًا يتطلب بناء بنية تحتية لبث الوسائط (الصوت). ولكن بعيدًا عن تقديم الصوت كأجزاء قابلة للبث، كانت هناك مهام معالجة وسائط طويلة المدى وخط أنابيب RAG واسع النطاق يلبي احتياجات النسخ والترميز والتضمين وتحديثات الوسائط المتسلسلة. بناء MVP بعقلية الإنتاج جعلنا نكرر حتى حققنا نظامًا سلسًا. كان نهجنا هو دمج الميزات والمجموعة الأساسية من الأولويات.
خلال عملية البناء، جاء كل تكرار كاستجابة للحاجة الفورية وغالبًا "الشاملة". كان الاهتمام الأولي هو وضع المهام في قائمة الانتظار، والتي كانت كافية مع Redis؛ ببساطة أطلقنا ونسينا. أعطانا Bull MQ في إطار عمل NEST JS تحكمًا أفضل في عمليات إعادة المحاولة والتراكمات وقائمة انتظار الرسائل الميتة. محليًا ومع بعض الحمولات في الإنتاج، حصلنا على تدفق الوسائط بشكل صحيح. سرعان ما أثقلنا وزن المراقبة:
السجلات → سجل المهام (الطلبات، الاستجابات، الأخطاء).
المقاييس → كم / كم مرة تعمل هذه المهام، تفشل، تكتمل، إلخ.
التتبعات → المسار الذي اتخذته المهمة عبر الخدمات (الوظائف/الطرق المستدعاة ضمن مسار التدفق).
يمكنك حل بعض هذه المشكلات من خلال تصميم واجهات برمجة التطبيقات وبناء لوحة تحكم مخصصة لتوصيلها، ولكن مشكلة قابلية التوسع ستكون كافية. وفي الواقع، قمنا بتصميم واجهات برمجة التطبيقات.
تحدي إدارة سير العمل الخلفي المعقد وطويل المدى، حيث يجب أن تكون الإخفاقات قابلة للاسترداد، ويجب أن تكون الحالة دائمة، أصبح Inngest خلاصنا المعماري. لقد أعاد تشكيل نهجنا بشكل أساسي: كل مهمة خلفية طويلة المدى تصبح وظيفة خلفية، يتم تشغيلها بواسطة حدث محدد.
على سبيل المثال، سيؤدي حدث Transcription.request إلى تشغيل وظيفة TranscribeAudio. قد تحتوي هذه الوظيفة على خطوات تشغيل لـ: fetch_audio_metadata و deepgram_transcribe و parse_save_trasncription و notify_user.
البدائية الأساسية للمتانة هي خطوات التشغيل. يتم تقسيم الوظيفة الخلفية داخليًا إلى خطوات التشغيل هذه، وكل منها يحتوي على كتلة ذرية صغيرة من المنطق.
ملخص وظيفة Inngest:
import { inngest } from 'inngest-client';
export const createMyFunction = (dependencies) => {
return inngest.createFunction(
{
id: 'my-function',
name: 'My Example Function',
retries: 3, // retry the entire run on failure
concurrency: { limit: 5 },
onFailure: async ({ event, error, step }) => {
// handle errors here
await step.run('handle-error', async () => {
console.error('Error processing event:', error);
});
},
},
{ event: 'my/event.triggered' },
async ({ event, step }) => {
const { payload } = event.data;
// Step 1: Define first step
const step1Result = await step.run('step-1', async () => {
// logic for step 1
return `Processed ${payload}`;
});
// Step 2: Define second step
const step2Result = await step.run('step-2', async () => {
// logic for step 2
return step1Result + ' -> step 2';
});
// Step N: Continue as needed
await step.run('final-step', async () => {
// finalization logic
console.log('Finished processing:', step2Result);
});
return { success: true };
},
);
};
يوفر نموذج Inngest المدفوع بالأحداث رؤية تفصيلية لكل تنفيذ لسير العمل:
التحذير من الاعتماد على معالجة الأحداث النقية هو أنه بينما يضع Inngest وظائف التنفيذ في قائمة الانتظار بكفاءة، فإن الأحداث نفسها لا يتم وضعها في قائمة انتظار داخلية بالمعنى التقليدي لوسيط الرسائل. يمكن أن يكون غياب قائمة انتظار الأحداث الصريحة مشكلة في سيناريوهات حركة المرور العالية بسبب ظروف التسابق المحتملة أو الأحداث المفقودة إذا كانت نقطة نهاية الاستيعاب مثقلة.
لمعالجة هذا وفرض متانة الحدث الصارمة، قمنا بتنفيذ نظام قائمة انتظار مخصص كمخزن مؤقت.
كان نظام قائمة انتظار AWS البسيط (SQS) هو النظام المختار (على الرغم من أن أي نظام قائمة انتظار قوي قابل للتنفيذ)، نظرًا لبنيتنا التحتية الحالية على AWS. قمنا بتصميم نظام قائمة انتظار مزدوج: قائمة انتظار رئيسية و قائمة انتظار الرسائل الميتة (DLQ).
أنشأنا بيئة عمل Elastic Beanstalk (EB) مهيأة خصيصًا لاستهلاك الرسائل مباشرة من قائمة الانتظار الرئيسية. إذا فشلت معالجة رسالة في قائمة الانتظار الرئيسية بواسطة عامل EB عددًا محددًا من المرات، تنقل قائمة الانتظار الرئيسية تلقائيًا الرسالة الفاشلة إلى DLQ المخصصة. هذا يضمن عدم فقدان أي حدث بشكل دائم إذا فشل في التشغيل أو تم التقاطه بواسطة Inngest. تختلف بيئة العمل هذه عن بيئة خادم الويب EB القياسية، حيث تتمثل مسؤوليتها الوحيدة في استهلاك الرسائل ومعالجتها (في هذه الحالة، إعادة توجيه الرسالة المستهلكة إلى نقطة نهاية Inngest API).
جزء غير معلن وذو صلة من بناء البنية التحتية على نطاق المؤسسة هو أنها تستهلك الموارد، وهي طويلة المدى. توفر بنية الخدمات المصغرة قابلية التوسع لكل خدمة. ستدخل التخزين وذاكرة الوصول العشوائي ومهلات الموارد في اللعبة. على سبيل المثال، انتقلت مواصفاتنا لنوع مثيل AWS بسرعة من t3.micro إلى t3.small، وهي الآن مثبتة على t3.medium. بالنسبة للمهام الخلفية طويلة المدى وكثيفة وحدة المعالجة المركزية، يفشل التوسع الأفقي باستخدام مثيلات صغيرة لأن الاختناق هو الوقت الذي يستغرقه معالجة مهمة واحدة، وليس حجم المهام الجديدة التي تدخل قائمة الانتظار.
المهام أو الوظائف مثل الترميز والتضمين عادة ما تكون مقيدة بوحدة المعالجة المركزية و مقيدة بالذاكرة. مقيدة بوحدة المعالجة المركزية لأنها تتطلب استخدامًا مستدامًا ومكثفًا لوحدة المعالجة المركزية، و مقيدة بالذاكرة لأنها غالبًا ما تتطلب ذاكرة وصول عشوائي كبيرة لتحميل نماذج كبيرة أو معالجة ملفات أو حمولات كبيرة بكفاءة.
في النهاية، وفرت هذه البنية المعززة، التي تضع متانة SQS والتنفيذ المتحكم فيه لبيئة عمل EB مباشرة في اتجاه تدفق واجهة برمجة تطبيقات Inngest، مرونة أساسية. حققنا ملكية صارمة للأحداث، وألغينا ظروف التسابق أثناء ارتفاعات حركة المرور، واكتسبنا آلية رسائل ميتة غير متطايرة. استفدنا من Inngest لقدراته على تنسيق سير العمل وتصحيح الأخطاء، بينما اعتمدنا على بدائيات AWS لتحقيق أقصى قدر من إنتاجية الرسائل ومتانتها. النظام الناتج ليس قابلاً للتوسع فحسب، بل قابل للتدقيق بدرجة عالية، مما يترجم بنجاح المهام الخلفية المعقدة وطويلة المدى إلى خطوات صغيرة آمنة وقابلة للمراقبة ومتسامحة مع الفشل.
تم نشر بناء Spotify للمواعظ في الأصل في Coinmonks على Medium، حيث يواصل الناس المحادثة من خلال تسليط الضوء والرد على هذه القصة.


