DDIA·مطالعهSTUDY

نقشه راهRoadmap/ فصل ۱۲Chapter 12

پردازش جریانیStream Processing

در پردازش جریانی، داده مرتب می‌رسد و پایانِ کلی ندارد. باید روشن کنیم ترتیب چیست، پیامِ تکراری چه اثری دارد، دادهٔ دیررس را کجا حساب می‌کنیم و پس از خرابی از کجا ادامه می‌دهیم..
Batch assumes a bounded input with a known end — but real user data never completes: it is unbounded and arrives piece by piece. The lazy answer is artificial daily batches; the right answer is processing each event continuously — stream processing. This chapter answers three questions: how events are transported (messaging and logs), how they relate to databases (sync, CDC, the state/stream duality), and how they are processed (windows, joins, time, and exactly-once).
۱۳13 نمودار تعاملیinteractive diagrams· محور: پیام‌رسانی · CDC · پنجره‌ها · joinها · exactly-onceTheme: messaging · CDC · windows · joins · exactly-once· منبع: فصل ۱۲ کتاب DDIA ویرایش ۲Source: DDIA 2nd Edition, Chapter 12

۰0 نقشهٔ راه فصلChapter roadmap

ایدهٔ کلیدی فصلChapter thesis

در پردازش جریانی، داده مرتب می‌رسد و پایانِ کلی ندارد. باید روشن کنیم ترتیب چیست، پیامِ تکراری چه اثری دارد، دادهٔ دیررس را کجا حساب می‌کنیم و پس از خرابی از کجا ادامه می‌دهیم.

مثال فنی: پیام تا اثر پایدار

وضعیت اولیه: رویداد e7 می‌گوید سفارش ۴۲ به مبلغ ۲۰۰ ثبت شده است.

اجرای مرحله‌به‌مرحله: مصرف‌کننده آن را می‌خواند، جمع فروش را تغییر می‌دهد و موقعیت پیشرفت را ذخیره می‌کند؛ crash بین این دو مرحله می‌تواند تکرار ایجاد کند.

نتیجه و دلیل: طراحی استریم باید هم نگهداری پیام و هم اتمی بودنِ اثر و پیشرفت را مشخص کند.

مثال‌های این فصل سناریوهای آموزشیِ برگرفته از کارهای روزمره‌اند. عددهای انتخاب‌شده برای توضیح‌اند؛ معماریِ داخلیِ یک شرکت مشخص را توصیف نمی‌کنند.

In batch, a job's input and output are files. The streaming equivalent: an event (a small, self-contained, immutable record of something that happened at a point in time, usually with a timestamp), a topic (a group of same-kind events — the equivalent of a filename), and a broker (the equivalent of the filesystem: where the producer writes and the consumer reads).

And just as immutable input made replay and testing possible in Chapter 11, the append-only log plays the same role here: consuming means reading, not deleting — so you can reprocess yesterday.

نگاه اولپردازش دسته‌ای (فصل ۱۱)پردازش جریانی (این فصل)
ورودیمحدود و معلوم؛ پایانش مشخص استنامحدود؛ هیچ‌وقت «کامل» نمی‌شود
واحدِ دادهفایلِ رکوردهارویدادِ immutable با timestamp
واحدِ دسته‌بندینامِ فایل / مسیرتاپیک / استریم
رسانهٔ اتصالفایل‌سیستمِ توزیع‌شدهبروکرِ پیام (سنتی یا لاگ‌محور)
مصرفخواندنِ فایل (تکرارپذیر)در بروکرِ سنتی مخرب است (حذف با ack)؛ در لاگ‌محور فقط خواندن است (تکرارپذیر)
مرتب‌سازیممکن و مرکزی (shuffle)روی dataset نامحدود بی‌معناست؛ فقط ترتیبِ درونِ پارتیشن تضمین می‌شود
تحملِ خرابیاجرای دوبارهٔ task و دورریختنِ خروجیِ ناقصنمی‌شود تا «پایان» صبر کرد؛ microbatch، checkpoint، کامیتِ اتمی، idempotence
First lookBatch processing (Ch. 11)Stream processing (this chapter)
InputBounded and known; has a definite endUnbounded; never “complete”
Unit of dataFile of recordsImmutable event with timestamp
Grouping unitFilename / pathTopic / stream
Connecting mediumDistributed filesystemMessage broker (traditional or log-based)
ConsumptionReading a file (repeatable)Destructive in traditional brokers (delete on ack); read-only in log-based ones (repeatable)
SortingPossible and centralized (shuffle)Meaningless on unbounded data; only in-partition order is guaranteed
Fault toleranceRe-run the task, discard partial outputCan't wait for “the end”; microbatch, checkpoint, atomic commit, idempotence
چیدمان فصلChapter layout

بخش ۱ و ۲ دربارهٔ انتقال است: از پیام‌رسانیِ مستقیم تا بروکر، و از ack تا دامِ ترتیب. بخش ۳ بروکرِ لاگ‌محور را می‌شکافد (پارتیشن، offset، بازپخش). بخش ۴ پلِ میان پایگاه داده و استریم است: مسابقهٔ dual-write، CDC و دوگانِ state/stream. بخش ۵ پردازش است: کاربردها، joinها، پنجره‌ها و زمان. بخش ۶ با exactly-once و یک درختِ تصمیم تمام می‌کند. اگر فقط وقتِ دو بخش را داری، بخش ۴ و ۵ را بخوان — قلبِ فصل آنجاست.

Sections 1–2 are about transport: from direct messaging to brokers, and from acks to the ordering trap. Section 3 dissects the log-based broker (partitions, offsets, replay). Section 4 bridges databases and streams: the dual-write race, CDC, and the state/stream duality. Section 5 is processing: use cases, joins, windows and time. Section 6 closes with exactly-once and a decision tree.

If you only have time for two sections, read 4 and 5 — that is where the heart of the chapter is.

۱1 انتقالِ رویدادهاTransporting events

۱.۱. رویداد، تاپیک، تولیدکننده و مصرف‌کننده1.1. Events, topics, producers and consumers

رویداد خبرِ یک اتفاقِ رخ‌داده است. تولیدکننده آن را می‌فرستد، تاپیک دستهٔ منطقیِ پیام‌هاست و مصرف‌کننده آن را برای کارِ خودش می‌خواند.

مثال فنی: چهار نقش روی یک سفارش

وضعیت اولیه: سرویس سفارش رویداد {id:e7, order_id:42, amount:200} تولید می‌کند.

اجرای مرحله‌به‌مرحله: پیام در topic سفارش‌ها ثبت می‌شود؛ مصرف‌کنندهٔ گزارش آن را می‌خواند و جمع را تغییر می‌دهد.

نتیجه و دلیل: تولیدکننده منشأ پیام، topic جریان دسته‌بندی‌شده و مصرف‌کننده مجری پردازش است؛ رویداد یک رخداد با هویت و زمان مشخص است.

When the input is a file (a sequence of bytes), the first step is usually parsing it into a sequence of records. In the streaming world a record is called an event, but it is practically the same thing: a small, self-contained, immutable entity detailing something that happened at a point in time — a user click, a purchase, a sensor temperature, a web-server log line. An event usually carries a timestamp too (wall-clock time; how it differs from a monotonic clock was covered in Chapter 9).

رویداد را می‌شود encode کرد (متن، JSON، باینری — فصل ۵) و هم ذخیره‌اش کرد (فایل، جدول، داکیومنت) و هم روی شبکه فرستاد. در بچ، فایل یک‌بار نوشته و شاید توسط چند job خوانده می‌شود؛ در استریم هم رویداد یک‌بار توسط تولیدکننده (producer / publisher / sender) تولید و شاید توسط چند مصرف‌کننده (consumer / subscriber / recipient) پردازش می‌شود. رویدادهای هم‌جنس در تاپیک (topic / stream) جمع می‌شوند — معادلِ نامِ فایل در فایل‌سیستم.

An event can be encoded (text, JSON, binary — Chapter 5), stored (file, table, document), and sent over the network. In batch, a file is written once and perhaps read by several jobs; in streaming, an event is produced once by a producer (publisher / sender) and perhaps processed by several consumers (subscribers / recipients). Same-kind events gather in a topic (stream) — the equivalent of a filename in a filesystem.

در ظاهر، فایل یا پایگاه داده برای وصل‌کردنِ تولیدکننده و مصرف‌کننده کافی است: تولیدکننده بنویسد، مصرف‌کننده دوره‌ای poll کند — دقیقاً همان کاری که بچِ روزانه می‌کند. ولی با حرکت به‌سمتِ تأخیرِ کم، polling گران می‌شود: هرچه بیشتر poll کنی، درصدِ بیشتری از درخواست‌ها دستِ خالی برمی‌گردند. بهتر است مصرف‌کننده مطلع شود که رویدادِ تازه آمده. پایگاه‌های داده رابطه‌ای تریگر دارند ولی محدود و حاشیه‌ای‌اند؛ برای همین ابزارهای تخصصیِ اعلانِ رویداد ساخته شدند.

On the surface, a file or a database is enough to connect producer and consumer: the producer writes, the consumer polls periodically — exactly what a daily batch does. But as you move toward low latency, polling gets expensive: the more often you poll, the more requests return empty-handed. It is better for the consumer to be notified that a fresh event arrived. Relational databases have triggers, but they are limited and marginal; that is why specialized event-notification tools were built.

۱.۲. پیام‌رسانیِ مستقیم در برابر بروکر + نمودار ۱1.2. Direct messaging vs brokers + Diagram 1

در پیام‌رسانی مستقیم، فرستنده و گیرنده باید شرایطِ ارتباط را هماهنگ کنند. بروکر میان آن‌ها قرار می‌گیرد تا پذیرش، نگهداری و تحویل پیام را طبق قرارداد مدیریت کند.

مثال فنی: ارسال مستقیم در برابر بروکر

وضعیت اولیه: سرویس گزارش برای ۱۰ دقیقه خاموش است، ولی سفارش‌ها ادامه دارند.

اجرای مرحله‌به‌مرحله: ارسال مستقیم بدون ذخیرهٔ پایدار می‌تواند پیام‌ها را گم کند؛ brokerِ پایدار آن‌ها را طبق سیاست retention نگه می‌دارد تا گزارش برگردد.

نتیجه و دلیل: broker وابستگی زمانی را کم می‌کند؛ ظرفیت دیسک، دوام و مدت نگهداری هنوز باید کافی باشند.

The simplest messenger is a direct channel like a Unix pipe or a TCP connection between producer and consumer — but those connect exactly one sender to one receiver, while a messenger must connect many producers to one topic and one topic to many consumers.

  • پیام‌رسانیِ مستقیم (بدونِ واسطه): UDP multicast در بازارِ سهام (تأخیرِ کم، بازیابیِ بستهٔ گم‌شده در سطحِ اپ)، کتابخانه‌های بدونِ بروکر مثل ZeroMQ و nanomsg، عامل‌های متریک مثل StatsD روی UDP (شمارنده‌ها فقط در صورتِ رسیدنِ همهٔ پیام‌ها درست‌اند)، و webhookها (تولیدکننده با HTTP/RPC به مصرف‌کننده push می‌کند). فرضِ مشترکشان: طرفین دائم آنلاین‌اند؛ مصرف‌کنندهٔ آفلاین پیام را از دست می‌دهد، و اگر خودِ تولیدکننده crash کند، بافرِ retry هم همراهش می‌میرد.
  • بروکرِ پیام (message broker / queue): سروری شبیه پایگاه‌داده‌ای که برای استریمِ پیام بهینه شده. تولیدکننده به بروکر می‌نویسد، بروکر به مصرف‌کننده تحویل می‌دهد. با متمرکزشدنِ داده در بروکر، آمدورفت و crash کلاینت‌ها راحت تحمل می‌شود و سؤالِ durability به بروکر منتقل می‌شود (بعضی فقط در حافظه، بعضی با تنظیم روی دیسک). مصرف‌کننده‌ها ناهمگام‌اند: تولیدکننده فقط منتظرِ تأییدِ بافرشدن در بروکر می‌ماند، نه پردازشِ نهایی.
  • Direct messaging (no middleman): UDP multicast in stock markets (low latency, app-level lost-packet recovery), brokerless libraries like ZeroMQ and nanomsg, metric agents like StatsD over UDP (counters are only right if every message arrives), and webhooks (the producer pushes to the consumer over HTTP/RPC). Their shared assumption: both sides are permanently online; an offline consumer loses messages, and if the producer itself crashes, its retry buffer dies with it.
  • Message broker (queue): a server like a database, optimized for message streams. The producer writes to the broker; the broker delivers to the consumer. With data centralized in the broker, client churn and crashes are easily tolerated, and the durability question moves to the broker (in-memory only for some, on disk by configuration for others). Consumers are asynchronous: the producer only waits for the broker's buffering ack, not for final processing.

بروکرها شبیه پایگاه داده‌اند (حتی بعضی در پروتکلِ کامیتِ د two-phase با XA/JTA شرکت می‌کنند) ولی چند تفاوتِ عملی دارند: دیتا را تا حذفِ صریح نگه می‌دارند درحالی‌که بعضی بروکرها پیامِ تحویل‌شده را خودکار پاک می‌کنند (پس برای ذخیرهٔ بلندمدت مناسب نیستند)؛ فرضشان صف‌های کوتاه است (بافرِ زیاد → افتِ throughput)؛ به‌جای زبانِ کوئری، اشتراک روی الگوی تاپیک دارند؛ و به‌جای نتیجه بر اساسِ snapshotِ نقطه‌ای، اعلانِ تغییر می‌دهند (پیامِ تازه رسید — چیزی که پایگاه داده ندارد). این همان نگاهِ سنتیِ JMS/AMQP است (RabbitMQ، ActiveMQ و هم‌خانواده‌ها).

