إيه اللي بيحصل جوه السيستم خطوة بخطوة: من لحظة ما المستخدم يعمل تنبيه، لحد ما السعر يوصل والتنبيه يتنفذ. اختار سيناريو، ودوس تشغيل أو امشي خطوة خطوة. وتحت، القرارات وليه.
كل لون = نوع رحلة:alert writesprice tickstriggered alertfailure
قبل وبعد الـ Sorted Sets
من ./bench.sh 60 2000 على نفس المليون تنبيه (AAPL عليه 100,001). الأرقام من القياس الفعلي.
Hash (before)
Sorted Sets (now)
الأسعار اللي اتعالجت في الثانية
1,679
1,998
التنبيهات اللي اتفحصت في الثانية
336,265
0
الـ evaluator مشغول
99%
50%
الـ lag في آخر ثانية
10.5 s
31 ms
أقصى lag في الـ 60 ثانية
18.4 s
171 ms
رسايل متأخرة في Kafka في الآخر
24,960
0
القرارات وليه
كل قرار في السيستم ده كان ليه بديل، والانترفيور غالباً هيسأل عنه. كل سؤال تحته: اخترنا إيه، وليه، وإمتى البديل يبقى هو الصح.
الرسايل والـ streaming
ليه Kafka مش RabbitMQ؟Kafka
الترتيب لكل سهم. الـ key هو الـ symbol، فكل أسعار AAPL بتنزل على نفس الـ partition وبتتقري بالترتيب. في RabbitMQ، لو كذا consumer بيسحبوا من نفس الـ queue، الترتيب بيضيع، وممكن سعر قديم يتعالج بعد سعر أحدث منه.
الرسالة مابتتمسحش لما تتقري. Kafka عبارة عن log. تقدر ترجّع الـ consumer لورا وتعيد المعالجة (عملنا كده في bench.sh بـ rpk group seek). RabbitMQ بيمسح الرسالة بعد الـ ack.
الـ scaling بالـ consumer groups. زوّد evaluators، وKafka بيوزع الـ 12 partition عليهم لوحده. وأي خدمة جديدة (analytics مثلاً) تقرا نفس الأسعار بـ group تاني، من غير ما تأثر على حد.
الحجم. 2,000 رسالة في الثانية رقم صغير على Kafka، والـ batching معمول عشان الـ throughput العالي.
إمتى RabbitMQ؟ للـ task queues: إرسال الإشعارات نفسها، لما كل رسالة محتاجة retry لوحدها، أو priority، أو routing معقد، ومش فارق الترتيب.
ليه Redpanda مش Kafka نفسه؟Redpanda
نفس الـ Kafka API بالظبط. الكود بيستخدم kafkajs و rdkafka العاديين، ومش عارف إنه بيكلم Redpanda.
binary واحد من غير JVM ولا ZooKeeper، فبيقوم في ثواني على اللابتوب، وبياكل رامات أقل، ومعاه Console جاهز.
في الـ production؟ الاتنين ينفعوا، أو Kafka managed زي MSK أو Confluent. التغيير هيبقى في KAFKA_BROKERS بس.
ليه partition بالـ symbol؟ وليه 12 بالذات؟key = symbol
نفس السهم دايماً بيروح لنفس الـ partition، فبيروح لنفس الـ evaluator. ده بيضمن إن الأسعار بتتعالج بترتيبها، ومحدش بيقيّم سعر قديم بعد سعر جديد.
عدد الـ partitions هو أقصى عدد evaluators يشتغلوا مع بعض. 12 بيدينا مساحة نكبر فيها.
بنختار الرقم من الأول، لأن زيادة partitions بعدين بتغيّر السهم ده هيروح على أنهي partition، وفي اللحظة دي الترتيب ممكن يتلخبط.
العيب: السهم الواحد مابيتقسمش. لو سهم واحد بقى سخن جداً (hot partition)، زيادة الـ consumers مش هتفيده. الحل وقتها تقسيم تنبيهات السهم نفسه، مش زيادة partitions.
ليه الـ evaluator بيبعت لـ Kafka بدل ما يحدّث الداتابيز بنفسه؟alert-triggered topic
الـ evaluator هو المسار السخن. لو استنى Postgres مع كل تنبيه بيتنفذ، هيتأخر في الأسعار اللي جاية وراه.
الـ event ممكن يتقري من أكتر من مكان: Laravel بيحدّث الحالة، وخدمة الإشعارات بتبعت push، من غير ما الـ evaluator يعرف أي حاجة عن الاتنين.
لو Laravel وقع، الـ events بتستناه في Kafka، ومابتضيعش.
التخزين
ليه PostgreSQL مش Cassandra؟PostgreSQL
الحجم مش محتاج. 5 مليون مستخدم في كام تنبيه لكل واحد، يعني عشرات الملايين من الصفوف. ده Postgres واحد يشيله بسهولة. وكتابة التنبيهات بتحصل من المستخدمين، مش من الأسعار، فهي قليلة.
الـ Outbox محتاج transaction. التنبيه وسطر الـ outbox لازم يتكتبوا مع بعض أو مايتكتبوش خالص. Cassandra مافيهاش transaction بين صفين في partitions مختلفة.
الـ idempotency محتاجة conditional update.UPDATE ... WHERE status = 'active' رخيصة في Postgres. في Cassandra المقابل ليها (lightweight transactions) غالي.
استعلامات متنوعة. تنبيهات المستخدم، والتنبيهات النشطة لسهم، وإعادة بناء الكاش. Cassandra بتتصمم على استعلام واحد معروف من الأول.
إمتى Cassandra؟ لو هنخزن كل الأسعار (time series بمليارات الصفوف وكتابة مستمرة)، أو محتاجين multi-region writes. وقتها Cassandra أو TimescaleDB تبقى منطقية.
ليه Redis؟ ما Postgres عنده index ويقدر يعمل range query.Redis
صح، index على (symbol, target_price) كان هيجاوب نفس السؤال. بس ده هيبقى 2,000 query في الثانية على نفس الداتابيز اللي بتخدم الـ API.
Redis بيرد في أقل من ملّي ثانية من الذاكرة، وبيعزل مسار القراءة السخن عن مصدر الحقيقة. لو الـ evaluators زادت، Postgres مش هيحس.
لو Redis اتمسح، Postgres لسه فيه كل حاجة، و alerts:rebuild-cache بيرجّعه.
العيب: بقى عندنا نسختين من البيانات، ومحتاجين حاجة تخليهم متطابقين. وده سبب الـ Outbox.
ليه Sorted Set مش Hash زي الانترفيو؟ZSET + HASH
الـ Hash بيدي O(1) في الإضافة والحذف، بس ده تحسين لمسار الكتابة. القراءة مع كل سعر كانت بتجيب كل تنبيهات السهم: 100 ألف لأبل مع كل tick.
في الـ Sorted Set، الـ score هو السعر المستهدف، فـ ZRANGEBYSCORE بترجّع بس التنبيهات اللي السعر حققها، بـ O(log n + k).
القياس: التأخير نزل من 18 ثانية لـ 31ms، والـ evaluator بقى مشغول 50% بدل 99%.
التمن: الذاكرة زادت من 81MB لـ 114MB، وكل تنبيه بقى مكتوب في مفتاحين بدل واحد.
الـ API والخدمات
ليه REST مش GraphQL أو gRPC؟REST
الـ API هنا CRUD بسيط على resource واحد: اعمل تنبيه، عدّله، امسحه، اعرض تنبيهاتي. ده بالظبط اللي REST معمول له.
أي client يقدر يكلمه: موبايل، أو ويب، أو curl. ومن غير schema ولا code generation.
GraphQL بيفيد لما الـ client محتاج يجمّع بيانات من أشكال كتير في request واحد. وده مش موجود هنا.
gRPC بيفيد بين الخدمات وبعضها. والخدمات عندنا مابتكلمش بعض مباشرة أصلاً، بتكلم بعض عن طريق Kafka.
والإشعار نفسه؟ مش REST. السيرفر هو اللي بيبدأ، فبيوصل بـ push notification أو WebSocket أو SSE.
ليه لغتين، Laravel وNode، مش لغة واحدة؟Laravel + Node
Laravel للـ API: validation وmigrations وORM وtests جاهزين. وده الجزء اللي فيه business logic.
Node للـ evaluator: process واحد شغال على طول، بيقرا stream ويعمل I/O كتير مع Redis وKafka، والـ event loop مناسب لده.
العقد بينهم واضح ومكتوب: شكل مفاتيح Redis، وشكل الـ event. وفيه tests على الناحيتين.
بصراحة: لغة واحدة كانت هتنفع. اللغتين هنا قرار تعليمي، ولما الفريق صغير، لغة واحدة غالباً أوفر.
الاعتمادية
ليه Outbox مش Dual Write؟Transactional Outbox
مافيش transaction واحدة بين Postgres وRedis. في الـ Dual Write، لو Redis وقع بعد ما Postgres اتكتب، التنبيه بيتسجل ومابيوصلش الكاش أبداً. جربناها: 500 للمستخدم، وصفوف مكررة لما عمل retry.
بالـ Outbox، الـ request بيكتب في Postgres بس (التنبيه + سطر outbox في نفس الـ transaction)، والـ relay بيوصّل لـ Redis بعدين. Redis واقع؟ التنبيه بيتأخر، بس مابيضيعش.
التمن: eventual consistency. التنبيه بيوصل Redis بعد ما يتعمل بحوالي 200ms.
ليه relay بيعمل polling مش CDC بـ Debezium؟polling 200ms
الـ polling مش محتاج أي infrastructure زيادة: query على جدول كل 200ms، و FOR UPDATE SKIP LOCKED.
200ms تأخير على تنبيه المستخدم لسه عامله مش هتفرق معاه.
إمتى CDC؟ لو حجم التغييرات كبر، أو التأخير بقى مهم. Debezium بيقرا الـ WAL بتاع Postgres مباشرة، من غير polling ومن غير جدول outbox بيكبر. أو كخطوة وسط: LISTEN/NOTIFY عشان الـ relay يصحى فوراً.
ليه relay واحد بس؟1 replica
لو اتنين relays أخدوا تغييرين لنفس التنبيه، ممكن القديم يتكتب في Redis بعد الجديد.
Relay واحد بيوصّل مئات التغييرات في الثانية، وده أكتر بكتير من سرعة المستخدمين في إنهم يعملوا تنبيهات.
لو احتجنا نكبر: نقسّم الشغل بـ alert_id % N، فكل تنبيه يبقى ليه relay واحد بس مسؤول عنه.