عمليات الضخ هي حيث تنهار خلفيات SaaS في الإنتاج. استيراد CSV يحتوي 50,000 صف لا يمكن تشغيله داخل معالج HTTP. هذه هي البنية التي نستخدمها للمعالجة غير المتزامنة.
كل منتج SaaS يصل في النهاية إلى مستخدم يحاول استيراد 100,000 صف من جدول بيانات. أو تصدير ثلاث سنوات من سجل المعاملات. أو تشغيل إعادة حساب عبر مجموعة بيانات مستأجر كاملة. هذه العمليات تكسر معالجات HTTP، وتستنفد مهل موزعي الحمل، وتستنزف اتصالات قاعدة البيانات إذا لم تكن لديك بنية متعمدة لها.
لماذا لا يمكن لمعالجات HTTP تشغيل العمليات الضخمة
معالجات HTTP لها ثلاثة حدود على الأقل تجعلها غير مناسبة للعمليات الضخمة:
مهل موزع الحمل: AWS ALB لديه مهلة افتراضية 60 ثانية للخمول. معظم APIs المستضافة على ECS خلف موزع حمل. استيراد يحتوي 100,000 صف يستغرق 3 دقائق سيصطدم بمهلة TCP عند موزع الحمل قبل الانتهاء.
إلغاء سياق الطلب: إذا انقطع العميل أو أُغلق تبويب المتصفح، يُلغى r.Context(). أي استعلامات قاعدة بيانات مرتبطة بهذا السياق تفشل.
الذاكرة: قراءة ملف CSV بحجم 200 ميجابايت في الذاكرة في معالج طلب ستُسبب ارتفاعاً في RSS مع كل استيراد متزامن.
البنية الصحيحة: قبول الملف، وضع مهمة في قائمة الانتظار، إرجاع معرف المهمة للعميل، والسماح للعميل بالاستطلاع للتقدم أو تلقي إشعار عند الانتهاء.
نموذج المهمة في PostgreSQL
جدول مهمة ضخمة بسيط لكن كامل:
CREATE TABLE bulk_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
total_rows INTEGER,
processed_rows INTEGER NOT NULL DEFAULT 0,
failed_rows INTEGER NOT NULL DEFAULT 0,
error_summary JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
error_summary تخزن مصفوفة JSONB من الأخطاء على مستوى الصف حتى يتمكن المستخدمون من رؤية الصفوف التي فشلت ولماذا.
معالج HTTP: القبول والتخزين والوضع في قائمة الانتظار
المعالج رقيق عمداً: يقبل الملف، يرفعه إلى التخزين الكائني (S3/R2)، ينشئ سجل المهمة، يضع المهمة في قائمة الانتظار، ويعيد معرف المهمة. يعود في أقل من ثانية. العميل يتلقى معرف المهمة ويبدأ الاستطلاع.
العامل: المعالجة على دفعات
العامل ينزل الملف من التخزين ويعالجه على دفعات:
معالجة على دفعات من 500، وليس صفاً بصف. الإدراجات صفاً بصف أبطأ بشكل كبير بسبب الحمل لكل عبارة في PostgreSQL.
تحديث التقدم في كل دفعة. العميل الذي يستطلع /jobs/{id} يرى تحديثاً ذا معنى كل 500 صف.
التحقق من الإلغاء في الحلقة. إذا ألغى المستخدم المهمة عبر واجهة المستخدم، يتحقق العامل من السياق ويخرج بشكل نظيف.
الإدراج على دفعات مع PostgreSQL unnest
لإدراج دفعة من الصفوف بكفاءة، استخدم نهج unnest بدلاً من بناء جملة VALUES ضخمة:
INSERT INTO contacts (workspace_id, email, name)
SELECT $1, u.email, u.name
FROM unnest($2::text[], $3::text[])
AS u(email, name)
ON CONFLICT (workspace_id, email) DO UPDATE
SET name = EXCLUDED.name
هذا يرسل استعلاماً واحداً لكل دفعة بدلاً من واحد لكل صف. بالنسبة لـ 500 صف، الفرق هو تقريباً 2 ملي ثانية مقابل 1000 ملي ثانية.
التصدير: بث مجموعات البيانات الكبيرة
لا يمكنك تحميل نتيجة استعلام تحتوي 200,000 صف في الذاكرة. استخدم LIMIT/OFFSET مع البث:
- معالجة 1000 صف في كل مرة
- الكتابة في ملف مؤقت
- رفع الملف المكتمل إلى التخزين
- إنشاء URL موقع مسبقاً صالح لساعة واحدة
سجل المهمة النهائي يحتوي على URL التنزيل الموقع مسبقاً. يتلقاه العميل عند الاستطلاع ويُظهر زر التنزيل.
الدروس الرئيسية من الإنتاج
العمليات الضخمة يجب ألا تعمل أبداً داخل معالجات HTTP. ابث الرفعات إلى التخزين الكائني فوراً. عالج على دفعات من 500 باستخدام إدراجات unnest. حدث التقدم في كل دفعة. تحقق من إشارات الإلغاء في حلقة المعالجة.