Brokers resemble databases (some even join two-phase commit via XA/JTA) but differ in practice: databases keep data until explicitly deleted, while some brokers auto-delete delivered messages (so they don't fit long-term storage); they assume short queues (deep buffering kills throughput); instead of a query language they offer topic-pattern subscriptions; and instead of point-in-time snapshot results they emit change notifications (a fresh message arrived — something databases lack). This is the traditional JMS/AMQP view (RabbitMQ, ActiveMQ and kin).

نمودار ۱ — توپولوژی publish/subscribe: متعادل‌سازیِ بار، fan-out و پارتیشن‌های لاگDiagram 1 · Publish/subscribe topologies: load balancing, fan-out and log partitions باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: ارسال مستقیم در برابر بروکر

وضعیت اولیه: سرویس گزارش برای ۱۰ دقیقه خاموش است، ولی سفارش‌ها ادامه دارند.

اجرای مرحله‌به‌مرحله: ارسال مستقیم بدون ذخیرهٔ پایدار می‌تواند پیام‌ها را گم کند؛ brokerِ پایدار آن‌ها را طبق سیاست retention نگه می‌دارد تا گزارش برگردد.

نتیجه و دلیل: broker وابستگی زمانی را کم می‌کند؛ ظرفیت دیسک، دوام و مدت نگهداری هنوز باید کافی باشند.

نمودار دو نیمِ جدا دارد که هر کدام در یک قاب (region) محصورند؛ از نیمهٔ بالایی شروع کن: AMQP/JMS world. سمتِ چپ دو جعبهٔ Producer 1 و Producer 2 است که هر دو با فلشِ send به جعبهٔ میانیِ JMS Broker می‌رسند — زیرنویسش می‌گوید assigns each message و برچسبش ack + redeliver: کارِ بروکر همین تخصیصِ پیام‌هاست.

از سمتِ راستِ بروکر دو فلش بیرون می‌زند: یکی با برچسبِ queue به جعبهٔ Queue: orders (زیرنویس: load balancing) و یکی با برچسبِ publish به جعبهٔ Topic: prices (زیرنویس: fan-out to all). حالا ستونِ آخر را ببین: از Queue دو فلشِ 1 of 2 به Consumer 1 و Consumer 2 می‌رسد — یعنی هر پیام فقط به یکی از این دو می‌رسد و کار تقسیم می‌شود. ولی از Topic دو فلشِ to all به Subscriber 1 و Subscriber 2 می‌رسد — یعنی هر پیام به هر دو می‌رسد. همین تضادِ «یکی» در برابرِ «همه» قلبِ بخشِ بعدی است.

نیمهٔ پایینی، Log-based world است. سمتِ چپ Producer A و Producer B با فلش‌های append به Partition 0 و Partition 1 می‌نویسند (برچسبِ هر دو پارتیشن: append-only با زیرنویسِ ordered offsets). از هر پارتیشن یک فلشِ read in order به مصرف‌کنندهٔ هم‌ردیفش در Group A می‌رسد: Group A, C1 با زیرنویسِ owns partition 0 و Group A, C2 با owns partition 1 — یعنی هر پارتیشن دقیقاً یک مالک دارد. و آخرش جعبهٔ پهنِ Group B, C1 با زیرنویسِ reads every partition و برچسبِ fan-out across groups: گروهِ دوم مستقل از گروهِ اول، همان پارتیشن‌ها را دوباره می‌خواند. دو فلشِ بدونِ برچسبِ آن — یکی از بالای Partition 0 از کریدورِ بالا، یکی از پایینِ Partition 1 از کریدورِ پایین — هیچ فلشِ دیگری را قطع نمی‌کنند.

درسِ این نمودار: در مدلِ سنتی، بروکر تصمیم می‌گیرد هر پیام به چه کسی برسد؛ در مدلِ لاگ‌محور، مصرف‌کننده با offset تصمیم می‌گیرد تا کجا خوانده — و همین تفاوت، بازپخش را ممکن می‌کند.

Read the top half (AMQP/JMS world) first: Producer 1 and Producer 2 both send into the middle JMS Broker (assigns each message, ack + redeliver). Two arrows leave the broker: a queue arrow to Queue: orders (load balancing) and a publish arrow to Topic: prices (fan-out to all).

From the Queue, two 1 of 2 arrows reach Consumer 1 and Consumer 2 — each message goes to one of them, splitting the work. From the Topic, two to all arrows reach Subscriber 1 and Subscriber 2 — each message goes to both. That “one vs all” contrast is the heart of the next section.

The bottom half (Log-based world) flips the responsibility: Producer A/B append to Partition 0/1 (append-only, ordered offsets); each partition is read in order by exactly one owner in Group A, while Group B independently re-reads every partition (fan-out across groups). Lesson: in the classic model the broker decides who gets each message; in the log model the consumer decides with its offset — and that difference makes replay possible.

۱.۳. متعادل‌سازیِ بار در برابر fan-out1.3. Load balancing vs fan-out

متعادل‌سازی بار یعنی یک کار میان چند مصرف‌کننده تقسیم شود. Fan-out یعنی گروه‌های مستقل هرکدام همان رویداد را برای هدفِ جدا دریافت کنند.

مثال فنی: دو گروه، چند worker

وضعیت اولیه: topic سفارش‌ها ۴ پارتیشن دارد؛ گروه ارسال دو worker و گروه تحلیل دو worker دارد.

اجرای مرحله‌به‌مرحله: داخل هر گروه پارتیشن‌ها تقسیم می‌شوند؛ هر گروه مستقل همهٔ جریان را دنبال می‌کند.

نتیجه و دلیل: load balancing تقسیم کارِ یک گروه است؛ fan-out رساندن جریان به گروه‌های مستقل است.

When several consumers read one topic, there are two patterns (Fig. 12-1):

  • متعادل‌سازیِ بار (load balancing): هر پیام به یکی از مصرف‌کننده‌ها می‌رسد؛ بروکر دلخواه تخصیص می‌دهد. وقتی پردازشِ هر پیام گران است و می‌خواهی با اضافه‌کردنِ مصرف‌کننده موازی‌اش کنی. (در AMQP با چند کلاینت روی یک صف، در JMS با shared subscription.)
  • پخش به همه (fan-out): هر پیام به همهٔ مصرف‌کننده‌ها می‌رسد؛ هر کدام مستقل به همان پخش گوش می‌دهند — معادلِ جریانیِ چند job بچ که یک فایلِ ورودی را می‌خوانند. (در JMS با topic subscription، در AMQP با exchange binding.)
  • Load balancing: each message reaches one of the consumers; the broker assigns at will. For when processing each message is expensive and you want to parallelize by adding consumers. (In AMQP with several clients on one queue; in JMS with a shared subscription.)
  • Fan-out: each message reaches all consumers; each listens to the same broadcast independently — the streaming equivalent of several batch jobs reading one input file. (In JMS with a topic subscription; in AMQP with an exchange binding.)

این دو ترکیب هم می‌شوند: مثلاً با consumer groupهای کافکا، هر پیام به یک مصرف‌کننده در هر گروه می‌رسد (تعادلِ بار درونِ گروه) و به همهٔ گروه‌ها می‌رسد (fan-out میانِ گروه‌ها) — دقیقاً همان چیزی که در نیمهٔ پایینیِ نمودار ۱ دیدی.

The two compose: with Kafka consumer groups, each message reaches one consumer within each group (load balancing inside the group) and all groups (fan-out across groups) — exactly what the bottom half of Diagram 1 shows.

۲2 تأیید، تحویلِ مجدد و ترتیبAcknowledgment, redelivery and ordering

۲.۱. سه راهِ مواجهه با مصرف‌کنندهٔ کند2.1. Three ways to handle a slow consumer

وقتی تولید سریع‌تر از مصرف است، باید پیام را کنار گذاشت، موقتاً ذخیره کرد یا سرعتِ تولید را پایین آورد. انتخاب به ارزش پیام و ظرفیت بافر وابسته است.

مثال فنی: حساب backlog

وضعیت اولیه: تولید 1000 msg/s و مصرف 700 msg/s است.

اجرای مرحله‌به‌مرحله: backlog هر ثانیه ۳۰۰ پیام و در ۱۰ دقیقه ۱۸۰ هزار پیام زیاد می‌شود؛ افزایش بافر فقط زمان می‌خرد.

نتیجه و دلیل: باید نرخ تولید کم، ظرفیت مصرف زیاد یا طبق نیاز بخشی حذف شود؛ صف نامحدود راه‌حلِ پایدار اختلاف نرخ نیست.

If the producer sends faster than the consumer processes, there are three options: dropping messages, buffering them in a queue, or backpressure / flow control — stalling the producer until there is room. Unix pipes and TCP take the third path with a small fixed buffer: when full, the sender blocks until the receiver takes something. If messages are buffered, you must work out what happens as the queue grows: it won't fit in memory? It spills to disk? What does disk do to performance? And when the disk fills, then what?

پرسشِ دوم: اگر نودی crash کند یا موقت آفلاین شود، پیامی گم می‌شود؟ مثل پایگاه داده، durability ترکیبِ نوشتن روی دیسک و replication است — و هزینه دارد. اگر گاهی بتوانی پیام گم کنی، روی همان سخت‌افزار throughput بیشتر و تأخیرِ کمتری می‌گیری. قابل‌قبول‌بودنِ گم‌شدن به کاربرد بستگی دارد: برای readingهای دوره‌ایِ سنسور، یک نقطهٔ گم‌شده مهم نیست (مقدارِ تازه دقایقی بعد می‌آید — ولی حواست باشد اگر خیلی پیام بیفتد، نادرستیِ متریک‌ها معلوم نمی‌شود)؛ ولی اگر داری رویداد می‌شماری، هر پیامِ گم‌شده یعنی شمارندهٔ غلط، پس تحویلِ مطمئن لازم است.

Second question: if a node crashes or goes briefly offline, is any message lost? Like a database, durability is disk writes plus replication — and it costs. If you can occasionally lose messages, the same hardware buys higher throughput and lower latency. Whether loss is acceptable depends on the use case: for periodic sensor readings one lost dot hardly matters (a fresh value arrives minutes later — though beware that if many messages drop, metric inaccuracy goes unnoticed); but if you are counting events, every lost message means a wrong counter, so reliable delivery is required.

نمودار مفهومی — سه راهِ مواجهه با مصرف‌کنندهٔ کندConcept diagram — three ways to handle a slow consumer باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: حساب backlog

وضعیت اولیه: تولید 1000 msg/s و مصرف 700 msg/s است.

اجرای مرحله‌به‌مرحله: backlog هر ثانیه ۳۰۰ پیام و در ۱۰ دقیقه ۱۸۰ هزار پیام زیاد می‌شود؛ افزایش بافر فقط زمان می‌خرد.

نتیجه و دلیل: باید نرخ تولید کم، ظرفیت مصرف زیاد یا طبق نیاز بخشی حذف شود؛ صف نامحدود راه‌حلِ پایدار اختلاف نرخ نیست.

سه کارتِ کنارِ هم سه راهِ بخشِ ۲.۱ را نشان می‌دهند: DROP (صورتی — انداختنِ پیام وقتی عقبی؛ برای readingهای سنسور که یک نقطهٔ گم‌شده مهم نیست، ولی برای شمارش نه)، BUFFER (زرد — صف در حافظه و بعد دیسک؛ بروکرِ لاگ‌محور همان بافرِ حلقه‌ایِ غول‌پیکر است ولی بالاخره پر می‌شود، پس lag را مانیتور کن)، و BACKPRESSURE (سبز — متوقف‌کردنِ تولیدکننده مثلِ لولهٔ Unix و TCP با بافرِ کوچکِ ثابت؛ هیچ‌چیز گم نمی‌شود ولی کند می‌شوی).

نوارِ پایینی پرسشِ دوم را جواب می‌دهد: دوام یعنی نوشتن روی دیسک + replication و بهایش throughput و تأخیر است — «به‌اندازهٔ کافی بادوام برای کارت، نه بیشتر و نه کمتر».

Three side-by-side cards show §2.1's three options: DROP (pink — discard messages when behind; fine for sensor readings where one lost dot hardly matters, wrong for counting), BUFFER (yellow — queue in memory, then disk; the log-based broker is that giant ring buffer, but it fills eventually, so monitor the lag), and BACKPRESSURE (green — stall the producer like Unix pipes and TCP with a small fixed buffer; nothing lost, but slower).

The bottom strip answers the second question: durability means disk writes plus replication, priced in throughput and latency — “durable enough for the job, no more, no less.”

۲.۲. دامِ به‌هم‌ریختنِ ترتیب + نمودار ۲2.2. The reordering trap + Diagram 2

اگر مصرف‌کننده پس از انجام کار تأیید ندهد، پیام ممکن است دوباره تحویل شود. این تکرار می‌تواند با ترتیبِ پیام‌های دیگر تداخل پیدا کند؛ پردازش موازی هم اهمیت دارد.

مثال فنی: Retry ترتیب تکمیل را عوض می‌کند

وضعیت اولیه: پیام اول set status=paid در worker A گیر کرده؛ پیام دوم set status=shipped در B زودتر تمام می‌شود.

اجرای مرحله‌به‌مرحله: تحویل دوبارهٔ پیام اول می‌تواند بعداً وضعیت را به paid برگرداند، اگر نسخه یا ترتیبِ هر سفارش کنترل نشود.

نتیجه و دلیل: ترتیب تحویل با ترتیب پایانِ پردازش برابر نیست؛ کلیدگذاری، سریالی کردن یا کنترل نسخه برای همان موجودیت لازم است.

A consumer can crash at any moment: the broker delivered the message but it was never processed, or only half-processed. The fix is acknowledgment: the client must explicitly confirm a message is done so the broker removes it from the queue. If the connection closes without an ack or times out, the broker assumes the message was never processed and redelivers it to another consumer. (Subtlety: the message may be fully processed with only the ack lost on the network — handling that needs an atomic-commit protocol, unless the operation is idempotent or exactly-once isn't required.)

نمودار ۲ — تأیید، تحویلِ مجدد و دامِ به‌هم‌ریختنِ ترتیبDiagram 2 · Acknowledgment, redelivery and the reordering trap باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: Retry ترتیب تکمیل را عوض می‌کند

وضعیت اولیه: پیام اول set status=paid در worker A گیر کرده؛ پیام دوم set status=shipped در B زودتر تمام می‌شود.

اجرای مرحله‌به‌مرحله: تحویل دوبارهٔ پیام اول می‌تواند بعداً وضعیت را به paid برگرداند، اگر نسخه یا ترتیبِ هر سفارش کنترل نشود.

نتیجه و دلیل: ترتیب تحویل با ترتیب پایانِ پردازش برابر نیست؛ کلیدگذاری، سریالی کردن یا کنترل نسخه برای همان موجودیت لازم است.

چهار ستون داریم: Producer 1 (منبعِ مرتبِ رویداد)، Message Broker (صفِ متعادل‌شده)، Consumer 1 (پردازش و ack می‌کند) و Consumer 2 (روی m3 می‌میرد). اول Producer 1 با فلشِ درشتِ send m1, m2, m3, m4, m5 in order هر پنج پیام را به‌ترتیب می‌فرستد — یادداشتِ زیرش تأکید می‌کند ترتیبِ تولیدکننده m1 تا m5 است.

بعد تحویلِ متعادل‌شده شروع می‌شود: deliver m1 به مصرف‌کنندهٔ ۱ و برگشتِ ack m1؛ بعد deliver m2 به مصرف‌کنندهٔ ۲ و ack m2. بعد بروکر m3 را به مصرف‌کنندهٔ ۲ می‌دهد و هم‌زمان m4 را به مصرف‌کنندهٔ ۱ — یادداشت می‌گوید m4 وقتی تحویل شد که مصرف‌کنندهٔ ۲ هنوز مشغولِ m3 بود. این‌جا نقطهٔ شکست است: فلشِ خط‌چینِ crash while processing m3 — no ack؛ اتصال بسته/timeout شده و بروکر فرض می‌کند m3 هیچ‌وقت پردازش نشد.

ولی مصرف‌کنندهٔ ۱ بی‌خبر ادامه می‌دهد و ack m4 را می‌فرستد — یادداشتش می‌گوید تا این‌جا فقط m4 را پردازش کرده، هم‌زمان با crash روی m3. حالا بروکر m3 را redeliver می‌کند — با تأکیدِ درشت و یادداشتِ «تلاشِ مجدد به مصرف‌کنندهٔ دیگر، در زمانی دیرتر» — و بعد deliver m5. آخرین فلشِ برگشتی ack m3, m5 است با یادداشتِ پایانی: ترتیبِ پردازشِ مصرف‌کنندهٔ ۱ شد m4, m3, m5 — یعنی m3 و m4 خلافِ ترتیبِ تولیدکننده آمدند.

سه کارتِ پایین را هم بخوان: پروتکلِ تأیید (حالتِ گم‌شدنِ ack و نیاز به idempotence یا کامیتِ اتمی)، دامِ به‌هم‌ریختن (حتی تضمینِ ترتیبِ JMS/AMQP هم وقتی تعادلِ بار با تحویلِ مجدد قاطی شود نمی‌تواند جلویش را بگیرد؛ راه‌حل: صفِ جدا به‌ازای هر مصرف‌کننده یا پیام‌های مستقل)، و پیام‌های سمی با صفِ نامه‌های مرده. درس: ترکیبِ «تعادلِ بار + تحویلِ مجدد» ذاتاً ترتیب را به‌هم می‌زند — اگر پیام‌ها وابستگیِ علّی دارند، این دام واقعی است.

Four columns: Producer 1 (the ordered event source), Message Broker (the balanced queue), Consumer 1 (processes and acks) and Consumer 2 (dies on m3). First the producer sends m1–m5 in order; delivery is load-balanced — m1/m2 go out and are acked — then the broker hands m3 to Consumer 2 while m4 goes to Consumer 1.

The breaking point is the dashed crash while processing m3 — no ack arrow. The connection closed, so the broker assumes m3 was never processed — while Consumer 1 innocently continues and acks m4. The broker then redelivers m3 to the other consumer (later in time), followed by m5; the final ack m3, m5 leaves Consumer 1's processing order as m4, m3, m5 — m3 and m4 against producer order.

The three cards generalize: the ack protocol (lost acks need idempotence or atomic commit), the reordering trap (even JMS/AMQP ordering guarantees can't survive load balancing mixed with redelivery — fix it with a separate queue per consumer or independent messages), and poison messages with dead-letter queues. Lesson: “load balancing + redelivery” inherently reorders — a real trap wherever messages have causal dependencies.

۲.۳. پیامِ سمی و صفِ نامه‌های مرده2.3. Poison messages and dead-letter queues

پیام سمی ورودی‌ای است که بارها پردازش را شکست می‌دهد. صفِ پیام‌های ناموفق کمک می‌کند آن را برای بررسی جدا کنیم؛ صرفِ جدا کردن، خطا را رفع نمی‌کند.

مثال فنی: پیام سمی

وضعیت اولیه: پیام e7 نسخهٔ schema نامعتبر دارد و هر بار decoder را می‌شکند.

اجرای مرحله‌به‌مرحله: پس از تلاش‌های محدود، پیام با علت خطا و هویت اصلی به DLQ می‌رود؛ پس از اصلاح، بازپردازشِ کنترل‌شده انجام می‌شود.

نتیجه و دلیل: retry بی‌پایان همان خطا را تکرار و ممکن است پارتیشن را معطل کند؛ DLQ هم بدون رسیدگی، محل گم‌شدنِ خاموشِ داده می‌شود.

Redelivery doesn't just reorder; it can waste resources or wedge the flow forever. The classic scenario: a producer that serializes badly — say it drops a required key from a JSON object. The keyless message crashes the consumer, which restarts; no ack arrives, the broker resends, the next consumer dies too — and the loop runs forever. With a hard ordering guarantee no progress is possible at all; with relaxed ordering you progress but burn resources on messages that never get acked.

راه‌حل، صفِ نامه‌های مرده (dead letter queue / DLQ) است: به‌جای retry تا ابد، پیام به صفِ دیگری منتقل می‌شود تا مصرف‌کننده‌ها آزاد شوند. روی DLQ معمولاً مانیتورینگ است — هر پیامِ آن یک خطاست و اپراتور تصمیم می‌گیرد: برای همیشه بیندازد، دستی اصلاح و بازتولید کند، یا کدِ مصرف‌کننده را درست کند. DLQ در بیشتر سیستم‌های صف رایج است و سیستم‌های لاگ‌محور و پردازشِ استریم هم حالا از آن پشتیبانی می‌کنند.

The fix is a dead letter queue (DLQ): instead of retrying forever, the message moves to a separate queue so consumers stay free. DLQs are normally monitored — each message in one is an error, and the operator decides: drop it forever, fix and reproduce it manually, or fix the consumer code. DLQs are common in most queueing systems, and log-based and stream-processing systems support them now too.

۳3 بروکرهای لاگ‌محورLog-based brokers

۳.۱. لاگ، پارتیشن و offset3.1. Logs, partitions and offsets

بروکرِ لاگ‌محور، پیام‌ها را در پارتیشن‌های مرتب نگه می‌دارد. Offset جای پیام در همان پارتیشن است و محلِ ادامهٔ خواندن را مشخص می‌کند؛ شمارهٔ سراسریِ همهٔ پیام‌ها نیست. شرح رسمیِ لاگ و تضمین‌های پردازش در Kafka.

مثال فنی: Offset جایگاهِ یک پارتیشن است

وضعیت اولیه: پیام e7 در پارتیشن ۲ با offset=۱۰۱ ثبت شده است.

اجرای مرحله‌به‌مرحله: گروه A تا offset ۱۰۰ و گروه B تا ۹۰ پیش رفته‌اند؛ پیشرفتشان مستقل است.

نتیجه و دلیل: offset شناسهٔ سراسری همهٔ topic نیست؛ برای موقعیت، نام topic و partition هم لازم است.

Sending a packet over the network or a request to a service is usually a transient act that leaves no permanent trace — and lightweight AMQP/JMS brokers inherited that transient mindset: even when they write a message to disk, they delete it soon after delivery. Databases and filesystems are the opposite: whatever is written is, as a rule, recorded forever until someone explicitly deletes it. That mindset difference hugely affects “derived data”: in batch you can repeat processing many times without harming the input, but in traditional messaging receiving a message with ack is destructive (the message leaves the broker) — so you can't rerun the same consumer and get the same result. And if you add a fresh consumer, it only gets messages after its registration; earlier messages are gone — unlike files and databases, where a late-arriving client still reads the past.

چرا ترکیبی نداشته باشیم: ذخیرهٔ بادوامِ پایگاه داده‌ها + اعلانِ کم‌تأخیرِ پیام‌رسان‌ها؟ این همان ایدهٔ بروکرِ لاگ‌محور است. لاگ فقط دنباله‌ای append-only از رکوردها روی دیسک است (همان که در موتورهای ذخیره‌سازیِ فصل ۴، replication فصل ۶ و اجماعِ فصل ۱۰ دیدیم). تولیدکننده با append به انتهای لاگ پیام می‌فرستد، مصرف‌کننده با خواندنِ ترتیبی پیام می‌گیرد، و وقتی به انتهای لاگ رسید، منتظرِ اعلانِ پیامِ تازه می‌ماند — دقیقاً مثل tail -f در Unix.

Why not have both: database-like durable storage plus messenger-like low-latency notification? That is the log-based broker idea. A log is just an append-only sequence of records on disk (the same one we saw in Chapter 4 storage engines, Chapter 6 replication and Chapter 10 consensus). The producer sends by appending to the log tail; the consumer receives by reading sequentially; and on reaching the tail it waits for fresh-message notification — exactly like tail -f in Unix.

برای throughput بیشتر از یک دیسک، لاگ shard می‌شود (فصل ۷): هر shard لاگِ مستقلی روی ماشینِ خودش است و تاپیک گروهی از shardهاست که پیام‌های هم‌جنس حمل می‌کنند (شکل ۱۲-۳ کتاب — نیمهٔ پایینیِ نمودار ۱). در هر shard که کافکا به آن پارتیشن می‌گوید، بروکر به هر پیام شمارهٔ ترتیبیِ صعودی می‌دهد: offset. چون پارتیشن append-only است، پیام‌های درونِ یک پارتیشن کاملاً مرتب‌اند؛ ولی هیچ تضمینِ ترتیبی میانِ پارتیشن‌ها نیست. Apache Kafka و Amazon Kinesis همین‌طور کار می‌کنند (و با sharding روی چند ماشین و replication به میلیون‌ها پیام در ثانیه و تحملِ خرابی می‌رسند).

For throughput beyond one disk, the log is sharded (Chapter 7): each shard is an independent log on its own machine, and a topic is a group of shards carrying same-kind messages (Fig. 12-3 — the bottom half of Diagram 1). Within each shard, which Kafka calls a partition, the broker assigns every message an ascending sequence number: the offset. Because a partition is append-only, messages inside one partition are fully ordered; but there is no ordering guarantee across partitions. Apache Kafka and Amazon Kinesis work this way (reaching millions of messages per second with fault tolerance through multi-machine sharding and replication).

۳.۲. لاگ در برابر پیام‌رسانیِ سنتی3.2. Logs vs traditional messaging

  • fan-outِ بدیهی: چند مصرف‌کننده مستقل لاگ را می‌خوانند بی‌آن‌که روی هم اثر بگذارند؛ خواندن، پیام را از لاگ پاک نمی‌کند. برای تعادلِ بار، بروکر به‌جای تخصیصِ تک‌تکِ پیام‌ها، shardهای کامل را به نودهای گروهِ مصرف‌کننده می‌دهد و هر کلاینت پیام‌های shard خودش را تک‌نخی و ترتیبی می‌خواند.
  • سقفِ موازی‌سازی = تعدادِ shardها: چون پیام‌های یک shard به یک نود می‌رسند، نودهای همکارِ یک تاپیک حداکثر به‌اندازهٔ shardهای آن تاپیک‌اند. (می‌شود کارِ یک shard را هم تقسیم کرد ولی مدیریتِ offset را پیچیده می‌کند؛ تک‌نخ به‌ازای shard ترجیح دارد و موازی‌سازی با shardِ بیشتر.)
  • انسدادِ سرِ صف (head-of-line blocking): اگر یک پیام کند پردازش شود، پیام‌های بعدیِ همان shard پشتش می‌مانند.
  • Trivial fan-out: several independent consumers read the log without affecting each other; reading never deletes from the log. For load balancing, instead of assigning individual messages the broker hands whole shards to consumer-group nodes, and each client reads its own shard single-threaded and in order.
  • Parallelism ceiling = shard count: since one shard's messages reach one node, the cooperating nodes of a topic are at most its shard count. (One shard's work can be split, but that complicates offset management; one thread per shard is preferred, and you parallelize with more shards.)
  • Head-of-line blocking: if one message is slow to process, the later messages of the same shard wait behind it.

در صفِ کاریِ معمول، محور تحویل و تأییدِ هر پیام است؛ در لاگ، محور نگهداریِ تاریخچه و موقعیتِ خواندنِ هر گروه است. قابلیت‌های دقیق به محصول و تنظیمات بستگی دارند.

مثال فنی: مصرف پیام، حذف فوری نیست

وضعیت اولیه: گروه گزارش پیام ۱۰۱ را خوانده است، اما گروه حسابرسی هنوز در ۸۰ است.

اجرای مرحله‌به‌مرحله: در broker لاگ‌محور، مصرف گروه اول پیام را از لاگ برای گروه دوم حذف نمی‌کند؛ retention سیاست مستقل دارد.

نتیجه و دلیل: گروه‌ها می‌توانند با سرعت جدا پیش بروند، تا وقتی دادهٔ لازم از retention خارج نشده باشد.

So the rule of thumb: if messages are expensive and you want to parallelize them one by one and ordering matters little, a lightweight JMS/AMQP broker is better; if throughput is high, each message processes fast and ordering matters, the log-based approach shines — though the boundary between the two architectures is blurring (Kafka now supports lightweight JMS/AMQP-style consumer groups too). And since order survives only within a shard, every event that must stay ordered must go to the same shard — e.g. by partitioning on the user ID key.

مصرفِ ترتیبیِ shard یک هدیه هم دارد: لازم نیست بروکر برای تک‌تکِ پیام‌ها ack نگه دارد؛ کافی است دوره‌ای offset مصرف‌کننده را ثبت کند. همهٔ پیام‌های با offset کمتر پردازش شده‌اند، بزرگ‌ترها هنوز دیده نشده‌اند. همین دفترداریِ کمتر + batching و pipelining، throughput سیستم‌های لاگ‌محور را بالا می‌برد. بهایش: اگر مصرف‌کننده بمیرد، از آخرین offsetِ ثبت‌شده ادامه می‌دهد نه آخرین offsetِ دیده‌شده — پس بعضی پیام‌ها را دوبار می‌بیند. این offset دقیقاً همان شمارهٔ ترتیبیِ لاگ در replication تک‌رهبریِ پایگاه داده است (فصل ۶): بروکر مثل پایگاه دادهِ رهبر است و مصرف‌کننده مثل دنباله‌رو.

Sequential shard consumption brings a gift: the broker needn't track per-message acks; periodically recording the consumer offset is enough. Every message below the offset is processed; larger ones unseen. That lighter bookkeeping plus batching and pipelining is what lifts log-based throughput. The price: if the consumer dies it resumes from the last recorded offset, not the last seen one — so it sees some messages twice. That offset is exactly the log sequence number of single-leader database replication (Chapter 6): the broker is like the leader database, the consumer like a follower.

حسابِ سرانگشتیِ بافرِ دیسکیDisk-buffer arithmetic

اگر فقط append کنی، بالاخره دیسک پر می‌شود: لاگ به segment تقسیم می‌شود و segmentهای قدیمی پاک یا آرشیو می‌شوند. یعنی لاگ عملاً بافرِ حلقه‌ایِ (circular / ring buffer) محدود اما بزرگی است که پیام‌های قدیمی‌تر از گنجایشش را می‌اندازد. حسابِ کتاب: دیسکِ ۲۰ ترابایتی با توانِ نوشتنِ ترتیبیِ 250MB/s حدود ۲۲ ساعت طول می‌کشد تا پر شود — پس لاگِ دیسکی حتی در بدترین حالت دست‌کم ۲۲ ساعت پیام بافر می‌کند، و در عمل (که پهنای باند کامل مصرف نمی‌شود) چند روز تا چند هفته. خیلی از بروکرها حالا پیام‌ها را در object storage هم نگه می‌دارند (tiered storage) که هم ارزان‌تر است و هم یکپارچگی با بچ و انبارِ داده را ساده می‌کند.

If you only append, the disk eventually fills: the log splits into segments and old segments are deleted or archived. So the log is effectively a bounded but large circular / ring buffer that drops messages older than its capacity. The math: a 20 TB disk with 250 MB/s sequential write takes about 22 hours to fill — so even in the worst case a disk log buffers at least 22 hours of messages, and in practice (bandwidth never fully consumed) days to weeks. Many brokers now also keep messages in object storage (tiered storage), which is cheaper and simplifies integration with batch and the warehouse.

۳.۳. بازپخش و بافرِ دیسکی3.3. Replay and disk buffering

بازپخش یعنی دوباره خواندنِ پیام‌های نگه‌داشته‌شده از یک نقطهٔ قبلی. لاگ بافرِ نامحدود نیست؛ اگر مصرف‌کننده بیش از دورهٔ نگهداری عقب بماند، تاریخچهٔ لازم ممکن است حذف شود.

مثال فنی: Replay بعد از اصلاح فرمول

وضعیت اولیه: خطای محاسبه از offset ۱۰۰۰ تا ۲۰۰۰ رخ داده و لاگ هنوز موجود است.

اجرای مرحله‌به‌مرحله: پردازشگر اصلاح‌شده در خروجی تازه از ۱۰۰۰ بازپخش می‌کند؛ اثر قبلی باید جایگزین یا با هویت پیام از شمارش دوباره مصون شود.

نتیجه و دلیل: عقب بردن offset به‌تنهایی جدول قدیمی را پاک یا اصلاح نمی‌کند؛ مقصد هم بخشی از برنامهٔ replay است.

In the three-way classification (drop / buffer / backpressure), the log-based approach is buffering with a large fixed buffer (bounded by disk space). If a consumer lags so far that its offset points at a deleted segment, those messages are lost — i.e. the broker effectively drops messages older than the buffer. You can monitor a consumer's lag behind the log tip and alert when it grows; since the buffer is big, a human operator gets time to fix the slow consumer and catch up before anything is dropped.

  • فقط همان مصرف‌کننده آسیب می‌بیند — سرویسِ بقیه مختل نمی‌شود. مزیتِ عملیاتیِ بزرگی است: می‌توانی لاگِ production را برای توسعه و دیباگ آزمایشی مصرف کنی بی‌آن‌که نگرانِ سرویسِ production باشی. وقتی مصرف‌کننده خاموش یا crash می‌کند، مصرفِ منابعش قطع می‌شود — تنها چیزی که می‌ماند offset اوست. (برخلافِ بروکرهای سنتی که باید صف‌های مصرف‌کنندهٔ خاموش را پاک کنی تا حافظهٔ مصرف‌کننده‌های فعال را نخورند.)
  • بازپخش (replay): در بروکرِ سنتی مصرف مخرب است، ولی در لاگ‌محور مصرف مثل خواندنِ فایل است — عملیاتی read-only که لاگ را عوض نمی‌کند. تنها اثرِ جانبی، جلو رفتنِ offset است که دستِ خودِ مصرف‌کننده است؛ پس می‌توانی یک کپی از مصرف‌کننده را با offset دیروز بالا بیاوری، خروجی را جای دیگری بنویسی، و روزِ گذشته را هر چند بار که خواستی با کدِ متفاوت بازپردازش کنی — دقیقاً مثل بچ که دادهٔ مشتق‌شده را با تبدیلِ تکرارپذیر از ورودی جدا می‌کند.
  • Only that consumer suffers — everyone else's service is undisturbed. A big operational win: you can consume the production log for experimental development and debugging without fearing for production. When the consumer is off or crashed, its resource consumption stops — all that remains is its offset. (Unlike traditional brokers, where you must purge a dormant consumer's queues so they don't eat active consumers' memory.)
  • Replay: in a traditional broker consumption is destructive, but in a log-based one it is like reading a file — a read-only operation that doesn't change the log. The only side effect is advancing the offset, which is the consumer's own business; so you can raise a copy of the consumer at yesterday's offset, write output elsewhere, and reprocess the past with different code as many times as you like — exactly like batch separating derived data from input with a repeatable transform.

۴4 پایگاه داده‌ها و استریم‌هاDatabases and streams

وضعیتِ ذخیره‌شده و جریانِ تغییراتِ آن به هم مربوط‌اند. با داشتن یک تصویرِ شروع و تغییراتِ بعدی می‌توان نسخهٔ دیگری از داده ساخت، به شرط حفظِ مرز و ترتیبِ لازم.

مثال فنی: State چگونه جریان می‌شود؟

وضعیت اولیه: جدول کالا برای P7 قیمت ۱۰۰ دارد و سپس به ۱۲۰ تغییر می‌کند.

اجرای مرحله‌به‌مرحله: CDC تغییر commitشده را به رویداد تبدیل می‌کند و مصرف‌کننده از آن نمای جست‌وجو می‌سازد.

نتیجه و دلیل: پایگاه داده می‌تواند منشأ stream باشد؛ مرز snapshot و تغییرهای بعدی برای ساخت نمای کامل لازم است.

Brokers and databases were always seen as two separate categories, but log-based brokers brought database ideas into messaging — and the reverse works too: streaming ideas can be brought into databases. You can even make the event stream the system of record: instead of a data model mutated by updates and deletes, model every state change as an immutable event in an append-only log and derive readable materialized views from it — the event sourcing of Chapter 3. A log-based broker (one that never deletes old events) is well-built for this. But you needn't go as far as event sourcing; even with a mutable model an event stream helps the database — because every database write is itself an event that can be captured, stored and processed. The replication log (Chapter 6) is the leader's write-event stream, and the state-machine replication principle (Chapter 10) is the same: if each event is a write and each replica processes the same events in the same order, all reach the same final state.

۴.۱. مسابقهٔ نوشتنِ دوگانه + نمودار ۳4.1. The dual-write race + Diagram 3

نوشتنِ جداگانه در دو سیستم می‌تواند ناقص بماند یا به ترتیبِ متفاوت اجرا شود. داشتنِ retry ساده، لزوماً اختلافِ پایگاه داده و نمای دوم را برطرف نمی‌کند.

مثال فنی: Dual-write وسط کار می‌شکند

وضعیت اولیه: برنامه قیمت P7 را در مرجع ۱۲۰ می‌کند و پیش از نوشتن ایندکس crash می‌کند.

اجرای مرحله‌به‌مرحله: مرجع ۱۲۰ و ایندکس ۱۰۰ می‌مانند؛ در دو ویرایش هم‌زمان، حتی ترتیب اعمال دو مقصد می‌تواند متفاوت شود.

نتیجه و دلیل: retry ساده همیشه ترتیب را درست نمی‌کند؛ یک لاگ معتبرِ تغییر یا outbox مسیر بازپخش‌پذیر می‌دهد.

No single system covers every need: an OLTP database for user requests, a cache for speed, a text index for search, a warehouse for analytics — each keeps its own copy in its own optimized shape, so they must stay in sync. With the warehouse, periodic ETL (full copy + bulk load, i.e. batch) usually does this. An occasionally used alternative is dual write: application code explicitly writes to each system — first the database, then the search index, then the cache invalidation. But dual writes have two serious problems, the first shown in Diagram 3.

نمودار ۳ — مسابقهٔ نوشتنِ دوگانه: دو سیستم، دو ترتیبDiagram 3 · The dual-write race: two systems, two orders باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: Dual-write وسط کار می‌شکند

وضعیت اولیه: برنامه قیمت P7 را در مرجع ۱۲۰ می‌کند و پیش از نوشتن ایندکس crash می‌کند.

اجرای مرحله‌به‌مرحله: مرجع ۱۲۰ و ایندکس ۱۰۰ می‌مانند؛ در دو ویرایش هم‌زمان، حتی ترتیب اعمال دو مقصد می‌تواند متفاوت شود.

نتیجه و دلیل: retry ساده همیشه ترتیب را درست نمی‌کند؛ یک لاگ معتبرِ تغییر یا outbox مسیر بازپخش‌پذیر می‌دهد.

پنج ستون داریم: Client 1 (می‌خواهد X = A شود)، Client 2 (می‌خواهد X = B شود)، Application (به هر دو سیستم می‌نویسد)، Database (رهبرِ خودش را دارد) و Search Index (آن هم رهبرِ خودش را دارد). دو فلشِ اول هم‌زمان‌اند: set X = A از مشتریِ ۱ و set X = B از مشتریِ ۲ به اپلیکیشن.

حالا interleaving بدشانس: اپلیکیشن اول write X = A را به پایگاه داده می‌فرستد (یادداشت: write مشتریِ ۱ اول اعمال شد) و بعد write X = B (یادداشت: مقدارِ نهاییِ پایگاه داده B است). ولی در ایندکسِ جستجو ترتیب برعکس می‌رسد: اول index X = B (یادداشت: write مشتریِ ۲ اول رسید) و بعد index X = A (یادداشت: مقدارِ نهاییِ ایندکس A است).

دو فلشِ خط‌چینِ پایانی حکم را می‌گوید: فلشِ DB = B, index = A از پایگاه داده به ایندکس با یادداشتِ «هیچ خطایی رخ نداد — واگراییِ بی‌صدا؛ تشخیصِ هم‌زمانی نیست»؛ و فلشِ if one write fails با یادداشتِ «آن یکی باز هم کامیت می‌شود — هردو-یا-هیچ‌کدام مسئلهٔ کامیتِ اتمی (2PC) است، نه هم‌زمانی». سه کارت هم چارچوب می‌دهد: چیدمان (دو رهبرِ مستقل یعنی هیچ ترتیبِ write واحدی نیست)، خودِ مسابقه (A بعد B در پایگاه داده در برابرِ B بعد A در ایندکس، بدونِ خطا)، و چراییِ ماندگاربودنِ خرابی (رونویسیِ بی‌صدا؛ بردارِ نسخه متوجه می‌شد؛ و راهِ CDC فقط در یک خط ارجاع شده — نمودارِ بعدی). درس: بدونِ رهبرِ واحد، ترتیبِ writeها در دو سیستم می‌تواند برای همیشه واگرا بماند.

Five columns: Client 1 (wants X = A), Client 2 (wants X = B), the Application (writes to both systems), the Database (with its own leader) and the Search Index (with its own leader too). The first two arrows are simultaneous: set X = A from Client 1 and set X = B from Client 2 into the application.

Then the unlucky interleaving: the application sends write X = A to the database first (Client 1's write applied first) and then write X = B (the database's final value is B). But the writes reach the search index in reverse: index X = B first (Client 2's write arrived first), then index X = A (the index's final value is A).

The two dashed arrows at the bottom give the verdict: DB = B, index = A — “no error occurred: silent divergence, undetectable concurrency”; and if one write fails — “the other still commits: both-or-neither is the atomic-commit (2PC) problem, not concurrency.” The three cards frame it: the setup (two independent leaders means no single write order), the race itself (A-then-B in the database vs B-then-A in the index, with no error), and why the damage persists (silent overwrite; a version vector would have noticed; the CDC way out is referenced in one line — the next diagram). Lesson: without a single leader, write orders in two systems can diverge forever.

مشکلِ دومِ نوشتنِ دوگانه تحملِ خرابی است نه هم‌زمانی: یکی از writeها موفق و دیگری ناموفق می‌شود و دو سیستم ناسازگار می‌مانند. این‌که «هر دو موفق شوند یا هر دو شکست بخورند» همان مسئلهٔ کامیتِ اتمی است که حلش گران است (2PC فصل ۸). اگر فقط یک پایگاه دادهِ تک‌رهبری داشتی، رهبر ترتیبِ writeها را تعیین می‌کرد و replication ماشینِ وضعیت کار می‌کرد؛ ولی در شکلِ بالا رهبرِ واحدی نیست — پایگاه داده رهبرِ خودش را دارد و ایندکس رهبرِ خودش را (وضعیتی شبیه replication چندرهبریِ فصل ۶). ای‌کاش واقعاً فقط یک رهبر بود — مثلاً پایگاه داده — و ایندکس دنباله‌رویِ پایگاه داده می‌شد. آیا در عمل می‌شود؟

The second dual-write problem is fault tolerance, not concurrency: one write succeeds and the other fails, leaving the two systems inconsistent. Getting “both succeed or both fail” is the atomic-commit problem, which is expensive to solve (Chapter 8's 2PC). With a single-leader database the leader would order the writes and state-machine replication would work; but in the figure above there is no single leader — the database has its own leader and the index its own (a situation like Chapter 6's multi-leader replication). How nice it would be if there were truly one leader — the database, say — with the index following it. Is that practical?

۴.۲. CDC: یک رهبر، چند دنباله‌رو + نمودار ۴4.2. CDC: one leader, many followers + Diagram 4

CDC تغییرهای پایگاه داده را به یک جریانِ قابل‌مصرف تبدیل می‌کند. مصرف‌کننده می‌تواند نمای دیگری را به‌روز کند؛ مرزِ تصویر اولیه، ترتیب، حذف‌ها و بازتلاش باید مدیریت شوند.

مثال فنی: CDC با مرز سازگار

وضعیت اولیه: snapshot جدول تا موقعیت لاگ ۵۰۰ گرفته شده است.

اجرای مرحله‌به‌مرحله: snapshot وارد جست‌وجو می‌شود و سپس تغییرهای پس از ۵۰۰ به‌ترتیب مناسب اعمال می‌شوند؛ lag و gap کنترل می‌شوند.

نتیجه و دلیل: اگر مرز اشتباه باشد، تغییرِ حین snapshot جا می‌افتد یا نادرست تکرار می‌شود؛ صرفِ خواندن دوره‌ای کل جدول CDC دقیق نیست.

The trouble with most databases' replication logs was that they were treated as internal implementation detail, not public API — for decades there was no documented way to tap the change log. But recently change data capture (CDC) has been taken seriously: observing every change written to the database and extracting it in a form replicable to other systems — more attractive still when changes are immediately available as a stream. Just capture the changes and continuously apply them to the search index; if the change log is applied in the same order, the index data matches the database. The index and every other derived system are just consumers of the change stream — and Diagram 3's race is solved: although requests A and B reach the database concurrently, the database itself orders their execution and writes them in that order to the replication log; the index picks up and applies the same order. If another system (say, the warehouse) is needed later, just add another consumer to the same stream.

نمودار ۴ — CDC: یک رهبر، چند دنباله‌روDiagram 4 · CDC: one leader, many followers باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: CDC با مرز سازگار

وضعیت اولیه: snapshot جدول تا موقعیت لاگ ۵۰۰ گرفته شده است.

اجرای مرحله‌به‌مرحله: snapshot وارد جست‌وجو می‌شود و سپس تغییرهای پس از ۵۰۰ به‌ترتیب مناسب اعمال می‌شوند؛ lag و gap کنترل می‌شوند.

نتیجه و دلیل: اگر مرز اشتباه باشد، تغییرِ حین snapshot جا می‌افتد یا نادرست تکرار می‌شود؛ صرفِ خواندن دوره‌ای کل جدول CDC دقیق نیست.

این یک dataflow پنج‌ستونه است: System of Record، Replication Log، CDC Capture، Change Stream و Derived Data. مسیرِ اصلی ردیفِ پایین است: OLTP Database (برچسب: decides write order) با فلشِ commit in one order به Replication Log (برچسب: in commit order)؛ بعد فلشِ read the log به CDC Connector (زیرنویس: Debezium-style)؛ بعد فلشِ change events (با طبقه‌بندیِ keyed by primary key) به Log-Based Broker (برچسب: log-compacted)؛ و از آن‌جا چهار فلشِ کوتاهِ index، cache، load و feed به ستونِ آخر: Search Index، Cache، Data Warehouse و Stream Processor (هر چهارتا با زیرنویسِ derived data system).

دو مسیرِ کمکی هم هست که معنایشان در کارت‌هاست: از OLTP Database فلشِ خط‌چینِ consistent copy به جعبهٔ Initial Snapshot می‌رود و از آن‌جا فلشِ then apply changes from offset به ایندکس — یعنی اول snapshot در offset معلوم، بعد اعمالِ تغییرات از همان offset. و فلشِ same transaction از پایگاه داده به جعبهٔ Outbox Table (برچسب: written in one txn) و از آن‌جا reads outbox schema به کانکتور — یعنی CDC به‌جای اسکیمای داخلی، اسکیمای پایدارِ outbox را می‌خواند. درس: مصرف‌کننده‌های مشتق‌شده فقط دنباله‌روهای رهبرِ واحدند، و ترتیبِ واحدِ او واگرایی را ناممکن می‌کند.

A five-column dataflow: System of Record, Replication Log, CDC Capture, Change Stream and Derived Data. The main path is the bottom row: the OLTP Database (decides write order) via commit in one order to the Replication Log (in commit order); then read the log to the CDC Connector (Debezium-style); then change events (keyed by primary key) to the Log-Based Broker (log-compacted); and from there four short index / cache / load / feed arrows to the last column: Search Index, Cache, Data Warehouse and Stream Processor (all four derived data systems).

Two helper paths carry the meaning in the cards: from the OLTP Database a dashed consistent copy arrow goes to Initial Snapshot, then then apply changes from offset to the index — first a snapshot at a known offset, then changes from that offset. And a same transaction arrow from the database to the Outbox Table (written in one txn), then reads outbox schema to the connector — so CDC reads a stable outbox schema instead of the internal one. Lesson: derived consumers are just followers of a single leader, and its single order makes divergence impossible.

عملاً CDC پایگاه دادهِ مبدأ را رهبر و بقیه را دنباله‌رو می‌کند، و بروکرِ لاگ‌محور برای حملِ رویدادهای تغییر خوش‌ساخت است چون ترتیب را حفظ می‌کند. لاگِ replication منطقی (row-based) با همهٔ چالش‌هایش (تغییرِ اسکیما، مدل‌کردنِ update) همین کار را می‌کند؛ پروژهٔ متن‌بازِ Debezium (با کانکتورِ MySQL و PostgreSQL و Oracle و …) و فریم‌ورکِ Kafka Connect همین مسیر را هموار کرده‌اند؛ Maxwell برای MySQL با parse کردنِ binlog و GoldenGate برای Oracle مشابه‌اند. مثل بروکرها، CDC معمولاً ناهمگام است: پایگاه دادهِ مبنا منتظرِ اعمالِ تغییر روی مصرف‌کننده نمی‌ماند — خوبی‌اش این است که مصرف‌کنندهٔ کند سیستمِ مبنا را اذیت نمی‌کند، و بدی‌اش این‌که همهٔ مسائلِ lagِ replication (فصل ۶) این‌جا هم هست.

In effect CDC makes the source database the leader and everything else a follower, and the log-based broker is well-built for carrying change events because it preserves order. The logical (row-based) replication log does this job with all its challenges (schema change, modelling updates); the open-source Debezium project (with MySQL, PostgreSQL, Oracle connectors and more) and the Kafka Connect framework paved the way; Maxwell for MySQL by parsing the binlog and GoldenGate for Oracle are similar. Like brokers, CDC is usually asynchronous: the record database doesn't wait for the change to be applied downstream — the upside is a slow consumer never bothers the record system; the downside is that all Chapter-6 replication-lag issues apply here too.

۴.۳. snapshot، compaction و outbox4.3. Snapshots, compaction and outbox

Snapshot تصویرِ یک نقطه است؛ compaction می‌تواند آخرین مقدار هر کلید را در لاگ نگه دارد؛ outbox، تغییرِ داده و ثبتِ پیامِ قابل‌ارسال را در یک تراکنش کنار هم قرار می‌دهد.

مثال فنی: Outbox کنار سفارش

وضعیت اولیه: در همان تراکنش، سفارش ۴۲ و ردیف outbox با event_id=e7 نوشته می‌شوند.

اجرای مرحله‌به‌مرحله: relay پیام را منتشر می‌کند و ممکن است پس از crash دوباره آن را بفرستد؛ مصرف‌کننده e7 را تکراری تشخیص می‌دهد.

نتیجه و دلیل: outbox شکافِ ثبت سفارش و ثبت قصد انتشار را می‌بندد؛ تحویل فیزیکیِ دقیقاً یک‌باره را خودکار تضمین نمی‌کند.

  • snapshot اولیه: اگر لاگِ همهٔ تغییراتِ تاریخ را داشتی، با بازپخشش کلِ وضعیت را می‌ساختی؛ ولی معمولاً نگه‌داشتنِ همهٔ تاریخ پرهزینه است و لاگ باید کوتاه شود. پس برای آوردنِ سیستمِ مشتق‌شدهٔ تازه (مثلاً ایندکسِ متنیِ نو) به snapshotِ سازگار نیاز داری (فصل ۶) — و آن snapshot باید متناظر با position/offset معلومی در لاگِ تغییر باشد تا بدانی از کجا به بعد تغییرات را اعمال کنی. بعضی ابزارهای CDC این را توکار دارند، بعضی دستی‌اش گذاشته‌اند؛ Debezium با الگوریتمِ watermarkingِ DBLog نتفلیکس snapshot تدریجی می‌دهد.
  • فشرده‌سازیِ لاگ (log compaction): همان ایدهٔ موتورهای ذخیره‌سازیِ فصل ۴ — موتور دوره‌ای رکوردهای هم‌کلید را پیدا می‌کند، تکراری‌ها را می‌اندازد و فقط تازه‌ترین update هر کلید را نگه می‌دارد (شکل ۱۲-۶ کتاب با ویدیوهای گربه!). update با مقدارِ null ویژه (tombstone) یعنی حذفِ کلید. فضای دیسکِ لاگِ فشرده فقط به محتوای فعلی بستگی دارد نه تعدادِ writeهای تاریخ. همین ایده در CDC جواب می‌دهد: اگر هر تغییر کلیدِ اصلی داشته باشد و هر update جایگزینِ قبلی شود، نگه‌داشتنِ فقط تازه‌ترین write هر کلید کافی است — و مصرف‌کنندهٔ تازه می‌تواند از offset صفر کلِ لاگ را اسکن کند و کپیِ کاملِ پایگاه داده را بگیرد بی‌آن‌که snapshot تازه‌ای از مبدأ لازم باشد. کافکا این ویژگی را دارد و به بروکر اجازه می‌دهد نقشِ ذخیرهٔ بادوام را هم بازی کند، نه فقط پیام‌رسانیِ گذرا.
  • الگوی outbox: با CDC اسکیمای داخلیِ پایگاه داده عملاً API عمومی می‌شود (حذفِ یک ستون، مصرف‌کننده‌های پایین‌دست را می‌شکند — و چون CDC استریم است، مصرف‌کننده‌اش ممکن است سرویسِ production باشد و قطعیِ روبه‌مشتری بدهد؛ data contractها برای همین‌اند). راهِ جداکردنِ اسکیمای داخلی از خارجی، جدولِ outbox با اسکیمای خودش است که CDC آن را می‌خواند نه مدلِ دامنه را. این هم dual-write است — ولی هر دو write در یک سیستم (پایگاه داده) و در یک تراکنش‌اند، پس چالش‌های بخشِ قبل را ندارد. بهایش: نگه‌داشتنِ تبدیلِ میانِ دو اسکیما و حجمِ write بیشتر در پایگاه داده.
  • CDC در برابر event sourcing: هر دو همهٔ تغییرات را لاگ می‌کنند ولی در سطحِ abstraction متفاوت: در CDC اپ با پایگاه داده جهش‌پذیر کار می‌کند و لاگ از سطحِ پایین (parse لاگِ replication) استخراج می‌شود — پس ترتیبِ استخراج دقیقاً همان ترتیبِ واقعیِ writeهاست و مسابقهٔ نمودار ۳ پیش نمی‌آید. در event sourcing منطقِ اپ صریح روی رویدادهای immutableِ سطحِ اپلیکیشن ساخته شده؛ رویدادها نیتِ کنشِ کاربر را می‌گویند نه مکانیکِ update را، پس رویدادِ بعدی معمولاً قبلی را override نمی‌کند و برای بازسازیِ وضعیتِ نهایی کلِ تاریخ لازم است (compaction مثل CDC ممکن نیست). انتخاب بستگی به وضعیت دارد: event sourcing تغییرِ بزرگی است (فصل ۳) ولی CDC را با کمترین تغییر به پایگاه دادهِ موجود اضافه می‌کنی — حتی شاید اپِ نویسنده اصلاً نفهمد CDC در جریان است.
  • Initial snapshot: if you had the log of all historical changes, replaying it would build the whole state; but keeping all of history is usually too expensive and the log must be shortened. So bringing up a fresh derived system (say, a new text index) needs a consistent snapshot (Chapter 6) — and that snapshot must correspond to a known position/offset in the change log so you know where onward to apply changes from. Some CDC tools have this built in, some left it manual; Debezium offers gradual snapshots via Netflix's DBLog watermarking algorithm.
  • Log compaction: the same idea as Chapter 4 storage engines — the engine periodically finds same-key records, drops duplicates and keeps only each key's latest update (Fig. 12-6 with the cat videos!). An update with a special null value (tombstone) means key deletion. A compacted log's disk footprint depends only on current content, not on the number of historical writes. The same idea works in CDC: if every change has a primary key and each update supersedes the previous one, keeping only each key's latest write suffices — and a fresh consumer can scan the whole log from offset zero and take a full database copy without needing a fresh snapshot from the source. Kafka has this feature, letting the broker also play durable storage, not just transient messaging.
  • Outbox pattern: with CDC the database's internal schema effectively becomes public API (dropping a column breaks downstream consumers — and since CDC is a stream, its consumer may be a production service, causing customer-facing outages; data contracts exist for this). The way to separate internal from external schema is an outbox table with its own schema that CDC reads instead of the domain model. This is dual-write too — but both writes are in one system (the database) and in one transaction, so it lacks the previous section's challenges. Its price: maintaining the conversion between the two schemas and higher write volume in the database.
  • CDC vs event sourcing: both log all changes but at different abstraction levels: in CDC the app works with a mutable database and the log is extracted from a low level (parsing the replication log) — so the extracted order is exactly the real write order and Diagram 3's race never arises. In event sourcing the app logic is explicitly built on immutable application-level events; events state the user's intent, not update mechanics, so a later event usually doesn't override the earlier one and rebuilding the final state needs the whole history (CDC-style compaction is impossible). The choice is situational: event sourcing is a big change (Chapter 3), while CDC can be added to an existing database with minimal change — the writing app may never even know CDC is running.

۴.۴. دوگانِ state و stream + نمودار ۵4.4. The state/stream duality + Diagram 5

جریانِ رویدادها تاریخچهٔ تغییر را بیان می‌کند و وضعیت، نتیجهٔ اعمالِ آن تغییرهاست. بازسازی نیازمندِ تاریخچهٔ کافی یا تصویرِ شروعِ معتبر و تابعِ پردازشِ مشخص است.

مثال فنی: ساخت state از رخدادها

وضعیت اولیه: جریان کلید K به‌ترتیب K=1,K=3,K=DELETE است.

اجرای مرحله‌به‌مرحله: اعمال این تغییرها state نهاییِ «K وجود ندارد» می‌سازد؛ compaction می‌تواند برای بازسازی state آخرین تغییر لازمِ هر کلید را نگه دارد.

نتیجه و دلیل: stream تاریخچهٔ تغییر و state حاصلِ اعمال آن است؛ حذف‌ها و retention باید در bootstrap درست تفسیر شوند.

In Chapter 11 we saw batch profit from input immutability; the same immutability principle powers event sourcing and CDC. We usually think of the database as holding current state (optimized for reading), but any state that changes is the product of events that mutated it over time: the list of free seats is the product of processed reservations; an account balance the product of credits and debits. No matter how much is done and redone, the fact remains that those events happened. The key idea: mutable state and the append-only log of immutable events are not in conflict; they are two sides of one coin. The log of all changes is the evolution of state over time — mathematically inclined: application state is the integral of the event stream over time, and the change stream is the time-derivative of state (Fig. 12-7). As Gray and Reuter put it (1992): “Fundamentally, there is no need to keep a database; the log has all the information. The only reason for storing a database (i.e. the current end of the log) is retrieval efficiency.” And log compaction is the bridge between the two pictures: only each record's latest version remains.

نمودار ۵ — دوگانِ state و stream: انتگرال، مشتق و compactionDiagram 5 · State/stream duality: integral, derivative and compaction باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: ساخت state از رخدادها

وضعیت اولیه: جریان کلید K به‌ترتیب K=1,K=3,K=DELETE است.

اجرای مرحله‌به‌مرحله: اعمال این تغییرها state نهاییِ «K وجود ندارد» می‌سازد؛ compaction می‌تواند برای بازسازی state آخرین تغییر لازمِ هر کلید را نگه دارد.

نتیجه و دلیل: stream تاریخچهٔ تغییر و state حاصلِ اعمال آن است؛ حذف‌ها و retention باید در bootstrap درست تفسیر شوند.

ستونِ چپ event stream / changelog است: شش سطر با offset از صفر تا پنج — (mew, 12)، (purr, 3)، (mew, 17)، (scratch, 5)، (mew, 21) — و زیرش برچسبِ append-only · immutable · replayable. جدولِ راست application state است: سه سطرِ کلید → تازه‌ترین مقدار — mew · 21، purr · 3، scratch · 5 — با یادداشتِ reads: O(1) lookup by key و برچسبِ mutable · read-optimized · keyed lookup. میانشان دو پیکانِ بزرگ: از استریم به state با برچسبِ integrate over time (apply events) و از state به استریم با differentiate by time (change data capture). زیرِ این دو، نوارِ نقل‌قولِ وسط‌چین همان جملهٔ گری و رویتر است.

نوارِ پایینیِ تمام‌عرض، log compaction را کارشده نشان می‌دهد: سمتِ چپ before — full history (همان تاریخچه با سطرِ tombstoneِ (purr, ⊥) که خط خورده)، وسط فلشِ compact با یادداشتِ keep latest per key، و سمتِ راست after — compaction فقط با (mew, 21) و (scratch, 5) — و یادداشتِ «چرا مهم است»: لاگِ فشرده ≈ کپیِ کاملِ پایگاه داده، پس سیستمِ مشتق‌شدهٔ تازه می‌تواند از offset صفر بازپخش کند. درس: لاگ را سیستمِ مبنا بدان و state را مشتق‌شده از آن — آن‌وقت جریانِ داده در سیستم قابلِ استدلال می‌شود.

Left column, the event stream / changelog: six rows with offsets zero to five — (mew, 12), (purr, 3), (mew, 17), (scratch, 5), (mew, 21) — labelled append-only · immutable · replayable. Right table, the application state: three key → latest-value rows — mew · 21, purr · 3, scratch · 5 — with reads: O(1) lookup by key and mutable · read-optimized · keyed lookup. Between them two big arrows: stream-to-state integrate over time (apply events) and state-to-stream differentiate by time (change data capture), with the Gray–Reuter quote centered beneath.

The full-width bottom strip works through log compaction: left before — full history (including the struck-through tombstone row (purr, ⊥)), middle compact arrow (keep latest per key), right after — compaction with only (mew, 21) and (scratch, 5) — “why it matters”: a compacted log ≈ a full database copy, so a fresh derived system can replay from offset zero. Lesson: treat the log as the record and state as derived from it — then dataflow through the system becomes reason-about-able.

چرا immutable؟ چند خاصیتِ عملیWhy immutable? Practical properties
  • حسابرسی: حسابدارها قرن‌هاست immutable کار می‌کنند — دفترِ کلِ append-only، و اشتباه با تراکنشِ جبرانی درست می‌شود نه پاک‌کردن. اگر کدِ باگی دادهٔ بد بنویسد، با لاگِ append-only فهمیدنِ «چه شد» و برگشتن بسیار ساده‌تر از بازنویسیِ مخرب است.
  • اطلاعاتِ بیشتر از وضعیت: مشتری قلمی را به سبد اضافه و بعد حذف می‌کند — از نظرِ انجامِ سفارش خنثی شد، ولی برای تحلیل دانستنِ «به آن فکر کرده» ارزشمند است. لاگِ رویداد این را دارد، پایگاه‌داده‌ای که با حذف پاک می‌کند نه.
  • چند view از یک لاگ: با جداکردنِ state جهش‌پذیر از لاگِ immutable، چند نمایشِ خواندنی از همان لاگ می‌سازی (مثل چند مصرف‌کنندهٔ یک استریم)؛ افزودنِ ویژگیِ تازه یعنی ساختنِ view تازه کنارِ سیستمِ قدیمی، نه migration پیچیدهٔ اسکیما.
  • هم‌زمانیِ ساده‌تر: در event sourcing هر کنشِ کاربر یک رویدادِ خودکفاست — فقط یک append اتمی به لاگ. اگر لاگ و state هم‌شارد باشند، مصرف‌کنندهٔ تک‌نخیِ لاگ اصلاً به کنترلِ هم‌زمانی برای write نیاز ندارد؛ لاگ با تعریفِ ترتیبِ سریال، نادترمینیسمِ هم‌زمانی را حذف می‌کند.
  • Auditing: accountants have worked immutably for centuries — an append-only ledger, with mistakes fixed by compensating transactions, not deletion. If buggy code writes bad data, an append-only log makes understanding “what happened” and rolling back far simpler than destructive overwrite.
  • More information than state: a customer adds an item to the basket then removes it — neutral for fulfilling the order, but knowing “they considered it” is valuable for analytics. The event log has it; a database that deletes on remove does not.
  • Several views of one log: by separating mutable state from the immutable log you build several readable views of the same log (like several consumers of one stream); adding a fresh feature means building a fresh view beside the old system, not a complex schema migration.
  • Simpler concurrency: in event sourcing each user action is a self-contained event — just one atomic log append. If log and state are co-sharded, the log's single-threaded consumer needs no write concurrency control at all; by defining the serial order, the log removes concurrency nondeterminism.

دو سایه هم هست. اول، مصرف‌کننده‌های لاگ معمولاً ناهمگام‌اند: کاربر write می‌کند و از view مشتق‌شده می‌خواند و write خودش را هنوز نمی‌بیند (همان «خواندنِ writeهای خودت» فصل ۶). همگام‌کردنش تراکنشِ توزیع‌شده یا انتظار می‌خواهد که معمولاً غیرعملی است — پس viewها ناهمگام به‌روز می‌شوند. دوم، محدودیتِ immutability: بعضی workloadها update/delete سنگین روی dataset کوچک دارند (تاریخِ immutable انفجار می‌کند)، و گاهی قانون (مثل GDPR) یا لو رفتنِ دادهٔ حساس، حذفِ واقعی می‌خواهد — یعنی بازنویسیِ تاریخ و وانمودکردنِ این‌که داده هیچ‌وقت نوشته نشده (در Datomic به آن excision می‌گویند). حذفِ واقعی شگفت‌انگیز سخت است (کپی همه‌جاست، بکاپ‌ها عمداً immutable‌اند)؛ یک راه crypto-shredding است: داده را رمزنگاری‌شده نگه دار و برای حذف، کلید را فراموش کن — ولی این فقط مسئله را جابه‌جا می‌کند (انبارِ کلید جهش‌پذیر می‌شود) و باید از قبل تصمیم بگیری چه داده‌ای با چه کلیدی رمز شود.

There are two shadows. First, log consumers are usually asynchronous: the user writes and reads from a derived view, not yet seeing their own write (the Chapter-6 “read your own writes”). Synchronizing it needs distributed transactions or waiting, usually impractical — so views update asynchronously. Second, the limits of immutability: some workloads heavily update/delete a small dataset (immutable history explodes), and sometimes law (like GDPR) or leaked sensitive data demands real deletion — i.e. rewriting history and pretending the data was never written (in Datomic called excision). Real deletion is surprisingly hard (copies everywhere, backups deliberately immutable); one way is crypto-shredding: keep data encrypted and forget the key to delete — but that only moves the problem (the key store becomes mutable) and you must decide upfront which data is encrypted with which key.

۵5 پردازشِ استریم‌هاProcessing streams

پردازش جریانی روی داده‌ای کار می‌کند که دائماً می‌رسد و پایانِ کلی ندارد. به‌جای صبر برای تکمیلِ همهٔ داده، نتیجه‌ها مرحله‌به‌مرحله تغییر می‌کنند.

مثال فنی: یک رخداد، سه پردازش

وضعیت اولیه: سفارش تازه به مبلغ ۲۰۰ رسیده است.

اجرای مرحله‌به‌مرحله: یک پردازشگر الگوی تقلب را بررسی می‌کند، دیگری جمعِ دقیقه را تغییر می‌دهد و سومی نمای وضعیت سفارش را می‌سازد.

نتیجه و دلیل: همگی streaming هستند، اما state، شرط تکمیل و تحمل تأخیرشان یکسان نیست.

So far we saw where streams come from (user events, sensors, database writes) and how they are carried. What remains: what you can do with a stream. Three broad ways: 1) write the events into a database/cache/index for others to query (the streaming equivalent of batch uses — especially when the stream consumer is that database's sole writer); 2) deliver events to humans (alert emails, pushes, realtime dashboards); 3) process one or more input streams into one or more output streams — possibly as a multi-stage pipeline. The rest of the chapter is about that third way: the snippet turning a stream into a derived stream (operator / job), kin to Unix processes and Chapter-11 MapReduce jobs — with the same dataflow pattern (reads input read-only, writes output append-only elsewhere) and the same sharding and parallelization. Simple operations like transform and filter work the same way.

یک تفاوتِ سرنوشت‌ساز با بچA fateful difference from batch

استریم هیچ‌وقت تمام نمی‌شود. پس مرتب‌سازی روی dataset نامحدود بی‌معناست و joinهای sort-merge (فصل ۱۱) به کار نمی‌آیند؛ و مکانیزمِ تحملِ خرابی هم باید عوض شود — با job بچی که چند دقیقه کار کرده می‌شود task شکسته را از اول restart کرد، ولی با job استریمی که چند سال کار کرده، restart از اولِ تاریخ بعد از crash گزینهٔ واقعی نیست.

A stream never ends. So sorting an unbounded dataset is meaningless and sort-merge joins (Chapter 11) don't apply; and the fault-tolerance mechanism must change too — a batch job that ran minutes can restart its broken task from scratch, but a streaming job that ran years can't restart from the beginning of history after a crash as a realistic option.

۵.۱. سه کاربرد: CEP، تحلیل، نماهای مادی‌شده5.1. Three uses: CEP, analytics, materialized views

پردازش رویداد می‌تواند الگوی خاصی را تشخیص دهد، آمار بسازد یا نمای خواندنی را به‌روز کند. تفاوت در نوع خروجی و شرایطِ زمانی است.

مثال فنی: CEP با جمع‌زدن فرق دارد

وضعیت اولیه: برای یک حساب، سه ورود ناموفق و سپس یک ورود موفق در دو دقیقه رخ داده است.

اجرای مرحله‌به‌مرحله: CEP ترتیب و فاصلهٔ رخدادها را برای یک الگو می‌سنجد؛ تحلیل معمول می‌تواند فقط تعداد ورود ناموفق در دقیقه را جمع کند.

نتیجه و دلیل: یک مجموع ساده، ترتیبِ «سه شکست سپس موفقیت» را حفظ نمی‌کند؛ نوع سؤال state لازم را تعیین می‌کند.

  • پردازشِ رویدادِ پیچیده (complex event processing / CEP): رویکردِ دههٔ ۹۰ برای تحلیلِ استریم، مخصوصِ اپ‌هایی که دنبالِ الگوی خاصی از رویدادند — مثل regex که الگوی کاراکتر در رشته پیدا می‌کند، CEP با زبانِ declarative (شبیه SQL) یا GUI الگوی رویداد در استریم تعریف می‌کند و موتور با ماشینِ وضعیتِ داخلی match می‌کند و رویدادِ پیچیده با جزئیاتِ الگوی پیداشده منتشر می‌کند. نسبتِ کوئری و داده این‌جا برعکس پایگاه دادهِ عادی است: معمولاً داده دائم است و کوئری گذرا و فراموش می‌شود؛ در موتورِ CEP کوئری‌ها بلندمدت ذخیره‌اند و با هر رویدادِ تازه بررسی می‌شود الگویی match شده یا نه. نمونه‌ها: Esper، Apama، StreamBase — و پردازشگرهای توزیع‌شده مثل Flink و Spark هم SQL روی استریم دارند.
  • تحلیلِ استریم (stream analytics): مرزِ باریکی با CEP دارد ولی کمتر دنبالِ دنبالهٔ خاص و بیشتر دنبالِ تجمیع و متریکِ آماری روی حجمِ زیاد است: نرخِ یک نوع رویداد، میانگینِ غلتان، مقایسه با بازهٔ قبلی (تشخیصِ روند یا هشدارِ غیرعادی). این آمارها معمولاً روی بازهٔ زمانیِ ثابت (پنجره — بخشِ ۵.۳) حساب می‌شوند. گاهی از الگوریتم‌های احتمالاتی (Bloom، HyperLogLog برای کاردینالیتی، تخمینِ پرسنتیل) استفاده می‌شود که جوابِ تقریبی با حافظهٔ خیلی کمتر می‌دهند — ولی این فقط بهینه‌سازی است؛ هیچ چیزِ ذاتاً تقریبی در پردازشِ جریانی نیست. فریم‌ورک‌ها: Storm، Spark Streaming، Flink، Samza، Beam، Kafka Streams (و سرویس‌های میزبانی‌شده مثل Dataflow و Stream Analytics).
  • نگه‌داشتنِ نماهای مادی‌شده (materialized views): استریمِ تغییراتِ پایگاه داده، سیستم‌های مشتق‌شده (کش، ایندکس، انبارِ داده) را به‌روز نگه می‌دارد — و در event sourcing هم وضعیتِ اپ همان view مادی‌شده است. برخلافِ تحلیل، پنجرهٔ محدودِ زمانی کافی نیست: ساختنِ view شاید همهٔ رویدادها را بخواهد (پنجره‌ای که تا ابتدایِ زمان برمی‌گردد)؛ Kafka Streams و ksqlDB روی compaction کافکا همین کاربرد را می‌سازند. پایگاه داده‌ها view مادی‌شده دارند ولی معمولاً با بچِ دوره‌ای یا REFRESH MATERIALIZED VIEW تازه می‌کنند — دو ایراد برای استریم: ناکارایی (هر بار همهٔ داده بازپردازش می‌شود) و کهنگی (تغییرِ مبدأ تا به‌روزرسانیِ بعدی دیده نمی‌شود). راهِ عمومی، نگه‌داریِ تدریجیِ view (incremental view maintenance / IVM) است: کوئریِ SQL به عملگرهای محاسبهٔ تدریجی تبدیل می‌شود و فقط دادهٔ عوض‌شده بازحساب می‌شود — پس به‌روزرسانی خیلی پربسامدتر و تازه‌تر می‌شود (Materialize، RisingWave، ClickHouse، Feldera: رویدادهای تازه در حافظه بافر و دوره‌ای روی view دیسکی اعمال می‌شوند).
  • دو کاربردِ کوچک‌تر: جستجو روی استریم (مثل CEP ولی برای تک‌رویداد با معیارِ پیچیده — کوئری‌ها ذخیره‌اند و سندها با آن‌ها match می‌شوند؛ ویژگیِ percolator الاستیک‌سرچ) و معماریِ رویدادمحور در برابر RPC (فصل ۵): فریم‌ورک‌های actor بیشتر مکانیزمِ هم‌زمانی‌اند تا تکنیکِ مدیریتِ داده — ارتباطشان گذرا و یک‌به‌یک است (نه بادوام و چندمشترکی)، و چرخهٔ دلخواه دارند (نه خطِ بدونِ چرخهٔ مشتق‌شدگیِ خوش‌تعریف)؛ و خیلی‌هاشان با crash تحویل را تضمین نمی‌کنند مگر retry اضافه کنی.
  • Complex event processing (CEP): the 1990s approach to stream analysis, for apps hunting a specific event pattern — like a regex finding character patterns in a string, CEP defines event patterns in a stream with a declarative language (SQL-like) or GUI, and the engine matches with internal state machines, publishing a complex event detailing the found pattern. The query/data ratio here is the inverse of a normal database: usually data is permanent and queries transient and forgotten; in a CEP engine queries are long-lived and stored, and each fresh event is checked for a match. Examples: Esper, Apama, StreamBase — and distributed processors like Flink and Spark have SQL on streams too.
  • Stream analytics: a thin border with CEP, but less after specific sequences and more after aggregation and statistics over volumes: the rate of an event type, rolling averages, comparison with the previous window (trend detection or anomaly alerts). These stats are usually computed over a fixed time range (a window — §5.3). Probabilistic algorithms are sometimes used (Bloom, HyperLogLog for cardinality, percentile estimation) that give approximate answers with far less memory — but that is only optimization; there is nothing inherently approximate about stream processing. Frameworks: Storm, Spark Streaming, Flink, Samza, Beam, Kafka Streams (plus hosted services like Dataflow and Stream Analytics).
  • Maintaining materialized views: the database's change stream keeps derived systems (cache, index, warehouse) fresh — and in event sourcing the app state is itself the materialized view. Unlike analytics, a bounded time window doesn't suffice: building a view may need all events (a window reaching back to the beginning of time); Kafka Streams and ksqlDB build exactly this on Kafka compaction. Databases have materialized views but usually refresh them with periodic batch or REFRESH MATERIALIZED VIEW — two flaws for streams: inefficiency (all data reprocessed every time) and staleness (source changes unseen until the next refresh). The general fix is incremental view maintenance (IVM): the SQL query compiles to incremental operators and only changed data is recomputed — so updates get far more frequent and fresher (Materialize, RisingWave, ClickHouse, Feldera: fresh events buffer in memory and periodically apply onto the disk view).
  • Two smaller uses: search on a stream (like CEP but for single events against complex criteria — queries are stored and documents matched against them; Elasticsearch's percolator feature) and event-driven architecture vs RPC (Chapter 5): actor frameworks are more a concurrency mechanism than a data-management technique — their communication is transient and one-to-one (not durable and multi-subscriber), with arbitrary cycles (not the well-defined acyclic derivation pipeline); and many don't guarantee delivery on crash unless you add retries.

۵.۲. سه نوع join استریمی + نمودار ۶5.2. Three stream joins + Diagram 6

در اتصال جریانی، دادهٔ دو جریان یا یک جریان و یک جدول با هم ترکیب می‌شوند. چون ورودی پایان ندارد، باید زمان، مدت نگهداری و رفتارِ تغییرها مشخص باشد.

مثال فنی: سه join با سه state

وضعیت اولیه: کلیک و نمایش تبلیغ هر دو جریان‌اند؛ مشخصات کاربر یک جدول است.

اجرای مرحله‌به‌مرحله: stream-stream کلیک را به نمایش در بازهٔ زمانی وصل می‌کند؛ stream-table مشخصات کاربر را هنگام رخداد می‌خواند؛ table-table دو نمای به‌روز را هماهنگ می‌کند.

نتیجه و دلیل: هر join زمان نگهداری و معنای نسخهٔ طرف مقابل را می‌خواهد؛ join دو streamِ نامحدود بدون محدودیت state نامحدود می‌سازد.

Since stream processing generalizes the data pipeline to incremental processing of unbounded datasets, batch's join need exists here too — but because a fresh event may arrive at any moment, joining on a stream is harder. Distinguish three kinds:

  • join استریم–استریم (window join): مثلاً برای نرخِ کلیکِ هر URL در نتایجِ جستجو، باید رویدادِ «جستجو» و رویدادِ «کلیک» با session id یکسان به هم برسند. کلیک شاید هیچ‌وقت نیاید (رهاکردنِ جستجو) و فاصله‌اش از ثانیه تا روزها متغیر باشد؛ حتی به‌خاطرِ تأخیرِ شبکه ممکن است کلیک قبل از جستجو برسد. پس پنجره انتخاب می‌کنی (مثلاً کلیکِ حداکثر یک‌ساعت‌بعد به جستجو می‌چسبد). دقت: جاسازیِ جزئیاتِ جستجو در رویدادِ کلیک معادلِ join نیست — آن فقط کلیک‌شده‌ها را می‌گوید، ولی برای نرخِ دقیق به جستجوهای بی‌کلیک هم نیاز داری. پیاده‌سازی: پردازشگر state نگه می‌دارد (مثلاً همهٔ رویدادهای یک ساعتِ گذشته به تفکیکِ session)؛ با هر رویداد، به ایندکس اضافه‌اش می‌کند و ایندکسِ طرفِ دیگر را برای همان session چک می‌کند؛ match شد منتشر می‌کند، و اگر جستجو بدونِ کلیک منقضی شد، «کلیک‌نشده» منتشر می‌کند.
  • join استریم–جدول (stream enrichment): همان join شکل ۱۱-۲ (رویدادهای فعالیت + جدولِ پروفایل)، ولی پیوسته: ورودی استریمِ رویدادِ فعالیت با user id است و خروجی همان رویدادِ غنی‌شده با اطلاعاتِ پروفایل. lookup از راهِ دور کند و برای پایگاه داده خطرناک است (فصل ۱۱)؛ راهش کپیِ محلیِ پایگاه داده در پردازشگر است (hash join — اگر کوچک باشد جدولِ hash در حافظه، وگرنه ایندکسِ دیسکِ محلی). تفاوت با بچ: snapshotِ نقطه‌ایِ ثابت نیست، چون پایگاه داده در طولِ عمرِ پردازشگر عوض می‌شود — پس کپیِ محلی را باید با CDC به‌روز نگه داشت: پردازشگر هم مشترکِ changelog پروفایل‌هاست و هم استریمِ فعالیت؛ با هر ساخت/تغییرِ پروفایل، کپیِ محلی را عوض می‌کند. پس عملاً join دو استریم است (رویدادهای فعالیت + updateهای پروفایل) — با این تفاوت که سمتِ جدول پنجره‌ای تا «ابتدایِ زمان» دارد (نامتناهیِ مفهومی) و نسخهٔ تازه جای قدیمی را می‌گیرد، و سمتِ استریم شاید اصلاً پنجره نگه ندارد.
  • join جدول–جدول (materialized view maintenance): مثلاً کشِ تایم‌لاینِ شبکهٔ اجتماعی — به‌جای گران‌بودنِ خواندن (پیمودنِ همهٔ دنبال‌شونده‌ها و merge)، صندوقِ ورودیِ هر کاربر را مادی می‌کنی: با هر پستِ تازه، به تایم‌لاینِ همهٔ دنبال‌کننده‌ها اضافه؛ با حذفِ پست/اکانت، از همه حذف؛ با follow تازه، پست‌های اخیر اضافه؛ با unfollow، حذف. پردازشگر استریمِ پست‌ها و استریمِ followها را می‌گیرد و پایگاه دادهِ دنبال‌کننده‌ها را نگه می‌دارد تا بداند با هر پست کدام تایم‌لاین‌ها را به‌روز کند — یعنی view مادی‌شدهٔ join دو جدول (posts JOIN follows با group by دنبال‌کننده). نگاهِ قشنگ: اگر استریم را مشتقِ جدول بدانی (شکل ۱۲-۷) و join را ضربِ دو جدول، استریمِ تغییراتِ joinِ مادی‌شده از قاعدهٔ مشتقِ ضرب پیروی می‌کند: (u·v)′ = u′v + uv′ — هر تغییرِ پست‌ها با آخرینِ دنبال‌کننده‌ها join می‌شود و برعکس.
  • Stream–stream join (window join): e.g. for each URL's click rate in search results, the “search” event and the “click” event with the same session id must meet. The click may never come (abandoned search) and its distance varies from seconds to days; network delay may even deliver the click before the search. So you pick a window (e.g. clicks up to one hour later attach to the search). Note: embedding search details in the click event is not a join — that only tells you about clicked ones, while an exact rate also needs clickless searches. Implementation: the processor keeps state (e.g. all last-hour events by session); with each event it adds to the index and checks the other side's index for the same session; on match it emits, and if a search expires clickless it emits “unclicked”.
  • Stream–table join (stream enrichment): the same join as Fig. 11-2 (activity events + profile table), but continuous: input is the activity-event stream keyed by user id, output the same events enriched with profile info. Remote lookup is slow and dangerous for the database (Chapter 11); the fix is the database's local copy in the processor (hash join — in-memory hash table if small, local disk index otherwise). Difference from batch: no fixed point-in-time snapshot, since the database changes over the processor's lifetime — so the local copy must be kept fresh with CDC: the processor subscribes to both the profile changelog and the activity stream; on each profile create/change it mutates the local copy. So it is effectively a two-stream join (activity events + profile updates) — except the table side has a window to “the beginning of time” (conceptually infinite) with fresh versions replacing old ones, while the stream side may keep no window at all.
  • Table–table join (materialized view maintenance): e.g. a social network's timeline cache — instead of expensive reads (walking all followees and merging), you materialize each user's inbox: on each fresh post, append to all followers' timelines; on post/account deletion, remove from all; on a fresh follow, add recent posts; on unfollow, remove. The processor takes the post stream and the follow stream and keeps the follower database to know which timelines each post updates — i.e. the materialized view of a two-table join (posts JOIN follows grouped by follower). The neat view: if you see the stream as derived from the table (Fig. 12-7) and join as table multiplication, the materialized join's change stream obeys the product-derivative rule: (u·v)′ = u′v + uv′ — each post change joins the latest followers and vice versa.

هر سه join یک نقطهٔ مشترک دارند: پردازشگر از یک ورودی state می‌سازد و با رکوردهای ورودیِ دیگر آن را کوئری می‌کند — و ترتیبِ رویدادهای نگه‌دارندهٔ state مهم است (اول follow بعد unfollow، یا برعکس؟). در لاگِ shardشده مثل کافکا ترتیب فقط درونِ یک shard حفظ می‌شود، نه میانِ استریم‌ها و shardها — پس اگر رویدادهای دو استریم نزدیکِ هم رخ دهند، با چه ترتیبی پردازش می‌شوند؟ مثلاً update پروفایل با کدام رویدادهای فعالیت join می‌شود: آن‌هایی که قبلش پردازش شدند (پروفایلِ قدیمی) یا بعدش (جدید)؟ اگر ترتیبِ میانِ استریم‌ها نامعین باشد، join ناقطعی (nondeterministic) می‌شود: با اجرای دوباره روی همان ورودی لزوماً همان نتیجه نمی‌گیری. در انبارِ داده به این می‌گویند slowly changing dimension و راهش شناسهٔ یکتا برای هر نسخه است (مثلاً هر تغییرِ نرخِ مالیات شناسهٔ تازه می‌گیرد و فاکتور شناسهٔ نرخِ زمانِ فروش را دارد) — ولی بهایش نگه‌داشتنِ همهٔ نسخه‌ها و ناممکن‌شدنِ compaction است؛ راهِ دیگر denormalize کردن (گذاشتنِ نرخ در خودِ رویدادِ فروش).

All three joins share one point: the processor builds state from one input and queries it with the other's records — and the order of state-holding events matters (follow first then unfollow, or the reverse?). In a sharded log like Kafka, order survives only within a shard, not across streams and shards — so if two streams' events happen close together, in what order are they processed? E.g. which activity events does a profile update join with: those processed before it (old profile) or after (new)? If cross-stream order is undefined, the join becomes nondeterministic: rerunning on the same input needn't give the same result. Warehouses call this slowly changing dimension, fixed with a unique ID per version (e.g. each tax-rate change gets a fresh ID and the invoice carries the sale-time rate's ID) — but at the price of keeping all versions and making compaction impossible; the other way is denormalizing (putting the rate in the sale event itself).

نمودار ۶ — سه join استریمی: استریم–استریم، استریم–جدول، جدول–جدولDiagram 6 · Three stream joins: stream–stream, stream–table, table–table باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: سه join با سه state

وضعیت اولیه: کلیک و نمایش تبلیغ هر دو جریان‌اند؛ مشخصات کاربر یک جدول است.

اجرای مرحله‌به‌مرحله: stream-stream کلیک را به نمایش در بازهٔ زمانی وصل می‌کند؛ stream-table مشخصات کاربر را هنگام رخداد می‌خواند؛ table-table دو نمای به‌روز را هماهنگ می‌کند.

نتیجه و دلیل: هر join زمان نگهداری و معنای نسخهٔ طرف مقابل را می‌خواهد؛ join دو streamِ نامحدود بدون محدودیت state نامحدود می‌سازد.

سه پنلِ کنارِ هم، هر کدام با سربرگِ رنگی و ساختارِ «دو ورودی → عملگرِ stateدار → یک خروجی + نوارِ annotation». پنلِ اول Stream–Stream Join · Window: دو جعبهٔ ورودیِ search events و click events (هر دو با کلیدِ session id) با پیکان به جعبهٔ WindowJoinOperator می‌رسند که stateاش «بافرِ یک ساعتِ گذشته به تفکیکِ session» است و یادداشت می‌گوید ورودی‌های بافر با بسته‌شدنِ پنجره اخراج می‌شوند؛ خروجیِ join output دو حالت دارد: clicked (جستجو کلیک گرفت) و not clicked (پنجره تنها منقضی شد). نوارِ annotation می‌گوید کلیک ممکن است قبل از جستجو برسد، پنجره سقفِ انتظار را می‌بندد و state با بسته‌شدنِ پنجره دور ریخته می‌شود.

پنلِ دوم Stream–Table Join · Enrichment: ورودی‌ها activity events (کلید: user id) و profile changelog (استریمِ CDC)‌اند که به StreamTableJoin می‌رسند — عملگرِ «استریم ⋈ جدول با پنجرهٔ نامتناهی» که stateاش «کپیِ محلیِ پروفایل‌ها (hash join)» است و نسخهٔ تازه جای قدیمی را می‌گیرد؛ خروجی enriched activity events است. پنلِ سوم Table–Table Join · Materialized View: دو changelogِ پست‌ها و followها به عملگری می‌رسند که stateاش «لیستِ دنبال‌کننده‌ها + پست‌های اخیر» است و خروجی‌اش «تغییراتِ تایم‌لاین (view مادی‌شدهٔ posts ⋈ follows)» — با annotation قاعدهٔ مشتقِ ضرب: هر تغییر با آخرینِ وضعیتِ طرفِ دیگر join می‌شود. درس: هر سه join «state از یک ورودی + lookup با ورودیِ دیگر»‌اند؛ تفاوت فقط در کرانِ پنجره است (محدود / نامتناهی / نامتناهیِ دوطرفه).

Three side-by-side panels, each with a colored header and a “two inputs → stateful operator → one output + annotation strip” structure. Panel 1, Stream–Stream Join · Window: two input boxes, search events and click events (both keyed by session id), arrow into WindowJoinOperator, whose state is “last-hour buffer by session” with a note that buffered inputs are evicted when the window closes; the join output has two cases: clicked (the search got a click) and not clicked (the window expired alone). The annotation strip says a click may arrive before the search, the window caps the wait, and state is discarded when the window closes.

Panel 2, Stream–Table Join · Enrichment: inputs are activity events (key: user id) and the profile changelog (a CDC stream), reaching StreamTableJoin — a “stream ⋈ table with infinite window” operator whose state is “local profile copy (hash join)” with fresh versions replacing old ones; output is enriched activity events. Panel 3, Table–Table Join · Materialized View: the two changelogs of posts and follows reach an operator whose state is “follower list + recent posts” and whose output is “timeline changes (the materialized posts ⋈ follows view)” — annotated with the product-derivative rule: each change joins the other side's latest state. Lesson: all three joins are “state from one input + lookup with the other”; they differ only in window bounds (bounded / infinite / doubly infinite).

۵.۳. چهار نوع پنجره + نمودار ۷5.3. Four window types + Diagram 7

پنجره، دامنهٔ زمانی یا تعدادیِ محاسبه را مشخص می‌کند. پنجرهٔ ثابت، لغزان، متحرک و نشست، مرزهای متفاوتی دارند و یک رویداد ممکن است در چند نتیجه نقش داشته باشد.

مثال فنی: چهار پنجره با یک جریان

وضعیت اولیه: برای یک کلید، رخدادها در 10:01,10:04,10:12 رسیده‌اند و زمانِ خود رخداد ملاک است.

اجرای مرحله‌به‌مرحله: tumbling پنج‌دقیقه‌ای دو رخداد اول را در [10:00,10:05) می‌گذارد؛ hopping با اندازهٔ ۱۰ و گام ۵ آن‌ها را در دو پنجرهٔ هم‌پوشان می‌شمارد؛ sliding ده‌دقیقه‌ای در ۱۰:۱۲ فقط ۱۰:۰۴ و ۱۰:۱۲ را دارد.

نتیجه و دلیل: session با قانون «فاصلهٔ بیش از پنج دقیقه، نشست تازه» دو رخداد اول را یک نشست و رخداد سوم را نشست بعد می‌گیرد؛ تعریف مرز هر پنجره باید مشخص باشد.

Once the event timestamp is known, the next step is defining a window over a time range for aggregation (counts, averages). Four common kinds:

نمودار ۷ — چهار نوع پنجره روی محورِ زمانِ رویدادDiagram 7 · Four window types on the event-time axis باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: چهار پنجره با یک جریان

وضعیت اولیه: برای یک کلید، رخدادها در 10:01,10:04,10:12 رسیده‌اند و زمانِ خود رخداد ملاک است.

اجرای مرحله‌به‌مرحله: tumbling پنج‌دقیقه‌ای دو رخداد اول را در [10:00,10:05) می‌گذارد؛ hopping با اندازهٔ ۱۰ و گام ۵ آن‌ها را در دو پنجرهٔ هم‌پوشان می‌شمارد؛ sliding ده‌دقیقه‌ای در ۱۰:۱۲ فقط ۱۰:۰۴ و ۱۰:۱۲ را دارد.

نتیجه و دلیل: session با قانون «فاصلهٔ بیش از پنج دقیقه، نشست تازه» دو رخداد اول را یک نشست و رخداد سوم را نشست بعد می‌گیرد؛ تعریف مرز هر پنجره باید مشخص باشد.

چهار باندِ روی هم با یک مقیاسِ مشترکِ ۰ تا ۱۰ دقیقه و راهنمای «نقطه = رویداد، مستطیل = پنجره». بخشِ اول Tumbling (اندازهٔ ثابت، بدونِ هم‌پوشانی): مستطیل‌های پشتِ سرِ همِ [0,1)، [1,2) تا [9,10) — یادداشت می‌گوید هر رویداد دقیقاً به یک پنجره می‌رسد (پیاده‌سازی: round-down کردنِ timestamp به دقیقه). بخشِ دوم Hopping (اندازهٔ ثابت، hop ثابت): مستطیل‌های ۵دقیقه‌ایِ روی‌هم‌افتادهٔ [0,5)، [1,6) تا [5,10) که یک دقیقه یک دقیقه جلو می‌روند — هموارسازی با هم‌پوشانی (اول پنجره‌های tumbling یک‌دقیقه‌ای، بعد تجمیعِ چند همسایه).

بخشِ سوم Sliding: دو رویداد با فاصلهٔ ۴.۵ دقیقه در یک پنجرهٔ لغزان‌اند درحالی‌که هیچ پنجرهٔ ثابتِ ۵دقیقه‌ای آن‌ها را یک‌جا نمی‌گذاشت (یادداشتِ «پنجره رویدادها را دنبال می‌کند») — پیاده‌سازی: بافرِ مرتبِ رویدادها و اخراجِ کهنه‌ها با انقضا. باندِ چهارم Session: خوشه‌های رویدادِ یک کاربر با مستطیل‌های پهنای‌متغیر، و شکافِ «۳۰ دقیقه بی‌فعالیتی ← پایانِ session» که پنجره را می‌بندد — بدونِ مدتِ ثابت. درس: tumbling/hopping/session مرزِ ثابت دارند و ارزان‌اند؛ sliding state شناور می‌خواهد و با پنجرهٔ بزرگ یا throughput بالا، state موقتِ زیادی روی ماشین‌ها می‌خواهد (حافظه یا دیسک).

Four stacked bands sharing a 0–10 minute scale, with the legend “dot = event, rectangle = window”. Band 1, Tumbling (fixed size, no overlap): back-to-back rectangles [0,1), [1,2) through [9,10) — the note says each event lands in exactly one window (implementation: rounding the timestamp down to the minute). Band 2, Hopping (fixed size, fixed hop): overlapping 5-minute rectangles [0,5), [1,6) through [5,10) advancing one minute at a time — smoothing via overlap (first one-minute tumbling windows, then aggregating several neighbors).

Band 3, Sliding: two events 4.5 minutes apart share a sliding window although no fixed 5-minute window would hold them together (“the window follows the events”) — implementation: a sorted event buffer with expiry evicting the stale. Band 4, Session: one user's event clusters in variable-width rectangles, with the “30 minutes idle → session ends” gap closing the window — no fixed duration. Lesson: tumbling/hopping/session have fixed boundaries and are cheap; sliding needs floating state, and with big windows or high throughput it needs lots of temporary machine state (memory or disk).

  • غلتان (tumbling): طولِ ثابت، هر رویداد دقیقاً در یک پنجره (مثلاً همهٔ رویدادهای دقیقهٔ ۱۰:۰۳ در یک پنجره).
  • پرشی (hopping): طولِ ثابت ولی پنجره‌های متوالی هم‌پوشانی دارند (مثلاً پنجرهٔ ۵دقیقه‌ای با hop یک‌دقیقه‌ای: ۱۰:۰۳ تا ۱۰:۰۷، بعد ۱۰:۰۴ تا ۱۰:۰۸).
  • لغزان (sliding): همهٔ رویدادهایی که در فاصلهٔ معینی از هم‌اند (مثلاً دو رویداد با فاصلهٔ ۴.۵ دقیقه در پنجرهٔ ۵دقیقه‌ای‌اند، هرچند هیچ مرزِ ثابتی آن‌ها را یک‌جا نکند).
  • نشست (session): بدونِ مدتِ ثابت — همهٔ رویدادهای نزدیکِ همِ یک کاربر، و پایان وقتی که کاربر مدتی (مثلاً ۳۰ دقیقه) غیرفعال بوده؛ رایج در تحلیلِ وب‌سایت.
  • Tumbling: fixed length, each event in exactly one window (e.g. all 10:03-minute events in one window).
  • Hopping: fixed length but consecutive windows overlap (e.g. a 5-minute window with a one-minute hop: 10:03–10:07, then 10:04–10:08).
  • Sliding: all events within a given distance of each other (e.g. two events 4.5 minutes apart share a 5-minute window, though no fixed boundary would hold them together).
  • Session: no fixed duration — all nearby events of one user, ending when the user has been idle a while (e.g. 30 minutes); common in website analytics.

عملیاتِ پنجره معمولاً state موقت نگه می‌دارد: گاهی با اندازهٔ ثابت (شمارنده فقط یک عدد است هرقدر هم پنجره بزرگ شود)، گاهی بافرِ رویداد تا پایانِ پنجره (پنجرهٔ لغزان یا join استریمی) — پس باید مطمئن شوی ماشین‌ها گنجایشِ state را دارند.

Window operations usually keep temporary state: sometimes fixed-size (a counter is just one number however big the window grows), sometimes an event buffer until the window ends (sliding windows or stream joins) — so you must make sure the machines have room for the state.

۵.۴. زمانِ رویداد در برابر زمانِ پردازش + نمودار ۸5.4. Event time vs processing time + Diagram 8

زمان رویداد می‌گوید اتفاق چه موقع رخ داده؛ زمان پردازش می‌گوید چه موقع به پردازشگر رسیده است. قطع اینترنت و صف باعث می‌شوند این دو متفاوت باشند.

مثال فنی: رخداد دیر رسیده

وضعیت اولیه: خرید در 10:02 رخ داده، اما به‌علت قطع دستگاه در 10:12 به پردازشگر رسیده است.

اجرای مرحله‌به‌مرحله: گزارشِ event-time آن را در بازهٔ 10:00–10:05 می‌گذارد؛ processing-time آن را در بازهٔ دریافت قرار می‌دهد.

نتیجه و دلیل: برای گزارش رخداد واقعی، lateness و اصلاح نتیجه لازم است؛ دو ساعت، دو سؤال متفاوت را جواب می‌دهند.

Stream processors deal with time a lot (e.g. “the average of the last five minutes”), and what “the last five minutes” means is surprisingly subtle. In batch, tasks chew through history fast, and when time separation is needed they look at each event's embedded timestamp — looking at the executing machine's clock is meaningless, since run time has nothing to do with occurrence time (a minutes-long batch may read a year of history; the timeline that matters is that year, not those minutes). And that use of the event timestamp makes processing deterministic: rerunning on the same input gives the same result.

ولی خیلی از فریم‌ورک‌های جریانی برای پنجره‌بندی از ساعتِ محلیِ ماشینِ پردازشگر استفاده می‌کنند (زمانِ پردازش — processing time): ساده است و اگر فاصلهٔ تولید تا پردازش ناچیز باشد معقول؛ ولی با هر lag محسوس می‌شکند. پردازش به هزار دلیل عقب می‌افتد: صف، خرابیِ شبکه، contention، restart مصرف‌کننده، یا بازپردازشِ گذشته بعد از رفعِ باگ. تأخیر حتی ترتیب را هم غیرقابل‌پیش‌بینی می‌کند: کاربر اول به سرورِ A و بعد به سرورِ B درخواست می‌زند، ولی رویدادِ B زودتر به بروکر می‌رسد — پردازشگر اول B را می‌بیند بعد A را، خلافِ ترتیبِ رخداد. تشبیهش فیلم‌های جنگِ ستارگان است: قسمتِ IV در ۱۹۷۷ و قسمتِ I در ۱۹۹۹ آمد؛ اگر به ترتیبِ اکران ببینی (processing time)، با ترتیبِ روایت (شمارهٔ قسمت = event timestamp) ناسازگاری — انسان کنار می‌آید ولی الگوریتمِ استریم باید صریح برایش نوشته شود.

But many streaming frameworks window by the processing machine's local clock (processing time): simple, and reasonable when the produce-to-process gap is negligible — but it breaks with any significant lag. Processing lags for a thousand reasons: queues, network failures, contention, consumer restarts, or reprocessing the past after a bugfix. Delay even makes order unpredictable: the user requests server A first then server B, but B's event reaches the broker sooner — the processor sees B first, then A, against occurrence order. The analogy is the Star Wars films: episode IV came in 1977, episode I in 1999; watching in release order (processing time) clashes with story order (episode number = event timestamp) — humans cope, but a streaming algorithm must be explicitly written for it.

نمودار ۸ — آرتیفکتِ پنجره‌بندی با زمانِ پردازش (شکل ۱۲-۸ کتاب)Diagram 8 · Processing-time windowing artifact (Fig. 12-8) باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: رخداد دیر رسیده

وضعیت اولیه: خرید در 10:02 رخ داده، اما به‌علت قطع دستگاه در 10:12 به پردازشگر رسیده است.

اجرای مرحله‌به‌مرحله: گزارشِ event-time آن را در بازهٔ 10:00–10:05 می‌گذارد؛ processing-time آن را در بازهٔ دریافت قرار می‌دهد.

نتیجه و دلیل: برای گزارش رخداد واقعی، lateness و اصلاح نتیجه لازم است؛ دو ساعت، دو سؤال متفاوت را جواب می‌دهند.

دو پنلِ روی هم. پنلِ بالا «زمانِ رویداد — خطِ زمانیِ واقعی»: نقطه‌های یکدستِ رویداد روی محورِ دقیقه‌ها، و وسطش نوارِ سایه‌خوردهٔ deploy / restart که پردازشگر حدود یک دقیقه پایین است — یادداشت‌ها می‌گویند هر رویداد timestamp خودش را نگه می‌دارد و بعد backlog یک‌جا flush می‌شود. پنلِ پایین «زمانِ پردازش — گروه‌بندی با ساعتِ پردازشگر»: نمودارِ میله‌ایِ پنجره‌های یک‌دقیقه‌ای — میله‌های شبح‌وارِ شمارشِ event-time یکدست‌اند، پنجرهٔ زمانِ قطعی خالی است (down)، و همهٔ backlog در یک پنجره می‌افتد و میلهٔ قرمزِ spurious spike را می‌سازد با پیکانِ «flush backlog به یک پنجره» — یعنی نرخِ ثابتِ واقعی، جهشِ مصنوعیِ ناگهانی به نظر می‌رسد. جعبهٔ کناری حکم می‌کند: از timestamp رویداد + watermark استفاده کن، نه ساعتِ پردازشگر — و همان قاعده برای بچ هم هست: خروجیِ بچ باید تابعِ دادهٔ ورودی باشد نه زمانِ اجراشدنِ job. درس: قاطی‌کردنِ این دو زمان، دادهٔ بد تولید می‌کند.

Two stacked panels. Top panel, “event time — the real timeline”: uniform event dots on the minute axis, with a shaded deploy / restart strip in the middle where the processor is down about a minute — the notes say each event keeps its own timestamp and the backlog later flushes at once.

Bottom panel, “processing time — grouping by the processor clock”: a bar chart of one-minute windows — ghost bars of the event-time counts are uniform, the outage-time window is empty (down), and the whole backlog lands in one window, building the red spurious-spike bar with the “flush backlog into one window” arrow — i.e. a truly flat rate looks like a sudden artificial jump. The side box rules: use event timestamps + watermarks, not the processor clock — and the same rule holds for batch: a batch's output must be a function of its input data, not of when the job ran. Lesson: mixing the two times produces bad data.

مسئلهٔ ساعتِ چه کسی عمیق‌تر هم می‌شود: اپِ موبایلِ آفلاین رویدادها را روی دستگاه بافر می‌کند و ساعت‌ها بعد می‌فرستد (stragglerهای افراطی)؛ timestamp باید زمانِ تعامل با ساعتِ دستگاه باشد، ولی ساعتِ دستگاهِ دستِ کاربر قابلِ اعتماد نیست (عمداً یا سهواً غلط). زمانِ رسیدن به سرور (ساعتِ دستِ خودت) دقیق‌تر ولی کم‌معناتر است. یک راه، ثبتِ سه timestamp است: زمانِ رخداد (ساعتِ دستگاه)، زمانِ ارسال (ساعتِ دستگاه)، زمانِ دریافت (ساعتِ سرور — با تفریقِ دومی از سومی انحرافِ ساعتِ دستگاه تخمین زده و روی timestamp اعمال می‌شود). این مسئله فقط مالِ استریم نیست؛ در بچ هم همان است، فقط در استریم چون گذرِ زمان را بیشتر حس می‌کنی به چشم می‌آید.

The “whose clock” problem runs deeper: an offline mobile app buffers events on-device and sends them hours later (extreme stragglers); the timestamp should be the device-clock interaction time, but the user-controlled device clock is untrustworthy (wrong deliberately or accidentally). The server's arrival time (your own clock) is more precise but less meaningful. One way out is recording three timestamps: occurrence time (device clock), send time (device clock), receive time (server clock — subtracting the second from the third estimates the device skew and corrects the timestamp). This problem isn't stream-only; batch has it too, only in streams you feel time passing more and notice it.

۵.۵. چرخهٔ حیاتِ پنجره + نمودار ۹5.5. Window lifecycle + Diagram 9

بسته شدنِ پنجره یعنی طبق سیاستِ مشخص، نتیجه را نهایی یا قابل‌اصلاح بدانیم. Watermark تخمینی از پیشرفتِ زمان رویداد است؛ تضمین نمی‌کند هیچ پیامِ دیررسی نخواهد آمد.

مثال فنی: Watermark حکمِ قطعیِ نرسیدن نیست

وضعیت اولیه: پنجره تا 10:05 است و سیاست، اصلاح تا دو دقیقه پس از پایان پنجره را می‌پذیرد.

اجرای مرحله‌به‌مرحله: watermark از ۱۰:۰۵ عبور می‌کند و خروجی اولیه منتشر می‌شود؛ تا مرز مجاز، پیام دیررس می‌تواند نتیجه را اصلاح کند.

نتیجه و دلیل: watermark برآورد پیشرفت event-time است؛ سیاستِ دادهٔ دیرتر و پاک‌سازی state باید جدا تعریف شود.

The subtle problem of defining windows with event time: you're never sure whether all of a window's events arrived or more are on the way. E.g. you count events in one-minute windows; minute-37's events are counted and now most inputs are minutes 38 and 39 — when do you declare minute 37's window done and emit its counter? You can time out (no fresh event for that window for a while, so declare it ready), but some events may be buffered on another machine (network partition). You must cope with those late stragglers arriving after the announcement; two broad ways: ignore them (normally a small fraction — metric the dropped count and alert if it grows) or publish a correction (the window's updated value including the laggards, possibly retracting the previous output). Sometimes a special message says “from now on no message with timestamp before t will come” and the consumer triggers the window with it — but with producers on different machines each producer's threshold must be tracked separately, and adding/removing producers gets harder.

نمودار ۹ — چرخهٔ حیاتِ پنجرهٔ event-time: watermark، دیررس‌ها، اصلاحیهDiagram 9 · Event-time window lifecycle: watermarks, stragglers, corrections باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: Watermark حکمِ قطعیِ نرسیدن نیست

وضعیت اولیه: پنجره تا 10:05 است و سیاست، اصلاح تا دو دقیقه پس از پایان پنجره را می‌پذیرد.

اجرای مرحله‌به‌مرحله: watermark از ۱۰:۰۵ عبور می‌کند و خروجی اولیه منتشر می‌شود؛ تا مرز مجاز، پیام دیررس می‌تواند نتیجه را اصلاح کند.

نتیجه و دلیل: watermark برآورد پیشرفت event-time است؛ سیاستِ دادهٔ دیرتر و پاک‌سازی state باید جدا تعریف شود.

ماشینِ وضعیت با سه ردیف: Event-time window (مسیرِ اصلی)، Restart and replay و Terminal outcomes. مسیرِ اصلی چهار state پشتِ سرِ هم است: Window Open (بازهٔ ثابتِ [t0, t1)) با فلشِ events arrive به Accumulating (رویدادها با timestamp می‌نشینند، شمارنده رشد می‌کند)؛ بعد فلشِ watermark passes t1 به state لوزیِ Watermark Passed (پیامی دیگری قبل از t1 نمی‌آید، آستانه جلو رفته)؛ و بعد close window به Result Emitted (پنجره بسته شد، مقدار بیرون رفت).

از Result Emitted دو فلش به ردیفِ پایانی می‌رود: straggler: ignore به Straggler Dropped (رویدادِ دیر نادیده، با متریکِ شمارشِ انداخته‌شده + هشدار) و straggler: correct به Correction Published (مقدارِ به‌روزشده با پس‌گرفتنِ نتیجهٔ قبلی). و حلقهٔ restart در ردیفِ میانی: فلشِ خط‌چینِ job restarts از Accumulating به Job Restarted (لاگِ ورودی بازپخش می‌شود، قطعی بر اساسِ event time) و برگشتِ درشتِ replay by event time — یعنی بازسازیِ پنجره دقیقاً همان قبلی می‌شود. سه کارت هم چرایی‌ها را می‌گوید: چرا پنجره‌ها از event time استفاده می‌کنند (قطعی‌بودن)، کی پنجره تمام‌شده حساب می‌شود (timeout تقریبی در برابرِ watermark)، و دو راهِ برخورد با دیررس‌ها. درس: با event time هیچ‌وقت از «تمام‌شدن» مطمئن نیستی — پس یا بی‌خیالِ دیررس شو (با متریک) یا اصلاحیه بده (با retraction).

A three-row state machine: the main Event-time window path, Restart and replay, and Terminal outcomes. The main path is four consecutive states: Window Open (fixed range [t0, t1)) via events arrive to Accumulating (timestamped events land, the counter grows); then watermark passes t1 to the diamond Watermark Passed (no more pre-t1 messages coming, the threshold advanced); then close window to Result Emitted (window closed, value out).

From Result Emitted two arrows go to the bottom row: straggler: ignore to Straggler Dropped (late event ignored, with dropped-count metric + alert) and straggler: correct to Correction Published (updated value retracting the previous result). And the restart loop in the middle row: dashed job restarts from Accumulating to Job Restarted (input log replayed, deterministic on event time) and the bold return replay by event time — rebuilding the window yields exactly the same one. The three cards explain the whys: why windows use event time (determinism), when a window counts as done (approximate timeout vs watermark), and the two ways of handling laggards. Lesson: with event time you're never sure of “done” — so either drop the late ones (with metrics) or correct (with retraction).

۶6 تحملِ خرابی و exactly-onceFault tolerance and exactly-once

پس از خرابی، پردازشگر باید وضعیت و محل خواندن را به شکل سازگار بازیابی کند. ادعای exactly-once فقط در محدودهٔ مشخصِ ورودی، وضعیت و خروجی معنا دارد.

مثال فنی: شکافِ اثر و offset

وضعیت اولیه: جمع فعلی ۱۰۰۰ است و پیام ۱۰۱ مبلغ ۲۰۰ دارد.

اجرای مرحله‌به‌مرحله: worker جمع را ۱۲۰۰ می‌کند، ولی قبل از ذخیرهٔ پیشرفت crash می‌کند؛ replayِ بی‌حفاظ آن را ۱۴۰۰ می‌کند.

نتیجه و دلیل: «دقیقاً یک اثر» به اتمی بودن state و پیشرفت، یا dedup معتبر نیاز دارد؛ فقط ack کردن پیام کافی نیست.

In Chapter 11 we saw batch tolerate failure simply: the broken task restarts from scratch on another machine and its partial output is discarded; since input is immutable and each task's output goes to a separate file that appears only on successful completion. The result looks as if no failure happened — as if each input record was processed exactly once, although restarts mean some records were processed several times with only one visible effect. This is called exactly-once semantics (effectively-once is the more precise description). Streams have the same problem but no simpler: waiting for a task's “end” to reveal output is impossible, since the stream is infinite and never ends.

۶.۱. microbatch و checkpoint6.1. Microbatching and checkpoints

Microbatch ورودی را به دسته‌های کوچک تقسیم می‌کند. Checkpoint یک نقطهٔ بازیابیِ وضعیت و پیشرفت است؛ پس از خرابی، پردازش از نقطهٔ سازگار ادامه پیدا می‌کند.

مثال فنی: Checkpoint سازگار

وضعیت اولیه: در checkpoint شمارهٔ ۷، جمع ۱۲۰۰ و موقعیت ورودی تا پیام ۱۰۱ با هم ثبت شده‌اند.

اجرای مرحله‌به‌مرحله: پس از crash، state و ورودی از همان مرز بازیابی می‌شوند؛ خروجی بیرونی هم باید با قرارداد checkpoint سازگار باشد.

نتیجه و دلیل: بازیابی state تا ۱۰۱ همراه offsetِ ۱۰۰ همان مرز نیست و می‌تواند دوباره‌شماری ایجاد کند.

One way is cutting the stream into small blocks and treating each block like a miniature batch: microbatching (in Spark Streaming; batch size usually about a second — an efficiency tradeoff: smaller batches mean more scheduling and coordination overhead, bigger batches mean more latency before results show). Microbatching also builds a tumbling window the size of the batch (in processing time, not event timestamps); jobs wanting bigger windows must explicitly carry state from one microbatch to the next. The different approach in Flink: periodically producing a rolling checkpoint of state and writing it to durable storage; if an operator crashes it restarts from the freshest checkpoint and discards output between the last checkpoint and the crash. Checkpoints are triggered by barriers in the message stream — like microbatch boundaries but without imposing a particular window size. Within a stream processor's boundaries, both approaches give batch's exactly-once.

درونِ مرزهای فریم‌ورکِ پردازشِ استریم، هر دو رویکرد همان exactly-once بچ را می‌دهند. ولی به‌محض این‌که خروجی از پردازشگر بیرون می‌رود (write در پایگاه داده، انتشار در بروکرِ بیرونی، فرستادنِ ایمیل)، فریم‌ورک دیگر نمی‌تواند خروجیِ microbatch شکسته را دور بریزد — restart باعث می‌شود اثرِ بیرونی دوبار رخ دهد، و microbatch/checkpoint به‌تنهایی جلویش را نمی‌گیرد.

But the moment output leaves the processor (a database write, a publish to an external broker, sending an email), the framework can no longer discard the broken microbatch's output — a restart makes the external effect happen twice, and microbatch/checkpoint alone can't stop it.

۶.۲. کامیتِ اتمی و idempotence6.2. Atomic commit and idempotence

برای اثرِ یک‌باره، ثبت خروجی و پیشرفت باید هماهنگ باشد یا نوشتنِ تکراری اثرِ اضافه نگذارد. تضمینِ داخلِ بروکر خودبه‌خود به هر سیستمِ بیرونی منتقل نمی‌شود.

مثال فنی: یک تراکنش برای اثر و شناسه

وضعیت اولیه: در مقصد SQL، event_id=e7 unique است و جمع فروش باید ۲۰۰ زیاد شود.

اجرای مرحله‌به‌مرحله: داخل یک تراکنش، شناسه ثبت و جمع زیاد می‌شود؛ اگر شناسه قبلاً هست، افزایش دوباره انجام نمی‌شود.

نتیجه و دلیل: commitِ offset جدا می‌تواند عقب بماند و پیام تکرار شود، اما این مقصد اثر را دوباره اعمال نمی‌کند؛ حفظ شناسه به افق replay وابسته است.

For exactly-once appearance under failure, you must ensure all outputs and side effects of processing one event persist if and only if processing succeeded: messages to downstream operators or external systems (even emails and pushes), database writes, operator state changes, and input-message acknowledgment (including advancing the offset in a log-based broker) — all must happen atomically: all or none. Sound familiar? It is Chapter 8's “exactly-once message processing” with distributed transactions and 2PC. But in a narrower environment such an atomic commit can be implemented efficiently (Google's Dataflow, VoltDB, Kafka): unlike XA, which transacts across heterogeneous technologies, these keep the transaction internal — managing both state changes and messaging inside the framework — and the protocol overhead is amortized by processing several input messages in one transaction.

راهِ دیگرِ دورریختنِ خروجیِ ناقصِ task شکسته، تکیه بر idempotence است (فصل ۵): عملیاتی که چند بار اجرایش همان اثرِ یک‌بار را دارد — مثل حذفِ کلید در key-value store (حذفِ دوباره اثری ندارد) در برابرِ افزایشِ شمارنده (اجرای دوباره یعنی دو بار افزایش). حتی عملیاتِ غیر-idempotent را معمولاً با کمی metadata می‌شود idempotent کرد: موقعِ مصرف از کافکا هر پیام offset ماندگارِ صعودی دارد؛ موقعِ نوشتن در پایگاه دادهِ بیرونی می‌توانی offset پیامی که آخرین write را trigger کرد همراهِ مقدار بنویسی — پس می‌فهمی update قبلاً اعمال شده و دوباره انجامش نمی‌دهی (مدیریتِ state در Trident استورم هم همین ایده است). تکیه بر idempotence چند فرض می‌خواهد: restart باید همان پیام‌ها را به همان ترتیب بازپخش کند (بروکرِ لاگ‌محور همین کار را می‌کند)، پردازش قطعی باشد، و نودِ دیگری هم‌زمان همان مقدار را update نکند — موقعِ failover از نودی به نودِ دیگر شاید fencing لازم شود (فصل ۹) تا نودی که مرده فرض شده ولی زنده است دخالت نکند. با همهٔ این قیدها، عملیاتِ idempotent با سربارِ کم به exactly-once می‌رسد.

The other way to discard a broken task's partial output is relying on idempotence (Chapter 5): an operation whose repeated execution has the same effect as running once — like deleting a key in a key-value store (deleting again has no effect) versus incrementing a counter (rerunning means incrementing twice). Even non-idempotent operations can usually be made idempotent with a little metadata: when consuming from Kafka each message has a durable ascending offset; when writing to an external database you can store, alongside the value, the offset of the message that triggered the last write — so you know the update was already applied and skip redoing it (Storm's Trident state management is the same idea). Relying on idempotence needs assumptions: restarts must replay the same messages in the same order (a log-based broker does exactly that), processing must be deterministic, and no other node may concurrently update the same value — on failover from one node to another, fencing may be needed (Chapter 9) so the presumed-dead-but-alive node doesn't interfere. With all these constraints, idempotent operations reach exactly-once at low overhead.

و state؟ هر پردازشِ stateدار (تجمیعِ پنجره‌ای، جدول‌ها و ایندکس‌های join) باید بعد از خرابی state را بازیابی کند: یا state را در datastore راهِ دور نگه دار و replicate کن (ولی کوئریِ راهِ دور به‌ازای هر پیام کند است)، یا state را محلی نگه دار و دوره‌ای replicate کن تا task تازه بعد از خرابی بخواند و بی‌اتلاف ادامه دهد (Flink دوره‌ای snapshot می‌گیرد؛ Kafka Streams تغییرهای state را در تاپیکِ جداگانهٔ کافکا با compaction می‌فرستد، شبیه CDC؛ VoltDB با پردازشِ افزونهٔ هر ورودی روی چند نود). گاهی اصلاً replicate لازم نیست چون state از استریم‌های ورودی بازسازی می‌شود (تجمیعِ پنجرهٔ کوتاه را سریع بازپخش می‌کنی؛ replica محلیِ پایگاه داده را از change stream فشرده می‌سازی) — همه‌چیز به کاراییِ زیرساخت بستگی دارد و هیچ راهی برای همهٔ وضعیت‌ها ایده‌آل نیست.

And state? Every stateful process (windowed aggregation, join tables and indexes) must recover state after failure: either keep state in a remote datastore and replicate it (but a remote query per message is slow), or keep state local and periodically replicate so a fresh task reads it after failure and continues loss-free (Flink snapshots periodically; Kafka Streams sends state changes to a separate compacted Kafka topic, CDC-like; VoltDB by redundantly processing each input on several nodes). Sometimes no replication is needed at all because state rebuilds from the input streams (replay a short-window aggregation fast; build a database's local replica from the compacted change stream) — everything depends on infrastructure efficiency, and no way is ideal for all situations.

نمودار مفهومی — exactly-once: درونِ فریم‌ورک و بیرونِ آنConcept diagram — exactly-once: inside and outside the framework باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: یک تراکنش برای اثر و شناسه

وضعیت اولیه: در مقصد SQL، event_id=e7 unique است و جمع فروش باید ۲۰۰ زیاد شود.

اجرای مرحله‌به‌مرحله: داخل یک تراکنش، شناسه ثبت و جمع زیاد می‌شود؛ اگر شناسه قبلاً هست، افزایش دوباره انجام نمی‌شود.

نتیجه و دلیل: commitِ offset جدا می‌تواند عقب بماند و پیام تکرار شود، اما این مقصد اثر را دوباره اعمال نمی‌کند؛ حفظ شناسه به افق replay وابسته است.

دو ردیف را جدا بخوان. ردیفِ بالا «درونِ فریم‌ورک» است — همان ترفندِ بچ: MICROBATCH (اسپارک — بریدنِ استریم به بلوک‌های حدوداً یک‌ثانیه‌ای که هر کدام مثلِ بچِ مینیاتوری اجرا می‌شوند و بلوکِ شکسته restart می‌شود) و CHECKPOINT (فلینک — snapshot غلتانِ state با barrier و restart از تازه‌ترین). ردیفِ پایین «اثرهای بیرونی» است که فریم‌ورک نمی‌تواند پس بگیرد: ATOMIC COMMIT (state + پیام + offset با هم و همه‌یاهیچ‌کدام، درونِ یک فریم‌ورک) و IDEMPOTENT SINK (نگه‌داشتنِ offset کافکا همراهِ write تا تکرار بی‌اثر شود — با فرضِ بازپخشِ هم‌ترتیب و fencing).

نوارِ پایینی حکم می‌دهد: exactly-once = effectively-once — اثرِ مرئی انگار یک‌بار، حتی اگر بازتلاش شده.

Read the two rows separately. The top row is “inside the framework” — batch's own trick: MICROBATCH (Spark — cutting the stream into ~1-second blocks, each running like a mini-batch, with broken blocks restarted) and CHECKPOINT (Flink — rolling state snapshots via barriers, restarting from the latest). The bottom row is “outside effects” the framework cannot unsend: ATOMIC COMMIT (state + messages + offset together, all-or-nothing, inside one framework) and IDEMPOTENT SINK (storing the Kafka offset with the write so repeats go harmless — assuming same-order replay and fencing).

The bottom strip rules: exactly-once = effectively-once — the visible effect as if once, even when retried.

۶.۳. درختِ تصمیم + نمودار ۱۰6.3. Decision tree + Diagram 10

انتخاب بروکر و تضمینِ پردازش را از نیاز شروع کنیم: ترتیب، بازپخش، موازی‌سازی و پیامدِ تکرار. برچسبِ یک ابزار جای بررسیِ کل مسیر را نمی‌گیرد.

مثال فنی: مرز تضمین را تا مقصد دنبال کن

وضعیت اولیه: پردازشگر از Kafka می‌خواند و روی topic دیگری می‌نویسد، یا به API پیامک زنگ می‌زند.

اجرای مرحله‌به‌مرحله: تراکنش Kafka می‌تواند outputِ Kafka و offset را در دامنهٔ خودش هماهنگ کند؛ API پیامک بیرون آن نیازمند کلیدِ تکرارپذیری و قرارداد خودش است.

نتیجه و دلیل: exactly-once را برای مسیر مشخص می‌گوییم؛ تضمین داخل broker را بی‌دلیل به هر اثر بیرونی تعمیم نمی‌دهیم.

Now we see all the chapter's choices in one tree: first which broker, then which delivery meaning. Note the tree's main path (start → q1 → q3 → out_log → q4 → out_idem) is today's common recommendation: if you want replay and throughput is high, a log-based broker; and since external effects are usually outside the framework, you effectively reach “practically exactly once” with idempotent writes.

نمودار ۱۰ — درختِ تصمیم: کدام بروکر و کدام معنای تحویل؟Diagram 10 · Decision tree: which broker, which delivery guarantee باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: مرز تضمین را تا مقصد دنبال کن

وضعیت اولیه: پردازشگر از Kafka می‌خواند و روی topic دیگری می‌نویسد، یا به API پیامک زنگ می‌زند.

اجرای مرحله‌به‌مرحله: تراکنش Kafka می‌تواند outputِ Kafka و offset را در دامنهٔ خودش هماهنگ کند؛ API پیامک بیرون آن نیازمند کلیدِ تکرارپذیری و قرارداد خودش است.

نتیجه و دلیل: exactly-once را برای مسیر مشخص می‌گوییم؛ تضمین داخل broker را بی‌دلیل به هر اثر بیرونی تعمیم نمی‌دهیم.

درخت در پنج ردیف است: Broker Decision، AMQP / JMS Queue، Log-Based Broker، Delivery Semantics و Processing Guarantee — و چون بلند است، صفحه برایش ارتفاعِ بیشتر گذاشته. از Pick a Messaging Design شروع کن: سؤالِ اول Need to Replay Old Messages? (بازپردازش، حسابرسی، مصرف‌کنندهٔ تازه) — اگر نه، به سؤالِ دوم می‌روی؛ اگر بله، به سؤالِ سوم. سؤالِ دوم Order per Entity Matters? (ترتیبِ رویدادهای هر موجودیت) است: نه → پیامدِ AMQP / JMS Broker (ack دانه‌ای، تعادلِ بار)؛ بله → پیامدِ Log Broker (پارتیشن‌بندی با کلید، بازپخش). سؤالِ سوم Throughput High, Msgs Fast? (کارِ ارزان به‌ازای هر پیام) است: بله → همان Log Broker؛ نه → به سؤالِ دوم برمی‌گردی (پیام‌های گران اول باید از فیلترِ ترتیب بگذرند).

از پیامدِ لاگ، سؤالِ چهارم شروع می‌شود: Duplicates Acceptable? (تحویلِ مجدد بعد از crash) — بله → پیامدِ Idempotent Writes (at-least-once + dedupe با offset)؛ نه → سؤالِ پنجم: All Effects in Framework? (state و sinkها با هم کامیت می‌شوند؟) — بله → پیامدِ Atomic Sink Commit (checkpoint + sink تراکنشی)؛ نه → دوباره Idempotent Writes، چون بیرونِ فریم‌ورک «عملاً دقیقاً یک‌بار» سقف است. درس: انتخابِ بروکر را replay و ترتیب و throughput تعیین می‌کند، و انتخابِ تضمین را محلِ اثهای جانبی — و بیشترِ دنیای واقعی روی «at-least-once + idempotent» می‌ایستد.

The tree has five rows: Broker Decision, AMQP / JMS Queue, Log-Based Broker, Delivery Semantics and Processing Guarantee — and since it is tall, the page gives it extra height. Start at Pick a Messaging Design: first question Need to Replay Old Messages? (reprocessing, auditing, fresh consumers) — no goes to the second question; yes to the third.

Second question, Order per Entity Matters? (each entity's event order): no → the AMQP / JMS Broker outcome (per-message ack, load balancing); yes → the Log Broker outcome (keyed partitioning, replay). Third question, Throughput High, Msgs Fast? (cheap work per message): yes → the same Log Broker; no → back to the second question (expensive messages must first pass the ordering filter). From the log outcome the fourth question starts: Duplicates Acceptable? (redelivery after crash) — yes → the Idempotent Writes outcome (at-least-once + offset dedup); no → the fifth question: All Effects in Framework? (state and sinks committed together?) — yes → the Atomic Sink Commit outcome (checkpoint + transactional sink); no → Idempotent Writes again, since outside the framework “practically exactly once” is the ceiling.

Lesson: replay, ordering and throughput pick the broker, and the side effects' location picks the guarantee — and most of the real world stands on “at-least-once + idempotent”.

نقشهٔ فصل — پردازش جریانی در یک نگاهChapter map — stream processing at a glance باز کردن در صفحه جدا ↗Open standalone ↗
روایت مدرس از نمودارTeacher narration

مثال فنی برای خواندنِ نمودار: پیام تا اثر پایدار

وضعیت اولیه: رویداد e7 می‌گوید سفارش ۴۲ به مبلغ ۲۰۰ ثبت شده است.

اجرای مرحله‌به‌مرحله: مصرف‌کننده آن را می‌خواند، جمع فروش را تغییر می‌دهد و موقعیت پیشرفت را ذخیره می‌کند؛ crash بین این دو مرحله می‌تواند تکرار ایجاد کند.

نتیجه و دلیل: طراحی استریم باید هم نگهداری پیام و هم اتمی بودنِ اثر و پیشرفت را مشخص کند.

گرهٔ میانی STREAM PROCESSING است: رویدادهای نامحدود که همیشه روشن‌اند. ردیفِ بالا واحدها و حمل است: رویداد و تاپیک (immutable + timestamp)، بروکرها (مستقیم در برابرِ صفی در برابرِ لاگ‌محور)، و تقسیمِ کار در برابرِ پخش (یکی در برابرِ همه). دو گرهِ کناری نظم می‌دهند: لاگ و offset (ترتیبِ درونِ پارتیشن، بازپخش‌پذیر) و CDC (یک رهبر، چند دنباله‌رو). ردیفِ پایین پردازش و تضمین است: دوگانِ state/stream (انتگرال، مشتق، compaction)، پنجره‌ها و زمان (event time + watermark)، و exactly-once (اتمیک یا idempotent).

برچسبِ یال‌ها مسیر را می‌گوید: واحدها حمل می‌شوند، تقسیم می‌شوند، مرتب می‌مانند، دنبال می‌شوند، مشتق می‌شوند، کران می‌گیرند و زنده می‌مانند. خطِ پایینی خلاصهٔ فصل است: رویدادها با بروکر به state می‌رسند، پنجره‌ها و joinها شکلشان می‌دهند و exactly-once صادقشان نگه می‌دارد.

The center node is STREAM PROCESSING: unbounded events, always on. The top row is units and carriage: events and topics (immutable + timestamp), brokers (direct vs queued vs log-based), and splitting work vs broadcast (one of them vs all). The two side nodes impose order: the log and offsets (in-partition order, replayable) and CDC (one leader, many followers). The bottom row is processing and guarantees: the state/stream duality (integrate, derive, compact), windows and time (event time + watermarks), and exactly-once (atomic or idempotent).

The edge labels trace the path: units are carried, split, kept ordered, followed, derived, bounded and kept alive. The bottom line is the chapter in one sentence: events ride brokers into state; windows and joins shape them; exactly-once keeps them honest.

۷7 بستهٔ مصاحبهInterview pack

۷.۱. همه‌چیز در یک نگاه7.1. 7.1 Everything at a glance

مفهومتعریفِ یک‌خطینکته‌ی مصاحبه
eventرکوردِ کوچکِ خودکفای immutable از رخدادی در نقطه‌ای از زمان + timestampمعادلِ رکورد در بچ؛ خطِ لاگ هم رویداد است
topic / streamدستهٔ رویدادهای هم‌جنسمعادلِ نامِ فایل در فایل‌سیستم
producer / consumerتولیدکنندهٔ یک‌باره، مصرف‌کنندهٔ شاید چندبارهpublisher/sender و subscriber/recipient هم می‌گویند
polling vs notificationpoll دوره‌ای با تأخیرِ کم گران است (درخواستِ دستِ‌خالی)تریگرِ پایگاه داده محدود است؛ پیام‌رسان تخصصی لازم است
drop / buffer / backpressureسه راهِ تولیدِ سریع‌تر از مصرفلولهٔ Unix و TCP راهِ سوم‌اند با بافرِ کوچکِ ثابت
direct messagingبدونِ واسطه: UDP multicast، ZeroMQ، StatsD، webhookفرض: همیشه آنلاین؛ مصرف‌کنندهٔ آفلاین از دست می‌دهد
message brokerسرورِ متمرکزِ تحویلِ پیام؛ سؤالِ durability این‌جاستمصرف‌کننده ناهمگام است (فقط تأییدِ بافر)
broker vs databaseبروکر پاک می‌کند، پایگاه داده نگه می‌دارد؛ بروکر اعلان می‌دهد، پایگاه داده snapshotبروکرِ سنتی برای ذخیرهٔ بلندمدت نیست
load balancingهر پیام به یکی از مصرف‌کننده‌هابرای کارِ گران؛ با اضافه‌کردنِ مصرف‌کننده مقیاس می‌گیرد
fan-outهر پیام به همهٔ مصرف‌کننده‌هامعادلِ چند job بچ روی یک فایل
consumer groupsتعادلِ بار درونِ گروه + fan-out میانِ گروه‌هاالگوی ترکیبیِ کافکا
acknowledgmentاعلامِ صریحِ پایانِ پردازش تا پیام از صف برودبی‌ack یعنی تحویلِ مجدد به مصرف‌کنندهٔ دیگر
reordering trapتعادلِ بار + تحویلِ مجدد = به‌هم‌ریختنِ ترتیب (m4 بعد m3)حتی با تضمینِ ترتیبِ JMS/AMQP؛ راه: صفِ جدا
poison message + DLQپیامِ همیشه‌کشنده حلقهٔ retry می‌سازد؛ DLQ آزادش می‌کندروی DLQ مانیتورینگ است؛ هر پیامش یک خطاست
log-based brokerappend به لاگ + خواندنِ ترتیبی + tail -fمصرف read-only است؛ بازپخش ممکن
partition + offsetshard لاگ + شمارهٔ صعودیِ هر پیام در پارتیشنترتیبِ کامل درونِ پارتیشن، هیچ تضمینی میانشان
consumer offsetثبتِ دوره‌ایِ پیشرفت به‌جای ack دانه‌ایresume از قدیمی‌تر = تکرار؛ مثلِ LSN در replication
max consumers = #partitionsهر shard یک مالک داردموازی‌سازی با shardِ بیشتر، نه مصرف‌کنندهٔ بیشتر
head-of-line blockingپیامِ کند، بقیهٔ همان shard را نگه می‌داردمثلِ توصیفِ کارایی فصل ۲
ring buffer on disksegment قدیمی پاک می‌شود؛ عقب‌ماندگیِ زیاد = ازدست‌دادنحداقل ~۲۲ ساعت بافر؛ lag را مانیتور کن
dual-write raceدو رهبرِ مستقل → دو ترتیب → واگراییِ دائمِ بی‌صداشکل ۱۲-۴؛ بدونِ version vector حتی نمی‌فهمی
CDCاستخراجِ لاگِ تغییرِ پایگاه داده به‌صورتِ استریمِ مرتبDebezium، Kafka Connect؛ ناهمگام (lag دارد)
initial snapshotکپیِ سازگار در offset معلوم + اعمالِ تغییرات از همان‌جاDBLog نتفلیکس برای snapshot تدریجی
log compactionفقط تازه‌ترین مقدارِ هر کلید می‌ماند (+ tombstone برای حذف)بازسازیِ سیستمِ تازه از offset صفر؛ کافکا دارد
outbox patterndual-write در یک تراکنشِ یک سیستم (دامنه + outbox)CDC اسکیمای پایدار می‌خواند؛ اسکیمای داخلی آزاد است
CDC vs event sourcingلاگِ سطحِ پایینِ جهش‌پذیر در برابرِ رویدادِ سطحِ بالای immutableCDC تدریجی اضافه می‌شود؛ sourcing بازطراحی است
state = ∫events, stream = d(state)/dtوضعیت انتگرالِ رویداد، استریم مشتقِ وضعیت (شکل ۱۲-۷)لاگ مبناست، state مشتق‌شده؛ compaction پلِ میانشان
CEPالگوی رویداد مثلِ regex؛ کوئری‌های ماندگار، دادهٔ گذرابرعکسِ پایگاه داده؛ Esper/Apama، SQL روی استریم
stream analyticsتجمیع و متریک روی پنجره؛ تقریبی‌بودن فقط بهینه‌سازی استBloom/HyperLogLog؛ Storm/Flink/Beam
IVMتبدیلِ SQL به عملگرِ تدریجی؛ فقط دادهٔ عوض‌شدهREFRESH دوره‌ای ناکارآمد و کهنه است؛ Materialize/RisingWave
stream–stream joinدو استریمِ رویداد در پنجره (جستجو+کلیک با session id)کلیک شاید اول برسد؛ انقضا = «کلیک‌نشده»
stream–table joinغنی‌سازی با کپیِ محلیِ جدول + CDC (پنجرهٔ نامتناهی سمتِ جدول)hash join محلی؛ نسخهٔ تازه جای قدیمی
table–table joinنگه‌داریِ view مادی‌شده؛ (u·v)′ = u′v + uv′تایم‌لاین = posts ⋈ follows
event time vs processing timetimestamp رویداد (قطعی) در برابرِ ساعتِ پردازشگر (شکننده با lag)شکل ۱۲-۸: restart جهشِ مصنوعی می‌سازد
watermark«بعد از این، پیامی قبل از t نمی‌آید»؛ trigger پنجرهبه‌ازای هر تولیدکننده جدا پیگیری می‌شود
stragglersدیررسِ بعد از بستنِ پنجره: بی‌خیال (با متریک) یا اصلاحیه (با retraction)ساعتِ دستگاه قابلِ اعتماد نیست؛ سه timestamp
tumbling / hopping / sliding / sessionبدونِ هم‌پوشانی / با هم‌پوشانی / دنبال‌کنندهٔ رویداد / بر اساسِ سکوناسلایدینگ و join state بافر می‌خواهند
exactly-once (effectively-once)اثرِ مرئی انگار یک‌بار، هرچند پردازش چند باردر استریم: microbatch/checkpoint + کامیتِ اتمی/idempotence
microbatch vs checkpointبلوکِ ~۱ثانیه‌ای در برابرِ snapshot غلتان با barrierهر دو درونِ فریم‌ورک exactly-once؛ بیرون نه (اثرِ دوم)
idempotent sinkoffset کافکا همراهِ write؛ تکرار بی‌اثرفرض: بازپخشِ هم‌ترتیب + قطعی‌بودن + fencing
ConceptOne-line definitionInterview note
eventSmall self-contained immutable record of something that happened at a point in time + timestampThe record's equivalent in batch; a log line is an event too
topic / streamA group of same-kind eventsThe filename's equivalent in a filesystem
producer / consumerOne-time producer, possibly several consumersAlso called publisher/sender and subscriber/recipient
polling vs notificationPeriodic polling is expensive at low latency (empty-handed requests)Database triggers are limited; a specialized messenger is needed
drop / buffer / backpressureThree ways to outproduce consumptionUnix pipes and TCP take the third with a small fixed buffer
direct messagingNo middleman: UDP multicast, ZeroMQ, StatsD, webhooksAssumes always-online; offline consumers lose out
message brokerCentralized message-delivery server; durability lives hereIts consumer is asynchronous (buffering ack only)
broker vs databaseBrokers delete, databases keep; brokers notify, databases snapshotTraditional brokers aren't for long-term storage
load balancingEach message to one of the consumersFor expensive work; scales by adding consumers
fan-outEach message to all consumersLike several batch jobs on one file
consumer groupsLoad balancing inside groups + fan-out across groupsKafka's combined pattern
acknowledgmentExplicit “processing done” so the message leaves the queueNo ack means redelivery to another consumer
reordering trapLoad balancing + redelivery = broken order (m4 before m3)Even JMS/AMQP ordering guarantees don't stop it; fix: separate queues
poison message + DLQAn always-fatal message builds an infinite retry loop; DLQ frees itDLQs are monitored; each message in one is an error
log-based brokerAppend to a log + sequential read + tail -fConsumption is read-only; replay possible
partition + offsetLog shard + each message's ascending number in the partitionTotal order inside a partition, none across them
consumer offsetPeriodic progress recording instead of per-message ackResume from older = duplicates; like the LSN in replication
max consumers = #partitionsEach shard has one ownerParallelize with more shards, not more consumers
head-of-line blockingA slow message holds up the rest of its shardLike the Chapter-2 performance story
ring buffer on diskOld segments deleted; big lag = loss~22h minimum buffer; monitor lag
dual-write raceTwo independent leaders → two orders → permanent silent divergenceFig. 12-4; without a version vector you don't even notice
CDCExtracting the database change log as an ordered streamDebezium, Kafka Connect; asynchronous (has lag)
initial snapshotConsistent copy at a known offset + applying changes from thereNetflix DBLog for gradual snapshots
log compactionOnly each key's latest value remains (+ tombstone for deletes)Rebuilding a fresh system from offset zero; Kafka has it
outbox patternDual write in one transaction of one system (domain + outbox)CDC reads a stable schema; the internal schema stays free
CDC vs event sourcingLow-level log of a mutable model vs high-level immutable eventsCDC is added gradually; sourcing is a redesign
state = ∫events, stream = d(state)/dtState is the event integral, stream the state derivative (Fig. 12-7)The log is the record, state is derived; compaction bridges them
CEPEvent patterns like regex; long-lived queries, transient dataThe database's inverse; Esper/Apama, SQL on streams
stream analyticsAggregation and metrics over windows; probabilistic = just optimizationBloom/HyperLogLog; Storm/Flink/Beam
IVMCompiling SQL into incremental operators; only changed dataPeriodic REFRESH is inefficient and stale; Materialize/RisingWave
stream–stream joinTwo event streams in a bounded window (search+click with session id)Click may arrive first; expiry = “unclicked”
stream–table joinEnrichment with a local copy + CDC (infinite window on the table side)Local hash join; fresh replaces old
table–table joinMaintaining a materialized view; (u·v)′ = u′v + uv′Timeline = posts ⋈ follows
event time vs processing timeEvent timestamp (deterministic) vs processor clock (fragile under lag)Fig. 12-8: restarts build artificial spikes
watermark“After this, no message before t will come”; window triggerTracked separately per producer
stragglersPost-close laggards: ignore (with metric) or correct (with retraction)Device clocks untrustworthy; three timestamps
tumbling / hopping / sliding / sessionNo overlap / with overlap / event-following / idle-basedSliding and joins need buffers
exactly-once (effectively-once)Visible effect as if once, though processed several timesIn streams: microbatch/checkpoint inside + atomic commit/idempotence outside
microbatch vs checkpoint~1s blocks vs rolling barrier snapshotsBoth exactly-once inside the framework; outside, effects duplicate
idempotent sinkKafka offset stored alongside the write; repeats harmlessAssumes same-order replay + determinism + fencing

۷.۲. سوالات پرتکرار مصاحبه7.2. 7.2 Frequent interview questions

۱. رویداد، تاپیک، تولیدکننده و مصرف‌کننده را در یک خط تعریف کن.1. Define event, topic, producer and consumer in one line each.

رویداد خبرِ اتفاق است؛ تولیدکننده آن را می‌نویسد، تاپیک دستهٔ منطقیِ آن است و مصرف‌کننده برای هدفِ خود آن را می‌خواند. قالب و معنای رویداد باید مشترک باشند.

مثال فنی: چهار نقش روی یک سفارش

وضعیت اولیه: سرویس سفارش رویداد {id:e7, order_id:42, amount:200} تولید می‌کند.

اجرای مرحله‌به‌مرحله: پیام در topic سفارش‌ها ثبت می‌شود؛ مصرف‌کنندهٔ گزارش آن را می‌خواند و جمع را تغییر می‌دهد.

نتیجه و دلیل: تولیدکننده منشأ پیام، topic جریان دسته‌بندی‌شده و مصرف‌کننده مجری پردازش است؛ رویداد یک رخداد با هویت و زمان مشخص است.

Event: a small self-contained immutable record of something that happened + timestamp. Topic: a group of same-kind events (the filename equivalent). The producer produces once; there may be several consumers, each processing independently.

۲. چرا polling پایگاه داده برای استریم جواب نمی‌دهد؟2. Why doesn't database polling work for streams?

Polling می‌تواند بارِ تکراری بسازد و تغییرهای بینِ دو خواندن را از قلم بیندازد؛ حذف و ترتیب نیز سخت‌تر دیده می‌شوند. یک منبعِ تغییرِ پایدار قراردادِ روشن‌تری می‌دهد.

مثال فنی: Polling چه رخدادی را جا می‌اندازد؟

وضعیت اولیه: رکورد از pending به paid و پیش از polling بعدی به shipped می‌رود.

اجرای مرحله‌به‌مرحله: خواندن دوره‌ایِ وضعیت فقط shipped را می‌بیند و رخداد paid را بازسازی نمی‌کند؛ لاگ تغییر هر دو transition را حمل می‌کند.

نتیجه و دلیل: snapshot وضعیت با تاریخچهٔ همهٔ تغییرها برابر نیست؛ timestampِ آخرین تغییر هم رخدادهای حذف‌شده را خودکار برنمی‌گرداند.

Because as you move toward low latency, most polls return empty-handed and overhead climbs; the consumer should instead be notified. Database triggers are limited and marginal too — hence the need for a specialized messenger.

۳. وقتی تولید سریع‌تر از مصرف است، سه راه چیست؟ مثال بزن.3. When production outruns consumption, what are the three options? Give an example.

سه راه: حذفِ پیام، بافر کردن یا فشارِ برگشتی و کاهشِ تولید. ارزشِ داده، ظرفیت و پیامدِ انتظار تعیین می‌کنند کدام پذیرفتنی است.

مثال فنی: حساب backlog

وضعیت اولیه: تولید 1000 msg/s و مصرف 700 msg/s است.

اجرای مرحله‌به‌مرحله: backlog هر ثانیه ۳۰۰ پیام و در ۱۰ دقیقه ۱۸۰ هزار پیام زیاد می‌شود؛ افزایش بافر فقط زمان می‌خرد.

نتیجه و دلیل: باید نرخ تولید کم، ظرفیت مصرف زیاد یا طبق نیاز بخشی حذف شود؛ صف نامحدود راه‌حلِ پایدار اختلاف نرخ نیست.

Drop, buffer, backpressure (stalling the producer). Unix pipes and TCP take the third with a small fixed buffer; when full, the sender stays blocked until the receiver reads.

۴. پیام‌رسانیِ مستقیم چه فرضی دارد و کی می‌شکند؟4. What does direct messaging assume, and when does it break?

ارتباطِ مستقیم معمولاً به حضور و توانِ هم‌زمانِ طرف‌ها وابسته‌تر است. قطعِ گیرنده و کندیِ مصرف‌کننده نیاز به مدیریتِ صریحِ بافر و تحویل ایجاد می‌کند.

مثال فنی: ارسال مستقیم در برابر بروکر

وضعیت اولیه: سرویس گزارش برای ۱۰ دقیقه خاموش است، ولی سفارش‌ها ادامه دارند.

اجرای مرحله‌به‌مرحله: ارسال مستقیم بدون ذخیرهٔ پایدار می‌تواند پیام‌ها را گم کند؛ brokerِ پایدار آن‌ها را طبق سیاست retention نگه می‌دارد تا گزارش برگردد.

نتیجه و دلیل: broker وابستگی زمانی را کم می‌کند؛ ظرفیت دیسک، دوام و مدت نگهداری هنوز باید کافی باشند.

It assumes both sides are permanently online. An offline consumer loses outage-time messages; and if the producer itself crashes, its retry buffer dies with it. So its fault tolerance is limited and app code must know loss is possible (UDP multicast, ZeroMQ, StatsD, webhooks).

۵. بروکر چه مشکلی را حل می‌کند و مصرف‌کننده‌اش چرا ناهمگام است؟5. What problem does a broker solve, and why is its consumer asynchronous?

بروکر تولید و مصرف را از نظرِ زمان و مکان جدا می‌کند و طبقِ قرارداد پیام را نگه می‌دارد. مصرف‌کننده با سرعتِ خودش می‌خواند؛ ماندگاریِ واقعی به نوع و تنظیمات وابسته است.

مثال فنی: ارسال مستقیم در برابر بروکر

وضعیت اولیه: سرویس گزارش برای ۱۰ دقیقه خاموش است، ولی سفارش‌ها ادامه دارند.

اجرای مرحله‌به‌مرحله: ارسال مستقیم بدون ذخیرهٔ پایدار می‌تواند پیام‌ها را گم کند؛ brokerِ پایدار آن‌ها را طبق سیاست retention نگه می‌دارد تا گزارش برگردد.

نتیجه و دلیل: broker وابستگی زمانی را کم می‌کند؛ ظرفیت دیسک، دوام و مدت نگهداری هنوز باید کافی باشند.

By centralizing data in the broker, client churn and crashes are tolerated and durability moves to the broker (in-memory or on-disk). The producer only waits for the buffering ack, and delivery to the consumer happens at some unknown future — hence asynchronous.

۶. متعادل‌سازیِ بار و fan-out چه فرقی دارند؟ ترکیبشان چیست؟6. How do load balancing and fan-out differ? What combines them?

تقسیمِ بار، هر پیام را میان اعضای یک گروه واگذار می‌کند؛ fan-out همان خبر را به گروه‌های مستقل می‌رساند. می‌توان درونِ هر گروه چند مصرف‌کننده داشت.

مثال فنی: دو گروه، چند worker

وضعیت اولیه: topic سفارش‌ها ۴ پارتیشن دارد؛ گروه ارسال دو worker و گروه تحلیل دو worker دارد.

اجرای مرحله‌به‌مرحله: داخل هر گروه پارتیشن‌ها تقسیم می‌شوند؛ هر گروه مستقل همهٔ جریان را دنبال می‌کند.

نتیجه و دلیل: load balancing تقسیم کارِ یک گروه است؛ fan-out رساندن جریان به گروه‌های مستقل است.

In the former each message reaches one consumer (splitting expensive work; scale with more consumers); in the latter each message reaches all (independent broadcast, like several batch jobs on one file). Combined: the Kafka consumer group — load balancing inside groups, fan-out across groups.

۷. ack چیست و تحویلِ مجدد چطور ترتیب را به‌هم می‌زند؟7. What is an ack, and how does redelivery break ordering?

Ack اعلامِ رسیدگی طبقِ قرارداد است. اگر پس از اثرگذاری گم شود، پیام دوباره می‌رسد؛ ممکن است پیام‌های بعدی قبلاً اجرا شده باشند و ترتیبِ اثرها به هم بخورد.

مثال فنی: Retry ترتیب تکمیل را عوض می‌کند

وضعیت اولیه: پیام اول set status=paid در worker A گیر کرده؛ پیام دوم set status=shipped در B زودتر تمام می‌شود.

اجرای مرحله‌به‌مرحله: تحویل دوبارهٔ پیام اول می‌تواند بعداً وضعیت را به paid برگرداند، اگر نسخه یا ترتیبِ هر سفارش کنترل نشود.

نتیجه و دلیل: ترتیب تحویل با ترتیب پایانِ پردازش برابر نیست؛ کلیدگذاری، سریالی کردن یا کنترل نسخه برای همان موجودیت لازم است.

The consumer must explicitly announce processing is done so the message leaves the queue; no ack (close or timeout) means redelivery to another consumer. Since the first consumer concurrently processed and acked the next message, the redelivered message arrives later — like m4 before m3. Even JMS/AMQP ordering guarantees can't stop the “load balancing + redelivery” combination.

۸. پیامِ سمی چیست و DLQ چه می‌کند؟8. What is a poison message, and what does a DLQ do?

پیامِ سمی ورودی‌ای است که پردازشِ تکراری‌اش شکست می‌خورد. DLQ آن را برای بررسی جدا می‌کند تا بقیه پیش بروند؛ اصلاح، ترتیب و بازگرداندن باید برنامه داشته باشند.

مثال فنی: پیام سمی

وضعیت اولیه: پیام e7 نسخهٔ schema نامعتبر دارد و هر بار decoder را می‌شکند.

اجرای مرحله‌به‌مرحله: پس از تلاش‌های محدود، پیام با علت خطا و هویت اصلی به DLQ می‌رود؛ پس از اصلاح، بازپردازشِ کنترل‌شده انجام می‌شود.

نتیجه و دلیل: retry بی‌پایان همان خطا را تکرار و ممکن است پارتیشن را معطل کند؛ DLQ هم بدون رسیدگی، محل گم‌شدنِ خاموشِ داده می‌شود.

A message that kills every consumer (e.g. JSON missing a required key) and, since no ack comes, retries forever — wasting resources or wedging everything (with hard ordering). The DLQ moves the message to a separate queue so the flow stays free; DLQs are monitored and the operator decides to drop it, fix it manually, or fix the code.

۹. بروکرِ لاگ‌محور چطور کار می‌کند و مصرفش چرا تکرارپذیر است؟9. How does a log-based broker work, and why is its consumption repeatable?

پیام‌ها در لاگِ پارتیشن نگه داشته می‌شوند و گروهِ مصرف‌کننده موقعیتِ خواندنش را ثبت می‌کند. تا وقتی تاریخچه موجود است، می‌تواند دوباره بخواند؛ بازپخشِ اثرهای بیرونی باید کنترل شود.

مثال فنی: Offset جایگاهِ یک پارتیشن است

وضعیت اولیه: پیام e7 در پارتیشن ۲ با offset=۱۰۱ ثبت شده است.

اجرای مرحله‌به‌مرحله: گروه A تا offset ۱۰۰ و گروه B تا ۹۰ پیش رفته‌اند؛ پیشرفتشان مستقل است.

نتیجه و دلیل: offset شناسهٔ سراسری همهٔ topic نیست؛ برای موقعیت، نام topic و partition هم لازم است.

The producer appends, the consumer reads sequentially and waits at the log tail for notification (like tail -f). Reading doesn't change the log (only the offset advances, and it is the consumer's own business) — so you can raise a copy of the consumer at yesterday's offset and replay the past.

۱۰. offset چیست و چرا تکرارِ پیام پیش می‌آید؟10. What is an offset, and why do messages repeat?

Offset جای پیام در یک پارتیشن است. اگر اثر اعمال شود ولی موقعیت ذخیره نشود، پس از بازیابی همان پیام دوباره خوانده می‌شود.

مثال فنی: شکافِ اثر و offset

وضعیت اولیه: جمع فعلی ۱۰۰۰ است و پیام ۱۰۱ مبلغ ۲۰۰ دارد.

اجرای مرحله‌به‌مرحله: worker جمع را ۱۲۰۰ می‌کند، ولی قبل از ذخیرهٔ پیشرفت crash می‌کند؛ replayِ بی‌حفاظ آن را ۱۴۰۰ می‌کند.

نتیجه و دلیل: «دقیقاً یک اثر» به اتمی بودن state و پیشرفت، یا dedup معتبر نیاز دارد؛ فقط ack کردن پیام کافی نیست.

Each message's ascending number in its partition (total order inside a partition, none across them). The broker only records the offset periodically (no per-message ack); after a crash you resume from the last recorded one, not the last seen — so the messages in between repeat. Like the LSN in replication.

۱۱. چرا تعدادِ مصرف‌کننده‌ها سقف دارد؟11. Why is the consumer count capped?

در مدلِ معمولِ گروهِ مصرف‌کنندهٔ لاگ، هر پارتیشن هم‌زمان به یک عضو واگذار می‌شود؛ تعدادِ اعضای فعال برای آن تاپیک به پارتیشن‌ها محدود است. این سقفِ همهٔ مدل‌های پردازش نیست.

مثال فنی: سقف موازی‌سازیِ یک گروه

وضعیت اولیه: topic چهار پارتیشن و گروه شش مصرف‌کننده در مدل consumer group معمول Kafka دارد.

اجرای مرحله‌به‌مرحله: حداکثر چهار مصرف‌کننده هم‌زمان مالک پارتیشن فعال‌اند و دو تای دیگر بدون تخصیص می‌مانند.

نتیجه و دلیل: افزودن worker بیش از تعداد پارتیشن، در این مدل ظرفیت را خودکار زیاد نمی‌کند؛ پردازش درون‌پارتیشن و حفظ ترتیب هم محدودیت دارند.

Because a whole shard goes to one node; a topic's cooperating nodes are at most its shard count. If one message is slow, the rest of its shard waits behind it (head-of-line blocking). For expensive per-message work where order doesn't matter, a traditional broker is better.

۱۲. مسابقهٔ dual-write را با شکل توضیح بده.12. Explain the dual-write race with the figure.

نوشتن در مرجع و مقصدِ دوم جداست؛ یکی می‌تواند شکست بخورد یا دو تغییر به ترتیبِ متفاوت برسند. باید مسیرِ مشترکِ معتبرِ تغییر یا تصمیمِ هماهنگ داشت.

مثال فنی: Dual-write وسط کار می‌شکند

وضعیت اولیه: برنامه قیمت P7 را در مرجع ۱۲۰ می‌کند و پیش از نوشتن ایندکس crash می‌کند.

اجرای مرحله‌به‌مرحله: مرجع ۱۲۰ و ایندکس ۱۰۰ می‌مانند؛ در دو ویرایش هم‌زمان، حتی ترتیب اعمال دو مقصد می‌تواند متفاوت شود.

نتیجه و دلیل: retry ساده همیشه ترتیب را درست نمی‌کند؛ یک لاگ معتبرِ تغییر یا outbox مسیر بازپخش‌پذیر می‌دهد.

Two clients concurrently want X as A and B; both write the database first, then the index. The database sees A then B (final: B) but the index sees B then A (final: A) — permanent silent divergence with no error at all. Cause: two independent leaders and no single order; without a version vector you don't even notice. Fix: one leader (the database) + CDC.

۱۳. CDC چیست و چه چیزهایی لازم دارد؟13. What is CDC, and what does it need?

CDC تغییرها را از مرجع به جریان تبدیل می‌کند. تصویرِ شروع، نقطهٔ لاگ، حذف‌ها، ترتیبِ لازم، قراردادِ ساختار و تکرار باید مدیریت شوند تا نمای مقصد قابل‌بازیابی بماند.

مثال فنی: CDC با مرز سازگار

وضعیت اولیه: snapshot جدول تا موقعیت لاگ ۵۰۰ گرفته شده است.

اجرای مرحله‌به‌مرحله: snapshot وارد جست‌وجو می‌شود و سپس تغییرهای پس از ۵۰۰ به‌ترتیب مناسب اعمال می‌شوند؛ lag و gap کنترل می‌شوند.

نتیجه و دلیل: اگر مرز اشتباه باشد، تغییرِ حین snapshot جا می‌افتد یا نادرست تکرار می‌شود؛ صرفِ خواندن دوره‌ای کل جدول CDC دقیق نیست.

Extracting the database change log as an ordered stream (Debezium, Kafka Connect) that derived systems apply in the same order — the database leads, the rest follow. It is asynchronous (has lag). For a fresh system: a consistent snapshot at a known offset + applying from there (or a compacted log from offset zero). Stabilize the schema with the outbox (single-transaction dual write).

۱۴. log compaction چطور جای snapshot را می‌گیرد؟14. How does log compaction replace the snapshot?

در لاگِ فشرده‌شده، نسخهٔ لازمِ هر کلید می‌تواند برای بازسازیِ وضعیتِ فعلی بماند، به شرطِ قراردادِ کافی برای حذف و نگهداری. این کار همهٔ تاریخچه یا هر نوعِ snapshot را جایگزین نمی‌کند.

مثال فنی: Bootstrap با لاگ compactشده

وضعیت اولیه: برای کلیدها تغییرهای A=1,A=2,B=5 ثبت شده‌اند.

اجرای مرحله‌به‌مرحله: پس از compaction، مصرف‌کننده می‌تواند با آخرین مقدارهای موجود، stateِ A=2 و B=5 را بسازد؛ tombstone و retention حذف‌ها باید پوشش داده شوند.

نتیجه و دلیل: لاگ compactشده snapshotِ یک نقطهٔ زمانیِ خودکار نیست؛ bootstrap و شروع مصرفِ پیوسته باید مرز و سیاست روشن داشته باشند.

Only each key's latest value remains (deletes via tombstone); footprint depends only on current content. A fresh consumer scans the whole log from offset zero and takes a full copy — with no fresh snapshot needed from the source. (For event sourcing, where events are intents not replacements, it doesn't work.)

۱۵. دوگانِ state و stream یعنی چه؟15. What does the state/stream duality mean?

جریان تغییرها، وضعیت را با اعمالِ مرحله‌ای می‌سازد؛ وضعیتِ فعلی همهٔ گذشته را حفظ نمی‌کند. برای بازسازی باید تاریخچهٔ کافی یا تصویرِ شروعِ معتبر داشته باشیم.

مثال فنی: ساخت state از رخدادها

وضعیت اولیه: جریان کلید K به‌ترتیب K=1,K=3,K=DELETE است.

اجرای مرحله‌به‌مرحله: اعمال این تغییرها state نهاییِ «K وجود ندارد» می‌سازد؛ compaction می‌تواند برای بازسازی state آخرین تغییر لازمِ هر کلید را نگه دارد.

نتیجه و دلیل: stream تاریخچهٔ تغییر و state حاصلِ اعمال آن است؛ حذف‌ها و retention باید در bootstrap درست تفسیر شوند.

Mutable state and the immutable log are two sides of one coin: state = the event integral over time, stream = the time-derivative of state. Treat the log as the record and state as derived — then replay, auditing and building several views from one log become natural. (Gray and Reuter: the log has all the information.)

۱۶. CEP و تحلیلِ استریم چه فرقی دارند؟16. How do CEP and stream analytics differ?

CEP به الگوی رویدادهای مرتبط واکنش می‌دهد؛ تحلیلِ جریانی معمولاً آمار و تجمیع می‌سازد. خروجی و نیازِ ترتیب و زمان می‌توانند متفاوت باشند.

مثال فنی: CEP با جمع‌زدن فرق دارد

وضعیت اولیه: برای یک حساب، سه ورود ناموفق و سپس یک ورود موفق در دو دقیقه رخ داده است.

اجرای مرحله‌به‌مرحله: CEP ترتیب و فاصلهٔ رخدادها را برای یک الگو می‌سنجد؛ تحلیل معمول می‌تواند فقط تعداد ورود ناموفق در دقیقه را جمع کند.

نتیجه و دلیل: یک مجموع ساده، ترتیبِ «سه شکست سپس موفقیت» را حفظ نمی‌کند؛ نوع سؤال state لازم را تعیین می‌کند.

CEP hunts specific multi-event patterns (like regex; long-lived queries, transient data — the database's inverse). Analytics hunts aggregation and metrics over windows (rates, rolling averages); its probabilistic algorithms are only optimization, not the nature of streams.

۱۷. سه join استریمی را مقایسه کن.17. Compare the three stream joins.

جریان–جریان رویدادهای دو جریان را با محدودهٔ زمان ترکیب می‌کند؛ جریان–جدول رویداد را با وضعیت کامل می‌کند؛ جدول–جدول تغییرهای دو وضعیت را در نمای مشترک منعکس می‌کند.

مثال فنی: سه join با سه state

وضعیت اولیه: کلیک و نمایش تبلیغ هر دو جریان‌اند؛ مشخصات کاربر یک جدول است.

اجرای مرحله‌به‌مرحله: stream-stream کلیک را به نمایش در بازهٔ زمانی وصل می‌کند؛ stream-table مشخصات کاربر را هنگام رخداد می‌خواند؛ table-table دو نمای به‌روز را هماهنگ می‌کند.

نتیجه و دلیل: هر join زمان نگهداری و معنای نسخهٔ طرف مقابل را می‌خواهد؛ join دو streamِ نامحدود بدون محدودیت state نامحدود می‌سازد.

Stream–stream: two event streams in a bounded window (search+click; expiry = unclicked). Stream–table: enrichment with a local copy + CDC (infinite window on the table side; fresh replaces old). Table–table: maintaining a materialized view from two changelogs with (u·v)′ = u′v + uv′. All three are “state from one input + lookup with the other”; undefined cross-stream order makes a join nondeterministic.

۱۸. event time و processing time چرا قاطی می‌شوند؟ راه چیست؟18. Why do event time and processing time get mixed? What's the fix?

زمانِ وقوع با زمانِ رسیدن فرق دارد. پنجرهٔ مبتنی بر زمانِ رویداد و سیاستِ پیامِ دیررس، نسبت دادنِ داده را روشن می‌کنند؛ watermark تضمینِ نبودِ دیررس نیست.

مثال فنی: رخداد دیر رسیده

وضعیت اولیه: خرید در 10:02 رخ داده، اما به‌علت قطع دستگاه در 10:12 به پردازشگر رسیده است.

اجرای مرحله‌به‌مرحله: گزارشِ event-time آن را در بازهٔ 10:00–10:05 می‌گذارد؛ processing-time آن را در بازهٔ دریافت قرار می‌دهد.

نتیجه و دلیل: برای گزارش رخداد واقعی، lateness و اصلاح نتیجه لازم است؛ دو ساعت، دو سؤال متفاوت را جواب می‌دهند.

Many frameworks window by the machine clock; under lag (restarts, backlogs) a flat rate becomes an artificial spike (Fig. 12-8). Fix: window by event timestamp (deterministic and replayable) + watermarks for closing, and for laggards either ignore (with metric) or correct (with retraction). Device clocks are untrustworthy — use three timestamps.

۱۹. چهار نوع پنجره را با مثال بگو.19. Describe the four window types with an example each.

پنجرهٔ ثابت مرزِ جدا دارد؛ پنجرهٔ لغزان در فاصله‌های منظم روی بازه‌های هم‌پوشان خروجی می‌دهد؛ پنجرهٔ متحرک فاصلهٔ زمانیِ رویدادها را دنبال می‌کند؛ نشست با سکوتِ میان فعالیت‌ها جدا می‌شود.

مثال فنی: چهار پنجره با یک جریان

وضعیت اولیه: برای یک کلید، رخدادها در 10:01,10:04,10:12 رسیده‌اند و زمانِ خود رخداد ملاک است.

اجرای مرحله‌به‌مرحله: tumbling پنج‌دقیقه‌ای دو رخداد اول را در [10:00,10:05) می‌گذارد؛ hopping با اندازهٔ ۱۰ و گام ۵ آن‌ها را در دو پنجرهٔ هم‌پوشان می‌شمارد؛ sliding ده‌دقیقه‌ای در ۱۰:۱۲ فقط ۱۰:۰۴ و ۱۰:۱۲ را دارد.

نتیجه و دلیل: session با قانون «فاصلهٔ بیش از پنج دقیقه، نشست تازه» دو رخداد اول را یک نشست و رخداد سوم را نشست بعد می‌گیرد؛ تعریف مرز هر پنجره باید مشخص باشد.

Tumbling: the 10:03 minute is one window, no overlap. Hopping: 5-minute windows with a one-minute hop (smoothing). Sliding: two events 4.5 minutes apart share a window (the window follows the events; needs a sorted buffer). Session: a user's activity cluster until 30 minutes idle (variable width, common in web analytics).

۲۰. در استریم به exactly-once چطور می‌رسی؟20. How do you reach exactly-once in streams?

وضعیت، محلِ خواندن و خروجی باید در محدودهٔ تضمین هماهنگ بازیابی شوند، یا نوشتنِ تکراری اثرِ اضافه نگذارد. مقصدِ بیرونی هم باید در این قرارداد باشد.

مثال فنی: مرز تضمین را تا مقصد دنبال کن

وضعیت اولیه: پردازشگر از Kafka می‌خواند و روی topic دیگری می‌نویسد، یا به API پیامک زنگ می‌زند.

اجرای مرحله‌به‌مرحله: تراکنش Kafka می‌تواند outputِ Kafka و offset را در دامنهٔ خودش هماهنگ کند؛ API پیامک بیرون آن نیازمند کلیدِ تکرارپذیری و قرارداد خودش است.

نتیجه و دلیل: exactly-once را برای مسیر مشخص می‌گوییم؛ تضمین داخل broker را بی‌دلیل به هر اثر بیرونی تعمیم نمی‌دهیم.

Like batch (retry + discarding the partial) but with no “end”: inside the framework with microbatch (~1 second) or rolling checkpoints (barriers); for external effects either internal atomic commit (state + messages + offset together) or idempotent writes (Kafka offset alongside the value; assumes same-order replay + determinism + fencing). Its more precise name is effectively-once.

۷.۳. اشتباهات رایج در جواب‌های مصاحبه7.3. 7.3 Common interview mistakes

  • «کافکا ترتیبِ سراسری تضمین می‌کند» — نه؛ فقط درونِ هر پارتیشن. رویدادهای دو پارتیشن می‌توانند هر ترتیبی برسند؛ اگر ترتیبِ موجودیتی لازم است، کلیدِ پارتیشن‌بندی‌اش کن.
  • «ack یعنی دقیقاً یک‌بار» — نه؛ ack یعنی دست‌کم یک‌بار (تحویلِ مجدد + گم‌شدنِ ack = تکرار). دقیقاً یک‌بار به کامیتِ اتمی یا idempotence هم نیاز دارد.
  • «مصرف‌کننده اضافه کن تا سریع شود» — فقط تا سقفِ تعدادِ پارتیشن‌ها؛ بعدش نودِ بیکار می‌ماند. اول پارتیشن را زیاد کن.
  • «لاگ را پاک کن مثلِ صف» — مصرفِ لاگ read-only است؛ پاک‌کردنِ دستی، بازپخشِ بقیه را خراب می‌کند. نگه‌داشت با retention/compaction مدیریت می‌شود نه حذفِ دستی.
  • «dual-write با try/catch درست می‌شود» — نه؛ مسابقهٔ ترتیب (شکل ۱۲-۴) با هیچ خطایی رخ نمی‌دهد و try/catch نمی‌فهمدش. فقط رهبرِ واحد + CDC.
  • «CDC همان event sourcing است» — نه؛ CDC لاگِ سطحِ پایینِ مدلِ جهش‌پذیر را استخراج می‌کند (ترتیبِ واقعیِ writeها)، sourcing رویدادِ سطحِ بالای immutable می‌نویسد (نیتِ کاربر). compaction برای دومی جواب نمی‌دهد.
  • «پنجره را با ساعتِ سرور ببند» — با lag، دادهٔ بد می‌سازی (شکل ۱۲-۸). پنجره با event time + watermark؛ ساعتِ پردازشگر فقط وقتی که lag ناچیزِ تضمینی است.
  • «دیررس‌ها را همیشه بی‌خیال شو» — بستگی دارد: برای متریکِ نرخ شاید، ولی برای صورت‌حساب باید اصلاحیه بدهی (و قبلی را پس بگیری). سیاست را صریح انتخاب کن و متریکش کن.
  • «میکروبچ یعنی exactly-once سرتاسری» — فقط درونِ فریم‌ورک؛ اثرِ بیرونی (پایگاه داده، ایمیل) با retry تکرار می‌شود مگر کامیتِ اتمی یا idempotent باشد.
  • «join استریمی مثلِ join بچ است» — نه؛ ورودی تمام نمی‌شود پس به پنجره و state نیاز داری، ترتیبِ میانِ استریم‌ها نامعین است (ناقطعی‌بودن)، و انقضا خودش یک خروجی است («کلیک‌نشده»).
  • “Kafka guarantees global order” — no; only inside each partition. Two partitions' events can arrive in any order; if per-entity order is needed, partition-key it.
  • “Ack means exactly once” — no; ack means at-least-once (redelivery + lost acks = duplicates). Exactly-once also needs atomic commit or idempotence.
  • “Add consumers to go faster” — only up to the partition count; beyond that nodes idle. Raise partitions first.
  • “Delete from the log like a queue” — log consumption is read-only; manual deletion breaks everyone else's replay. Retention is managed by retention/compaction, not manual deletes.
  • “Dual writes are fixed with try/catch” — no; the ordering race (Fig. 12-4) happens with zero errors and try/catch can't see it. Only a single leader + CDC.
  • “CDC is the same as event sourcing” — no; CDC extracts the low-level log of a mutable model (real write order), sourcing writes high-level immutable events (user intent). Compaction doesn't work for the latter.
  • “Close windows by the server clock” — under lag you manufacture bad data (Fig. 12-8). Window by event time + watermark; the processor clock only when lag is guaranteed negligible.
  • “Always ignore laggards” — it depends: fine for a rate metric, but billing needs corrections (retracting the old). Pick the policy explicitly and metric it.
  • “Microbatch means end-to-end exactly-once” — only inside the framework; external effects (database, email) duplicate on retry unless atomic or idempotent.
  • “A stream join is like a batch join” — no; input never ends so you need windows and state, cross-stream order is undefined (nondeterminism), and expiry is itself an output (“unclicked”).

۷.۴. چک‌لیستِ ۵ دقیقه‌ای قبل از مصاحبه7.4. 7.4 Five-minute checklist

  • سه‌تاییِ drop/buffer/backpressure و مثالِ لوله/TCP را بگو.
  • تعادلِ بار (یکی) در برابرِ fan-out (همه) + ترکیبِ consumer group را بگو.
  • مکانیزمِ m4-قبل-از-m3 (تعادلِ بار + تحویلِ مجدد) را از حفظ بکش.
  • پارتیشن/offset: ترتیبِ درون در برابرِ میان + سقفِ موازی‌سازی + تکرارِ بعد از resume.
  • مسابقهٔ A/B در dual-write و راهِ CDC (رهبرِ واحد، ترتیبِ واحد).
  • compaction (تازه‌ترین هر کلید + tombstone) و outbox (dual-write تک‌تراکنشی) را بگو.
  • دوگان: state = انتگرالِ رویداد، stream = مشتقِ وضعیت.
  • سه join (پنجرهٔ محدود / نامتناهی / دوطرفه) + قاعدهٔ (u·v)′ را بگو.
  • event time در برابرِ processing time + شکلِ جهشِ مصنوعی + watermark و دو سیاستِ دیررس.
  • چهار پنجره (غلتان/پرشی/لغزان/نشست) را با یک مثال بگو.
  • exactly-once = microbatch/checkpoint درون + کامیتِ اتمی/idempotence بیرون (effectively-once).
  • Recite the drop/buffer/backpressure trio with the pipe/TCP example.
  • Load balancing (one) vs fan-out (all) + the consumer-group combination.
  • Draw the m4-before-m3 mechanism (load balancing + redelivery) from memory.
  • Partition/offset: order inside vs across + parallelism ceiling + post-resume duplicates.
  • The A/B dual-write race and the CDC fix (single leader, single order).
  • Compaction (latest per key + tombstone) and outbox (single-transaction dual write).
  • The duality: state = event integral, stream = state derivative.
  • The three joins (bounded / infinite / two-sided windows) + the (u·v)′ rule.
  • Event time vs processing time + the artificial-spike figure + watermarks and the two laggard policies.
  • The four windows (tumbling/hopping/sliding/session) with one example each.
  • Exactly-once = microbatch/checkpoint inside + atomic commit/idempotence outside (effectively-once).

۸8 تمرین‌های مصاحبه و آزمایشگاهInterview exercises and hands-on labs

این تمرین‌ها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبه‌ای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path

تمرین ۱ — شمارندهٔ exactly-once روی کافکا و پایگاه دادهExercise 1 — an exactly-once counter on Kafka and a database

L3 · Staff / PrincipalL3 · Staff / Principalاولویت اصلیCore priority45 دقیقه بحث45 min discussion

checkpoint با کلید (topic,partition) همراه totalها commit می‌شود؛ epoch مالک در DB و در همان تراکنش بررسی می‌شود.Checkpoints keyed by (topic,partition) commit with totals; the sink database checks owner epoch within that transaction.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

Kafka با سه partition و click count در DB را طراحی کن. اثر و checkpoint هر topic/partition را در یک تراکنش ذخیره کن؛ last_offset هر کاربر فقط با key-to-partition ثابت معنا دارد. crash، ACK گم‌شده، rebalance و stale consumer را با fencing و replay منظم تحلیل کن.Count clicks from three Kafka partitions into a database. Commit effects and topic/partition checkpoints together. Explain fixed key routing for per-user offsets, crashes, lost ACKs, ordered replay, rebalances and stale-consumer fencing.

معیار پاسخ خوبWhat a good answer demonstrates

  • offset دو partition مقایسه نشدهPartition-scoped offsets
  • commit اثر و checkpoint اتمیک استAtomic output/checkpoint
  • پرش offset، writer هم‌زمان و rebalance پوشش داده شده‌اندOrdering, rebalance and fencing

پیگیری مصاحبه:Interview follow-up: تعداد partitionها تغییر کند و کاربر جابه‌جا شود؛ dedup key چگونه مهاجرت می‌کند؟Partition count changes and a user moves. How do you migrate the dedup identity?

آزمایشگاه تکمیلی · زمان تقریبی ۴–۸ ساعتExtended hands-on laboratory · estimated 4–8 hours

تاپیک clicks سه پارتیشن با routing ثابت user_id دارد. جدول click_counts(user_id,total) و consumer_checkpoints(topic,partition,last_offset,owner_epoch) با کلید مرکب بساز. برای هر پارتیشن، سطر checkpoint را در تراکنش قفل کن، epoch را بررسی کن، فقط رکوردهای مرتبِ بعد از last_offset را اعمال کن و totalها و آخرین offset واقعاً پردازش‌شده را با هم commit کن؛ ACK بروکر بعد از commit است. هم‌زمانی دو batch در یک epoch هم باید با قفل/شرط تراکنشی کنترل شود. epoch گروه Kafka خودکار DB را fence نمی‌کند: انتقال مالکیت باید epoch معتبر را در sink نصب کند. crash، ACK گمشده، replay، مالک کهنه و دو پارتیشن با offset یکسان را بیازما. checkpoint را با TTL حذف نکن؛ نبود تاریخچهٔ لازم یا تغییر routing به cutover، event_id و بازسازی مشخص نیاز دارد.

Use a three-partition clicks topic with fixed user_id routing. Create click_counts(user_id,total) and consumer_checkpoints(topic,partition,last_offset,owner_epoch) with a composite checkpoint key. Lock the partition checkpoint inside a transaction, validate epoch, process ordered records newer than last_offset, and commit totals plus the last actually processed offset together; broker ACK follows commit. Serialize batches even within one epoch. Kafka group epochs do not automatically fence a database: ownership transfer must install a valid epoch at the sink. Test crashes, lost ACKs, replay, stale owners and equal offsets in different partitions. Never expire active checkpoints by TTL; missing history or routing changes require explicit cutover, event IDs and rebuilding.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Kafka 4.1 · design and delivery semantics · AWS · safe retries with idempotent APIs

تمرین ۲ — join جستجو–کلیک با دیررس‌هاExercise 2 — a search–click join with laggards

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific40 دقیقه بحث40 min discussion

event-time join و late data برای streaming مناسب‌اند؛ زمان ثابت چند دقیقه بدون توزیع lateness توجیه ندارد.Lateness choices need measurements rather than an arbitrary timeout.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

search و click را با session و event time در پنجره یک‌ساعته join کن. هر دو arrival order، dedup و تعریف CTR را پوشش بده. watermark، idle partition، allowed lateness و اصلاحیه را بر اساس دادهٔ lateness انتخاب کن؛ در throughput ده‌برابری state و backpressure را برآورد کن.Join search and click by session and event time. Handle either arrival order, dedup and CTR semantics. Choose watermarks, idle-partition policy, lateness and corrections; estimate tenfold state growth and backpressure.

معیار پاسخ خوبWhat a good answer demonstrates

  • processing time با event time یکی نیستEvent versus processing time
  • join چندبه‌چند CTR را بی‌دلیل چندبرابر نمی‌کندAvoid duplicate join inflation
  • late-drop و correction metric دارندMeasured lateness/correction policy

پیگیری مصاحبه:Interview follow-up: یک partition بیکار watermark را نگه دارد؛ پنجره‌ها را چطور می‌بندی؟An idle partition holds back the watermark. How do you close windows safely?

آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours

برای نرخِ کلیک، join استریمِ جستجو و کلیک با session id و پنجرهٔ یک‌ساعته می‌خواهی. تصمیم بگیر و مستدل بنویس: (الف) اگر کلیک قبل از جستجو برسد state را چطور می‌چینی که هر دو ترتیب جواب دهد؛ (ب) timeout بستنِ پنجره را چند دقیقه بعد از پایانِ ساعت می‌گذاری و watermark را از کجا می‌گیری؛ (پ) با دیررسِ بعد از بستن چه می‌کنی — بی‌خیال یا اصلاحیه؟ اثرِ هر کدام روی متریکِ «نرخِ کلیک» چیست و چطور متریکش می‌کنی؛ (ت) اگر throughput ده برابر شود، state بافر چقدر رشد می‌کند و کجایش را به دیسک می‌بری؟

For click rate, you want to join the search and click streams on session id with a one-hour window. Decide and justify in writing: (a) if a click arrives before the search, how do you arrange state so both orders work; (b) how many minutes past the hour do you set the closing timeout, and where do you get the watermark from; (c) what do you do with post-close laggards — ignore or correct? What is each choice's effect on the “click rate” metric, and how do you metric it; (d) if throughput grows tenfold, how much does buffer state grow and which part do you move to disk?

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Kafka 4.1 · design and delivery semantics · Atlassian · engineering interview handbook

تمرین ۳ — آوردنِ ایندکسِ تازه با CDC بدونِ قطعیExercise 3 — bringing a fresh index with CDC, outage-free

L3 · Staff / PrincipalL3 · Staff / Principalاولویت اصلیCore priority45 دقیقه بحث45 min discussion

snapshot سازگار باید با مرز لاگِ معتبر هماهنگ شود؛ event topic و state topic کلید و retention متفاوت دارند و bootstrap هزینهٔ قابل‌اندازه‌گیری دارد.A consistent snapshot must align with a valid log boundary; event and state topics have different keys/retention and bootstrap has measurable cost.

نسخهٔ بازبینی‌شده برای مصاحبهReviewed interview brief

ایندکس جست‌وجو را بدون dual write با قرارداد event مستقل از schema داخلی طراحی کن. تراکنش business+outbox، consistent snapshot مرجع و مرز LSN را مشخص کن. event-ID dedup را از compacted state با entity key و tombstone جدا کن؛ lag، WAL retention، rebuild و schema change را تست کن.Build a search view without dual writes or public table schemas. Specify business+outbox atomicity, a consistent record snapshot and its LSN boundary, event-ID dedup, compacted entity keys, tombstones, WAL retention and throttled rebuilding.

معیار پاسخ خوبWhat a good answer demonstrates

  • snapshot gap/duplicate و replay پوشش داده شده‌اندSnapshot boundary and replay
  • کلید compaction و delete semantics صریح استEntity key and tombstone semantics
  • فشار snapshot صفر فرض نشده و throttling داردBounded snapshot/WAL pressure

پیگیری مصاحبه:Interview follow-up: outbox قدیمی purge شده؛ مصرف‌کنندهٔ جدید از کجا state کامل را می‌سازد؟Old outbox rows have been purged. Where does a new consumer get complete state?

آزمایشگاه تکمیلی · زمان تقریبی ۴–۸ ساعتExtended hands-on laboratory · estimated 4–8 hours

business row و outbox(event_id,entity_key,entity_version,type,payload) را در یک تراکنش بنویس. snapshot کاملِ دادهٔ مرجع را با پروتکل connector و مرز LSN سازگار بگیر؛ صرف خواندن LSN قبل یا بعد از یک SELECT تضمین هماهنگی نیست. شروع کم‌پیچیدگی، snapshot رسمی connector است؛ snapshot از replica فقط با پشتیبانی نسخه و هماهنگی مرز مجاز است. topic رویداد با حفظ ترتیب هر entity و event_id برای dedup، و state topic اختیاری با کلید entity و compaction را جدا تعریف کن؛ null value روی کلید entity نقش tombstone دارد. بار IO، CPU، lag و WAL را با بودجهٔ اعلام‌شده محدود کن؛ «بی‌فشار» یا «همیشه تازه» تضمین نده. bootstrap ابتدا snapshot را نصب و سپس تغییرها را با version guard اعمال کند تا replay کهنه state را عقب نبرد. kill/restart، delete، outbox purge و توقف بیش از retention را با checksum در مرز یکسان بیازما؛ ساخت projection نباید بر بقای تمام تاریخ outbox تکیه کند.

Write business rows and outbox(event_id,entity_key,entity_version,type,payload) atomically. Obtain a complete record snapshot aligned with a consistent LSN using the connector protocol; reading an LSN around an arbitrary SELECT is insufficient. Prefer the connector's supported snapshot mode; replica snapshots require version support and coordinated boundaries. Separate an event topic with per-entity ordering plus event-ID dedup from an optional entity-keyed compacted state topic; null values at entity keys encode tombstones. Budget I/O, CPU, replica lag and WAL: never promise zero load or perpetual freshness. Install the snapshot before ordered catch-up and use version guards against stale replay. Test kill/restart, deletes, purged outbox and retention overruns with checksums at a common boundary; projection rebuilds cannot depend on retaining all historical outbox rows.

برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمی‌رود.The full laboratory is optional depth work and is not expected within the discussion time.

ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · Debezium · outbox event router · Debezium · PostgreSQL snapshot and streaming · Kafka 4.1 · design and delivery semantics