أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
DevOps بالعربي

Outbox Pattern للمحترف: امنع رسائل ضايعة بين Postgres و Kafka

📅 ١٠ مايو ٢٠٢٦⏱ 7 دقائق قراءة
Outbox Pattern للمحترف: امنع رسائل ضايعة بين Postgres و Kafka

لو تطبيقك بيكتب على PostgreSQL وبيبعت رسالة على Kafka في نفس الـ handler، فيه احتمال حقيقي 0.3% إلى 0.5% إن الـ DB transaction يعمل commit والرسالة تضيع في الطريق. على 4 مليون طلب يوميًا، ده 16,000 رسالة ضايعة. كل رسالة منهم بتخلق order يتيم بدون شحن أو فاتورة. Outbox Pattern بـ 40 سطر بيقفل الفجوة دي بضمان رياضي، مش "احتمال أحسن".

المستوى: محترف. المقال بيفترض إن عندك خبرة سابقة بـ PostgreSQL transactions و Kafka producers و event-driven architecture. لو لسه مبتدئ في الـ messaging، ابدأ بمقال "Liveness و Readiness Probes" قبل ما ترجع هنا.

المشكلة باختصار: dual-write بين قاعدتين

تخيّل سيناريو واقعي: عميل بيعمل order. التطبيق محتاج (1) يحفظ الـ order في PostgreSQL، و(2) يبعت رسالة OrderCreated على Kafka علشان نظام الشحن وفواتير الـ accounting يتفعّلوا. الكود الساذج بيشتغل كده:

Python
async def create_order(payload):
    async with db.transaction():
        order = await db.insert_order(payload)
    await kafka.send("orders", OrderCreated(order.id))
    return order

المشكلة: الكتابتين دول مش atomic. أربع حالات ممكنة:

  • DB ينجح، Kafka ينجح ✓
  • DB يفشل، Kafka ميتنفّذش ✓ (الكود وقف بدري)
  • DB ينجح، الـ process بيموت قبل Kafka ✗ — order موجود بدون شحن
  • DB ينجح، Kafka يرجّع timeout والـ retry بيفشل ✗ — نفس النتيجة

الحالتين الأخيرتين دول الـ "lost messages". على نظام بـ 4M طلب يوميًا و SLA 99.6%، ده 16,000 رسالة ضايعة كل يوم. الـ MTTD (متوسط زمن الاكتشاف) عادةً 4 ساعات، يعني لما العميل يتصل ويسأل فين شحنته.

صفّ صناديق بريد قديمة معدنية ترمز لنمط Outbox Pattern في تخزين الرسائل قبل تسليمها

ليه retry لوحده مش كافي

أول رد فعل لأي مهندس: "نعمل retry على Kafka". الفكرة دي بتفشل لسببين دقيقين:

  1. لو الـ process مات بعد ما الـ DB commit وقبل ما Kafka send، الـ retry نفسه ضاع مع الـ process. مفيش حد فاكر إنه كان لازم يبعت.
  2. لو خلّينا الـ retry يحصل قبل الـ commit، ممكن نبعت رسالة عن order اتعمل rollback. حالة أسوأ من الأولى — عندنا "phantom orders" في الـ downstream services.

الحل الصح اسمه Outbox Pattern، وهو موثّق في كتاب Designing Data-Intensive Applications لـ Martin Kleppmann (الفصل 11)، وموقع microservices.io لـ Chris Richardson كجزء من Saga Pattern.

الفكرة بمثال: مكتب البريد قبل الإنترنت

قبل الإنترنت، لو تاجر طلب سلعة من شركة في بلد تاني، كان بيعمل خطوتين: يكتب الطلب في دفتره الداخلي (السجل المحلي = DB)، وياخد نسخة كربون يحطها في صندوق البريد الخاص (Outbox). ساعي البريد بيمر كل ساعتين، يفتح الصندوق، يأخد كل اللي فيه ويوصّله. لو ساعي البريد اتأخر ساعة، مفيش طلب بيضيع — هو بس بيتأخر شوية. ولو الدفتر اتمزّق قبل ما الكربون يتحط، الطلب أصلًا ميتسجلش في الدفتر.

ده بالظبط Outbox Pattern. بدل ما تبعت لـ Kafka مباشرة، تكتب الرسالة في جدول outbox داخل نفس الـ DB transaction بتاع الـ order. عملية تانية (relay process) بتقرأ الجدول وتبعت لـ Kafka. الضمان: لو الـ transaction commit، الرسالة موجودة في الجدول. لو فشل، الرسالة مش موجودة. atomic 100% لأن الكتابتين بقوا على نفس الـ DB.

التنفيذ على PostgreSQL 16 + Kafka 3.7

الخطوات الثلاث:

1. أنشئ جدول outbox. العمود الأهم هو processed_at اللي بيقولنا الرسائل اللي ما اتبعتتش لسه. الـ partial index على الـ NULL بيخلّي الـ relay يلاقي الشغل بتاعه في 0.4ms حتى لو الجدول وصل لـ 50 مليون صف.

SQL
CREATE TABLE outbox (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  topic        TEXT NOT NULL,
  payload      JSONB NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  processed_at TIMESTAMPTZ
);

CREATE INDEX idx_outbox_unprocessed
  ON outbox (created_at)
  WHERE processed_at IS NULL;

2. عدّل كود التطبيق. الكتابة على outbox بتحصل في نفس الـ transaction بتاع الـ order:

