> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Apache Kafka: بث الأحداث بأحجام ضخمة
- URL: https://arabroot.io/articles/apache-kafka-بث-الاحداث-باحجام-ضخمة/
- Published: 2026-10-12T08:15:00.000Z
- Updated: 2026-10-11T11:26:33.000Z
- Description: كيف يحفظ عنقود Kafka أحداثك بنسخ على عدة سيرفرات، وماذا يحدث للمنتجين والمستهلكين عندما يسقط أحدها.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, هندسة البرمجيات وتصميم الأنظمة, قواعد البيانات, DevOps وCI/CD, الاستضافة الذاتية

في [مقال الطابور أم بث الأحداث](https://arabroot.io/articles/%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1-%D8%A7%D9%85-%D8%A8%D8%AB-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-message-queue-%D9%88-event-streaming/) شرحنا فكرة السجل Log الذي يبقى بعد القراءة، ورأينا ال Partitions والمفتاح Key ومجموعات المستهلكين Consumer Groups وإعادة القراءة Replay على وسيط واحد، وهذا يكفي لتفهم الفكرة، لكنه لا يجيب عن السؤال الذي يهمك عندما تشغل Apache Kafka فعلاً: ماذا يحدث عندما يتوقف أحد السيرفرات؟ هل تضيع الأحداث؟ وهل يتوقف المنتجون؟ وكيف يتوزع العمل من جديد عندما يسقط أحد المستهلكين؟ وهذا المقال يجيب عن ذلك بعنقود من ثلاثة سيرفرات بنظام KRaft، نوقف فيه السيرفرات واحداً بعد الآخر ونقرأ ما يحدث، وسوف نناقش في هذا المقال ما يلي:

- ما هي Apache Kafka، ومتى تناسبك، ومتى تكون أكبر من حاجتك.
- KRaft، ولماذا لم تعد Kafka تحتاج ZooKeeper منذ الإصدار 4.0.
- النسخ Replication، والنسخ المتزامنة ISR، والإعدادان `acks` و`min.insync.replicas`، وكيف يعملان معاً.
- المنتج الآمن عند التكرار Idempotent Producer، و«مرة واحدة بالضبط» Exactly-Once باختصار.
- إعادة التوزيع Rebalancing في مجموعة المستهلكين، والبروتوكول الجديد في Kafka 4.
- مدة الحفظ Retention والضغط Compaction.
- تجربة عملية كاملة بمخرجات حقيقية: الترتيب بالمفتاح، وإعادة التوزيع، وسيرفر متوقف مع `acks=all`، وموضوع مضغوط.

## ما هي Apache Kafka، ومتى تناسبك؟

Apache Kafka منصة لبث الأحداث Event Streaming مفتوحة المصدر برخصة Apache 2.0، تديرها مؤسسة Apache، وتعمل على JVM، وفكرتها كما رأينا في المقال السابق سجل يضاف إليه فقط ومقسم إلى Partitions، ويقرؤه أي عدد من المجموعات كل منها بسرعته. والذي يجعلها تتحمل الأحجام الضخمة أمران: أن الكتابة دائماً في آخر الملف فهي سريعة على القرص، وأن كل Partition يمكن أن يكون على سيرفر مختلف، فتضيف سيرفرات وPartitions كلما كبر الحمل.

وتناسبك Kafka في الحالات التالية:

- **أحداث يقرؤها أكثر من نظام:** حدث «تم إنشاء الطلب» تقرؤه خدمة الشحن والمخزون والتحليلات، وكل منها بسرعتها، وخدمة جديدة تبدأ من أول السجل.
- **أحجام كبيرة ومستمرة:** سجلات التطبيقات، وأحداث النقر في موقع كبير، وقراءات أجهزة إنترنت الأشياء، بمئات الآلاف في الثانية.
- **نقل التغييرات من قواعد البيانات CDC:** كل تغيير في جدول يصبح حدثاً تقرؤه الأنظمة الأخرى، وهو ما تفعله أدوات مثل Debezium مع جدول [Outbox](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-saga-%D9%88%D9%86%D9%85%D8%B7-outbox/#relay).
- **الترتيب لكل كيان مع التوازي:** كل أحداث العميل الواحد بالترتيب، وآلاف العملاء بالتوازي.

### متى تكون Kafka أكبر من حاجتك؟

وقد يتساءل البعض: إذا كانت Kafka تتحمل كل هذا فلماذا لا أستخدمها لكل شيء؟ والإجابة أن ثمنها في التشغيل حقيقي: عنقود من ثلاثة سيرفرات على الأقل حتى يتحمل سقوط واحد، وJVM تحتاج ذاكرة، وقرارات يجب أن تتخذها مبكراً مثل عدد ال Partitions والمفتاح، ومراقبة لتأخر كل مجموعة. لذلك تكون Kafka أكبر من حاجتك في الحالات التالية:

- مهام خلفية مثل إرسال البريد وإنشاء الفواتير، فهذه يناسبها طابور مثل [RabbitMQ](https://arabroot.io/articles/rabbitmq-%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-%D9%85%D9%88%D8%AB%D9%88%D9%82%D8%A9-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83/) بتأكيد لكل رسالة وإعادة محاولة جاهزة.
- عشرات الأحداث في الثانية بين خدمتين أو ثلاث، فهذا يكفيه [جدول في PostgreSQL](https://arabroot.io/articles/postgresql-%D9%84%D9%83%D9%84-%D8%B4%D9%8A%D8%A1-%D9%85%D8%AA%D9%89-%D8%AA%D9%83%D9%81%D9%8A%D9%83-%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%88%D8%A7%D8%AD%D8%AF%D8%A9/#queue) أو Stream في RabbitMQ.
- طلب ورد بين الخدمات، أو رسائل لحظية لا قيمة لها بعد ثوان، فهذه طبيعة وسيط خفيف مثل NATS.
- فريق صغير لا يوجد فيه من يتابع العنقود، والسبب أن العنقود الذي لا يراقبه أحد يفشل في أسوأ وقت.

## KRaft: لماذا لم تعد Kafka تحتاج ZooKeeper؟

لسنوات طويلة كانت Kafka تحتاج نظاماً ثانياً اسمه Apache ZooKeeper يحفظ البيانات الوصفية Metadata للعنقود: أي المواضيع موجودة، وأين ال Partitions، ومن قائد كل Partition، فكان عليك أن تشغل وتراقب وترقي نظامين مختلفين. ثم جاء KRaft، وهو أن تحفظ Kafka بياناتها الوصفية بنفسها في سجل داخلي يتفق عليه عدد من السيرفرات تسمى Controllers بخوارزمية Raft التي شرحناها في [مقال الأنظمة الموزعة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/#raft). وبحسب [إعلان Kafka 4.0](https://kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/?ref=arabroot.io) في مارس 2025، أصبحت Kafka 4.0 أول إصدار رئيسي يعمل دون ZooKeeper بالكامل، فإذا وجدت دليلاً يبدأ بتشغيل ZooKeeper فهو لإصدار قديم.

![مخطط من جزأين: في اليسار قبل Kafka 4.0 ثلاثة سيرفرات ZooKeeper منفصلة في الأعلى، وثلاثة سيرفرات Kafka في الأسفل ترسل إليها البيانات الوصفية. في اليمين Kafka 4 بنظام KRaft: ثلاثة سيرفرات kafka1 وkafka2 وkafka3 كل منها broker وcontroller، والمتحكمات تشكل Raft quorum أغلبيته اثنان من ثلاثة، مع ملاحظة أن التجربة تجمع الدورين وأن الإنتاج يفضل متحكمات منفصلة](https://arabroot.io/content/images/2026/10/kafka-02-kraft.webp)

في Kafka 4 تتفق المتحكمات Controllers على البيانات الوصفية بنفسها دون نظام خارجي

ويذكر [توثيق KRaft](https://kafka.apache.org/43/operations/kraft/?ref=arabroot.io) ثلاث قواعد يجب أن تعرفها:

- عدد المتحكمات ثلاثة أو خمسة، والأغلبية منها يجب أن تبقى تعمل، فثلاثة تتحمل سقوط واحد، وخمسة تتحمل سقوط اثنين.
- الوضع المجمع Combined Mode، أي أن يكون السيرفر الواحد Broker وController معاً كما في تجربتنا، غير منصوح به في البيئات الحساسة، والسبب أن المتحكم لا يكون معزولاً عن ضغط ال Broker، ولا تستطيع أن ترقيه أو توسعه وحده.
- عندما تفقد المتحكمات أغلبيتها لا يستطيع العنقود أن يغير قائد أي Partition، وسوف نرى أثر ذلك في التجربة عندما نوقف سيرفرين من ثلاثة.

## النسخ Replication والنسخ المتزامنة ISR

كل Partition له عدد من النسخ يحدده معامل النسخ Replication Factor، وواحدة منها القائد Leader الذي يكتب إليه المنتجون ويقرأ منه المستهلكون، والبقية تابعون Followers ينسخون منه. والتابع الذي يواكب القائد يكون ضمن مجموعة النسخ المتزامنة In-Sync Replicas واختصاراً ISR، وتعرفه [صفحة التصميم](https://kafka.apache.org/43/design/design/?ref=arabroot.io) بأنه التابع الذي له جلسة نشطة مع المتحكم ولم يتأخر عن سجل القائد أكثر من المدة `replica.lag.time.max.ms`. وهنا يأتي إعدادان يعملان معاً:

- **`acks` في المنتج:** بحسب [إعدادات المنتج](https://kafka.apache.org/43/configuration/producer-configs/?ref=arabroot.io)، القيمة `0` تعني ألا ينتظر المنتج أي رد، و`1` أن ينتظر القائد فقط، و`all` أن ينتظر كل النسخ المتزامنة، والقيمة الافتراضية اليوم هي `all`.
- **`min.insync.replicas` في الموضوع:** بحسب [إعدادات المواضيع](https://kafka.apache.org/43/configuration/topic-configs/?ref=arabroot.io) هو أقل عدد من النسخ المتزامنة، ومنها القائد، يجب أن يكون موجوداً حتى تنجح الكتابة مع `acks=all`، وقيمته الافتراضية 1.

ولاحظ المشكلة في القيم الافتراضية: `acks=all` مع `min.insync.replicas=1` يعني أن القائد إذا بقي وحده في ال ISR فسوف يقبل الكتابة ويؤكدها وهو النسخة الوحيدة، فإذا تلف قرصه بعد لحظة ضاعت الرسالة مع أنها «مؤكدة». لذلك الإعداد المعتاد للبيانات المهمة هو معامل نسخ 3 مع `min.insync.replicas=2`، فيتحمل الموضوع سقوط سيرفر واحد دون أن يتوقف، ولا يقبل أي كتابة إلا إذا حفظتها نسختان على الأقل، والصورة التالية تبين الحالات الثلاث كما رأيناها في التجربة:

![مخطط من ثلاث لوحات. الأولى: كل السيرفرات تعمل، المنتج بإعداد acks=all يكتب إلى broker 1 القائد، والقائد ينسخ إلى broker 2 وbroker 3، وال ISR يساوي 1,2,3 فتقبل الكتابة. الثانية: broker 3 متوقف وخارج ال ISR، وال ISR يساوي 1,2 وهو لا يقل عن min.insync.replicas=2 فتقبل الكتابة. الثالثة: الحالة نفسها لكن min.insync.replicas=3، فترفض الكتابة بالخطأ NotEnoughReplicasException](https://arabroot.io/content/images/2026/10/kafka-01-replication.webp)

مع ثلاث نسخ و min.insync.replicas=2 يتحمل الموضوع سقوط سيرفر واحد

ويجدر الإشارة هنا إلى الإعداد `unclean.leader.election.enable`، وقيمته الافتراضية `false`، أي أن Kafka لا تختار قائداً من خارج ال ISR حتى لو توقفت كل النسخ المتزامنة، وتصف صفحة التصميم هذا الاختيار بأنه مفاضلة بين التوفر والاتساق: إما أن تنتظر عودة نسخة متزامنة، أو أن تقبل قائداً قد تنقصه رسائل. ولا تقم بتفعيله في موضوع فيه بيانات مالية أو طلبات، والسبب أن الرسائل التي فاتت ذلك التابع تضيع دون أي خطأ.

## المنتج الآمن عند التكرار، و«مرة واحدة بالضبط»

إذا أرسل المنتج رسالة ولم يصله الرد بسبب انقطاع في الشبكة فسوف يعيد الإرسال، وقد تكون الأولى وصلت، فتكتب الرسالة مرتين، وهي المشكلة نفسها التي رأيناها مع تأكيد النشر في [تجربة RabbitMQ](https://arabroot.io/articles/rabbitmq-%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-%D9%85%D9%88%D8%AB%D9%88%D9%82%D8%A9-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83/#lab-confirms). وفي Kafka يحلها المنتج الآمن عند التكرار Idempotent Producer، فبحسب صفحة التصميم يعطي الوسيط كل منتج معرفاً، ويرسل المنتج مع كل رسالة رقماً تسلسلياً، فيتجاهل الوسيط الرسالة التي وصله رقمها من قبل، والخيار `enable.idempotence` قيمته الافتراضية `true` في المنتج الرسمي، فلا تقم بتعطيله إلا لسبب واضح.

ولاحظ أن هذا يحمي من التكرار بين المنتج والوسيط فقط، أما «مرة واحدة بالضبط» Exactly-Once فتحتاج أكثر من ذلك: المعاملات Transactions التي تكتب إلى عدة Partitions معاً فإما أن تكتب كلها أو لا يكتب شيء، مع مستهلكين بالإعداد `isolation.level=read_committed` فلا يقرؤون إلا ما اكتملت معاملته. وتقول صفحة التصميم بوضوح إن هذا الضمان داخل Kafka، فإذا كان المستهلك يكتب النتيجة في قاعدة بيانات أو يرسل بريداً فإن الضمان يحتاج تعاوناً من ذلك النظام، لذلك يبقى الحل العملي في أغلب الأنظمة هو المستهلك الآمن عند التكرار كما شرحناه في [مقال Idempotency](https://arabroot.io/articles/idempotency-%D9%83%D9%8A%D9%81-%D8%AA%D8%AC%D8%B9%D9%84-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%85%D9%86%D8%A9-%D8%B9%D9%86%D8%AF-%D8%AA%D9%83%D8%B1%D8%A7%D8%B1%D9%87%D8%A7/).

## إعادة التوزيع Rebalancing في مجموعة المستهلكين

رأينا في المقال السابق أن كل Partition يقرؤه عضو واحد في المجموعة، والسؤال: ماذا يحدث عندما ينضم عضو جديد أو يتوقف عضو؟ والإجابة أن المجموعة تعيد توزيع ال Partitions بين الأعضاء، وهذه هي إعادة التوزيع Rebalancing. وفي البروتوكول القديم كانت إعادة التوزيع توقف كل أعضاء المجموعة عن القراءة حتى ينتهي التوزيع الجديد، فإذا كانت المجموعة كبيرة أو كان أحد الأعضاء بطيئاً توقف العمل كله ثواني أو أكثر. وبحسب [إعلان Kafka 4.0](https://kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/?ref=arabroot.io) أصبح البروتوكول الجديد KIP-848 متاحاً للإنتاج، وفيه يحسب الوسيط التوزيع ويغيره تدريجياً دون إيقاف الجميع، وهو مفعل في الوسيط افتراضياً، لكن المستهلك يجب أن يطلبه بالإعداد `group.protocol=consumer`، وهذا ما سوف نستخدمه في التجربة.

## مدة الحفظ Retention والضغط Compaction

شرحنا مدة الحفظ في [المقال السابق](https://arabroot.io/articles/%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1-%D8%A7%D9%85-%D8%A8%D8%AB-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-message-queue-%D9%88-event-streaming/#retention)، وهي سياسة الحذف `cleanup.policy=delete` التي تحذف الأجزاء القديمة من السجل بعد مدة أو حجم. وهناك سياسة ثانية هي الضغط `cleanup.policy=compact`، وفيها لا تحذف Kafka بحسب العمر، وإنما تبقي آخر قيمة لكل مفتاح وتحذف القيم الأقدم له، فيصبح الموضوع أشبه بجدول: لكل مفتاح قيمته الحالية. والاستخدام الشائع لذلك هو حالة الكيانات، مثل آخر عنوان بريد لكل عميل أو آخر سعر لكل منتج، فالخدمة الجديدة تقرأ الموضوع من البداية فتحصل على الحالة الحالية لكل الكيانات دون أن تمر على تاريخها كاملاً. ولحذف مفتاح ترسل له رسالة قيمتها `null` تسمى Tombstone، فتحذف Kafka المفتاح بعد المدة `delete.retention.ms`، والصورة التالية تبين ما حدث في تجربتنا:

![مخطط: قبل الضغط سجل فيه تسع رسائل بال Offsets من 0 إلى 8: c1 بقيمة a@old، ثم c2 بقيمة b@old، ثم c1 بقيمة a@new، ثم c3، ثم c2 بقيمة b@new، ثم c1 بقيمة a@final، ثم c3 بقيمة null، ثم c4 وc5. بعد الضغط يبقى أربع رسائل فقط: offset 4 c2 b@new، وoffset 5 c1 a@final، وoffset 7 c4، وoffset 8 c5، وأرقام ال Offsets لم تتغير، وc3 حذف بعد delete.retention.ms](https://arabroot.io/content/images/2026/10/kafka-03-compaction.webp)

بعد الضغط يبقى لكل مفتاح آخر قيمة له، والمفتاح الذي وصلته قيمة null يحذف

ولاحظ أن الضغط لا يحدث فوراً، فالجزء النشط من السجل Active Segment لا يضغط أبداً، والمنظف Log Cleaner يعمل في الخلفية بحسب نسبة البيانات القديمة `min.cleanable.dirty.ratio` وقيمتها الافتراضية 0.5، لذلك قد يقرأ المستهلك قيمتين للمفتاح نفسه، ويجب أن يكون منطقه «آخر قيمة تفوز».

## تجربة عملية: عنقود Kafka بثلاثة سيرفرات

الآن سوف نبني عنقوداً من ثلاثة سيرفرات Apache Kafka 4.3.1، كل منها Broker وController، باستخدام ال Image الرسمية `apache/kafka`، وكل الأوامر بأدوات سطر الأوامر التي تأتي مع Kafka نفسها، ونفذناها من Git Bash على Windows مع Docker Engine 29.8.0، وإذا كنت على Git Bash فنفذ `export MSYS_NO_PATHCONV=1` أولاً حتى لا يغير المسارات التي تبدأ ب `/opt`، أما على Linux فلا تحتاجه. ولا يوجد أي منفذ منشور على الجهاز.

### ملف التجربة

أنشئ مجلداً فيه `compose.yaml` التالي:

```yaml
name: lab-kafka

x-kafka: &kafka
  image: apache/kafka:4.3.1
  healthcheck:
    test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 > /dev/null 2>&1"]
    interval: 5s
    retries: 40

x-env: &env
  KAFKA_PROCESS_ROLES: broker,controller             # each node is a broker and a KRaft controller
  KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
  KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
  KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
  KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
  KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
  CLUSTER_ID: Gjuwkjp7TX-PADH8FOk_jw              # generated once with kafka-storage.sh random-uuid
  KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
  KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
  KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
  KAFKA_DEFAULT_REPLICATION_FACTOR: 3
  KAFKA_MIN_INSYNC_REPLICAS: 2
  KAFKA_LOG_CLEANER_BACKOFF_MS: 5000                 # lab only: let the compaction thread wake up often
  KAFKA_HEAP_OPTS: "-Xms384m -Xmx384m"               # lab only: a small JVM heap per broker

services:
  kafka1:
    <<: *kafka
    hostname: kafka1
    container_name: lab-kafka-1
    environment:
      <<: *env
      KAFKA_NODE_ID: 1
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9092
  kafka2:
    <<: *kafka
    hostname: kafka2
    container_name: lab-kafka-2
    environment:
      <<: *env
      KAFKA_NODE_ID: 2
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka2:9092
  kafka3:
    <<: *kafka
    hostname: kafka3
    container_name: lab-kafka-3
    environment:
      <<: *env
      KAFKA_NODE_ID: 3
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka3:9092
```

في الإعداد أعلاه لاحظ التالي:

- كل السيرفرات تشترك في `CLUSTER_ID` واحد ولدناه مرة واحدة بالأمر `kafka-storage.sh random-uuid`، وفي `KAFKA_CONTROLLER_QUORUM_VOTERS` نفسها، وهي قائمة المتحكمات الثلاثة على المنفذ 9093.
- `KAFKA_DEFAULT_REPLICATION_FACTOR` و`KAFKA_MIN_INSYNC_REPLICAS` يجعلان كل موضوع جديد بثلاث نسخ وأقل عدد متزامن 2، وكذلك مواضيع Kafka الداخلية مثل `__consumer_offsets` التي تحفظ مواقع المجموعات.
- ذاكرة JVM 384 ميجابايت لكل سيرفر، وهذا للتجربة فقط حتى لا تستهلك الأجهزة الثلاثة ذاكرة جهازك، أما الإنتاج فيحتاج أكثر بكثير بحسب الحمل.
- لا توجد Volumes، فالبيانات داخل كل Container، وهذا يكفي للتجربة لأننا نوقف ال Containers ونعيد تشغيلها ولا نحذفها، أما في الإنتاج فاجعل مجلد البيانات على قرص دائم.

### التشغيل والتحقق من المتحكمات

```bash
docker compose up -d --wait
K="docker exec -i lab-kafka-1 /opt/kafka/bin"
$K/kafka-metadata-quorum.sh --bootstrap-server kafka1:9092 describe --status
```

والمخرج سوف يكون كما يلي:

```text
ClusterId:              Gjuwkjp7TX-PADH8FOk_jw
LeaderId:               2
LeaderEpoch:            12
HighWatermark:          104
MaxFollowerLag:         0
MaxFollowerLagTimeMs:   10
CurrentVoters:          [{"id": 1, "endpoints": ["CONTROLLER://kafka1:9093"]}, {"id": 2, "endpoints": ["CONTROLLER://kafka2:9093"]}, {"id": 3, "endpoints": ["CONTROLLER://kafka3:9093"]}]
CurrentObservers:       []
```

لاحظ أن المتحكمات الثلاثة اختارت المتحكم 2 قائداً للسجل الوصفي، وأن `MaxFollowerLag` صفر، أي أن المتحكمين الآخرين متزامنان معه، ولا يوجد أي ZooKeeper في الصورة.

### المفتاح يحدد ال Partition

ننشئ موضوعاً اسمه `orders` بثلاثة Partitions وثلاث نسخ، ثم ننشر ستة أحداث لثلاثة عملاء، والكلمة قبل النقطتين هي المفتاح:

```bash
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --create --topic orders --partitions 3 --replication-factor 3
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic orders
printf 'customer-1:OrderPlaced order=1\ncustomer-2:OrderPlaced order=2\ncustomer-1:OrderPaid order=1\ncustomer-3:OrderPlaced order=3\ncustomer-1:OrderShipped order=1\ncustomer-2:OrderPaid order=2\n' \
  | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic orders \
      --reader-property parse.key=true --reader-property key.separator=:
$K/kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic orders --from-beginning --max-messages 6 \
  --formatter-property print.partition=true --formatter-property print.offset=true --formatter-property print.key=true | sort
```

والمخرج سوف يكون كما يلي:

```text
Created topic orders.
Topic: orders	TopicId: VDfZTygKQOuKLvnvAMwywQ	PartitionCount: 3	ReplicationFactor: 3	Configs: min.insync.replicas=2
	Topic: orders	Partition: 0	Leader: 3	Replicas: 3,1,2	Isr: 3,1,2	Elr: 	LastKnownElr:
	Topic: orders	Partition: 1	Leader: 1	Replicas: 1,2,3	Isr: 1,2,3	Elr: 	LastKnownElr:
	Topic: orders	Partition: 2	Leader: 2	Replicas: 2,3,1	Isr: 2,3,1	Elr: 	LastKnownElr:
Partition:0	Offset:0	customer-1	OrderPlaced order=1
Partition:0	Offset:1	customer-1	OrderPaid order=1
Partition:0	Offset:2	customer-1	OrderShipped order=1
Partition:1	Offset:0	customer-3	OrderPlaced order=3
Partition:2	Offset:0	customer-2	OrderPlaced order=2
Partition:2	Offset:1	customer-2	OrderPaid order=2
```

في المخرج أعلاه لاحظ التالي:

- كل Partition له قائد على سيرفر مختلف: ال Partition 0 على السيرفر 3، وال 1 على السيرفر 1، وال 2 على السيرفر 2، فيتوزع الحمل على العنقود كله، وكل Partition له نسخ على السيرفرات الثلاثة، والثلاثة في ال ISR.
- الموضوع أخذ `min.insync.replicas=2` من إعداد الوسيط دون أن نحدده.
- أحداث العميل 1 الثلاثة في ال Partition 0 وبترتيب إرسالها، وهذا هو الضمان الذي شرحناه في المقال السابق، لكنه هنا على عنقود منسوخ.

### إعادة التوزيع: مستهلك ينضم ثم يسقط

نشغل مستهلكاً في Container مستقل في مجموعة اسمها `shipping` بالبروتوكول الجديد، وننظر في المجموعة، ثم نشغل مستهلكاً ثانياً:

```bash
consumer() {
  docker run -d --name lab-kafka-consumer-$1 --network lab-kafka_default -e KAFKA_HEAP_OPTS="-Xmx128m" \
    apache/kafka:4.3.1 /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka1:9092,kafka2:9092 \
    --topic orders --group shipping \
    --consumer-property group.protocol=consumer --consumer-property client.id=consumer-$1
}
consumer a; sleep 12
$K/kafka-consumer-groups.sh --bootstrap-server kafka1:9092 --describe --group shipping --members --verbose
consumer b; sleep 12
$K/kafka-consumer-groups.sh --bootstrap-server kafka1:9092 --describe --group shipping --members --verbose
```

والمخرج سوف يكون كما يلي:

```text
GROUP           CONSUMER-ID            HOST            CLIENT-ID       #PARTITIONS     CURRENT-EPOCH   CURRENT-ASSIGNMENT   TARGET-EPOCH    TARGET-ASSIGNMENT   
shipping        _meq9N5NR2CyufG3y02Pmg /192.168.112.5  consumer-a      3               2               orders:0,1,2         2               orders:0,1,2        

GROUP           CONSUMER-ID            HOST            CLIENT-ID       #PARTITIONS     CURRENT-EPOCH   CURRENT-ASSIGNMENT   TARGET-EPOCH    TARGET-ASSIGNMENT   
shipping        _meq9N5NR2CyufG3y02Pmg /192.168.112.5  consumer-a      2               3               orders:0,1           3               orders:0,1          
shipping        xSl3HfXdSNGDTb7fOymmbw /192.168.112.6  consumer-b      1               3               orders:2             3               orders:2
```

لاحظ أن المستهلك الأول أخذ ال Partitions الثلاثة وحده، وعندما انضم الثاني أخذ منه ال Partition 2 فقط وبقي الأول على 0 و1 دون أن يتوقف عنهما، والعمودان `CURRENT-ASSIGNMENT` و`TARGET-ASSIGNMENT` هما التوزيع الحالي والتوزيع الذي يريده الوسيط، وهذا هو التغيير التدريجي في البروتوكول الجديد. الآن ننشر ثلاثة أحداث ونرى من قرأ ماذا، ثم نوقف المستهلك الأول بالقوة:

```bash
printf 'customer-1:OrderPlaced order=4\ncustomer-2:OrderPlaced order=5\ncustomer-3:OrderPaid order=3\n' \
  | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic orders \
      --reader-property parse.key=true --reader-property key.separator=:
echo "--- a:"; docker logs lab-kafka-consumer-a
echo "--- b:"; docker logs lab-kafka-consumer-b
docker kill lab-kafka-consumer-a
# then repeat until consumer-a is gone from the group
$K/kafka-consumer-groups.sh --bootstrap-server kafka1:9092 --describe --group shipping --members
```

والمخرج بعد حذف أسطر التحذير سوف يكون كما يلي:

```text
--- a:
OrderPaid order=3
OrderPlaced order=4
--- b:
OrderPlaced order=5

GROUP           CONSUMER-ID            HOST            CLIENT-ID       #PARTITIONS
shipping        xSl3HfXdSNGDTb7fOymmbw /192.168.112.6  consumer-b      3
```

لاحظ أن حدث العميل 2 ذهب إلى المستهلك b لأنه صاحب ال Partition 2، وبعد إيقاف a بالقوة أخذ b ال Partitions الثلاثة، وفي تجربتنا خرج a من المجموعة بعد أقل من 30 ثانية، والمدة القصوى التي ينتظرها الوسيط مستهلكاً لا يرسل نبضات Heartbeats يحددها الإعداد `group.consumer.session.timeout.ms` وقيمته في هذا العنقود 45 ثانية. وهذا يعني أن أحداث ال Partitions التي كانت مع a تتأخر حتى تنتهي هذه المدة، فإذا كان التأخير مهماً في نظامك فاضبط هذه القيمة، وتذكر أن a قد يكون عالج أحداثاً لم يحفظ موقعها Offset بعد، فيقرؤها b من جديد، وهذا سبب آخر لأن يكون المستهلك آمناً عند التكرار.

### سيرفر متوقف مع acks=all

نوقف السيرفر 3 بالقوة، وننتظر قليلاً، ثم ننظر في الموضوع وننشر خمسة أحداث مع `acks=all`:

```bash
docker kill lab-kafka-3; sleep 15
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic orders | tail -3
seq 1 5 | sed 's/^/customer-9:event-/' | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic orders \
  --reader-property parse.key=true --reader-property key.separator=: --command-property acks=all \
  && echo "acks=all with 2 of 3 brokers: OK"
```

والمخرج سوف يكون كما يلي:

```text
	Topic: orders	Partition: 0	Leader: 1	Replicas: 3,1,2	Isr: 1,2	Elr: 	LastKnownElr:
	Topic: orders	Partition: 1	Leader: 1	Replicas: 1,2,3	Isr: 1,2	Elr: 	LastKnownElr:
	Topic: orders	Partition: 2	Leader: 2	Replicas: 2,3,1	Isr: 2,1	Elr: 	LastKnownElr: 
```

```text
acks=all with 2 of 3 brokers: OK
```

لاحظ أن ال Partition 0 كان قائده السيرفر 3، فانتقلت قيادته إلى السيرفر 1 تلقائياً، وأن السيرفر 3 خرج من ال ISR في ال Partitions الثلاثة، ومع ذلك نجحت الكتابة مع `acks=all` لأن في كل ISR نسختين، وهذا يساوي `min.insync.replicas=2`.

### نكسر العنقود: سيرفران من ثلاثة

الآن نوقف السيرفر 2 أيضاً، فيبقى سيرفر واحد، وهو متحكم واحد من ثلاثة:

```bash
docker kill lab-kafka-2; sleep 20
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic orders
echo "customer-9:event-7" | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic orders \
  --reader-property parse.key=true --reader-property key.separator=: --command-property acks=1 \
  --command-property delivery.timeout.ms=15000 --command-property request.timeout.ms=10000
```

والمخرج بعد حذف أجزاء من سجل الخطأ سوف يكون كما يلي:

```text
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.
 (org.apache.kafka.tools.TopicCommand)
[2026-10-11 07:19:25,388] ERROR Error when sending message to topic orders with key: 10 bytes, value: 7 bytes with error: (org.apache.kafka.clients.producer.internals.ErrorLoggingCallback)
org.apache.kafka.common.errors.TimeoutException: Expiring 1 record(s) for orders-2:15066 ms has passed since batch creation. The request has not been sent, or no server response has been received yet.
```

في المخرج أعلاه لاحظ التالي:

- حتى أمر وصف الموضوع لم يعمل وانتهت مهلته، والسبب أن المتحكم الوحيد الباقي ليس أغلبية، فلا يوجد قائد للبيانات الوصفية يجيب.
- الحدث بالمفتاح `customer-9` يذهب إلى ال Partition 2، وقائده كان السيرفر 2 الذي توقف، والسيرفر 1 عنده نسخة متزامنة منه، لكن نقل القيادة إليه قرار يحتاج أغلبية المتحكمات، لذلك فشلت الكتابة حتى مع `acks=1`، وفشلت بالطريقة نفسها عندما أرسلناها مع `acks=all`.
- والنتيجة أن العنقود المجمع من ثلاثة سيرفرات يتحمل سقوط سيرفر واحد فقط، وهذا ما يقوله توثيق KRaft، فإذا أردت تحمل سقوط اثنين فتحتاج خمسة متحكمات.

### خطأ شائع: min.insync.replicas يساوي عدد النسخ

نعيد السيرفرين، وننشئ موضوعاً للمدفوعات بثلاث نسخ لكن مع `min.insync.replicas=3`، وهو إعداد يختاره البعض ظناً أنه الأكثر أماناً، ثم نوقف السيرفر 3 فقط وننشر رسالتين، الأولى مع `acks=all` والثانية مع `acks=1`، ثم نقرأ الموضوع:

```bash
docker start lab-kafka-2 lab-kafka-3; docker compose up -d --wait
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --create --topic payments --partitions 1 --replication-factor 3 --config min.insync.replicas=3
docker kill lab-kafka-3; sleep 20
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic payments | tail -1
echo "p-1:paid 100" | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic payments \
  --reader-property parse.key=true --reader-property key.separator=: --command-property acks=all \
  --command-property delivery.timeout.ms=15000 --command-property request.timeout.ms=10000
echo "p-2:paid 200" | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic payments \
  --reader-property parse.key=true --reader-property key.separator=: --command-property acks=1
$K/kafka-get-offsets.sh --bootstrap-server kafka1:9092 --topic payments
```

والمخرج سوف يكون كما يلي:

```text
Created topic payments.
	Topic: payments	Partition: 0	Leader: 1	Replicas: 1,2,3	Isr: 1,2	Elr: 3	LastKnownElr:
[2026-10-11 10:06:09,629] ERROR Error when sending message to topic payments with key: 3 bytes, value: 8 bytes with error: (org.apache.kafka.clients.producer.internals.ErrorLoggingCallback)
org.apache.kafka.common.errors.NotEnoughReplicasException: Messages are rejected since there are fewer in-sync replicas than required.
payments:0:0
```

ثم نعيد السيرفر 3 ونقرأ الموضوع من البداية:

```bash
docker start lab-kafka-3; sleep 25
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic payments | tail -1
$K/kafka-get-offsets.sh --bootstrap-server kafka1:9092 --topic payments
$K/kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic payments --from-beginning --timeout-ms 15000 \
  --formatter-property print.key=true --formatter-property print.offset=true
```

```text
	Topic: payments	Partition: 0	Leader: 1	Replicas: 1,2,3	Isr: 1,2,3	Elr: 	LastKnownElr:
payments:0:1
Offset:0	p-2	paid 200
```

في المخرجين أعلاه لاحظ التالي:

- سقوط سيرفر واحد من ثلاثة أوقف كل الكتابة مع `acks=all` بالخطأ `NotEnoughReplicasException`، أي أن هذا الإعداد جعل الموضوع لا يتحمل أي عطل، وهو عكس ما أراده صاحبه، والقاعدة أن يكون `min.insync.replicas` أقل من معامل النسخ بواحد على الأقل.
- الرسالة مع `acks=1` قبلها القائد دون خطأ، لكن `kafka-get-offsets` أظهر أن نهاية الموضوع ما زالت صفراً، أي أن المستهلكين لا يرونها، والسبب أن علامة الماء العالية High Watermark، وهي آخر Offset يسمح للمستهلكين بقراءته، لا تتقدم بحسب [المقترح KIP-966](https://cwiki.apache.org/confluence/display/KAFKA/KIP-966%3A+Eligible+Leader+Replicas?ref=arabroot.io) إلا إذا كان حجم ال ISR لا يقل عن `min.insync.replicas`.
- العمود `Elr: 3` من المقترح نفسه، ومعناه أن السيرفر 3 مرشح مؤهل للقيادة Eligible Leader Replica، لأنه خرج من ال ISR بسبب نقصها عن الحد وعنده البيانات حتى علامة الماء العالية.
- بعد عودة السيرفر 3 عادت ال ISR ثلاثاً، وتقدمت العلامة إلى 1، فظهرت الرسالة للمستهلك، أما رسالة `acks=all` فلم تكتب أصلاً، فإذا لم يعد المنتج إرسالها بنفسه فقد ضاعت.

### موضوع مضغوط

ننشئ موضوعاً مضغوطاً اسمه `customer-email` يحفظ آخر بريد لكل عميل، وبإعدادات صغيرة جداً حتى نرى الضغط خلال دقيقة: جزء جديد من السجل كل خمس ثوان، ونسبة 1% من البيانات القديمة تكفي لبدء التنظيف، ثم ننشر قيماً متعددة لثلاثة عملاء، والقيمة `NULL` تعني Tombstone للعميل c3:

```bash
$K/kafka-topics.sh --bootstrap-server kafka1:9092 --create --topic customer-email --partitions 1 --replication-factor 3 \
  --config cleanup.policy=compact --config segment.ms=5000 --config min.cleanable.dirty.ratio=0.01 --config delete.retention.ms=1000
printf 'c1:a@old.example\nc2:b@old.example\nc1:a@new.example\nc3:c@example.com\nc2:b@new.example\nc1:a@final.example\nc3:NULL\n' \
  | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic customer-email \
      --reader-property parse.key=true --reader-property key.separator=: --reader-property null.marker=NULL
$K/kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic customer-email --from-beginning --timeout-ms 10000 \
  --formatter-property print.key=true --formatter-property print.offset=true
```

والمخرج سوف يكون كما يلي:

```text
Offset:0	c1	a@old.example
Offset:1	c2	b@old.example
Offset:2	c1	a@new.example
Offset:3	c3	c@example.com
Offset:4	c2	b@new.example
Offset:5	c1	a@final.example
Offset:6	c3	null
```

الآن ننشر رسالة جديدة بعد خمس ثوان حتى يغلق الجزء الحالي من السجل، وننتظر دورة المنظف، ثم رسالة أخرى، ثم نقرأ من البداية من جديد:

```bash
sleep 6; echo 'c4:d@example.com' | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic customer-email --reader-property parse.key=true --reader-property key.separator=:
sleep 45; echo 'c5:e@example.com' | $K/kafka-console-producer.sh --bootstrap-server kafka1:9092 --topic customer-email --reader-property parse.key=true --reader-property key.separator=:
sleep 30
$K/kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic customer-email --from-beginning --timeout-ms 10000 \
  --formatter-property print.key=true --formatter-property print.offset=true
```

والمخرج سوف يكون كما يلي:

```text
Offset:4	c2	b@new.example
Offset:5	c1	a@final.example
Offset:7	c4	d@example.com
Offset:8	c5	e@example.com
```

لاحظ أن القيم القديمة للعميلين c1 وc2 حذفت وبقيت آخر قيمة لكل منهما، وأن العميل c3 اختفى تماماً مع ال Tombstone الخاصة به بعد `delete.retention.ms`، وأن أرقام ال Offsets لم تتغير، فالفجوات بينها طبيعية في موضوع مضغوط، ولا تبن أي منطق على أن ال Offsets متتالية. ويسجل الوسيط عمل المنظف في الملف `/opt/kafka/logs/log-cleaner.log` داخل ال Container، ومنه هذا السطر:

```text
	Log cleaner thread 0 cleaned log customer-email-0 (dirty section = [7, 8])
```

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

### كم استهلك العنقود؟

```bash
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' | grep lab-kafka
```

```text
lab-kafka-consumer-b 221.5MiB / 31.29GiB
lab-kafka-1 665.6MiB / 31.29GiB
lab-kafka-2 433MiB / 31.29GiB
lab-kafka-3 376.6MiB / 31.29GiB
```

لاحظ أن كل سيرفر استهلك بين 376 و665 ميجابايت مع أن ذاكرة JVM 384 ميجابايت فقط، والسبب أن JVM تحتاج ذاكرة خارج ال Heap، وحتى مستهلك سطر الأوامر البسيط أخذ 221 ميجابايت لأنه برنامج Java أيضاً. وهذه أرقام عنقود تجربة شبه فارغ، أما في الإنتاج فتعطي Kafka ذاكرة Heap معتدلة وتترك أغلب ذاكرة السيرفر لنظام التشغيل، لأن Kafka تعتمد على ال Page Cache في القراءة والكتابة كما تشرح صفحة التصميم.

## التشغيل في الإنتاج

- **الواجهة:** Kafka لا تأتي بواجهة رسومية رسمية، وأدوات سطر الأوامر التي استخدمناها هي الأساس، وهناك واجهات مستقلة تعمل مع أي وسيط متوافق مع Kafka API، مثل [Redpanda Console](https://github.com/redpanda-data/console?ref=arabroot.io) التي سوف نراها في مقال Redpanda.
- **المراقبة:** أهم الأرقام: عدد ال Partitions التي نسخها أقل من المطلوب Under-Replicated Partitions، وعدد ال Partitions دون قائد، وتأخر كل مجموعة Consumer Lag، وحالة المتحكمات من `kafka-metadata-quorum.sh` كما فعلنا.
- **النسخ الاحتياطي:** حماية البيانات في Kafka هي النسخ على ثلاثة سيرفرات، أما النسخة الاحتياطية بمعنى نسخة في مكان آخر فتكون بعنقود ثان تنسخ إليه المواضيع المهمة بأداة مثل MirrorMaker 2 التي تأتي مع Kafka، وتذكر أن مدة الحفظ تحذف البيانات القديمة سواء أخذت نسخة أم لا.
- **الحماية:** عنقود التجربة دون تشفير ودون مستخدمين، أما في الإنتاج ففعل TLS بين العملاء والسيرفرات، والمصادقة SASL، والصلاحيات ACLs لكل تطبيق على المواضيع التي يحتاجها فقط.
- **القرارات المبكرة:** عدد ال Partitions، والمفتاح، ومعامل النسخ، و`min.insync.replicas`، فتغيير أي منها بعد أن يعمل النظام أصعب بكثير من اختياره صحيحاً من البداية.

## أخطاء شائعة

- **`min.insync.replicas=1` مع بيانات مهمة:** القيمة الافتراضية تسمح بتأكيد رسالة لم تحفظها إلا نسخة واحدة، فاجعلها 2 مع ثلاث نسخ.
- **`min.insync.replicas` يساوي عدد النسخ:** كما رأينا، يوقف الكتابة مع سقوط أي سيرفر.
- **عنقود من سيرفرين:** متحكمان لا يتحملان سقوط أي منهما، فالأغلبية من اثنين هي اثنان، فابدأ بثلاثة.
- **أدلة ZooKeeper:** Kafka 4 لا تعمل مع ZooKeeper، فإذا وجدت إعدادات `zookeeper.connect` في دليل فهو لإصدار قديم.
- **تفعيل `unclean.leader.election.enable` لحل توقف مؤقت:** يعيد الموضوع إلى العمل بقائد قد تنقصه رسائل، فتضيع بصمت.
- **الاعتماد على Exactly-Once مع أنظمة خارجية:** الضمان داخل Kafka، أما قاعدة البيانات أو البريد فتحتاج مستهلكاً آمناً عند التكرار.
- **Kafka لمهام خلفية صغيرة:** عنقود كامل لإرسال بريد التسجيل أكبر بكثير من الحاجة، والطابور أنسب.

## حذف المختبر

```bash
docker rm -f lab-kafka-consumer-a lab-kafka-consumer-b
docker compose down -v
```

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- Apache Kafka برخصة Apache 2.0 تناسب الأحداث التي يقرؤها أكثر من نظام والأحجام الكبيرة ونقل التغييرات، وهي أكبر من الحاجة للمهام الخلفية والأحجام الصغيرة.
- منذ Kafka 4.0 لا يوجد ZooKeeper، والمتحكمات تتفق على البيانات الوصفية بنظام KRaft، وتحتاج أغلبيتها حتى يعمل العنقود، فثلاثة تتحمل سقوط واحد.
- كل Partition له قائد ونسخ، و`acks=all` ينتظر النسخ المتزامنة ISR، و`min.insync.replicas=2` مع ثلاث نسخ يتحمل سقوط سيرفر واحد دون أن يقبل كتابة ضعيفة، كما رأينا في التجربة.
- `min.insync.replicas` يساوي عدد النسخ يوقف الكتابة مع أول عطل، والكتابة مع `acks=1` لا تظهر للمستهلكين حتى تعود ال ISR إلى الحد.
- المنتج الآمن عند التكرار مفعل افتراضياً، وExactly-Once ضمان داخل Kafka فقط، والمستهلك الآمن عند التكرار يبقى ضرورياً.
- البروتوكول الجديد لإعادة التوزيع في Kafka 4 ينقل ال Partitions تدريجياً، والموضوع المضغوط يبقي آخر قيمة لكل مفتاح.

وفي المقال التالي من المرحلة نشغل Redpanda، وهو بديل متوافق مع Kafka API، ونشغل عليه الكود نفسه ونقارن الموارد.

[![](https://arabroot.io/content/images/2026/10/queue-vs-streaming-cover-cover.webp)دليلالطابور Queue أم بث الأحداث Event Streaming؟ما يحدث للرسالة بعد قراءتها هو الفرق بين طابور مثل RabbitMQ وسجل أحداث مثل Kafka، ومنه تعرف أيهما يناسب مشروعك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B7%D8%A7%D8%A8%D9%88%D8%B1-%D8%A7%D9%85-%D8%A8%D8%AB-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-message-queue-%D9%88-event-streaming/)[![](https://arabroot.io/content/images/2026/10/rabbitmq-cover-cover.webp)دليلRabbitMQ: طوابير موثوقة لتطبيقاتك على سيرفرككيف تشغل RabbitMQ في عنقود من ثلاث عقد، وتوجه الرسائل بالموزعات، وتضمن ألا تضيع رسالة إذا سقطت عقدة.](https://arabroot.io/articles/rabbitmq-%D8%B7%D9%88%D8%A7%D8%A8%D9%8A%D8%B1-%D9%85%D9%88%D8%AB%D9%88%D9%82%D8%A9-%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%83/)[![](https://arabroot.io/content/images/2026/10/idempotency-cover-cover.webp)دليلIdempotency: كيف تجعل العملية آمنة عند تكرارهالماذا تصل الرسالة نفسها أكثر من مرة، وكيف تكتب مستهلكاً يطبقها مرة واحدة فقط مهما تكررت.](https://arabroot.io/articles/idempotency-%D9%83%D9%8A%D9%81-%D8%AA%D8%AC%D8%B9%D9%84-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%85%D9%86%D8%A9-%D8%B9%D9%86%D8%AF-%D8%AA%D9%83%D8%B1%D8%A7%D8%B1%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/distributed-systems-cover-cover.webp)دليلالأنظمة الموزعة للمبتدئين: ماذا يحدث عندما تنقطع الشبكةلماذا تتصرف الأنظمة الموزعة بشكل غريب عند انقطاع الشبكة، وكيف تتفق السيرفرات على قرار واحد رغم ذلك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ تجربة عنقود Apache Kafka 4.3.1 من ثلاثة سيرفرات بنظام KRaft في Docker.