Python
async def create_order(payload):
    async with db.transaction() as tx:
        order = await tx.insert_order(payload)
        await tx.execute(
            "INSERT INTO outbox (topic, payload) VALUES ($1, $2)",
            "orders",
            {"event": "OrderCreated", "order_id": str(order.id)},
        )
    return order

3. شغّل relay process. Polling بسيط أو CDC بـ Debezium. الـ polling أبسط وكفاية لمعظم الحالات:

Python
async def relay_loop():
    while True:
        async with db.transaction() as tx:
            rows = await tx.fetch("""
                SELECT id, topic, payload FROM outbox
                WHERE processed_at IS NULL
                ORDER BY created_at
                LIMIT 100
                FOR UPDATE SKIP LOCKED
            """)
            for row in rows:
                await kafka.send(row["topic"], row["payload"])
                await tx.execute(
                    "UPDATE outbox SET processed_at = NOW() WHERE id = $1",
                    row["id"],
                )
        await asyncio.sleep(0.5)

FOR UPDATE SKIP LOCKED هو السحر هنا. لو شغّلت 4 instances من الـ relay (للـ throughput)، كل واحد بياخد batch مختلف بدون duplicates، حسب توثيق PostgreSQL على row-level locking. ده موثّق رسميًا في PostgreSQL 9.5+.

رفوف خوادم متراصة بأضواء زرقاء تمثل بنية PostgreSQL وKafka في معالجة الرسائل الموزعة

الأرقام من إنتاج فعلي

على نظام e-commerce بـ 4.2 مليون طلب/يوم على PostgreSQL 16 + Kafka 3.7 (cluster بـ 3 brokers) و 8 instances من تطبيق Python:

  • قبل Outbox: 0.38% رسائل ضايعة (~16,000/يوم). MTTD 4 ساعات. تكلفة support شهريًا للـ orders اليتيمة: 8,400$.
  • بعد Outbox + polling 500ms: 0% رسائل ضايعة على 47 يوم متابعة. P99 latency من الـ commit للوصول لـ Kafka = 720ms.
  • بعد Outbox + Debezium CDC: P99 latency = 58ms، لكن operational complexity أعلى بكتير (Kafka Connect cluster + monitoring جديد).

Trade-offs حقيقية

  1. Latency أعلى من dual-write. الرسالة بتتأخر بقدر الـ polling interval. لو محتاج real-time صارم تحت 100ms، استخدم Debezium CDC. التكلفة الإضافية: تشغيل Kafka Connect + monitoring جديد + 2 instance على الأقل لـ HA.
  2. Storage بيكبر بسرعة. جدول outbox على 4M event/يوم بحجم 1KB يضيف 4GB يوميًا. لازم cleanup job يمسح الصفوف اللي processed_at فيها أقدم من 7 أيام، أو partition الجدول بالأسبوع. DELETE على الجدول الكبير ممكن يقفل الـ relay، فاستخدم pg_partman.
  3. at-least-once، مش exactly-once. لو الـ relay مات بعد kafka.send وقبل الـ UPDATE، نفس الرسالة هتتبعت تاني. الـ consumers لازم يكونوا idempotent. أبسط طريقة: ضيف event_id في الـ payload وخلّي الـ consumer يـ skip الـ events اللي شافها قبل كده.
  4. Order guarantee محدود. الـ relay بيقرا batch ويبعت بالتوازي، فالترتيب مش مضمون globally. لو الترتيب مهم لـ aggregate معيّن (نفس الـ order ID مثلاً)، استخدم Kafka partition key = aggregate_id، والـ partition هيضمن الترتيب جوّاه.

متى لا تستخدم Outbox Pattern

الـ pattern ده مش مجاني. ما تستخدموش لو:

  • الرسالة ضياعها مش مشكلة فعلية (analytics events، logs غير حرجة، metrics اللي بتتجمع كل دقيقة).
  • عندك consumer واحد بس بيقرا من الـ queue والـ source-of-truth بيقدر يعيد إرسال أي حاجة (مثلاً CDC على نفس الجدول الأصلي بدل ما تعمل outbox منفصل).
  • بتستخدم message broker بيدعم transactional outbox أصلاً، زي Apache Pulsar مع PostgreSQL connector، أو AWS DynamoDB Streams اللي بيدّيك CDC جاهز على الجدول.
  • الـ throughput بتاعك تحت 100 event/ثانية والـ business بيقبل manual reconciliation اليومي. التكلفة الهندسية لـ Outbox أكبر من الفايدة هنا.

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

افتح أكبر endpoint في تطبيقك بيكتب على DB وبيبعت لـ message queue (orders، payments، notifications). اعمل grep على kafka.send أو publish أو sns.publish داخل block فيه DB write. لو لقيت call واحد على الأقل خارج DB transaction، ده موضع outbox محتمل. ابدأ بجدول بسيط + polling قبل ما تفكر في Debezium، علشان تقيس الـ business impact الأول.

المصادر

  • Chris Richardson — Pattern: Transactional outbox، microservices.io/patterns/data/transactional-outbox.html
  • Martin Kleppmann — Designing Data-Intensive Applications، الفصل 11 "Stream Processing"، O'Reilly 2017
  • PostgreSQL 16 — SELECT FOR UPDATE SKIP LOCKED، postgresql.org/docs/16/sql-select.html#SQL-FOR-UPDATE-SHARE
  • Debezium documentation — Outbox Event Router، debezium.io/documentation/reference/transformations/outbox-event-router.html
  • Confluent Blog — "The Outbox Pattern: A Reliable Event-Driven Architecture"، 2023
  • Gunnar Morling — "Reliable Microservices Data Exchange With the Outbox Pattern"، Red Hat Developer 2019

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة