۱1 یکپارچهسازی داده (Data Integration)Data integration
۱.۱. یک مبنا، چند نمای مشتقشده1.1. One record, many derived views
وقتی چند ابزار از یک داده استفاده میکنند، بهتر است مرجعِ هر داده و مسیرِ ساختِ نسخههای دیگر روشن باشد. داشتنِ یک مرجع، جلوی تغییرهای مستقل و متناقض را میگیرد.
مثال فنی: مرجع و سه خروجی
وضعیت اولیه: مشخصات P7 در مرجع ثبت و رویداد تغییر با version=12 تولید میشود.
اجرای مرحلهبهمرحله: مصرفکنندهٔ جستوجو سند، مصرفکنندهٔ کش مقدار و مصرفکنندهٔ تحلیل ردیف گزارش را از همین تغییر میسازند.
نتیجه و دلیل: خروجیها مدلهای جدا دارند، اما منشأ مشترک و ترتیب معتبر، مسیر تطبیق و بازسازی آنها را معلوم میکند.
This chapter opens with a simple observation that stitches the whole book together: for a problem like “store data and find it later” there is no single right solution; every tool is built for a particular consumption pattern. So a complex application inevitably combines several pieces of software: an OLTP database for recording, a search index for words, a warehouse for analytics, a cache for display, an ML model for recommendations, and a notification service for news. The hard question isn't choosing the tool; it's mapping tools to conditions — and then keeping all these views together.
راهحلِ پیشنهادی: بعضی سیستمها را مبنا اعلام کن و بقیه را مشتقشده از آنها.
مثلاً تغییراتِ پایگاه دادهِ مبنا با CDC گرفته میشود و بههمانترتیب روی ایندکسِ
جستجو اعمال میشود. اگر CDC تنها راهِ بهروزرسانیِ ایندکس باشد، میتوانی مطمئن باشی ایندکس تماماً
از مبنا مشتق شده و (بهجز باگِ نرمافزاری) با آن سازگار است. نوشتن در پایگاه داده، تنها راهِ تزریقِ ورودیِ
تازه به سیستم است. این همان ایدهٔ replication ماشینِ حالت (فصل ۱۰) است که اینبار در سطحِ معماریِ کلِ
سازمان اعمال میشود.
The proposed fix: declare some systems the record and keep the rest derived from them. E.g. the record database's changes are captured with CDC and applied in the same order onto the search index. If CDC is the index's only update path, you can be sure the index is fully derived from the record and (barring software bugs) consistent with it. Writing to the database is the only way to inject fresh input into the system. This is the state-machine replication idea (Chapter 10), applied this time at the scale of a whole organization's architecture.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: مرجع و سه خروجی
وضعیت اولیه: مشخصات P7 در مرجع ثبت و رویداد تغییر با version=12 تولید میشود.
اجرای مرحلهبهمرحله: مصرفکنندهٔ جستوجو سند، مصرفکنندهٔ کش مقدار و مصرفکنندهٔ تحلیل ردیف گزارش را از همین تغییر میسازند.
نتیجه و دلیل: خروجیها مدلهای جدا دارند، اما منشأ مشترک و ترتیب معتبر، مسیر تطبیق و بازسازی آنها را معلوم میکند.
نمودار یک مسیرِ چپبهراست دارد. اول Client app (زیرنویس:
writes new input) با فلشِ «only input path» به
System-of-record DB (زیرنویس: durable source of truth)
میرسد — این همان نظمِ طلایی است: ورودیِ تازه فقط از همین یک در وارد میشود.
از پایگاه داده، فلشِ «CDC captures changes» به
Change stream / event log (زیرنویس:
same-order change feed) میرود؛ یعنی تغییرات بهترتیبِ commit گرفته میشوند.
حالا لاگ مثلِ یک هاب پخش میشود به ستونِ پنجتاییِ نماهای مشتقشده:
Full-text search index، Analytics / warehouse،
Cache / materialized view، ML model / recommendations
و Notification service. هر پنجتا داخلِ یک قابِ مشترکاند با برچسبِ
«same order, deterministic, idempotent» — یعنی همه بههمانترتیب میخوانند،
با تابعِ قطعی پردازش میکنند و نوشتنشان idempotent است.
سه کارتِ پایینِ نمودار، درس را کامل میکنند: اول هشدارِ «نوشتنِ مستقیم به دو store یعنی هرکدام ترتیبِ
خودشان را میگیرند و برای همیشه ناسازگار میمانند (شکلِ ۱۲-۴)»؛ بعد اصلِ مقابلش —
«همهٔ ورودی را از یک سیستمی رد کن که ترتیبِ کامل را تعیین میکند»؛ و سرانجام نکتهٔ عملیاتی:
«فرقی نمیکند CDC باشد یا event sourcing؛ مهم تصمیمگرفتن روی ترتیب است — و چون مشتقسازی
ناهمگام و سستپیوند است، خطا در یک گوشه به بقیه سرایت نمیکند».
The diagram runs left to right. First the Client app (writes new input) reaches the System-of-record DB (durable source of truth) via the “only input path” arrow — the golden discipline: fresh input enters through this one door only. From the database, the “CDC captures changes” arrow goes to the Change stream / event log (same-order change feed); changes are captured in commit order.
The log then fans out like a hub to the five-column derived views: Full-text search index, Analytics / warehouse, Cache / materialized view, ML model / recommendations and Notification service. All five sit in one shared frame labelled “same order, deterministic, idempotent” — all read in the same order, process with a deterministic function, and write idempotently.
The three cards at the bottom complete the lesson: first the warning — “writing directly to two stores means each takes its own order and they stay inconsistent forever (Fig. 12-4)”; then the opposite principle — “funnel all input through one system that decides the total order”; and finally the operational note: “CDC or event sourcing doesn't matter; deciding the order matters — and since derivation is asynchronous and loosely coupled, an error in one corner doesn't spread to the rest.”
۱.۲. چرا نوشتنِ دوگانه واگرا میشود1.2. Why dual writes diverge
نوشتن مستقیم در دو مقصد میتواند یکی را جا بیندازد یا تغییرها را به ترتیب متفاوت برساند. مسیرِ واحدِ تغییر کمک میکند نسخههای مشتقشده از ترتیبِ معتبر پیروی کنند.
مثال فنی: دو ترتیبِ ناسازگار
وضعیت اولیه: T1 قیمت ۱۰۰ و T2 قیمت ۱۲۰ را مینویسند.
اجرای مرحلهبهمرحله: مرجع T1 سپس T2 را اعمال میکند؛ ارسال جدا به ایندکس بهترتیب T2 سپس T1 میرسد و ایندکس روی ۱۰۰ میماند.
نتیجه و دلیل: موفقیت هر چهار نوشتن کافی نیست؛ لاگِ مرتبِ تغییر یا کنترل نسخه باید ترتیب معتبر را به مقصد برساند.
If the application writes directly to both the database and the search index, it falls into the Fig. 12-4 trap: two clients concurrently send two conflicting writes and the two systems apply them in two different orders. Here neither the database nor the index is “in charge of order”; each decides contradictorily and they stay inconsistent forever. The fix is funnelling all input through one system that decides every write's order — an application of the same state-machine-replication approach. Updating the derived system from the event log can usually be made deterministic and idempotent, simplifying error recovery.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: دو ترتیبِ ناسازگار
وضعیت اولیه: T1 قیمت ۱۰۰ و T2 قیمت ۱۲۰ را مینویسند.
اجرای مرحلهبهمرحله: مرجع T1 سپس T2 را اعمال میکند؛ ارسال جدا به ایندکس بهترتیب T2 سپس T1 میرسد و ایندکس روی ۱۰۰ میماند.
نتیجه و دلیل: موفقیت هر چهار نوشتن کافی نیست؛ لاگِ مرتبِ تغییر یا کنترل نسخه باید ترتیب معتبر را به مقصد برساند.
نمودار دو بند دارد. بندِ اول: «Two clients, two stores, no ordering authority».
پنج شرکتکننده داریم: client A و client B (هر دو
«concurrent writer»)، database
(«system of record») و search index
(«derived data»). چهار فلشِ درهم را دنبال کن:
A مقدارِ x=1 را مینویسد و B مقدارِ x=2 را —
ولی پایگاه داده اول A بعد B را اعمال میکند (یادداشت: «database order #1/#2»)
درحالیکه ایندکس اول B بعد A را میبیند («index order #1/#2»).
فلشِ پایانی با خطچین جمعبندی میکند: «DB x=2 ≠ index x=1» —
storeها برای همیشه ناسازگارند چون هیچکس مسئولِ ترتیب نیست.
بندِ دوم: «One ordering authority — a single total order».
اینبار هر دو کلاینت فقط در پایگاه داده مینویسند («all user input is funneled
through one system» و «the database decides one total order»).
بعد CDC: change log تغییرات را بهترتیبِ commit (اول x=1 بعد x=2) به
شرکتکنندهٔ پنجم یعنی event log میدهد و فلشِ
«apply in same total order» ایندکس را بهروز میکند —
با یادداشتِ «بهروزرسانیهای مشتق deterministic و idempotentاند، پس بازیابی ساده است».
فلشِ آخر: «consistent: x=2» با این پیام که CDC باشد یا event sourcing،
مهم تصمیمگرفتن روی ترتیبِ کامل است.
The diagram has two acts. Act one: “Two clients, two stores, no ordering authority”. Five participants: client A and client B (both “concurrent writers”), the database (“system of record”) and the search index (“derived data”). Follow the four tangled arrows: A writes x=1 and B writes x=2 — but the database applies A then B (note: “database order #1/#2”) while the index sees B then A (“index order #1/#2”). The closing dashed arrow summarizes: “DB x=2 ≠ index x=1” — the stores are inconsistent forever because nobody owns the order.
Act two: “One ordering authority — a single total order”. This time both clients only write to the database (“all user input is funnelled through one system”, “the database decides one total order”). Then CDC: change log hands the changes in commit order (first x=1, then x=2) to the fifth participant, the event log, and the “apply in same total order” arrow updates the index — noting “derived updates are deterministic and idempotent, so recovery is simple”. The last arrow: “consistent: x=2” — CDC or event sourcing, what matters is deciding the total order.
۱.۳. دادهٔ مشتق در برابر تراکنشِ توزیعشده1.3. Derived data vs distributed transactions
تراکنش توزیعشده میخواهد تصمیمِ تأییدِ چند بخش را هماهنگ کند. ساختِ نمای مشتقشده از لاگ، معمولاً اجازه میدهد مقصد کمی عقب بماند و بعد به مرجع برسد؛ این تضمینها یکسان نیستند.
مثال فنی: همگامیِ فوری یا مشتقسازیِ بعدی
وضعیت اولیه: تغییر قیمت در مرجع commit شده و جستوجو هنوز نسخهٔ قدیمی را نشان میدهد.
اجرای مرحلهبهمرحله: در dataflow، مصرفکننده بعداً تغییر را میرساند؛ در atomic commit، منابعِ participant به یک تصمیم مشترک commit/abort متعهد میشوند.
نتیجه و دلیل: روش اول lag میپذیرد و بازپخش میخواهد؛ روش دوم هماهنگی و مسیرِ انتظارِ خرابیِ خودش را دارد.
The classic approach to keeping systems in sync is the distributed transaction (2PC, Chapter 8). At an abstract level, both chase one goal with two means: transactions guarantee atomic application via atomic commit; the log-based system reaches correctness via deterministic retry and idempotence. The biggest difference is that a transactional system usually guarantees “after writing, you immediately read the fresh value” (read-your-writes), while an asynchronous derived system offers no such guarantee.
تراکنشهای توزیعشده در محیطهایی که حاضرند هزینهٔ عملکرد و عملیاتشان را بدهند موفق بودهاند؛ ولی
XA تحملِ خطا و عملکردِ ضعیفی دارد که کاربردش را شدیداً محدود میکند.
ساختنِ پروتکلِ بهتر ممکن است، ولی جاانداختنش در ابزارهای موجود بعید است بهزودی رخ دهد.
پس در غیابِ پروتکلِ خوبِ فراگیر، دادهٔ مشتقِ لاگمحور امیدبخشترین راهِ یکپارچهسازیِ سیستمهای
متفاوت است. البته گفتنِ «eventual consistency اجتنابناپذیر است، بساز و بسوز» هم بدونِ راهنمایِ
«چطور بسوزی» حرفِ مفیدی نیست — و ادامهٔ همین فصل همان راهنماست: چطور تضمینهای قویتر را روی
سیستمهای مشتقِ ناهمگام بسازیم و به نقطهٔ میانیِ بینِ تراکنشِ توزیعشده و لاگِ ناهمگام برسیم.
Distributed transactions have succeeded in environments willing to pay their performance and operational cost; but XA has poor fault tolerance and performance that sharply limits its use. Building a better protocol is possible, but getting it adopted into existing tools is unlikely to happen soon. So in the absence of a good universal protocol, log-based derived data is the most promising way to integrate different systems. Of course, saying “eventual consistency is inevitable, build and burn” without a “how to burn” guide isn't useful either — and the rest of this chapter is exactly that guide: how to build stronger guarantees on asynchronous derived systems and reach the middle ground between distributed transactions and the asynchronous log.
۱.۴. وقتی ترتیبِ کامل میشکند1.4. When total order breaks
مرتب کردنِ همهٔ رویدادهای جهان معمولاً لازم نیست و هماهنگیِ زیادی میخواهد. باید تشخیص دهیم کدام تغییرها واقعاً به هم وابستهاند و همان ترتیب را حفظ کنیم.
مثال فنی: علت روی دو پارتیشن
وضعیت اولیه: رویداد ایجاد سفارش ۴۲ روی پارتیشن A و لغو آن روی B قرار گرفته است.
اجرای مرحلهبهمرحله: مصرفکننده میتواند لغو را پیش از ایجاد ببیند؛ همپارتیشن کردنِ رخدادهای همان سفارش یا نگهداری وابستگی این مسئله را کنترل میکند.
نتیجه و دلیل: رخدادهای مستقل الزاماً ترتیب جهانی نمیخواهند؛ رخدادهای وابسته بدون قرارداد ترتیب، state نامعتبر میسازند.
While the system is small, building a fully ordered event log is feasible (single-leader databases build exactly that log). But with growing scale, limits surface:
- ساختنِ لاگِ کاملاً مرتب معمولاً یعنی همهٔ رویدادها از یک رهبرِ واحد رد شوند که ترتیب را تعیین کند. اگر نرخِ رویداد از توانِ یک ماشین بیشتر شود، باید لاگ را shard کنی — و آنوقت ترتیبِ رویدادهای دو shard مبهم است.
- اگر سرورها در چند region جغرافیایی باشند (تحملِ ازدسترفتنِ کلِ یک دیتاسنتر)، معمولاً هر دیتاسنتر رهبرِ خودش را دارد چون هماهنگیِ همگامِ بینقارهای کند است — پس ترتیبِ رویدادهای دو دیتاسنتر تعریفنشده است.
- در معماریِ میکروسرویس، هر سرویس state پایدارِ خودش را دارد و stateای بینِ سرویسها مشترک نیست — رویدادهای دو سرویسِ متفاوت هیچ ترتیبی ندارند.
- اپهایی که state سمتِ کلاینت را بلافاصله روی ورودیِ کاربر بهروز میکنند (حتی آفلاین)، باعث میشوند کلاینت و سرور رویدادها را به ترتیبهای متفاوت ببینند.
- Building a fully ordered log usually means all events pass through a single leader that decides the order. If the event rate exceeds one machine's power, you must shard the log — and then the order of two shards' events is ambiguous.
- If servers sit in several geographic regions (to survive losing a whole datacenter), each datacenter usually has its own leader since synchronous cross-continent coordination is slow — so the order of two datacenters' events is undefined.
- In a microservice architecture each service has its own durable state with no state shared between services — two different services' events have no order.
- Apps that immediately update client-side state on user input (even offline) make client and server see events in different orders.
به زبانِ صوری، تصمیمگرفتن روی ترتیبِ کاملِ رویدادها همان total order broadcast
است که (فصل ۱۰) معادلِ consensus است. و الگوریتمهای consensus برای جایی طراحی شدهاند که توانِ
یک نود برای کلِ جریانِ رویداد کافی باشد — مکانیزمی ندارند که چند نود کارِ «مرتبکردن» را تقسیم کنند.
Formally, deciding the total order of events is total order broadcast, which (Chapter 10) equals consensus. And consensus algorithms are designed for where one node's power suffices for the whole event flow — they have no mechanism for several nodes to share the “ordering” work.
حالا سؤالِ ظریفتر: اگر پیوندِ علّی بینِ رویدادها نباشد، نبودِ ترتیبِ کامل مشکلِ بزرگی نیست — رویدادهای
همزمان را میشود دلخواه مرتب کرد. بهروزرسانیهای متعددِ یک آبجکت را هم میشود با هدایتِ همهٔ
بهروزرسانیهای همان ID به یک shard، کاملاً مرتب کرد. ولی وابستگیهای علّی گاهی پنهاناند. مثالِ کتاب:
در یک شبکهٔ اجتماعی، کاربری دوستش را حذف میکند (unfriend) و بعد برای بقیهٔ
دوستانش پیامی دربارهٔ همان دوستِ سابق میفرستد — با این نیت که دوستِ سابق پیامِ تند را نبیند.
ولی اگر وضعیتِ دوستی یکجا و پیامها جای دیگری ذخیره شوند، وابستگیِ ترتیبیِ «اول unfriend، بعد پیام»
ممکن است گم شود و سرویسِ اعلان، پیام را قبل از unfriend پردازش کند و اشتباهی به دوستِ سابق هم اعلان بفرستد.
در واقع این اعلانها یک join بینِ پیامها و لیستِ دوستاناند — همان مسئلهٔ زمانِ joinها.
Now the subtler question: if there is no causal link between events, lacking total order isn't a big problem — concurrent events can be ordered arbitrarily. Multiple updates of one object can be totally ordered by routing all of that ID's updates to one shard. But causal dependencies are sometimes hidden. The book's example: in a social network, a user unfriends someone (unfriend) and then messages remaining friends about that ex-friend — intending the ex-friend not to see the harsh message. But if friendship state lives in one place and messages in another, the “unfriend first, then message” order may be lost, and the notification service may process the message before the unfriend and wrongly notify the ex-friend too. These notifications are really a join between messages and the friend list — the same join-timing problem.
نقطههای شروعِ پیشنهادیِ کتاب برای گرفتنِ علّیت، بدونِ گلوگاهِ broadcastِ ترتیبِ کامل:
The book's suggested starting points for capturing causality without the total-order-broadcast bottleneck:
- timestamp منطقی بدونِ هماهنگی ترتیبِ کامل میدهد (فصل ۱۰)، ولی گیرنده هنوز باید رویدادهای نامرتب را تحمل کند و متادیتای اضافه حمل شود.
- میشود رویدادی لاگ کرد که وضعیتِ دیدهشده توسط کاربر را ثبت کند و به آن شناسهٔ یکتا داد؛ بعد رویدادهای بعدی با ارجاع به همان شناسه، وابستگیِ علّی را ثبت میکنند.
- الگوریتمهای حلِ تعارض (فصل ۶) برای رویدادهایی که با ترتیبِ غیرمنتظره میرسند کمک میکنند — برای نگهداشتنِ state خوباند، ولی اگر action اثرِ بیرونی داشته باشد (مثلِ فرستادنِ اعلان) کاری نمیکنند.
- Logical timestamps give total order without coordination (Chapter 10), but the receiver must still tolerate out-of-order events and extra metadata is carried.
- You can log an event recording the user-seen state and give it a unique ID; later events cite that ID to record the causal dependency.
- Conflict-resolution algorithms (Chapter 6) help for unexpectedly ordered events — good for keeping state, but useless when the action has an external effect (like sending a notification).
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: علت روی دو پارتیشن
وضعیت اولیه: رویداد ایجاد سفارش ۴۲ روی پارتیشن A و لغو آن روی B قرار گرفته است.
اجرای مرحلهبهمرحله: مصرفکننده میتواند لغو را پیش از ایجاد ببیند؛ همپارتیشن کردنِ رخدادهای همان سفارش یا نگهداری وابستگی این مسئله را کنترل میکند.
نتیجه و دلیل: رخدادهای مستقل الزاماً ترتیب جهانی نمیخواهند؛ رخدادهای وابسته بدون قرارداد ترتیب، state نامعتبر میسازند.
این یک درختِ تصمیم است. ریشه Event Ordering Needed است با زمینهٔ
«total order = consensus» — یعنی همان اول یادآوری میکند که داریم دربارهٔ
مسئلهای بهسختیِ اجماع حرف میزنیم. سوالِ اول: Single Node Ordering?
(«one node must order all»). اگر جواب بله است، مستقیم میروی به برگِ
Total Order Log («single leader; simplest capped
throughput») — سادهترین راه، با سقفِ توانِ یک ماشین.
اگر نه، سوالِ دوم: Same-Object Updates?
(«ordering only per key»). بله یعنی فقط بهروزرسانیهای یک آبجکت باید مرتب باشند،
پس برگِ Shard by Object ID («one shard per key no
cross-shard order») — هر کلید به shard خودش، بدونِ هیچ ترتیبی بینِ shardها.
اگر مسئله فراتر از یک کلید است، سوالِ سوم: Causal Order Needed?
(«or are events unlinked?»). اگر واقعاً پیوندِ علّی نیست، برگِ
Order Arbitrarily («no causal link concurrent is
fine») — رویدادهای همزمان را دلخواه مرتب کن. و اگر علّیت لازم است، سوالِ چهارم:
Reference Seen State? («log seen state, cite ID»).
بله میشود برگِ Causal References («record the dependency
cite the seen event») — همان ایدهٔ «ثبتِ چیزی که کاربر دیده بود». نه هم میشود برگِ
Logical Timestamps («handle out-of-order side effects
unsafe») — با timestamp منطقی جلو میروی ولی میدانی اثرِ بیرونی هنوز ناامن است.
نکتهٔ خواندنِ درخت این است: هرچه به راست میروی، از «ترتیبِ قوی و گران» به «ترتیبِ ضعیف و ارزان»
میرسی؛ و هر برگ دقیقاً میگوید چه چیزی را فدا کردی و چه چیزی را نگه داشتی.
This is a decision tree. The root is Event Ordering Needed with the context “total order = consensus” — reminding you upfront that we're discussing a consensus-hard problem. First question: Single Node Ordering? (“one node must order all”). Yes goes straight to the Total Order Log leaf (“single leader; simplest, capped throughput”) — the simplest path, capped at one machine's power.
If not, second question: Same-Object Updates? (“ordering only per key”). Yes means only one object's updates must be ordered, so the Shard by Object ID leaf (“one shard per key, no cross-shard order”) — each key to its own shard, with no order between shards.
If the problem spans keys, third question: Causal Order Needed? (“or are events unlinked?”). If truly no causal link, the Order Arbitrarily leaf (“no causal link, concurrent is fine”) — order concurrent events arbitrarily. And if causality is needed, fourth question: Reference Seen State? (“log seen state, cite ID”). Yes becomes the Causal References leaf (“record the dependency, cite the seen event”) — the “record what the user had seen” idea. No becomes the Logical Timestamps leaf (“handle out-of-order, side effects unsafe”) — proceed with logical timestamps knowing external effects are still unsafe.
The way to read the tree: moving right goes from “strong, expensive order” to “weak, cheap order”; and each leaf says exactly what you sacrificed and what you kept.
۲2 batch و stream در کنار همBatch and stream together
۲.۱. حفظِ state مشتقشده2.1. Maintaining derived state
نمای مشتقشده نتیجهٔ تبدیلِ دادهٔ مبناست. برای حفظ آن باید تغییرهای جدید را اعمال کنیم و برای اصلاحِ منطق، امکانِ بازسازی از تاریخچه یا تصویر معتبر داشته باشیم.
مثال فنی: اصلاحِ سهم یک سفارش
وضعیت اولیه: گزارش روزانه جمع ۱۰۰۰ دارد و مبلغ سفارش ۴۲ از ۲۰۰ به ۲۵۰ اصلاح میشود.
اجرای مرحلهبهمرحله: پردازشگر سهم قبلی را کم و سهم جدید را اضافه میکند، یا تفاوت ۵۰ را طبق نسخهٔ معتبر اعمال میکند.
نتیجه و دلیل: افزودن کورکورانهٔ ۲۵۰، گزارش را ۱۲۵۰ میکند؛ نگهداری نمای مشتق به معنای دقیق update و delete نیاز دارد.
Integration's goal is data sitting in the right shape in all the right places: taking input, transforming, joining, filtering, aggregating, training models, evaluating, and writing to the right output. The tools are Chapters 11–12's batch and stream processors; and their outputs are those derived datasets: search indexes, materialized views, recommendations, aggregate metrics. We know their fundamental difference: stream input is unbounded, batch input bounded and known.
پردازشِ batch یک طعمِ قویِ functional دارد: تابعهای قطعیِ خالص که خروجیشان فقط به ورودی بستگی
دارد و جز خروجیِ صریح، side effect ندارند؛ ورودی immutable و خروجی append-only. پردازشِ stream هم
همین است، فقط عملگرها را گسترش میدهد تا state مدیریتشده و fault-tolerant هم داشته باشند.
این اصلِ «تابعِ قطعی با ورودی/خروجیِ مشخص» فقط برای تحملِ خطا خوب نیست؛ استدلال دربارهٔ dataflowهای
یک سازمان را هم ساده میکند: فرقی نمیکند مشتق ایندکس باشد یا مدلِ آماری یا کش — همه خطِ لولهایاند
که چیزی را از چیزِ دیگری مشتق میکنند.
Batch processing has a strong functional flavor: pure deterministic functions whose output depends only on input, with no side effects but the explicit output; immutable input and append-only output. Stream processing is the same, only extending the operators to have managed, fault-tolerant state too. This “deterministic function with clear input/output” principle isn't just good for fault tolerance; it simplifies reasoning about an organization's dataflows: derived index, statistical model or cache — all are pipelines deriving something from something else.
در اصل میشد سیستمِ مشتق را همگام نگه داشت — همانطور که پایگاه دادهِ رابطهای ایندکسِ ثانویه را در
همان تراکنشِ نوشتن بهروز میکند. ولی ناهمگامی همان چیزی است که سیستمِ لاگمحور را قوی میکند:
خطا در یک گوشه مهار میشود؛ درحالیکه تراکنشِ توزیعشده اگر یک شرکتکنندهاش بمیرد abort میشود و خطا
را به بقیه سرایت میدهد. همینطور ایندکسِ ثانویهای که مرزِ shard را رد میکند، قابلاعتمادترین و
مقیاسپذیرترین نگهداریاش همان نگهداریِ ناهمگام است.
In principle the derived system could be kept synchronous — as a relational database updates its secondary index in the same write transaction. But asynchrony is exactly what makes the log-based system strong: errors in one corner stay contained; while a distributed transaction aborts if one participant dies, spreading the error to the rest. Likewise the most reliable, scalable way to maintain a shard-crossing secondary index is asynchronous maintenance.
۲.۲. مهاجرتِ تدریجیِ نماها2.2. Gradual view migration
برای تغییرِ نمای داده بدون قطع خدمت، نسخهٔ تازه را کنارِ نسخهٔ فعلی میسازیم، تغییرهای زنده را به آن میرسانیم و پس از بررسی، خواندن را منتقل میکنیم.
مثال فنی: مهاجرت ایندکس بدون حفره
وضعیت اولیه: search-v1 فعال است و v2 باید فیلد برند داشته باشد.
اجرای مرحلهبهمرحله: snapshot تا موقعیت ۵۰۰ در v2 بارگذاری میشود؛ تغییرهای بعدی catch up میشوند؛ پس از تطبیق، خواندن به v2 منتقل میشود.
نتیجه و دلیل: تغییرهای حین backfill گم نمیشوند؛ cutover پیش از رسیدن به مرز لازم میتواند نمای کهنه را فعال کند.
Streams reflect input changes in derived views with low latency; batch lets you reprocess the accumulated historical volume and build a fresh view of the existing dataset. And reprocessing is a good mechanism for application evolution: without it, schema evolution stays limited to simple changes (a fresh optional field, a fresh record); with it, a dataset can be rebuilt into a wholly different model that serves the fresh need better.
قیاسِ ریلِ راهآهن — مهاجرتِ schema در دنیای غیرکامپیوتریThe railway-track analogy — schema migration in the non-computer world
در انگلستانِ قرنِ ۱۹ چند استانداردِ رقیب برای gauge (فاصلهٔ دو ریل) بود؛ قطارِ یک gauge روی
ریلِ gauge دیگر نمیرفت. بعد از تثبیتِ gauge استاندارد در ۱۸۴۶، باید ریلها تبدیل میشدند — ولی چطور،
بدونِ بستنِ خط برای ماهها؟ راهحل: اول ریل را با افزودنِ ریلِ سوم به dual gauge تبدیل کردند؛
این کار تدریجی انجام شد و وقتی تمام شد، قطارهای هر دو gauge میتوانستند روی خط بروند. سرانجام که همهٔ
قطارها به gauge استاندارد تبدیل شدند، ریلِ اضافه برداشته شد. «بازپردازشِ» ریلها و نگهداشتنِ نسخهٔ قدیم
و جدید کنار هم، تغییر را در طولِ سالها ممکن کرد — و هر مرحله برگشتپذیر بود.
In 19th-century England several competing standards existed for gauge (rail distance); a train of one gauge couldn't run on another gauge's rails. After the standard gauge was fixed in 1846, the rails had to convert — but how, without closing lines for months? The fix: first convert rails to dual gauge by adding a third rail; this was done gradually, and when finished, trains of both gauges could run on the line. Once all trains converted to the standard gauge, the extra rail was removed. “Reprocessing” the rails and keeping old and new versions side by side made the change possible over years — and every step was reversible.
نماهای مشتق همین اجازه را میدهند: اگر میخواهی dataset را بازسازی کنی، لازم نیست مهاجرت یک
سوییچِ ناگهانی باشد. schema قدیم و جدید را بهصورتِ دو نمایِ مستقلِ مشتق از همان دادهٔ
زیرین کنار هم نگه دار؛ بعد تعدادِ کمی کاربر را به نمای تازه ببر تا عملکردش را بسنجی و باگش را
پیدا کنی، درحالیکه اکثر کاربران هنوز به نمای قدیم میروند. تدریجاً سهمِ نمای تازه را زیاد کن و در
پایان نمای قدیم را حذف کن. زیباییاش این است که هر مرحله بهراحتی برگشتپذیر است؛ همیشه یک
سیستمِ سالم داری که به آن برگردی. و کمکردنِ ریسکِ آسیبِ برگشتناپذیر، جرئتِ جلورفتن و سرعتِ بهبود را
بیشتر میکند.
Derived views allow the same: to rebuild a dataset, migration needn't be a sudden switch. Keep old and new schemas as two independent views derived from the same underlying data side by side; then move a few users to the fresh view to gauge its performance and find its bugs while most users still go to the old view. Gradually raise the fresh view's share and drop the old view at the end. Its beauty is that every step easily reverses; you always have a healthy system to return to. And lowering the risk of irreversible damage increases the courage to move forward and the speed of improvement.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: مهاجرت ایندکس بدون حفره
وضعیت اولیه: search-v1 فعال است و v2 باید فیلد برند داشته باشد.
اجرای مرحلهبهمرحله: snapshot تا موقعیت ۵۰۰ در v2 بارگذاری میشود؛ تغییرهای بعدی catch up میشوند؛ پس از تطبیق، خواندن به v2 منتقل میشود.
نتیجه و دلیل: تغییرهای حین backfill گم نمیشوند؛ cutover پیش از رسیدن به مرز لازم میتواند نمای کهنه را فعال کند.
این یک ماشینِ وضعیت با دو lane است: Migration stages (مسیرِ اصلی) و
Reversible exit (خروجیِ پایانی). مسیرِ اصلی پنج گامِ شمارهدار دارد:
Old View Only («old schema serves all users» —
نمای قدیم دارد همه را سرو میکند)؛ بعد با فلشِ «reprocess history + keep
both» به Dual Views میروی («new view built by
reprocessing»، زمینه: «both views kept side by side») —
این همان «ریلِ سوم» است: نمای تازه با بازپردازش ساخته شده ولی قدیمی هنوز سر جایش است.
بعد با «shift a few users» به Canary میرسی
(«small % of users on new view» — پیداکردنِ باگ و سنجشِ عملکرد)،
با «no bugs found» به Ramping Up
(«gradually increase the share»)، و با «100% migrated,
drop old» به New View Only («old view
dropped» — مهاجرت تمام).
و نکتهٔ مهم: از دو گامِ میانی (Canary و Ramping Up) فلشِ «problems found»
به Rolled Back میرود («revert to old view» —
«every stage is reversible»). یعنی در هر نقطه که بفهمی اشتباه شده،
به نمای قدیم برمیگردی؛ هیچ مرحلهای نقطهٔ بیبازگشت نیست.
A state machine with two lanes: Migration stages (the main path) and Reversible exit (the final outlet). The main path has five numbered steps: Old View Only (“old schema serves all users”); then via “reprocess history + keep both” to Dual Views (“new view built by reprocessing”, context: “both views kept side by side”) — the “third rail”: the fresh view is built by reprocessing while the old one stays put.
Then via “shift a few users” to Canary (“small % of users on new view” — finding bugs, gauging performance), via “no bugs found” to Ramping Up (“gradually increase the share”), and via “100% migrated, drop old” to New View Only (“old view dropped” — migration done).
And the key point: from the two middle steps (Canary and Ramping Up) a “problems found” arrow goes to Rolled Back (“revert to old view” — “every stage is reversible”). At any point you realize something's wrong, you return to the old view; no step is a point of no return.
۲.۳. از lambda تا kappa2.3. From lambda to kappa
معماری Lambda معمولاً مسیرِ دستهای و زندهٔ جدا دارد؛ Kappa میکوشد بازپخشِ تاریخچه و پردازشِ زنده را با مسیرِ مشترک انجام دهد. مناسب بودن هرکدام به نیاز و محدودیتها بستگی دارد.
مثال فنی: یک منطق برای تاریخچه و زنده
وضعیت اولیه: جمع فروش هم از فایل تاریخی و هم از جریان تازه محاسبه میشود.
اجرای مرحلهبهمرحله: اگر دو کد قواعد refund متفاوت داشته باشند، خروجیها واگرا میشوند؛ منطق مشترک با replayِ ورودی نسخهدار این اختلاف را کم میکند.
نتیجه و دلیل: kappa فقط «stream همهچیز» نیست؛ تاریخچهٔ کافی، بازپردازش و کنترل state و اثرهای بیرونی هم لازماند.
The original proposal for integrating batch and stream was the lambda architecture, which had its own problems and is retired. Newer systems let batch computation (history reprocessing) and stream computation (fresh-event processing) be implemented in the same one system — an approach sometimes called kappa. Unifying in one system needs three things:
- بازپخشِ تاریخچه از همان موتورِ پردازشی که جریانِ رویدادهای تازه را میخورد (بروکرِ لاگمحور که replay بلد است؛ پردازندهای که از DFS/object store هم میخواند).
- semantics دقیقاً-یکبار برای پردازندهٔ stream — یعنی خروجی همان شود که انگار هیچ خطایی رخ نداده، حتی اگر رخ داده؛ مثلِ batch، با دورریختنِ خروجیِ ناقصِ taskهای شکستخورده.
- windowing بر اساسِ event time، نه processing time — چون هنگامِ بازپردازشِ تاریخچه، زمانِ پردازش بیمعناست (مثلِ APIی Beam که روی Flink یا Dataflow اجرا میشود).
- History replay from the same processing engine that eats the fresh-event flow (a log-based broker that knows replay; a processor that also reads from DFS/object stores).
- Exactly-once semantics for the stream processor — output identical to as if no error ever occurred, even when some did; like batch, by discarding broken tasks' partial output.
- Windowing on event time, not processing time — since when reprocessing history, processing time is meaningless (like the Beam API running on Flink or Dataflow).
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: یک منطق برای تاریخچه و زنده
وضعیت اولیه: جمع فروش هم از فایل تاریخی و هم از جریان تازه محاسبه میشود.
اجرای مرحلهبهمرحله: اگر دو کد قواعد refund متفاوت داشته باشند، خروجیها واگرا میشوند؛ منطق مشترک با replayِ ورودی نسخهدار این اختلاف را کم میکند.
نتیجه و دلیل: kappa فقط «stream همهچیز» نیست؛ تاریخچهٔ کافی، بازپردازش و کنترل state و اثرهای بیرونی هم لازماند.
کارتِ چپ (صورتی) معماریِ بازنشستهٔ lambda است: لایهٔ بچ همهٔ تاریخچه را بازپردازش میکند، لایهٔ سرعت فقط رویدادهای تازه را میخورد و لایهٔ سرو هر دو نما را merge میکند — یعنی دو مسیرِ کد، دو برابر باگ، و منطقِ mergeای که هیچوقت کاملاً درست نیست. پیکانِ میانی میگوید بهجایش یک موتور بگذار. کارتِ راست (سبز) رویکردِ kappa است: یک موتورِ استریم هر دو کار را میکند — تاریخچه را با بازپخش از همان موتور میخوانَد و رویدادهای تازه مثلِ همیشه جاریاند.
نوارِ پایینی سه شرطِ kappa را شماره میکند: بازپخشِ تاریخچه از همان موتور، semantics دقیقاً-یکبار (دورریختنِ خروجیِ ناقص مثلِ بچ)، و پنجرهبندی با event time — چون موقعِ بازپخش زمانِ پردازش بیمعناست.
The left card (pink) is the retired lambda architecture: the batch layer reprocesses all history, the speed layer eats only fresh events, and the serving layer merges both views — two code paths, double bugs, and merge logic never quite right. The middle arrow says put one engine instead. The right card (green) is the kappa approach: one stream engine does both jobs — history is read by replaying through that same engine while fresh events flow as usual.
The bottom strip numbers kappa's three conditions: history replay from the same engine, exactly-once semantics (discarding partial output like batch), and event-time windowing — since processing time is meaningless on replay.
۳3 unbundle کردنِ پایگاه دادهUnbundling the database
باز کردنِ اجزای پایگاه داده یعنی وظایفی مانند ذخیره، ایندکس و پردازش را میان ابزارهای جدا تقسیم کنیم. در مقابل، مسئولیت هماهنگیِ این اجزا بیشتر به معماریِ برنامه منتقل میشود.
مثال فنی: قطعات جدا، قرارداد مشترک
وضعیت اولیه: پایگاه داده سفارش را نگه میدارد و موتور دیگری جستوجوی متن را خدمت میدهد.
اجرای مرحلهبهمرحله: لاگ تغییر، مرز snapshot، موقعیت مصرف و مسیر بازسازی بین دو قطعه تعریف میشود.
نتیجه و دلیل: جدا کردن قابلیتهای پایگاه داده انعطاف میدهد، اما هماهنگیای را که موتور یکپارچه انجام میداد باید صریحاً طراحی کنیم.
At an abstract level, a database, a batch/stream processor and an operating system do one job: hold data and let you process and query it. A database keeps data in the records of a data model, a filesystem in files — but at their core both are “information management systems”; and the batch processor (Chapter 11) is like the distributed version of Unix. Real practical differences exist, of course (a directory with 10 million tiny files is a disaster for many filesystems; a table with 10 million tiny records is ordinary for a database). But the similarities and differences of operating systems and databases are worth exploring.
Unix و پایگاه دادهِ رابطهای با دو فلسفهٔ متفاوت سراغِ مدیریتِ اطلاعات رفتند: Unix یک انتزاعِ
سطحپایینِ سختافزار به برنامهنویس داد؛ پایگاه دادهِ رابطهای یک انتزاعِ سطحبالا که پیچیدگیِ
ساختارِ دیسک، همزمانی، بازیابی از crash را پنهان میکند. Unix لوله و فایل ساخت (دنبالهٔ بایت)؛ پایگاه داده
SQL و تراکنش. کدام بهتر است؟ بستگی دارد چه میخواهی. Unix «سادهتر» است به این معنا که پوششِ نازکی
روی سختافزار است؛ پایگاه دادهِ رابطهای «سادهتر» است به این معنا که یک کوئریِ اعلانیِ کوتاه میتواند از
زیرساختِ قدرتمندی (بهینهساز، ایندکس، کنترلِ همزمانی، replication) استفاده کند بیآنکه نویسندهٔ کوئری
جزئیاتِ پیادهسازی را بداند. این کشمکشِ چنددههای هنوز حل نشده (جنبشِ NoSQL را میشود تلاش برای بردنِ
رویکردِ سطحپایینِ سبکِ Unix به قلمروِ ذخیرهسازیِ توزیعشدهٔ OLTP دانست). این بخش میخواهد آن دو را
آشتی دهد تا بهترینِ هر دو را با هم داشته باشیم.
Unix and the relational database took two different philosophies to information management: Unix gave the programmer a low-level hardware abstraction; the relational database a high-level one hiding disk layout, concurrency, crash-recovery complexity. Unix built pipes and files (byte sequences); the database SQL and transactions. Which is better? Depends what you want. Unix is “simpler” in being a thin cover over hardware; the relational database is “simpler” in that a short declarative query can use powerful machinery (optimizer, indexes, concurrency control, replication) without the query author knowing implementation details. This decades-long tension is still unresolved (the NoSQL movement can be seen as trying to bring Unix-style lightweight low-level approaches into distributed OLTP storage). This section wants to reconcile the two and have the best of both.
۳.۱. ساختِ ایندکس یعنی بازپردازش3.1. Building an index is reprocessing
ساختِ ایندکسِ تازه، نوعی پردازشِ دوبارهٔ دادهٔ موجود است. بعد از ساخت اولیه باید تغییرهای همزمان هم برسند تا ایندکس هنگام استفاده از دادهٔ مرجع عقب نمانده باشد.
مثال فنی: افزودن فیلتر یعنی backfill
وضعیت اولیه: ایندکس فعال فقط عنوان کالا را دارد و فیلتر برند لازم شده است.
اجرای مرحلهبهمرحله: همهٔ کالاهای قدیمی با کدِ تازه تبدیل میشوند؛ تغییرهای زنده هم تا مرز مشخص وارد ایندکس جدید میشوند.
نتیجه و دلیل: افزودن ستون به schema بدون پردازش دادهٔ قبلی، مقدار برند را برای رکوردهای قدیمی ایجاد نمیکند.
Think of database features: secondary indexes, materialized views (precomputed caches of query results), replication logs, full-text indexes. And in Chapters 11–12 we talked about building exactly these with batch and stream. There seems a deep parallel between “internal database feature” and “external derived system”.
ببین وقتی CREATE INDEX را در پایگاه دادهِ رابطهای اجرا میکنی چه میشود: پایگاه داده
باید روی یک snapshot سازگار از جدول بچرخد، مقادیرِ فیلدِ ایندکسشده را بردارد، مرتب کند و ایندکس را
بنویسد؛ بعد backlog نوشتههایی که از زمانِ snapshot آمدهاند را پردازش کند (فرض بر اینکه جدول قفل
نبوده و نوشتن ادامه داشته)؛ و از آن به بعد ایندکس را با هر تراکنشِ نوشتن بهروز نگه دارد.
این فرآیند بهطرزِ شگفتی شبیه است به راهانداختنِ یک follower تازه (فصل ۶) و خیلی شبیه است به
bootstrap کردنِ CDC در سیستمِ جریانی. هر بار که CREATE INDEX میزنی، پایگاه داده
عملاً dataset موجود را بازپردازش میکند و ایندکس را بهعنوانِ نمای تازهای از دادهٔ موجود مشتق میکند.
Watch what happens when you run CREATE INDEX in a relational database: the database must scan a consistent snapshot of the table, take the indexed field's values, sort them and write the index; then process the backlog of writes that arrived since the snapshot (assuming the table wasn't locked and writes continued); and from then on keep the index fresh with every write transaction. This process is surprisingly similar to starting a fresh follower (Chapter 6) and very similar to bootstrapping CDC in a streaming system. Every time you run CREATE INDEX, the database effectively reprocesses the existing dataset and derives the index as a fresh view of existing data.
از این زاویه، جریانِ دادهٔ کلِ یک سازمان شبیه یک پایگاه دادهِ خیلی بزرگ میشود: هر فرآیندِ batch،
stream یا ETL که داده را از جایی و شکلی به جای و شکلِ دیگری میبرد، دارد مثلِ همان زیرسیستمِ پایگاه داده
عمل میکند که ایندکس یا viewی materialized را بهروز نگه میدارد. پردازندههای batch و stream میشوند
پیادهسازیهای مفصلی از trigger و stored procedure و الگوریتمِ نگهداریِ viewی materialized؛ و
سیستمهای مشتقشدهای که نگه میدارند میشوند «تیپهای ایندکسِ» متفاوت. فرقش این است که بهجای اینکه
همه داخلِ یک محصولِ یکپارچه باشند، تکهتکهاند: نرمافزارهای متفاوت، روی ماشینهای متفاوت، دستِ تیمهای متفاوت.
From this angle, a whole organization's dataflow looks like one very large database: every batch, stream or ETL process moving data from one place and shape to another acts like the database subsystem keeping an index or materialized view fresh. Batch and stream processors become elaborate implementations of triggers and stored procedures and the materialized-view maintenance algorithm; and the derived systems they maintain become different “index types”. The difference is that instead of all living inside one integrated product, they're fragmented: different software, on different machines, owned by different teams.
۳.۲. federated در برابر unbundled3.2. Federated vs unbundled
در جستوجوی فدره، یک درخواست از چند منبع داده میخواند و نتیجهها را ترکیب میکند. در معماریِ اجزای جدا، تغییرها از قبل میان ابزارها جریان دارند و نماهای مناسب ساخته میشوند.
مثال فنی: خواندن مشترک یا نوشتن مشترک
وضعیت اولیه: گزارش فروش به سفارش در A و موجودی در B نیاز دارد.
اجرای مرحلهبهمرحله: federation هنگام query از دو منبع میخواند؛ unbundling تغییرهای منابع را از قبل به نمای خدمتدهی میرساند.
نتیجه و دلیل: اولی هزینه و سازگاریِ خواندنِ همزمان و دومی هزینهٔ نگهداری و تازگیِ نما را دارد.
If you accept that no single data model or storage format fits every access pattern, there are two ways to combine different tools into one coherent system:
Federated databases (unifying reads)
در خواندنِ فدره، یک درخواست به چند منبع میرود و پاسخها کنار هم قرار میگیرند. معنای زمان و سازگاریِ پاسخها باید روشن باشد.
مثال فنی: Joinِ دوردست روی دو لحظه
وضعیت اولیه: query ابتدا موجودی را از A و سپس سفارشها را از B میخواند؛ بین این دو خرید تازه ثبت میشود.
اجرای مرحلهبهمرحله: بدون snapshot مشترک، نتیجه میتواند موجودیِ قبل و سفارشِ بعد را ترکیب کند.
نتیجه و دلیل: رابط query یکپارچه خودبهخود دیدگاه زمانیِ یکپارچه نمیدهد؛ تضمین snapshot بین منابع باید صریح باشد.
یک رابطِ کوئریِ یکپارچه روی موتورهای ذخیرهسازی و روشهای پردازشِ متنوع —
مثلِ foreign data wrapper در PostgreSQL و موتورهای federated مثلِ
Trino. اپی که مدلِ تخصصی میخواهد مستقیم به موتورِ زیرین وصل میشود؛
کسی هم که میخواهد از جاهای متفاوت ترکیب کند، از رابطِ federated استفاده میکند.
این راه، سنتِ رابطهای است: یک سیستمِ یکپارچه با زبانِ سطحبالا و semantic تمیز، ولی پیادهسازیِ پیچیده.
A unified query interface over diverse storage engines and processing methods — like foreign data wrappers in PostgreSQL and federated engines like Trino. Apps wanting a specialized model connect directly to the underlying engine; whoever wants to combine from different places uses the federated interface. This is the relational tradition: one integrated system with a high-level language and clean semantics, but complex implementation.
Unbundled databases (unifying writes)
در یکپارچهسازیِ مسیرِ تغییر، دادهٔ مرجع از قبل به ابزارهای تخصصی منتقل میشود تا هرکدام نمایشِ مناسبِ خواندن را نگه دارند.
مثال فنی: جریانِ نوشتن به دو مقصد
وضعیت اولیه: تغییر P7, version=12 باید هم به کش و هم به جستوجو برسد.
اجرای مرحلهبهمرحله: هر مقصد پیشرفت خودش را ثبت میکند و میتواند از لاگِ باقیمانده catch up کند؛ نسخهٔ قدیمی نباید نسخهٔ تازه را بازنویسی کند.
نتیجه و دلیل: مقصدها میتوانند موقتاً متفاوت باشند، ولی قرارداد ترتیب و replay امکان همگامی و ترمیم میدهد.
federation جوابِ کوئریِ فقطخواندنی روی چند سیستم را میدهد، ولی برای همگامکردنِ نوشتنها
روی آن سیستمها جوابِ خوبی ندارد. همانطور که داخلِ یک پایگاه داده «ساختنِ ایندکسِ سازگار» یک ویژگیِ
داخلی است، وقتی چند سیستمِ ذخیرهسازی را ترکیب میکنی هم باید مطمئن شوی همهٔ تغییرها با وجودِ
خطا به همهٔ جاهای درست میرسند. آسانکردنِ اتصالِ قابلاعتمادِ سیستمها (با CDC و لاگِ رویداد)
مثلِ unbundle کردنِ ویژگیِ نگهداریِ ایندکسِ پایگاه داده است — بهشکلی که نوشتنها را روی
فناوریهای متفاوت همگام کند. این راه، سنتِ Unix است: ابزارهای کوچکِ تککاره که با یک API
یکنواختِ سطحپایین (لوله) حرف میزنند و با زبانی سطحبالاتر (شل) ترکیب میشوند.
Federation answers read-only queries over several systems, but has no good answer for synchronizing writes across them. Just as “building a consistent index” inside one database is an internal feature, when combining several storage systems you must ensure every change reaches all the right places despite errors. Easing reliable system-to-system connection (with CDC and event logs) is like unbundling the database's index-maintenance feature — so that writes stay in sync across different technologies. This is the Unix tradition: small single-purpose tools speaking one uniform low-level API (pipes) and composed with a higher-level language (the shell).
۳.۳. چرا unbundle جواب میدهد3.3. Why unbundling works
این تفکیک زمانی مفید است که ابزارهای تخصصی با قراردادِ روشن به هم وصل شوند. لاگ، بازپخش و قواعدِ تکرار و ترتیب، امکانِ ترمیم و بازسازی را فراهم میکنند.
مثال فنی: خرابیِ نمای قابلبازسازی
وضعیت اولیه: shard جستوجو پاک شده، اما مرجع و تاریخچهٔ لازم سالماند.
اجرای مرحلهبهمرحله: نمای تازه از snapshot و تغییرهای پس از مرز آن ساخته و پیش از فعال شدن بررسی میشود.
نتیجه و دلیل: انعطاف unbundling به بازسازی وابسته است؛ بدون نگهداری ورودی و نسخهٔ تبدیل، از دست دادن نما ممکن است جبرانپذیر نباشد.
Federation and unbundling are two sides of one coin: building a reliable, scalable system from diverse parts. Mapping one data model to another (for federated read-only queries) takes thought but is ultimately a manageability problem. The hard engineering problem is keeping writes in sync across several storage systems — and that is our focus here. Its traditional approach is distributed transactions over heterogeneous systems, which is problematic; transactions inside one storage system or stream are feasible, but when data crosses the boundary of two different technologies, the asynchronous event log with idempotent writes is far stronger and more practical. E.g. distributed transactions inside some stream processors work well for exactly-once semantics; but when a transaction must involve systems written by different groups (writing from a stream processor to a distributed key-value store or a search index), the lack of a standard transaction protocol makes integration hard — and the ordered log with an idempotent consumer is a far simpler abstraction, implementable across heterogeneous systems.
مزیتِ بزرگِ یکپارچهسازیِ لاگمحور، پیوندِ سست بینِ قطعات است، در دو سطح:
The big advantage of log-based integration is loose coupling between parts, on two levels. At the system level: asynchronous flows make the whole system resilient to one part's outage or slowness — if a consumer slows or dies, the log buffers the messages; producer and other consumers continue unaffected; the broken consumer catches up when fixed, no data is lost, and the error stays contained. At the human level: unbundling lets different teams develop and maintain parts and services independently of each other.
- سطحِ سیستم: جریانهای ناهمگام کلِ سیستم را در برابرِ outage یا کندیِ یک قطعه قوی میکنند. اگر مصرفکنندهای کند شد یا مرد، لاگ پیامها را بافر میکند؛ تولیدکننده و بقیهٔ مصرفکنندهها بیتأثیر ادامه میدهند. مصرفکنندهٔ خراب وقتی درست شد catch-up میکند، دادهای از دست نمیرود و خطا مهار میماند. در مقابل، تعاملِ همگامِ تراکنشِ توزیعشده خطای محلی را به خرابیِ بزرگ تشدید میکند.
- سطحِ انسانی: unbundle اجازه میدهد قطعات و سرویسها را تیمهای متفاوت، مستقل از هم توسعه و نگهداری کنند. تخصصیشدن یعنی هر تیم روی «یک کارِ خوب» تمرکز کند با رابطِ مشخص به سیستمِ تیمهای دیگر. لاگِ رویداد رابطی است که همزمان هم بهقدرِ کافی قدرتمند است (دوام و ترتیبِ رویدادها → ویژگیِ سازگاریِ نسبتاً قوی) و هم بهقدرِ کافی عمومی که تقریباً برای هر نوع دادهای کاربرد دارد.
- System level: asynchronous flows harden the whole system against one part's outage or slowness. If a consumer slows or dies, the log buffers messages; the producer and other consumers continue unaffected. The broken consumer catches up when fixed, no data is lost and the error stays contained. Synchronous distributed-transaction interaction, by contrast, amplifies a local error into a large failure.
- Human level: unbundling lets different teams develop and maintain parts and services independently. Specialization means each team focuses on “one job well” with a clear interface to other teams' systems. The event log is an interface simultaneously powerful enough (event durability and order → fairly strong consistency properties) and general enough for almost any data type.
unbundle به چه معنا نیستWhat unbundling is not
اگر unbundle آینده باشد، جای پایگاه دادهها را به شکلِ فعلیشان نمیگیرد. پایگاه دادهها همچنان لازماند —
برای نگهداشتنِ state در پردازندههای stream و برای سروِ خروجیِ پردازشهای batch و stream.
موتورهای کوئریِ تخصصی هم برای workload خودشان مهم میمانند (موتورِ warehouse برای کوئریِ تحلیلیِ اکتشافی
عالی است). پیچیدگیِ گرداندنِ چند زیرساخت هم واقعی است: هر نرمافزار منحنیِ یادگیری و قلقِ عملیاتیِ خودش
را دارد، پس باید تا میشود قطعاتِ متحرکِ کمتری داشت. محصولِ یکپارچهٔ واحد ممکن است روی workload
هدفش عملکردِ بهتر و قابلپیشبینیتری بدهد. ساختن برای مقیاسی که لازم نداری، تلاشِ هدررفته و قفلشدن در
طراحیِ خشک است — نوعی بهینهسازیِ زودرس. هدفِ unbundle رقابت با تکتکِ پایگاه دادهها روی عملکرد نیست؛
هدف ترکیبِ چند پایگاه داده برای پوششِ طیفِ خیلی وسیعتری از workloadهاست. بحث پهناست، نه عمق.
پس اگر یک فناوری همهٔ نیازت را میدهد، همان را بگیر و خودت از نو نساز؛ مزیتِ unbundle فقط وقتی
بیرون میزند که هیچ تکهنرمافزاری بهتنهایی همهٔ نیازها را ندهد. خبرِ خوب: ابزارهای ترکیب دارند بهتر
میشوند — Debezium از خیلی پایگاه دادهها جریانِ تغییر میگیرد، پروتکلِ Kafka دارد استانداردِ de facto
جریانِ رویداد میشود، و موتورهای نگهداریِ نمایِ افزایشی کشِ کوئریهای پیچیده را ازپیشمحاسبه و بهروز میکنند.
If unbundling is the future, it doesn't replace databases as such. Databases are still needed — for holding state in stream processors and for serving batch/stream processing outputs. Specialized query engines stay important for their own workload (the warehouse engine is excellent for exploratory analytical queries). The complexity of running several infrastructures is real too: each software has its own learning curve and operational quirks, so keep the moving parts as few as possible. A single integrated product may give better, more predictable performance on its target workload. Building for scale you don't need is wasted effort locked into rigid design — a kind of premature optimization. Unbundling's goal isn't competing with each database on performance; it's combining several databases to cover a far wider workload range. The debate is breadth, not depth.
So if one technology covers all your needs, take it and don't rebuild it yourself; unbundling's advantage only shows when no single piece of software covers every need. Good news: composition tools keep improving — Debezium streams changes from many databases, the Kafka protocol is becoming the de facto event-streaming standard, and incremental-view engines precompute and refresh caches of complex queries.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: خواندن مشترک یا نوشتن مشترک
وضعیت اولیه: گزارش فروش به سفارش در A و موجودی در B نیاز دارد.
اجرای مرحلهبهمرحله: federation هنگام query از دو منبع میخواند؛ unbundling تغییرهای منابع را از قبل به نمای خدمتدهی میرساند.
نتیجه و دلیل: اولی هزینه و سازگاریِ خواندنِ همزمان و دومی هزینهٔ نگهداری و تازگیِ نما را دارد.
دو کارت دو راهِ ترکیبِ ابزارها را میگوید. کارتِ چپ (آبی) federated است: یک رابطِ کوئریِ یکپارچه (مثلِ Trino و wrapperهای خارجیِ PostgreSQL) که خواندنها را یکپارچه میکند — سنتِ رابطهای با semantic تمیز ولی پیادهسازیِ پیچیده. کارتِ راست (سبز) unbundled است: لاگِ رویداد و CDC که نوشتنها را روی فناوریهای متفاوت همگام میکند — همان unbundle کردنِ نگهداریِ ایندکسِ پایگاه داده، به سبکِ Unix با ابزارهای کوچک و API لولهمانند.
برچسبِ میانی («نوشتنها بخشِ سختاند») و نوارِ پایینی حکم میدهند: مسئلهٔ سخت همگامنگهداشتنِ نوشتنهاست نه نگاشتِ خواندنها؛ لاگِ مرتب + نوشتنِ idempotent از XA روی سیستمهای ناهمگن قویتر است؛ و معیار پهناست نه عمق — پوششِ طیفِ وسیعتری از workloadها.
The two cards state the two ways of combining tools. The left card (blue) is federated: one unified query interface (like Trino and Postgres foreign wrappers) unifying reads — the relational tradition with clean semantics but complex implementation. The right card (green) is unbundled: the event log and CDC syncing writes across different technologies — unbundling the database's index maintenance, Unix-style, with small tools and a pipe-like API.
The middle label (“writes are the hard part”) and the bottom strip rule: the hard problem is keeping writes in sync, not mapping reads; an ordered log plus idempotent writes beats XA across heterogeneous systems; and the metric is breadth, not depth — covering a wider workload range.
۴4 طراحی اپلیکیشن حول dataflowDesigning applications around dataflow
در طراحیِ جریانمحور، تغییرِ دادهٔ ورودی نتیجههای وابسته را بهروز میکند. شبیه صفحهگستردهای است که تغییر یک سلول، فرمولهای وابسته را دوباره محاسبه میکند.
مثال فنی: وابستگی خروجیها به داده
وضعیت اولیه: اصلاح سفارش ۴۲، جمع روز، جمع شعبه و ویژگی مدل را عوض میکند.
اجرای مرحلهبهمرحله: dataflow وابستگیها را مشخص میکند تا همان اصلاح به همهٔ خروجیهای وابسته برسد.
نتیجه و دلیل: اگر یک مسیر فقط insert و مسیر دیگر update را بفهمد، نام مشترک «رویداد» آنها را سازگار نمیکند.
The idea of “updating derived data when underlying data changes” isn't new. Spreadsheets have strong dataflow power: you put a formula in a cell (e.g. a column sum) and whenever the formula's input changes the result auto-recomputes — exactly what we want at data-system scale: a database record changes, its index auto-updates, dependent views and aggregations auto-refresh; without worrying about refresh technicalities and trusting it works. The difference from spreadsheets is that today's data system must be fault-tolerant, scalable and durable; and must integrate different technologies written by different groups over time. Expecting all software built with one language and framework is unrealistic.
۴.۱. کدِ اپ بهمثابهٔ تابعِ مشتقسازی4.1. App code as a derivation function
کدِ تبدیل، ورودیِ مرجع را به یک خروجیِ مشتقشده تبدیل میکند. برای بازپخشِ قابلاعتماد، ورودیهای جانبی و رفتارِ غیرقطعی هم باید کنترل شوند.
مثال فنی: تابعِ قابلبازپخش
وضعیت اولیه: رویداد فروش مبلغ دلاری و نرخِ زمان معامله rate=50000 را دارد.
اجرای مرحلهبهمرحله: تبدیل، مبلغ ریالی را با نرخِ همان رویداد محاسبه میکند؛ replay نباید API نرخِ امروز را بخواند.
نتیجه و دلیل: با ورودی و نسخهٔ منطق ثابت، نتیجه قابلتکرار میماند؛ وابستگی پنهان به زمان حال، خروجی تاریخی را تغییر میدهد.
When one dataset derives from another, a transform function sits in the middle. Some examples:
- ایندکسِ ثانویه با تابعِ سادهای ساخته میشود: بهازای هر سطر/سندِ جدولِ پایه، مقادیرِ ستونِ ایندکسشده را برمیدارد و بر اساسِ آنها مرتب میکند (اگر ایندکس SSTable یا B-tree باشد که بر اساسِ کلید مرتباند).
- ایندکسِ full-text با توابعِ پردازشِ زبان (تشخیصِ زبان، سگمنتیشنِ کلمه، stemming، اصلاحِ املا، مترادفیابی) و بعد ساختنِ ساختارِ جستجوی کارا (ایندکسِ معکوس) ساخته میشود.
- در سیستمِ ML میشود مدل را مشتق از دادهٔ آموزشی دانست (با استخراجِ ویژگی و تحلیلِ آماری)؛ و خروجیِ مدل روی ورودیِ تازه هم مشتق از همان ورودی و پارامترهای یادگرفتهشده است (یعنی غیرمستقیم، از دادهٔ آموزشی).
- کش اغلب تجمیعی از داده به همان شکلی است که قرار است در UI نمایش داده شود؛ پس پرکردنِ کش نیازمندِ دانستنِ این است که UI به کدام فیلدها ارجاع میدهد، و تغییرِ UI ممکن است تعریفِ پرکردنِ کش را عوض کند و بازسازیاش را لازم کند.
- A secondary index is built with a simple function: for each row/document of the base table, take the indexed column's values and sort by them (if the index is an SSTable or B-tree, which are key-sorted).
- A full-text index is built with language-processing functions (language detection, word segmentation, stemming, spell correction, synonym finding) then an efficient search structure (inverted index).
- In an ML system the model can be seen as derived from training data (via feature extraction and statistical analysis); and the model's output on fresh input is derived from that input and the learned parameters (i.e. indirectly, from training data).
- A cache is often an aggregation of data in the shape the UI will display; so filling the cache needs knowing which fields the UI references, and a UI change may redefine cache filling and require rebuilding it.
تابعِ مشتقِ ایندکسِ ثانویه آنقدر پرتکرار است که داخلِ خیلی پایگاه دادهها بهعنوانِ ویژگیِ اصلی تعبیه شده و
با یک CREATE INDEX صدایش میزنی. ولی برای full-textِ پیشرفته، تنظیمِ
دامنهخاص لازم است؛ و در ML، مهندسیِ ویژگی بدنام است به application-specific بودن و نیاز به دانشِ
دقیقِ تعاملِ کاربر و استقرار. وقتی تابعِ ساختِ dataset مشتق، تابعِ استانداردِ قالبی نیست، کدِ
سفارشی لازم است — و این دقیقاً جایی است که خیلی پایگاه دادهها به زحمت میافتند: trigger و stored
procedure و UDF دارند، ولی اینها در طراحیِ پایگاه داده کمی «بعداً اضافهشده» بودهاند.
The secondary-index derivation function is so common it's embedded in many databases as a core feature, invoked with one CREATE INDEX. But advanced full-text needs domain-specific tuning; and in ML, feature engineering is notorious for being application-specific, needing precise knowledge of user interaction and deployment. When the derived-dataset-building function isn't a standard template, custom code is needed — and that is exactly where many databases struggle: they have triggers, stored procedures and UDFs, but these were somewhat “bolted on” in database design.
۴.۲. جداییِ کد و state4.2. Separating code and state
جدا کردنِ کد و وضعیت یعنی عمرِ پردازشگر را از عمرِ داده مستقل کنیم. پردازشگر قابلجایگزینی است، اما وضعیت و محلِ ادامه باید در جایی معتبر حفظ شوند.
مثال فنی: کد عوض میشود، state میماند
وضعیت اولیه: پردازشگر جمع فروش تا offset ۱۰۰۱ پیش رفته و state آن پایدار است.
اجرای مرحلهبهمرحله: نسخهٔ تازه با schema سازگارِ state از همان checkpoint ادامه میدهد؛ در تغییر ناسازگار، مهاجرت یا بازسازی لازم است.
نتیجه و دلیل: restart کد نباید جمع را صفر و ورودی را از وسط شروع کند؛ state و موقعیت ورودی باید با هم معنی داشته باشند.
In theory the database could be the deployment environment for arbitrary application code, like the operating system. But in practice it turned out badly: it pairs poorly with modern development needs (dependency and package management, version control, rolling upgrades, evolvability, monitoring, calling network services, integrating with external systems). Cluster deployment and management tools (Kubernetes, Docker, Mesos, YARN and the rest) are built exactly for this — and since they focus on “one job well”, they do it far better than a database for which running UDFs is just one of dozens of features.
اکثر وباپهای امروز بهصورتِ سرویسِ stateless مستقر میشوند: هر درخواستی به هر سروری میتواند
برود و سرور بعد از پاسخ، همهچیز را فراموش میکند. اضافهوکمکردنِ سرور راحت است — ولی state باید
جایی برود (معمولاً پایگاه داده). روند این بوده که منطقِ stateless را از مدیریتِ state جدا نگه دارند: نه
منطقِ اپلیکیشن در پایگاه داده، نه state پایدار در اپلیکیشن. (به شوخیِ اهلِ functional programming:
«ما به جداییِ Church و state اعتقاد داریم» — اشاره به Alonzo Church و lambda calculus که state
mutable ندارد.)
Most web apps today deploy as stateless services: any request can go to any server, and after responding the server forgets everything. Adding/removing servers is easy — but state must go somewhere (usually a database). The trend has been keeping stateless logic separate from state management: no application logic in the database, no durable state in the application. (To borrow the functional programmers' joke: “we believe in the separation of Church and state” — a nod to Alonzo Church and the state-free lambda calculus.)
در این مدلِ رایج، پایگاه داده مثلِ یک متغیرِ مشترکِ mutable است که از روی شبکه بهصورتِ همگام
خوانده و بهروز میشود؛ و پایگاه داده دوام و کمی کنترلِ همزمانی و تحملِ خطا را به عهده میگیرد. ولی در
اکثر زبانها نمیشود به تغییرِ متغیر subscribe کرد — فقط میشود دورهای خواندش (patternِ
observer را خودت باید پیاده کنی؛ ویژگیِ داخلیِ زبان نیست). پایگاه دادهها همین رویکردِ منفعل را به ارث
بردهاند: اگر بخواهی بفهمی محتوای پایگاه داده عوض شده، اغلب تنها راه poll است (تکرارِ دورهایِ
کوئری). subscribe به تغییرها تازه دارد بهعنوانِ ویژگی ظهور میکند.
In this common model the database is like a shared mutable variable, read and updated synchronously over the network; and the database takes on durability plus some concurrency control and fault tolerance. But in most languages you can't subscribe to a variable's change — you can only poll it periodically (the observer pattern must be hand-built; it isn't a built-in language feature). Databases inherited the same passive approach: to learn database content changed, often the only way is polling (periodically repeating a query). Subscribing to changes is only now emerging as a feature.
dataflow: رابطهٔ کد و state را از نو مذاکره کنDataflow: renegotiating the code/state relationship
فکرکردن بر حسبِ dataflow یعنی بهجای اینکه پایگاه داده را متغیرِ منفعلی بدانی که اپلیکیشن دستکاریاش میکند،
خیلی بیشتر به تعامل و همکاریِ state، تغییرِ state و کدی که آنها را پردازش میکند فکر کنی:
کدِ اپلیکیشن به تغییرِ state در یکجا با راهانداختنِ تغییرِ state در جای دیگر واکنش نشان میدهد.
این ایده را در CDC، مدلِ actor، trigger و نگهداریِ نمایِ افزایشی دیدهایم؛ unbundle کردنِ پایگاه داده یعنی
اعمالِ همان به ساختِ dataset مشتق بیرونِ پایگاه دادهِ اصلی: کش، ایندکسِ جستجو، ML، سیستمِ تحلیلی —
با پردازشِ stream و سیستمِ پیامرسانی. نگهداریِ دادهٔ مشتق سه چیز میخواهد که بروکرِ لاگمحور میدهد:
ترتیبِ تغییرهای state (چون چند نما باید رویدادها را بههمانترتیب بخورند تا با هم سازگار بمانند)
و تحملِ خطا (گمشدنِ حتی یک پیام، مشتق را برای همیشه از منبع عقب میاندازد؛ هم تحویلِ پیام و هم
بهروزرسانیِ state مشتق باید قابلاعتماد باشند). ترتیبِ پایدار و پردازشِ fault-tolerant خواستههای
سختیاند، ولی خیلی ارزانتر و عملیاتیتر از تراکنشِ توزیعشدهاند؛ پردازندههای stream مدرن این تضمینها
را در مقیاس میدهند و اجازه میدهند کدِ اپلیکیشن بهصورتِ عملگرِ stream اجرا شود — کدِ دلخواهی که
توابعِ مشتقِ داخلیِ پایگاه داده معمولاً نمیدهند. و مثلِ ابزارهای Unix که با لوله زنجیر میشوند، عملگرهای
stream را میشود ترکیب کرد و سیستمهای بزرگ حولِ dataflow ساخت: هر عملگر جریانِ تغییرِ state را ورودی
میگیرد و جریانِ تغییرِ state دیگری خروجی میدهد.
Thinking in dataflow means renegotiating the code/state relationship: instead of seeing the database as a passive variable the application manipulates, think far more about the interaction of state, state change, and the code processing them: application code reacts to a state change in one place by triggering a state change elsewhere.
We've seen this idea in CDC, the actor model, triggers and incremental view maintenance; unbundling the database means applying it to building derived datasets outside the main database: caches, search indexes, ML, analytical systems — with stream processing and messaging. Maintaining derived data needs three things a log-based broker provides: ordered state changes (since several views must consume events in the same order to stay mutually consistent) and fault tolerance (losing even one message leaves the derivative permanently behind its source; both message delivery and derived-state updates must be reliable). Stable order and fault-tolerant processing are hard demands, but far cheaper and more operational than distributed transactions; modern stream processors give these guarantees at scale and let application code run as stream operators — arbitrary code that databases' built-in derivation functions usually don't offer. And like Unix tools chained with pipes, stream operators compose into large dataflow-built systems: each operator takes one state-change flow as input and emits another.
۴.۳. stream در برابر RPC4.3. Streams vs RPC
در RPC، برای هر نیاز از سرویسِ دیگر پاسخ میخواهیم. با جریان تغییرها میتوان نسخهٔ محلی ساخت و از آن خواند؛ این روش وابستگیِ لحظهای را با مسئلهٔ تازگی عوض میکند.
مثال فنی: نرخ ارز از جریان محلی
وضعیت اولیه: پردازشگر هر سفارش به آخرین نرخ معتبر نیاز دارد؛ سرویس نرخ موقتاً قطع میشود.
اجرای مرحلهبهمرحله: با subscription، آخرین نرخِ دریافتشده همراه نسخه و زمان در state محلی هست؛ با RPCِ هر سفارش، قطع سرویس مستقیماً مسیر را معطل میکند.
نتیجه و دلیل: جریان وابستگیِ لحظهای را کم میکند، اما سیاستِ نرخِ کهنه و زمانِ اعتبار باید مشخص باشد.
Today's dominant style is breaking capability into services talking over synchronous network requests (REST); its advantage over monoliths is organizational scalability with loose coupling (different teams, independent deploys). Composing stream operators in a dataflow system has many of the same properties, but its communication mechanism differs: one-way asynchronous message flow instead of synchronous request interaction. Beyond advantages like better fault tolerance, a dataflow system can also be faster than REST/RPC. The book's example: a customer buys an item priced in one currency but pays in another; conversion needs the day's exchange rate. Two ways:
- میکروسرویس: کدِ پردازشِ خرید، سرویس یا پایگاه دادهِ نرخِ ارز را برای نرخِ جاری کوئری میکند.
- dataflow: کدِ پردازشِ خرید از قبل مشترکِ جریانِ بهروزرسانیِ نرخِ ارز شده و نرخِ جاری را هر بار که عوض میشود در یک پایگاه دادهِ محلی ضبط میکند؛ موقعِ پردازشِ خرید مستقیم همان پایگاه دادهِ محلی را میخواند (شاید روی همان ماشین، حتی همان پروسه).
- Microservices: the purchase-processing code queries the exchange-rate service or database for the current rate.
- Dataflow: the purchase-processing code already subscribes to the exchange-rate update flow and records the current rate in a local database every time it changes; when processing the purchase it reads that same local database directly (perhaps on the same machine, even the same process).
راهِ دوم درخواستِ شبکهایِ همگام به سرویسِ دیگر را با کوئریِ محلی جایگزین میکند. در راهِ اول هم میشد
نرخ را محلی کش کرد، ولی برای تازهنگهداشتنِ کش باید دورهای poll میکردی یا مشترکِ جریانِ تغییر میشدی —
که دقیقاً همان کارِ راهِ دوم است. پس راهِ dataflow نهفقط سریعتر، که در برابرِ خرابیِ سرویسِ دیگر هم
قویتر است: سریعترین و قابلاعتمادترین درخواستِ شبکهای، هیچ درخواستی است! بهجای RPC حالا یک
joinِ جریانی بینِ رویدادهای خرید و رویدادهای بهروزرسانیِ نرخ داری. این join وابستهبهزمان است:
اگر رویدادهای خرید را بعداً بازپردازش کنی، نرخ عوض شده؛ برای بازسازیِ خروجیِ اصلی باید نرخِ تاریخیِ همان
لحظهٔ خرید را داشته باشی — چه با کوئریِ سرویس، چه با اشتراکِ جریان، این وابستگیِ زمانی را باید مدیریت کنی.
مشترکشدن در جریانِ تغییر (بهجای کوئریِ state جاری در لحظهٔ نیاز) ما را به مدلِ محاسباتیِ
spreadsheet-گونه نزدیک میکند: دادهای عوض شود، هر مشتقِ وابستهاش سریع بهروز میشود.
The second way replaces a synchronous network request to another service with a local query. The first way could cache the rate locally too, but keeping the cache fresh would need periodic polling or subscribing to the change flow — which is exactly the second way's work. So the dataflow way is not only faster but also more resilient to the other service's failure: the fastest, most reliable network request is no request at all! Instead of RPC you now have a streaming join between purchase events and rate-update events. That join is time-dependent: if you later reprocess purchase events, rates changed; to reproduce the original output you need that purchase moment's historical rate — whether by querying the service or subscribing to the flow, this time dependency must be managed. Subscribing to the change flow (instead of querying current state at need-time) brings us closer to a spreadsheet-like computation model: data changes, every dependent derivative updates fast.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: نرخ ارز از جریان محلی
وضعیت اولیه: پردازشگر هر سفارش به آخرین نرخ معتبر نیاز دارد؛ سرویس نرخ موقتاً قطع میشود.
اجرای مرحلهبهمرحله: با subscription، آخرین نرخِ دریافتشده همراه نسخه و زمان در state محلی هست؛ با RPCِ هر سفارش، قطع سرویس مستقیماً مسیر را معطل میکند.
نتیجه و دلیل: جریان وابستگیِ لحظهای را کم میکند، اما سیاستِ نرخِ کهنه و زمانِ اعتبار باید مشخص باشد.
نمودار دو بند دارد با پنج شرکتکننده: purchase events
(«stream of purchases»)، purchase processor،
local rate store («replica inside processor»)،
rate update stream («rate changes») و
exchange-rate service («sync RPC»).
بندِ اول «Microservices (sync RPC)»: رویدادِ خرید میرسد
(«purchase event arrives»)، پردازنده فلشِ
«get current rate (sync RPC)» را به سرویسِ نرخ میزند
(یادداشت: «adds latency + a failure dependency»)، نرخ برمیگردد
(«current rate» با یادداشتِ «کشِ محلی هم poll یا جریانِ تغییر میخواهد») و
خرید پردازش میشود.
بندِ دوم «Dataflow (subscribe ahead of time)»: اول جریانِ نرخ،
بهروزرسانیها را میفرستد («rate updates») و پردازنده هر تغییر را محلی ضبط
میکند («write rate update» — «subscribe ahead of
change»). حالا که رویدادِ خرید میرسد، پردازنده فقط فلشِ
«query local replica (no network)» را میزند —
با یادداشتِ معروفِ فصل: «سریعترین و قابلاعتمادترین درخواستِ شبکهای، هیچکدام است» —
و خرید پردازش میشود. روی همین فلشِ ورودِ خرید در بندِ دوم یادداشتِ مهمِ دیگری هست: «این یک joinِ
جریانی بینِ رویدادهای خرید و بهروزرسانیهای نرخ است — وابستهبهزمان: بازپردازش نرخِ تاریخی میخواهد».
The diagram has two acts with five participants: purchase events (“stream of purchases”), the purchase processor, the local rate store (“replica inside processor”), the rate update stream (“rate changes”) and the exchange-rate service (“sync RPC”). Act one, “Microservices (sync RPC)”: the purchase event arrives (“purchase event arrives”), the processor fires the “get current rate (sync RPC)” arrow at the rate service (note: “adds latency + a failure dependency”), the rate returns (“current rate”, noting “a local cache also wants poll or a change flow”) and the purchase is processed.
Act two, “Dataflow (subscribe ahead of time)”: first the rate flow sends updates (“rate updates”) and the processor records each change locally (“write rate update” — “subscribe ahead of change”). Now when the purchase event arrives, the processor only fires “query local replica (no network)” — with the chapter's famous note: “the fastest, most reliable network request is none” — and the purchase is processed. On that purchase-entry arrow in act two sits another key note: “this is a streaming join between purchase events and rate updates — time-dependent: reprocessing wants historical rates.”
۴.۴. مسیرِ نوشتن و مسیرِ خواندن4.4. Write path and read path
مسیر نوشتن داده را ثبت و نماهای مناسب را آماده میکند. مسیر خواندن از این نماها پاسخ میسازد؛ بخشی از هزینه میتواند از زمانِ خواندن به زمانِ تغییر منتقل شود.
مثال فنی: هزینه را کجا میپردازیم؟
وضعیت اولیه: برای هر خرید، جمع روزانه در نمای sales_by_day بهروز میشود.
اجرای مرحلهبهمرحله: مسیر نوشتن کارِ نگهداری نما را انجام میدهد؛ مسیر خواندن فقط مقدار آماده را میگیرد.
نتیجه و دلیل: خواندن سریع در عوض نوشتن و ترمیمِ پرهزینهتر است؛ اگر query عوض شود، نمای قدیمی ممکن است کافی نباشد.
The previous section's dataflow systems give you a process for building derived datasets and keeping them fresh — call it the write path: whenever information is written, it may pass several batch and stream stages until all derived datasets update; Fig. 13-1 shows search-index updating as an example. But why build a derived dataset at all? Because you'll probably want to query it later — that is the read path: when serving a user request, you read from the derivative, perhaps process further and build the answer. Together, the whole data journey from collection moment to consumption moment (probably by another human). The write path is the journey's precomputed part — eager, as soon as data arrives, unasked; the read path is the part happening only when someone asks (lazy). If you know functional languages, the write path resembles eager evaluation and the read path lazy evaluation.
dataset مشتق جایی است که دو مسیر به هم میرسند. پس یک بدهبستان است بینِ «چقدر کار موقعِ نوشتن» و
«چقدر کار موقعِ خواندن». ایندکسِ full-text نمونهٔ خوبی است: نوشتن باید ورودیِ ایندکسِ همهٔ termهای سند
را بهروز کند؛ خواندن باید هر کلمهٔ کوئری را بجوید و منطقِ Boolean بزند (AND برای «همهٔ کلمات»، OR برای
مترادفها). اگر ایندکس نداشتی، کوئری باید همهٔ سندها را scan میکرد (مثلِ grep) — نوشتن ارزان، خواندن
خیلی گران. در سرِ دیگرِ طیف، میشد جوابِ همهٔ کوئریهای ممکن را ازپیشمحاسبه کرد — خواندن فقط
«پیدا کن و برگردان»، ولی نوشتن وحشتناک گران؛ و چون مجموعهٔ کوئریهای ممکن نامتناهی است (نمایی در تعدادِ
termها)، اصلاً شدنی نیست. گزینهٔ میانی: فقط جوابِ پرکاربردترین کوئریها را ازپیشمحاسبه کن (کشِ
کوئریهای رایج — که میشود اسمش را viewی materialized هم گذاشت، چون با آمدنِ سندِ تازه باید بهروز شود)
و کوئریهای نادر را از ایندکس سرو کن. پس نقشِ کش و ایندکس و viewی materialized ساده است: جابهجاکردنِ
مرزِ بینِ مسیرِ خواندن و مسیرِ نوشتن — با ازپیشمحاسبه، کارِ بیشتری موقعِ نوشتن میکنی تا موقعِ
خواندن کمتر کار کنی. (این جابهجاییِ مرز، موضوعِ همان مطالعهٔ موردیِ timeline شبکهٔ اجتماعی در فصلِ ۱
بود — با مرزِ متفاوت برای سلبریتی و کاربرِ عادی. بعد از ۵۰۰ صفحه به نقطهٔ شروع برگشتیم!)
The derived dataset is where the two paths meet. So it's a tradeoff between “how much work at write time” and “how much at read time”. The full-text index is a good example: writing must update the index entries of every document term; reading must look up each query word and apply Boolean logic (AND for “all words”, OR for synonyms). Without an index, queries would scan all documents (like grep) — cheap writes, very expensive reads. At the spectrum's other end, you could precompute every possible query's answer — reading just “find and return”, but horribly expensive writing; and since the possible-query set is infinite (exponential in term count), not feasible at all. The middle option: precompute only the most-used queries' answers (a cache of common queries — also nameable a materialized view, since fresh documents must update it) and serve rare queries from the index. So the role of caches, indexes and materialized views is simple: moving the boundary between read path and write path — precomputing means doing more work at write time to do less at read time. (This boundary-shifting was the subject of the Chapter-1 social-timeline case study — with different boundaries for celebrities and ordinary users. After 500 pages we're back where we started!)
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: هزینه را کجا میپردازیم؟
وضعیت اولیه: برای هر خرید، جمع روزانه در نمای sales_by_day بهروز میشود.
اجرای مرحلهبهمرحله: مسیر نوشتن کارِ نگهداری نما را انجام میدهد؛ مسیر خواندن فقط مقدار آماده را میگیرد.
نتیجه و دلیل: خواندن سریع در عوض نوشتن و ترمیمِ پرهزینهتر است؛ اگر query عوض شود، نمای قدیمی ممکن است کافی نباشد.
نمودار یک زنجیرهٔ پنجگامی است: Document Updates
(«input events») با فلشِ «apply changes» به
Write Path (Eager) («transform and index» —
batch/stream) میرسد؛ بعد با «update index» به
Derived Dataset («search index: where paths meet» —
تگ: «caches / views shift this boundary»)؛ بعد با
«keyword query» به Read Path (Lazy)
(«query processing» — AND/OR بولی)؛ و سرانجام با
«ranked results» به User Response
(«final answer»).
زیرِ زنجیرهٔ اصلی دو زنجیرهٔ خطچینِ «چه میشد اگر» هست که مرز را عملاً جابهجا میکنند:
No Index («cheap writes» — تگ:
«expensive scans») که ورودی را «append as-is»
میکند و با «scan every doc» سرو میدهد؛ و
Precompute All («every query» — تگ:
«impossible») که «precompute answers» میکند و با
«lookup only» جواب میدهد. پیامش روشن است: ایندکس فقط یک نقطه روی این طیف
است، نه تنها نقطهٔ ممکن.
The diagram is a five-step chain: Document Updates (“input events”) via “apply changes” to the Write Path (Eager) (“transform and index” — batch/stream); then via “update index” to the Derived Dataset (“search index: where paths meet” — tag: “caches / views shift this boundary”); then via “keyword query” to the Read Path (Lazy) (“query processing” — Boolean AND/OR); and finally via “ranked results” to the User Response (“final answer”).
Under the main chain sit two dashed “what if” chains that effectively move the boundary: No Index (“cheap writes” — tag: “expensive scans”), which “appends as-is” the input and serves with “scan every doc”; and Precompute All (“every query” — tag: “impossible”), which “precomputes answers” and answers with “lookup only”. The message is clear: the index is just one point on this spectrum, not the only possible one.
۴.۵. کلاینتِ stateful و «خواندنها هم رویدادند»4.5. Stateful clients; reads are events too
کلاینت میتواند وضعیتِ محلی و اشتراکِ فعال داشته باشد. در این حالت پاسخ فقط یک عکسِ لحظهای نیست؛ تغییرهای مرتبط هم باید به کاربر برسند.
مثال فنی: درخواست خواندن بهصورت subscription
وضعیت اولیه: کلاینت با درخواست q7 میخواهد وضعیت سفارش ۴۲ را دنبال کند.
اجرای مرحلهبهمرحله: سیستم q7 را با تغییرهای سفارش join میکند و بعد از snapshot اولیه، updateها را میرساند؛ reconnect باید gap و تکرار را کنترل کند.
نتیجه و دلیل: خواندنِ یکباره پس از پاسخ تمام میشود؛ subscription یک وابستگیِ زنده و stateِ مدیریتپذیر میسازد.
The write/read-path boundary idea shows up in another context too. Browsers used to be stateless clients (offline you could only scroll the previous page); but single-page web apps and mobile apps now keep lots of state on-device. With durable local state, a class of apps can work offline and sync in the background when the network returns — and since the UI needn't wait for synchronous requests, the experience is far better. On-device state can be seen as a cache of server state: the page's pixels are a materialized view of the client app's model objects; and the model objects are a local replica of the far datacenter's state.
ولی صفحهٔ وبِ معمولی تغییرِ بعدیِ سرور را نمیفهمد تا reload کنی — کشِ کهنهای است که pollش نمیکنی.
پروتکلهای تازهتر (server-sent events، WebSocket) کانالی میدهند که سرور از همان راه تغییرِ state را
push کند و کهنگیِ state سمتِ کلاینت کم شود. در مدلِ مسیرها، این یعنی کشیدنِ مسیرِ نوشتن تا
خودِ کاربر: کلاینت برای state اولیه هنوز مسیرِ خواندن لازم دارد، ولی بعدش به جریانِ تغییرهای سرور
تکیه میکند. دستگاهها گاهی آفلایناند و اعلان را نمیگیرند — ولی همان تکنیکِ offset مصرفکنندهٔ بروکرِ
لاگمحور اینجا هم جواب میدهد: هر دستگاه یک subscriber کوچک به یک جریانِ کوچک است که بعد از وصلشدنِ
دوباره چیزی را از دست نمیدهد. تغییرهای state میتوانند یک مسیرِ نوشتنِ سرتاسری را طی کنند: از
تعاملِ یک دستگاه، از لاگ و مشتقها و پردازندهها، تا UI دستگاهِ دیگر — با تأخیرِ زیرِ یک ثانیه.
چرا همهٔ اپها اینطوری نیستند؟ چون فرضِ «کلاینتِ stateless + request/response» در پایگاه داده و کتابخانه و
پروتکل ریشه دوانده؛ خیلی datastoreها عملیاتِ «یک درخواست، یک پاسخ» دارند و خیلی کمترشان «یک درخواست،
جریانی از پاسخها در طولِ زمان» (subscribe به تغییر) بلدند. کشیدنِ مسیرِ نوشتن تا کاربر یعنی بازاندیشیِ
بنیادین بهسمتِ publish/subscribe — پرهزینه، ولی UI پاسخگوتر و آفلاینِ بهتر.
But the ordinary web page doesn't learn the server's next change until reload — a stale cache you don't poll. Newer protocols (server-sent events, WebSocket) give a channel for the server to push state changes down the same path, cutting client-side staleness. In the paths model, this means pulling the write path all the way to the user: the client still needs the read path for initial state, but then relies on the server's change flow. Devices are sometimes offline and miss notifications — but the log-based broker's consumer-offset technique works here too: each device is a tiny subscriber to a tiny flow that loses nothing after reconnecting. State changes can travel an end-to-end write path: from one device's interaction, through logs, derivatives and processors, to another device's UI — with sub-second latency. Why aren't all apps like this? Because the “stateless client + request/response” assumption is rooted in databases, libraries and protocols; many datastores have “one request, one response” operations and far fewer know “one request, a flow of responses over time” (subscribing to change). Pulling the write path to the user means fundamental rethinking toward publish/subscribe — costly, but a more responsive UI and better offline.
یک قدم جلوتر: تا اینجا نوشتنها از لاگ میرفتند و خواندنها درخواستِ گذرای شبکه بودند که مستقیم به نودِ
نگهدارنده میرفتند. ولی میشود درخواستِ خواندن را هم بهصورتِ جریانِ رویداد نشان داد و هم
رویدادِ خواندن و هم رویدادِ نوشتن را از یک عملگرِ stream رد کرد؛ عملگر با انتشارِ نتیجه به جریانِ خروجی
به رویدادِ خواندن جواب میدهد. وقتی خواندن و نوشتن هر دو رویداد باشند و به یک عملگر برسند، داری یک
joinِ stream-table بینِ جریانِ کوئریهای خواندن و پایگاه داده انجام میدهی: هر رویدادِ خواندن باید به
shardای برود که دادهٔ مربوط را دارد — درست مثلِ copartition کردنِ ورودیها روی همان کلید در joinهای
batch و stream. این تناظرِ «سروِ درخواست = join» بنیادین است: خواندنِ one-off از عملگرِ join رد میشود
و عملگر بلافاصله فراموشش میکند؛ درخواستِ subscribe یک joinِ پایدار است با رویدادهای گذشته و آیندهٔ
آنطرفِ join. لاگکردنِ رویدادهای خواندن برای ردیابیِ وابستگیِ علّی و provenance هم فایده دارد (میشود
بازسازی کرد کاربر قبل از تصمیم چه دیده بود — مثلاً تاریخِ ارسال و موجودیای که مشتری دیده روی خریدش اثر
میگذارد)؛ ولی هزینهٔ ذخیرهسازی و I/O اضافه دارد و بهینهسازیاش مسئلهٔ بازِ پژوهشی است.
One step further: so far writes went through the log and reads were transient network requests going straight to the holding node. But a read request can also be shown as an event flow, routing both read and write events through one stream operator; the operator answers the read event by publishing the result to the output flow. When reads and writes are both events reaching one operator, you're doing a stream-table join between the read-query flow and the database: each read event must go to the shard holding the relevant data — just like copartitioning inputs on the same key in batch and stream joins. This “serving requests = join” correspondence is fundamental: a one-off read passes through the join operator and the operator immediately forgets it; a subscribe request is a durable join with the other side's past and future events. Logging read events also helps causality and provenance tracking (reconstructing what the user saw before deciding — e.g. the send history and balance a customer saw affects their purchase); but it costs extra storage and I/O, and optimizing it is an open research problem.
برای کوئریِ تک-shard، فرستادن از دلِ stream و جمعکردنِ پاسخِ جریانی شاید overkill باشد؛ ولی این ایده درِ
اجرایِ توزیعشدهٔ کوئریهای پیچیدهٔ چندshardی را باز میکند با همان زیرساختِ مسیریابی و shard و joinِ
پردازندهٔ stream. نمونه: distributed RPC در Storm برای «اجتماعِ مجموعهٔ
فالوئرهای همهٔ کسانی که یک URL را پست کردهاند» (ترکیبِ نتیجهٔ خیلی shardها)، یا ارزیابیِ ریسکِ تقلبِ یک
خرید با join پیدرپی با پایگاههای داده reputation که هرکدام جدا shard شدهاند. اگر این جنس join چندshardی
لازم داری، احتمالاً سادهتر است از پایگاهدادهای استفاده کنی که همین ویژگی را بدهد تا اینکه با پردازندهٔ
stream پیادهاش کنی — ولی «کوئری بهمثابهٔ جریان» گزینهای است برای اپهای بزرگی که به سقفِ راهحلهای
آماده میخورند.
For single-shard queries, sending through the stream and gathering the streaming answer may be overkill; but this idea opens the door to distributed execution of complex multishard queries with the same routing, sharding and join machinery of the stream processor. Example: distributed RPC in Storm for “unioning the follower sets of everyone who posted a URL” (combining many shards' results), or fraud-risk scoring of a purchase by successive joins with reputation databases each sharded separately. If you need this kind of multishard join, it's probably simpler to use a database offering the feature than to implement it with a stream processor — but “queries as flows” is an option for big apps hitting ready-made solutions' ceilings.
۵5 هدفگرفتنِ درستی (Aiming for Correctness)Aiming for correctness
درستیِ کل برنامه به قراردادِ همهٔ مراحل وابسته است. ذخیرهساز قوی نمیتواند ورودیِ غلط، شناسهٔ اشتباه یا اثرِ بیرونیِ تکراری را خودبهخود اصلاح کند.
مثال فنی: تراکنش درست، هویت غلط
وضعیت اولیه: درخواست اول سفارش را با id=r7 ثبت میکند ولی پاسخ گم میشود.
اجرای مرحلهبهمرحله: کلاینت برای retry شناسهٔ r8 میسازد؛ هر تراکنش جداگانه کاملاً serializable است، اما دو سفارش ثبت میشوند.
نتیجه و دلیل: درستیِ دامنه از لایهٔ کلاینت تا مقصد ادامه دارد؛ isolationِ قوی، دو درخواست مستقل را یک نیت واحد تشخیص نمیدهد.
With a read-only stateless service, if something breaks it's no problem: you fix the bug, restart, and everything returns. A stateful system like a database isn't like that: it's supposed to remember (more or less) everything forever, so a failure's effect potentially lasts forever too — and needs more careful thought. We want to build apps that are reliable and correct (clear, understood semantics, despite various errors). Four decades of atomicity, isolation and durability were the tools for this; but these foundations are shakier than they look (witness the confusion of weak isolation levels). In some domains transactions are abandoned altogether, replaced with a model giving better performance and scalability but messier semantics. Everyone says “consistency” and few define it precisely. Kyle Kingsbury's Jepsen tests showed the naked gap between some products' “claimed guarantee” and “actual behavior” under network trouble and crashes — and even if the infrastructure were flawless, application code must still use features correctly, which is error-prone with hard-to-understand configuration (weak isolation, quorums and the rest).
اگر اپت گاهی خرابکردن یا گمکردنِ داده را تحمل میکند، زندگی ساده است (چشمها را ببند و امیدوار باش).
ولی اگر تضمینِ قویتر لازم داری، serializability و atomic commit راههای جاافتادهاند — با هزینه:
معمولاً فقط در یک دیتاسنتر جواب میدهند (معماریِ جغرافیایی را خط میزنند) و مقیاس و تحملِ خطا را محدود
میکنند. رویکردِ تراکنشیِ سنتی نمیرود، ولی حرفِ آخر هم در درست و مقاومکردنِ اپ نیست. این بخش راههای
دیگرِ فکرکردن به درستی در معماریِ dataflow را میکاود.
If your app tolerates occasionally breaking or losing data, life is simple (close your eyes and hope). But if you need stronger guarantees, serializability and atomic commit are the established paths — at a cost: they usually work only in one datacenter (ruling out geographic architectures) and limit scale and fault tolerance. The traditional transactional approach isn't leaving, but neither is it the last word on making apps correct and resilient. This section explores other ways of thinking about correctness in dataflow architectures.
۵.۱. سرکوبِ تکراری، سرتاسری5.1. End-to-end duplicate suppression
جلوگیری از تکرار باید از آغازِ درخواست تا محلِ ایجادِ اثر ادامه داشته باشد. ثبتِ شناسه و تغییرِ مربوط باید اتمیک باشند تا میان آنها فاصلهٔ خطرناک ایجاد نشود.
مثال فنی: یک هویت تا آخر مسیر
وضعیت اولیه: مرورگر برای خرید r7 میسازد و همان شناسه به سرویس و جدول درخواستها میرسد.
اجرای مرحلهبهمرحله: اثر و رکورد r7 در یک تراکنش ثبت میشوند؛ پاسخِ گمشده با استعلام یا تکرارِ همان r7 بازیابی میشود.
نتیجه و دلیل: dedup باید همان نیت کاربر را سرتاسری بشناسد؛ تولید شناسهٔ تازه در هر لایه این زنجیره را میشکند.
That an app uses a data system with a strong feature (like a serializable transaction) doesn't mean it's safe from lost or corrupted data: a bug writing or deleting wrong data isn't saved by a serializable transaction — itself an argument for immutable, append-only data, since recovering from mistakes is simpler if broken code can't destroy healthy data. But immutability alone doesn't cure everything; the subtler example is “exactly-once execution” of an operation.
ایده: اگر موقعِ پردازشِ پیام چیزی خراب شود، یا بیخیال میشوی (گمشدنِ پیام = ازدسترفتنِ داده) یا دوباره
تلاش میکنی. ولی اگر تلاشِ دوباره کنی، ممکن است پردازشِ اول در واقع موفق شده بوده و فقط تأییدش را نگرفتهای —
پس پیام دو بار پردازش میشود. دو بار پردازش نوعی خرابیِ داده است: دوبار شارژکردنِ مشتری یا دوبار شمردنِ
کانتر. دقیقاً-یکبار یعنی ترتیبی بدهی که اثرِ نهایی همان شود که انگار هیچ خطایی رخ نداده، حتی اگر
بهخاطرِ خطا بازتلاش شده. یکی از مؤثرترین راهها idempotent کردنِ عملیات است: اثرش یکی باشد، چه یک
بار اجرا شود چه چند بار. ولی idempotent کردنِ عملیاتی که ذاتاً idempotent نیست زحمت و دقت میخواهد:
شاید متادیتای اضافه لازم داشته باشی (مثلِ مجموعهٔ IDهای عملیاتی که مقداری را بهروز کردهاند) و موقعِ
failover باید fencing کنی (فصل ۹).
The idea: if something breaks while processing a message, either give up (lost message = lost data) or retry. But if you retry, the first attempt may actually have succeeded with only its acknowledgment lost — so the message gets processed twice. Processing twice is itself a data failure: charging the customer twice or double-counting a counter. Exactly-once means arranging for the final effect to equal as if no error ever happened, even if errors caused retries. One of the most effective ways is making operations idempotent: same effect whether run once or several times. But idempotizing an inherently non-idempotent operation takes work and care: it may need extra metadata (like the set of operation IDs that updated a value), and on failover you must fence (Chapter 9).
همین الگوی «سرکوبِ تکراری» جاهای دیگری هم هست. TCP با sequence number روی پکتها ترتیب را درست میکند و
پکتِ گمشده را بازمیفرستد و تکراری را حذف میکند — ولی فقط داخلِ یک اتصالِ TCP. فرض کن اتصالِ TCP،
اتصالِ کلاینت به پایگاه داده است و دارد تراکنشِ مثالِ ۱۳-۱ را اجرا میکند (در خیلی پایگاه دادهها تراکنش به اتصالِ
کلاینت گره خورده چون چند کوئری روی همان اتصال یعنی همان تراکنش). اگر کلاینت بعد از فرستادنِ COMMIT و قبل
از شنیدنِ پاسخ، قطعیِ شبکه و timeout بخورد، نمیداند تراکنش commit شده یا abort (همان وضعیتِ شکلِ ۹-۱).
The same “duplicate suppression” pattern appears elsewhere. TCP orders packets with sequence numbers, resends lost ones and drops duplicates — but only inside one TCP connection. Suppose the TCP connection is a client-to-database connection running Example 13-1's transaction (in many databases a transaction is tied to the client connection, since several queries on the same connection mean the same transaction). If the client, after sending COMMIT and before hearing the reply, hits a network break and timeout, it doesn't know whether the transaction committed or aborted (the same situation as Fig. 9-1).
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;
کلاینت میتواند وصل شود و تراکنش را بازتلاش کند، ولی حالا بیرونِ قلمروِ سرکوبِ تکراریِ TCP است.
و چون این تراکنش idempotent نیست، ممکن است بهجای ۱۱ دلار، ۲۲ دلار جابهجا شود. پس حتی این کد که مثالِ
استانداردِ atomicity تراکنش است، درست نیست — و بانکهای واقعی اینطوری کار نمیکنند. پروتکلِ 2PC نگاشتِ
یکبهیکِ «اتصالِ TCP = تراکنش» را میشکند (coordinator باید بعد از خطا بتواند به پایگاه داده وصل شود و بگوید
تراکنشِ in-doubt را commit کن یا abort) — ولی این هم برای «فقط یک بار اجرا شدن» کافی نیست.
حتی اگر تکراریِ بینِ کلاینت و سرورِ پایگاه داده را سرکوب کنی، هنوز شبکهٔ بینِ دستگاهِ کاربر و سرورِ اپلیکیشن
مانده: کاربر با اینترنتِ ضعیف POST را میفرستد، ولی قبل از گرفتنِ پاسخ سیگنال را از دست میدهد؛ پیامِ خطا
میبیند و دستی retry میکند (مرورگر هم میپرسد «مطمئنی دوباره submit کنی؟» — و کاربر بله میگوید چون
میخواهد عملیات انجام شود). از دیدِ وبسرور، retry یک درخواستِ جداست؛ و از دیدِ پایگاه داده، یک تراکنشِ جدا.
مکانیزمهای عادیِ dedup کمکی نمیکنند.
The client can reconnect and retry the transaction, but now it's outside TCP's duplicate-suppression territory. And since this transaction isn't idempotent, $22 may move instead of $11. So even this code — the textbook atomicity example — is incorrect — and real banks don't work this way. The 2PC protocol breaks the one-to-one “TCP connection = transaction” mapping (the coordinator must reconnect after failure and tell the database to commit or abort the in-doubt transaction) — but even that isn't enough for “executing just once”. Even suppressing duplicates between client and database server leaves the network between the user's device and the app server: the user on weak internet sends the POST but loses signal before the reply; sees an error and manually retries (the browser even asks “are you sure you want to resubmit?” — and the user says yes, wanting the operation done). To the web server the retry is a separate request; to the database, a separate transaction. Ordinary dedup mechanisms don't help.
راهحل: شناسهٔ یکتا را سرتاسر حمل کن (مثالِ ۱۳-۲)Fix: carry a unique ID end to end (Example 13-2)
برای idempotent کردنِ درخواست از چند hop شبکه، تکیه به مکانیزمِ تراکنشِ پایگاه داده کافی نیست؛ باید جریانِ
سرتاسریِ درخواست را دید. مثلاً بهازای هر درخواست شناسهٔ یکتا بساز (UUID) و بهعنوانِ فیلدِ مخفیِ فرم بگذار،
یا hash فیلدهای فرم را شناسه کن. اگر مرورگر POST را دو بار بفرستد، هر دو همان شناسه را دارند. بعد همان
شناسه را تا پایگاه داده حمل کن و چک کن که همیشه فقط یک درخواست با آن ID اجرا شود:
To idempotize a request across several network hops, the database's transaction mechanism isn't enough; you must see the request's end-to-end flow. E.g. mint a unique ID per request (a UUID) as a hidden form field, or hash the form fields into an ID. If the browser POSTs twice, both carry the same ID. Then carry that ID down to the database and check that only one request with that ID ever executes.
ALTER TABLE requests ADD UNIQUE (request_id);
BEGIN TRANSACTION;
INSERT INTO requests
(request_id, from_account, to_account, amount)
VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;
این کد به قیدِ یکتاییِ ستونِ request_id تکیه میکند: اگر تراکنشی بخواهد ID
تکراری insert کند، INSERT میشکند و تراکنش abort میشود و دو بار اثر نمیکند. پایگاههای داده رابطهای معمولاً
قیدِ یکتایی را درست نگه میدارند حتی در isolation ضعیف (درحالیکه چک-بعد-insert در سطحِ اپلیکیشن زیرِ
isolation غیرسریالشدنی میشکند — write skew فصل ۸). تازه جدولِ requests عملاً یک لاگِ رویداد هم هست
که برای event sourcing یا CDC به کار میآید: بهروزرسانیِ بالانسها لازم نیست در همان تراکنشِ درجِ رویداد
باشد، چون افزونهاند و میشود از رویداد در مصرفکنندهٔ پاییندستی مشتقشان کرد — بهشرطی که رویداد دقیقاً
یک بار پردازش شود، که باز همان شناسه تضمینش میکند.
This code relies on the request_id column's uniqueness constraint: if a transaction tries inserting a duplicate ID, the INSERT breaks and the transaction aborts, never applying twice. Relational databases usually uphold uniqueness correctly even under weak isolation (while check-then-insert at app level breaks under non-serializable isolation — Chapter 8's write skew). The requests table is also effectively an event log, useful for event sourcing or CDC: the balance updates needn't be in the same transaction as the event insert, since they're additive and can be derived from the event in a downstream consumer — provided the event is processed exactly once, which the same ID guarantees.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: یک هویت تا آخر مسیر
وضعیت اولیه: مرورگر برای خرید r7 میسازد و همان شناسه به سرویس و جدول درخواستها میرسد.
اجرای مرحلهبهمرحله: اثر و رکورد r7 در یک تراکنش ثبت میشوند؛ پاسخِ گمشده با استعلام یا تکرارِ همان r7 بازیابی میشود.
نتیجه و دلیل: dedup باید همان نیت کاربر را سرتاسری بشناسد؛ تولید شناسهٔ تازه در هر لایه این زنجیره را میشکند.
نمودار یک تایملاین است: یک محورِ زمانِ مشترک بالا («time →» —
«not to scale»)، شش lane افقی، و سه ناحیه که با خطچینِ عمودی جدا شدهاند.
ناحیهٔ اول «hop 1: TCP duplicate suppression»: جفتلاینِ فرستنده/گیرنده
(اپ به پایگاه داده روی یک اتصالِ TCP). پیکانِ اول «SYN · original» سالم میرسد؛
پیکانِ دومِ «retry» (خطچین) نوکش یک ✗ قرمز میخورد داخلِ جعبهٔ خطچینِ قرمزِ
«لایهٔ TCP» با برچسبِ «dropped» — یعنی TCP تکراری را داخلِ همان یک
اتصال حذف میکند. ولی یادداشتِ پلِ بینِ ناحیهها هشدار میدهد: «اتصال شکست → اتصالِ تازه: TCP
دیگر کمکی نمیکند».
ناحیهٔ دوم «hop 2: the timeout gap»: مرورگر به سرور دو پیکانِ
POST /transfer میفرستد که هر دو بجِ «req=0286…»
دارند — همان شناسهٔ یکتا. بینشان یک نشانگرِ قرمزِ خطچین است: «COMMIT sent, reply
lost — timed out?» با یادداشتِ «retry یعنی یک درخواستِ جدا، یک تراکنشِ جدا از دیدِ سرور».
ناحیهٔ سوم «the database: constraint does the job»: دو بلوکِ
BEGIN…INSERT request_id…COMMIT میرسند؛ اولی ✓ میگیرد و دومی با همان شناسه ✗
میخورد با برچسبِ «UNIQUE(request_id) rejects the dup — 2nd tx aborts»؛ و
خطِ نتیجه: «moved $11 once, not $22». بالای ناحیهها دو جعبهٔ کدِ مثالهای
۱۳-۱ (غیر-idempotent → ۲۲ دلار) و ۱۳-۲ (UNIQUE + اول INSERT → یک بار ۱۱ دلار) هست؛ و نوارِ پایین اخلاقیِ
کلِ فصل را میگوید: «dedup TCP (یک اتصال) ⊊ تراکنشِ پایگاه داده (یک hop) ⊊ شناسهٔ درخواست از کلاینت تا
پایگاه داده (سرتاسری)».
The diagram is a timeline: one shared time axis on top (“time →” — “not to scale”), six horizontal lanes, and three zones split by vertical dashed lines. Zone one, “hop 1: TCP duplicate suppression”: the sender/receiver lane pair (app to database on one TCP connection). The first “SYN · original” arrow arrives safely; the second “retry” arrow (dashed) takes a red ✗ into the red dashed “TCP layer” box labelled “dropped” — TCP kills the duplicate inside that one connection. But the bridge note between zones warns: “connection breaks → fresh connection: TCP no longer helps.”
Zone two, “hop 2: the timeout gap”: the browser sends the server two POST /transfer arrows, both badged “req=0286…” — the same unique ID. Between them a red dashed marker: “COMMIT sent, reply lost — timed out?” with the note “a retry is a separate request, a separate transaction in the server's eyes.” Zone three, “the database: the constraint does the job”: two BEGIN…INSERT request_id…COMMIT blocks arrive; the first gets ✓ and the second with the same ID gets ✗, labelled “UNIQUE(request_id) rejects the dup — 2nd tx aborts”; and the result line: “moved $11 once, not $22”. Above the zones sit the two code boxes of Examples 13-1 (non-idempotent → $22) and 13-2 (UNIQUE + INSERT-first → $11 once); and the bottom strip states the chapter's moral: “TCP dedup (one connection) ⊊ database transaction (one hop) ⊊ the request ID from client to database (end to end).”
آرگومانِ سرتاسری (Saltzer, Reed, Clark 1984)The end-to-end argument (Saltzer, Reed, Clark 1984)
سناریوی سرکوبِ تراکنشِ تکراری فقط یک نمونه از اصلی کلیتر است: «تابعِ موردنظر را فقط با دانش و کمکِ
اپلیکیشنِ دو سرِ سیستمِ ارتباطی میشود کامل و درست پیاده کرد؛ پس ارائهاش بهعنوانِ ویژگیِ خودِ سیستمِ
ارتباطی ممکن نیست (گاهی نسخهٔ ناقصش بهعنوانِ بهبودِ عملکرد به کار میآید).» در مثالِ ما تابع، سرکوبِ
تکراری بود: دیدیم TCP تکراری را در سطحِ اتصال سرکوب میکند و بعضی پردازندههای stream بهاصطلاح
semantics دقیقاً-یکبار در سطحِ پردازشِ پیام میدهند، ولی این برای جلوگیری از submit تکراریِ کاربر بعد
از timeout کافی نیست. TCP و تراکنشِ پایگاه داده و پردازندهٔ stream بهتنهایی نمیتوانند این تکراریها را
منتفی کنند؛ راهحل سرتاسری میخواهد: شناسهٔ تراکنش که از کلاینتِ کاربر تا پایگاه داده حمل شود. همین استدلال
برای چککردنِ integrity داده هم هست: checksumهای Ethernet و TCP و TLS خرابیِ پکت در شبکه را میگیرند،
ولی خرابیِ ناشی از باگِ نرمافزار در دو سرِ اتصال یا خرابیِ دیسک را نه — برای گرفتنِ همهٔ منابعِ خرابی،
checksum سرتاسری هم لازم است. و برای رمزنگاری هم همینطور (WiFi خانگی، TLS، و در نهایت رمزنگاریِ
سرتاسری). ویژگیهای سطحپایین بیفایده نیستند (بدونِ مرتبکردنِ پکتِ TCP، درخواستهای HTTP مدام بههم
میریختند) — فقط بهتنهایی برای درستیِ سرتاسری کافی نیستند. و این ما را به تزِ اصلی برمیگرداند:
صرفِ اینکه اپ از سیستمِ دادهای با ویژگیِ قوی استفاده میکند، تضمینِ رهایی از گمشدن/خرابشدنِ داده
نیست؛ خودِ اپلیکیشن هم باید اقدامِ سرتاسری (مثلِ سرکوبِ تکراری) بکند. حیف است، چون مکانیزمِ تحملِ خطا را
سخت میشود درست کرد؛ ولی بهنظر میرسد هنوز انتزاعِ درستی پیدا نکردهایم که ماشینِ تحملِ خطای سطحبالا
را جمعوجور کند تا کدِ اپلیکیشن نگرانش نباشد. تراکنش مدتها آن انتزاعِ مفید بود (طیفِ وسیعی از مسائل —
نوشتنِ همزمان، نقضِ قید، crash، قطعیِ شبکه، خرابیِ دیسک — را به دو خروجیِ commit/abort فرومیکاهد؛
سادهسازیِ بزرگی در مدلِ برنامهنویسی، ولی ناکافی). تراکنش گران است، مخصوصاً روی فناوریهای ناهمگن؛ و
وقتی از تراکنشِ توزیعشده فرار میکنیم چون گران است، ناچار میشویم مکانیزمِ تحملِ خطا را در کدِ اپلیکیشن
بازپیاده کنیم — و همانطور که نمونههای کتاب نشان دادند، استدلال دربارهٔ همزمانی و خرابیِ جزئی سخت و
خلافِ شهود است و بیشترِ مکانیزمهای سطحِ اپلیکیشن درست کار نمیکنند؛ نتیجه گمشدن یا خرابشدنِ داده
است. پس ارزش دارد انتزاعهای تحملِ خطایی را بکاویم که دادنِ ویژگیِ درستیِ سرتاسریِ خاصِ اپلیکیشن را
آسان کنند ولی عملکرد و ویژگیِ عملیاتیِ خوب را در محیطِ توزیعشدهٔ بزرگ نگه دارند.
The duplicate-transaction suppression scenario is just one instance of a broader principle: “the desired function can be completely and correctly implemented only with the knowledge and help of the applications at the communication system's endpoints; so providing it as a feature of the communication system itself is impossible (an incomplete version sometimes serves as a performance improvement).” In our example the function was duplicate suppression: TCP suppresses duplicates at connection level and some stream processors offer so-called exactly-once semantics at message-processing level, but neither suffices against a user's resubmit after timeout. TCP, database transactions and stream processors alone can't rule out these duplicates; the fix must be end to end: a transaction ID carried from the user's client to the database. The same argument holds for data-integrity checking: Ethernet, TCP and TLS checksums catch packet corruption on the network, but not corruption from software bugs at either end or disk failures — catching every failure source needs an end-to-end checksum too. And likewise for encryption (home WiFi, TLS, and ultimately end-to-end encryption). Low-level features aren't useless (without TCP packet ordering, HTTP requests would constantly scramble) — just insufficient alone for end-to-end correctness.
And this returns us to the main thesis: that an app uses a data system with a strong feature doesn't guarantee freedom from lost/corrupted data; the application itself must act end to end (like duplicate suppression). A pity, since fault-tolerance machinery is hard to get right; but it seems we haven't yet found the right abstraction that compactly delivers high-level fault-tolerant machinery so application code needn't worry. Transactions long were that useful abstraction (reducing a wide problem range — concurrent writes, constraint violations, crashes, network breaks, disk failures — to two outcomes, commit/abort; a big programming-model simplification, yet insufficient). Transactions are expensive, especially over heterogeneous technologies; and fleeing distributed transactions for their cost forces reimplementing fault-tolerance machinery in application code — and as the book's examples showed, reasoning about partial failure and concurrency is hard and counterintuitive, and most application-level mechanisms malfunction; the result is lost or corrupted data. So it's worth exploring fault-tolerance abstractions that ease giving application-specific end-to-end correctness while keeping good performance and operational properties in a large distributed environment.
۵.۲. قیدها: اجماع یا عذرخواهی5.2. Constraints: consensus or apology
برای بعضی قیدها باید پیش از پذیرش هماهنگ شویم؛ برای بعضی دیگر میتوان خطای محدود را پذیرفت و جبران کرد. این یک تصمیمِ روشنِ محصول است، نه حذفِ بیخبرِ تضمین.
مثال فنی: قید سخت یا جبرانِ پذیرفتهشده
وضعیت اولیه: نام کاربری sara باید فقط یک مالک داشته باشد؛ کالای قابلتأمین ممکن است موقتاً بیشفروش شود.
اجرای مرحلهبهمرحله: ثبت نام روی مسیر هماهنگ رقابت میکند؛ در فروش دوم، سیاستِ مشخصِ تأمین جایگزین یا بازپرداخت میتواند جبران کند.
نتیجه و دلیل: حذف هماهنگی بدون پذیرشِ صریحِ خطا و جبران، همان قید قبلی را نگه نمیدارد.
We saw end-to-end duplicate suppression with an ID carried from the client to the recording database. What about other constraints? Focus on the uniqueness constraint (the one Example 13-2 relied on): usernames or emails must identify the user uniquely; a file store can't hold two same-named files; two people can't book one flight or theater seat. Other constraints are much alike: account balances must not go negative; don't sell beyond warehouse stock; meeting rooms must have no overlapping bookings. The uniqueness-enforcement technique often works for this kind of constraint too.
قیدِ یکتایی در محیطِ توزیعشده نیازمندِ اجماع است (فصل ۱۰): اگر چند درخواستِ همزمان مقدارِ یکسان
داشته باشند، سیستم باید تصمیم بگیرد کدام عملیاتِ متعارض پذیرفته شود و بقیه بهعنوانِ نقضِ قید رد شوند.
رایجترین راهِ رسیدن به این اجماع، رهبرکردنِ یک نود و سپردنِ همهٔ تصمیمها به اوست — تا وقتی اشکالی ندارد
که funnel کردنِ همهٔ درخواستها از یک نود (حتی اگر کلاینت آنطرفِ دنیا باشد) و خرابشدنِ آن نود را تحمل
کنی. الگوریتمهای اجماع مثلِ Raft مسئلهٔ انتخابِ امنِ رهبرِ تازه بعد از خرابیِ رهبر و جلوگیری از
split-brain را حل میکنند. چکِ یکتایی را میشود با shard بر اساسِ مقداری که باید یکتا بماند
مقیاس داد: برای یکتاییِ request ID، همهٔ درخواستهای با همان request ID به همان shard بروند؛ برای یکتاییِ
username، shard بر اساسِ hash نامِ کاربری. ولی replication ناهمگامِ multi-leader منتفی است، چون رهبرهای
متفاوت میتوانند همزمان نوشتنِ متعارض را بپذیرند و مقادیر دیگر یکتا نیستند. اگر بخواهی هر نوشتنِ ناقضِ
قید را بلافاصله رد کنی، هماهنگیِ همگام گریزناپذیر است.
A uniqueness constraint in a distributed environment needs consensus (Chapter 10): if several concurrent requests share a value, the system must decide which conflicting operation is accepted and reject the rest as constraint violations. The most common way to this consensus is making one node the leader and entrusting it with every decision — fine while you tolerate funnelling every request through one node (even if the client is across the world) and that node breaking. Consensus algorithms like Raft solve safely electing a fresh leader after the leader's failure and preventing split-brain. Uniqueness checks scale by sharding on the value that must stay unique: for request-ID uniqueness, route all requests with the same request ID to the same shard; for username uniqueness, shard by username hash. But asynchronous multi-leader replication is out, since different leaders can concurrently accept conflicting writes and values stop being unique. If you want every constraint-violating write rejected immediately, synchronous coordination is unavoidable.
یکتایی در پیامرسانیِ لاگمحور: لاگِ مشترک تضمین میکند همهٔ مصرفکنندهها پیامها را بههمانترتیب
ببینند (تضمینِ total order broadcast که معادلِ consensus است). در رویکردِ پایگاه دادهِ unbundle با پیامرسانیِ
لاگمحور، میشود با رویکردِ خیلی مشابهی قیدِ یکتایی را enforce کرد: پردازندهٔ stream همهٔ پیامهای یک
shard لاگ را ترتیبی روی یک thread مصرف میکند؛ پس اگر لاگ بر اساسِ مقداری که باید یکتا بماند shard شده
باشد، پردازنده میتواند بدونِ ابهام و قطعی تصمیم بگیرد کدامیک از چند عملیاتِ متعارض در لاگ اول آمده.
مثلاً چند کاربر میخواهند یک username را بگیرند: (۱) هر درخواست بهصورتِ پیام در shardای که با hash
username تعیین میشود append میشود؛ (۲) پردازندهٔ stream درخواستها را ترتیبی میخواند و با پایگاه دادهِ
محلی ردیابی میکند کدام نامها گرفته شدهاند — برای هر نامِ آزاد، گرفته ثبت میکند و پیامِ success به
جریانِ خروجی میدهد، و برای نامِ گرفتهشده پیامِ reject؛ (۳) کلاینتی که نام را خواسته، جریانِ خروجی را
تماشا میکند و منتظرِ success/reject خودش میماند. این الگوریتم همان ساختارِ «رسیدن به اجماع با لاگِ
مشترک» فصل ۱۰ است؛ و با زیادکردنِ shard بهراحتی به توانِ بالا مقیاس میشود چون هر shard مستقل پردازش
میشود. این رویکرد فقط برای یکتایی نیست، برای خیلی قیدهای دیگر هم جواب میدهد؛ اصلِ بنیادینش این است که
هر نوشتنهایی که ممکن است باهم درگیر شوند به همان shard هدایت و ترتیبی پردازش میشوند. تعریفِ
«درگیری» به اپلیکیشن بستگی دارد، ولی پردازنده میتواند با منطقِ دلخواه درخواست را validate کند.
Uniqueness in log-based messaging: the shared log guarantees every consumer sees messages in the same order (the total-order-broadcast guarantee that equals consensus). In the unbundled-database approach with log-based messaging, the uniqueness constraint can be enforced with a very similar approach: the stream processor consumes one log shard's messages sequentially on one thread; so if the log is sharded by the value that must stay unique, the processor can unambiguously and deterministically decide which of several conflicting operations in the log came first. E.g. several users want one username: (1) each request appends as a message to the shard set by the username's hash; (2) the stream processor reads requests sequentially and tracks with a local database which names are taken — for each free name it records taken and emits a success message to the output flow, and for taken names a reject message; (3) the requesting client watches the output flow and waits for its own success/reject. This algorithm is the same “reaching consensus with a shared log” structure as Chapter 10; and it scales easily to high throughput with more shards since each shard processes independently. This approach isn't only for uniqueness — it works for many other constraints; its fundamental principle is routing every potentially conflicting write to the same shard for sequential processing. What counts as “conflict” depends on the application, but the processor can validate requests with arbitrary logic.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: قید سخت یا جبرانِ پذیرفتهشده
وضعیت اولیه: نام کاربری sara باید فقط یک مالک داشته باشد؛ کالای قابلتأمین ممکن است موقتاً بیشفروش شود.
اجرای مرحلهبهمرحله: ثبت نام روی مسیر هماهنگ رقابت میکند؛ در فروش دوم، سیاستِ مشخصِ تأمین جایگزین یا بازپرداخت میتواند جبران کند.
نتیجه و دلیل: حذف هماهنگی بدون پذیرشِ صریحِ خطا و جبران، همان قید قبلی را نگه نمیدارد.
این یک درختِ تصمیم است. ریشه Constraint Must Hold است با زمینهٔ
«reservation, seat, balance» — یعنی از اول معلوم است داریم دربارهٔ قیدهای
واقعی (رزرو، صندلی، بالانس) حرف میزنیم. سوالِ اول: Needs Strict Guarantee?
(«reject conflicts now»). اگر بله — باید همینالان درگیری را رد کنی —
مستقیم به برگِ Single Leader per Shard میروی
(«shard by constrained value» + «هماهنگیِ همگام»)؛ یعنی همان راهِ اجماع.
اگر تضمینِ سخت لازم نیست، سوالِ دوم: Can You Compensate Later?
(«viable apology path?» — آیا مسیرِ عذرخواهیِ شدنی داری؟). بله یعنی برگِ
Apologize and Compensate («write first, fix up
later» — «refunds, overbooking») — خوشبینانه بنویس و بعد درستش کن.
و اگر جبران هم شدنی نیست، سوالِ سوم: Must Decide Synchronously?
(«can the client wait?»). بله میشود برگِ
Shard by Conflict Key («same shard, ordered
check» — «success / reject events») — نوشتنهای درگیر به همان shard
و چکِ ترتیبی با رویدادِ success/reject. نه هم میشود برگِ Async Validate +
Notify («validate later, notify») — در لاگ بنویس و بعداً validate کن
و خبر بده. کارتِ پایینِ نمودار هم جمعبندیِ کلِ بخشِ ۵ را میدهد: «سیستمهای coordination-avoiding —
integrity قوی بدونِ consensus؛ چون integrity با timeliness فرق دارد».
This is a decision tree. The root is Constraint Must Hold with the context “reservation, seat, balance” — clear from the start that we're talking about real constraints (reservations, seats, balances). First question: Needs Strict Guarantee? (“reject conflicts now”). Yes — conflicts must be rejected right now — goes straight to the Single Leader per Shard leaf (“shard by constrained value” + “synchronous coordination”); the consensus path.
If no hard guarantee is needed, second question: Can You Compensate Later? (“viable apology path?” — is there a feasible apology path?). Yes means the Apologize and Compensate leaf (“write first, fix up later” — “refunds, overbooking”) — write optimistically, fix later. And if compensation isn't feasible either, third question: Must Decide Synchronously? (“can the client wait?”). Yes becomes the Shard by Conflict Key leaf (“same shard, ordered check” — “success / reject events”) — conflicting writes to the same shard with a sequential check and success/reject events. No becomes Async Validate + Notify (“validate later, notify”) — write to the log, validate later, notify. The card at the bottom summarizes all of Section 5: “coordination-avoiding systems — strong integrity without consensus; because integrity differs from timeliness.”
۵.۳. پرداختِ چندshardی بدونِ commit اتمیک5.3. Multishard payment without atomic commit
میتوان یک دستورِ معتبر را یکبار ثبت کرد و اثرهای آن را مرحلهای به شاردها رساند. این طراحی به تحویل پایدار، جلوگیری از تکرار، کنترل قواعد و راستیآزمایی نیاز دارد؛ رؤیتپذیریِ همزمانِ همهٔ اثرها را خودبهخود نمیدهد.
مثال فنی: دستور انتقالِ پایدار، اثر مرحلهای
وضعیت اولیه: دستور T7: A→B, amount=100 پایدار و پذیرفته شده است.
اجرای مرحلهبهمرحله: shard A کسر را با شناسهٔ T7 یکبار اعمال میکند؛ shard B واریز را با همان شناسه اعمال میکند؛ مراحل ناتمام retry و تطبیق میشوند.
نتیجه و دلیل: به شرطِ پروتکلِ درست، اعتبارسنجی و پیشرفت، اثرها کامل میشوند؛ تا تکمیل ممکن است وضعیت میانی دیده شود و atomic visibility نداریم.
When several shards are involved, atomically enforcing operations with constraints gets more attractive. In Example 13-2 there are potentially three shards: one for the request ID, one for the recipient account, one for the payer account — with no reason for all three to share one shard since they're independent. In the traditional database approach, running this transaction wants atomic commit across all three shards, effectively funnelling it with every other transaction of all three shards into one total order; and with cross-shard coordination now present, shards no longer process independently and throughput drops. But equivalent correctness is reachable without cross-shard transactions, using sharded logs and stream processors. Fig. 13-2 shows an example: a payment transaction that must check the source account holds enough money and, if so, atomically move the amount to the destination account minus a fee. The steps:
- درخواستِ انتقال از حسابِ مبدأ به مقصد، یک request ID یکتا از کلاینتِ کاربر میگیرد و در shardِ لاگی که بر اساسِ ID حسابِ مبدأ است append میشود.
- پردازندهٔ stream لاگِ درخواستها را میخواند و پایگاهدادهای نگه میدارد که state حسابِ مبدأ و IDهای درخواستهایی که قبلاً پردازش کرده را دارد (محتوای این پایگاه داده تماماً از لاگ مشتق شده). وقتی به درخواستی با ID نادیده میرسد، در پایگاه دادهِ محلی چک میکند حسابِ مبدأ پولِ کافی دارد یا نه. اگر داشت، پایگاه دادهِ محلی را بهروز میکند تا مبلغ روی حسابِ مبدأ رزرو شود و رویداد به چند لاگِ دیگر میفرستد: رویدادِ پرداختِ خروجی به shardِ لاگِ حسابِ مبدأ (لاگِ ورودیِ خودش)، رویدادِ پرداختِ ورودی به shardِ لاگِ حسابِ مقصد، و رویدادِ پرداختِ ورودی به shardِ لاگِ حسابِ کارمزد — و request ID اصلی در آن رویدادهای منتشرشده هست.
- سرانجام رویدادِ پرداختِ خروجی به پردازندهٔ حسابِ مبدأ برمیگردد (که ممکن است بینش رویدادهای نامرتبط گرفته باشد). پردازنده از روی request ID میفهمد این پرداختی است که قبلاً رزروش کرده و پرداخت را اجرا میکند و دوباره state محلیِ حسابِ مبدأ را بهروز میکند؛ و تکراریها را بر اساسِ request ID نادیده میگیرد.
- shardهای لاگِ حسابِ مقصد و کارمزد را taskهای مستقلِ پردازشِ stream مصرف میکنند؛ وقتی رویدادِ پرداختِ ورودی را میگیرند، state محلی را بهروز میکنند و رویدادها را بر اساسِ request ID dedup میکنند.
- The transfer request from source to destination account takes a unique request ID from the user's client and appends in the log shard keyed by the source account ID.
- The stream processor reads the request log and keeps a database holding the source account's state and the request IDs it already processed (this database's content is fully derived from the log). On reaching an unseen-ID request, it checks in the local database whether the source account holds enough money. If so, it updates the local database to reserve the amount on the source account and sends events to several other logs: the outgoing-payment event to the source account's log shard (its own input log), the incoming-payment event to the destination account's log shard, and the incoming-payment event to the fee account's log shard — with the original request ID present in those published events.
- Finally the outgoing-payment event returns to the source-account processor (which may have taken unrelated events in between). From the request ID the processor knows this is a payment it already reserved, executes the payment and updates the source account's local state again; and ignores duplicates by request ID.
- The destination- and fee-account log shards are consumed by independent stream-processing tasks; on taking the incoming-payment event they update local state and dedup events by request ID.
در شکل هر سه حساب در سه shard جدایند، ولی میتوانستند در یک shard هم باشند — فرقی نمیکند. تنها شرطها
ایناند: رویدادهای هر حساب دقیقاً بهترتیبِ لاگ با semantics حداقل-یکبار پردازش شوند و پردازندهها
deterministic باشند. مثلاً ببین اگر پردازندهٔ حسابِ مبدأ وسطِ پردازشِ درخواستِ پرداخت crash کند چه
میشود: پیامهای خروجی ممکن است قبل از crash منتشر شده باشند یا نه. بعد از recovery، همان درخواست
دوباره پردازش میشود (بهخاطرِ حداقل-یکبار) و همان تصمیمِ «پرداخت مجاز است یا نه» را میگیرد (چون
قطعی است) — پس همان پیامهای خروجی را با همان request ID به shardهای خروجی/ورودی/کارمزد میفرستد؛ و اگر
پیامها تکراری باشند، مصرفکنندههای پاییندستی بر اساسِ request ID نادیدهشان میگیرند.
atomicity در این سیستم از تراکنش نمیآید، از این واقعیت میآید که نوشتنِ رویدادِ درخواستِ اولیه در
لاگِ حسابِ مبدأ یک action اتمیک است. وقتی آن یک رویداد در لاگ هست، همهٔ رویدادهای پاییندستی سرانجام
نوشته میشوند — شاید بعد از recovery پردازندهها از crash، و شاید با تکراری، ولی سرانجام میآیند.
با semantics دقیقاً-یکبار پیادهسازی سادهتر میشود، چون state محلیِ پردازنده با مجموعهٔ پیامهایی که
پردازش کرده سازگار میماند؛ پس اگر crash کرد و بعضی پیامها را بازپردازش کرد، state محلیاش هم به همان
قبلِ آن پیامها برمیگردد.
In the figure all three accounts sit in three separate shards, but they could share one shard too — it makes no difference. The only conditions: each account's events process exactly in log order with at-least-once semantics, and processors are deterministic. E.g. see what happens if the source-account processor crashes mid-payment: the output messages may or may not have published before the crash. After recovery the same request reprocesses (thanks to at-least-once) and takes the same “payment allowed or not” decision (since deterministic) — so it sends the same output messages with the same request ID to the outgoing/incoming/fee shards; and if messages duplicate, downstream consumers ignore them by request ID. Atomicity in this system doesn't come from transactions but from the fact that writing the initial request event to the source account's log is one atomic action. Once that one event is in the log, all downstream events eventually get written — perhaps after processors recover from crashes, and perhaps duplicated, but eventually. Exactly-once semantics simplifies implementation, since the processor's local state stays consistent with the message set it processed; so if it crashes and reprocesses some messages, its local state returns to just before those messages too.
اگر کاربرِ شکلِ ۱۳-۲ بخواهد بفهمد انتقالش تأیید شد یا نه، میتواند مشترکِ shardِ لاگِ حسابِ مبدأ شود و
منتظرِ رویدادِ پرداختِ خروجی بماند. و برای اینکه اگر موجودی کافی نبود صریح به کاربر خبر بدهد، پردازنده
میتواند رویدادِ «پرداختِ ردشده» به همان shard بفرستد. با شکستنِ تراکنشِ چندshardی به چند stage با
shard متفاوت و استفاده از request ID سرتاسری، به همان ویژگیِ درستی میرسیم (هر درخواست دقیقاً یک بار روی
هر دو حسابِ پرداختکننده و گیرنده اعمال میشود)، حتی با وجودِ خطا — بدونِ پروتکلِ commit اتمیک.
If Fig. 13-2's user wants to know whether their transfer confirmed, they can subscribe to the source account's log shard and wait for the outgoing-payment event. And to explicitly tell the user when funds were insufficient, the processor can send a “payment declined” event to the same shard. By breaking the multishard transaction into stages with different shards and using an end-to-end request ID, we reach the same correctness property (each request applied exactly once on both payer and payee accounts), even under failure — with no atomic-commit protocol.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: دستور انتقالِ پایدار، اثر مرحلهای
وضعیت اولیه: دستور T7: A→B, amount=100 پایدار و پذیرفته شده است.
اجرای مرحلهبهمرحله: shard A کسر را با شناسهٔ T7 یکبار اعمال میکند؛ shard B واریز را با همان شناسه اعمال میکند؛ مراحل ناتمام retry و تطبیق میشوند.
نتیجه و دلیل: به شرطِ پروتکلِ درست، اعتبارسنجی و پیشرفت، اثرها کامل میشوند؛ تا تکمیل ممکن است وضعیت میانی دیده شود و atomic visibility نداریم.
نمودار پنج ستون دارد. اول Client Request
(«unique request ID» — «append») با فلشِ
«request + unique ID» به Source Processor
(«checks + reserves» — «derived DB») میرسد —
این تنها action اتمیکِ کلِ سیستم است: یک append. بعد با فلشِ
«reserve + emit 3 events» به هابِ میانیِ
3 Derived Events («same request ID») میروی —
همان سه رویدادی که پردازندهٔ مبدأ منتشر میکند.
از هاب سه فلش بیرون میزند (بدونِ هیچ تقاطعی): «outgoing payment» به
Source Log («outgoing+declined» —
«subscribe» — همان لاگی که کاربر برای فهمیدنِ نتیجه مشترکِ آن میشود)،
«incoming payment» به Destination Proc
(«apply + dedup» — «request ID»)، و
«fee payment» به Fees Processor
(«apply + dedup» — «request ID»).
فلشِ پایانی «completed / declined» به
Completed («eventually written» —
«no 2PC») میرسد. سه کارتِ نمودار هم درسها را نگه میدارند:
atomicity از همان یک append میآید؛ crash یعنی بازپردازشِ قطعی با همان خروجی و همان شناسه پس تکراریها
نادیده گرفته میشوند؛ و دقیقاً-یکبار کار را سادهتر میکند چون state با مجموعهٔ پردازششده سازگار میماند.
The diagram has five columns. First Client Request (“unique request ID” — “append”) reaches the Source Processor (“checks + reserves” — “derived DB”) via the “request + unique ID” arrow — the whole system's only atomic action: one append. Then via “reserve + emit 3 events” you go to the middle hub of 3 Derived Events (“same request ID”) — the same three events the source processor publishes.
Three arrows leave the hub (with no crossings): “outgoing payment” to the Source Log (“outgoing+declined” — “subscribe” — the same log the user subscribes to learn the result), “incoming payment” to the Destination Proc (“apply + dedup” — “request ID”), and “fee payment” to the Fees Processor (“apply + dedup” — “request ID”). The closing “completed / declined” arrow reaches Completed (“eventually written” — “no 2PC”). The diagram's three cards hold the lessons: atomicity comes from that one append; a crash means deterministic reprocessing with the same outputs and IDs, so duplicates are ignored; and exactly-once eases the work since state stays consistent with the processed set.
۵.۴. timeliness در برابر integrity5.4. Timeliness vs integrity
تازگی میگوید نتیجه چقدر زود دیده میشود؛ یکپارچگی میگوید داده گم، تکراری یا متناقض نشده است. یک نتیجه میتواند درست باشد ولی دیر دیده شود.
مثال فنی: تازگی با درستی فرق دارد
وضعیت اولیه: مرجع دو سفارش ۱۰۰ و ۲۰۰ دارد.
اجرای مرحلهبهمرحله: نمای اول فقط سفارش اول را دارد و جمعش ۱۰۰ است؛ نمای دوم هر دو را فوراً دیده ولی اولی را دوبار شمرده و جمعش ۴۰۰ است.
نتیجه و دلیل: اولی مشکل timeliness و دومی مشکل integrity دارد؛ جلوتر بودن موقعیت مصرف بهتنهایی درست بودن محاسبه را ثابت نمیکند.
The comfortable property of many transactional systems is that once a transaction commits, its writes are immediately visible to other transactions — formalized as strict serializability (Chapter 10). When you break an operation across several stream-processor stages, that doesn't hold. Log consumers are inherently asynchronous, so the producer doesn't wait for consumers to process its message. But the client may wait for its message to appear in the output flow — like the user waiting for the outgoing- or declined-payment event in Fig. 13-2, depending on whether the source account held enough money. In this example, the correctness of the source-balance check doesn't depend on whether the user waits for the result; waiting only synchronously informs the user whether the payment succeeded — that notification is separate from processing the request's effect.
کلیتر، واژهٔ «consistency» دو نیاز را قاطی میکند که ارزش دارد جدا بررسیشان کنی:
More generally, the word “consistency” mixes two needs worth examining separately:
Timeliness
تازگی، فاصلهٔ تغییرِ واقعی تا دیده شدنِ آن در خروجی را توصیف میکند.
مثال فنی: Lag قابلاندازهگیری
وضعیت اولیه: مرجع تا offset ۱۲۰ و گزارش تا ۱۱۰ پیش رفته است.
اجرای مرحلهبهمرحله: ۱۰ رخداد عقبمانده و زمان قدیمیترین رخدادِ اعمالنشده ثبت میشود؛ پس از catch up نتیجه تازه میشود.
نتیجه و دلیل: timeliness دربارهٔ زمانِ دیده شدنِ دادهٔ درست است؛ باید با معیار مناسب بار سنجیده شود، نه فقط روشن بودن worker.
یعنی مطمئن شوی کاربران سیستم را در حالتِ بهروز میبینند. اگر کاربر از کپیِ کهنه بخواند، ممکن است آن
را در حالتِ ناسازگار ببیند (مشکلهای replication lag، فصل ۶) — ولی آن ناسازگاری موقتی است و با
صبرکردن و دوبارهتلاشکردن حل میشود. قضیهٔ CAP «consistency» را به معنای linearizability به کار
میبرد که راهِ قویِ رسیدن به timeliness است؛ ویژگیهای ضعیفترِ timeliness مثلِ read-after-write هم
میتوانند مفید باشند.
Being sure users see the system up to date. Reading from a stale copy may show it in an inconsistent state (replication-lag problems, Chapter 6) — but that inconsistency is temporary and resolves with waiting and retrying. The CAP theorem uses “consistency” to mean linearizability, the strong way to timeliness; weaker timeliness properties like read-after-write can be useful too.
Integrity
یکپارچگی یعنی داده طبق قواعدِ تعیینشده گم، تکراری یا ناسازگار نشود؛ سرعتِ رسیدنِ نتیجه موضوعِ جداگانهای است.
مثال فنی: جمعِ سریع اما غلط
وضعیت اولیه: پیام فروش e7 مبلغ ۲۰۰ دو بار به جمع اولیهٔ ۱۰۰۰ اضافه شده است.
اجرای مرحلهبهمرحله: مصرفکننده تا آخر لاگ رسیده، ولی جمع ۱۴۰۰ است؛ dedup اتمیک میتوانست نتیجه را ۱۲۰۰ نگه دارد.
نتیجه و دلیل: backlog صفر درستی را ثابت نمیکند؛ خطای integrity میتواند پس از تمام شدن پردازش هم باقی بماند.
یعنی نبودِ خرابی — نه گمشدنِ داده، نه دادهٔ متناقض یا غلط. بهویژه اگر dataset مشتق بهعنوانِ viewای
روی دادهٔ زیرین نگهداری میشود، مشتقسازی باید درست باشد: ایندکسِ پایگاه داده باید محتوای پایگاه داده
را درست منعکس کند — ایندکسی که رکورد کم دارد به درد نمیخورد. اگر integrity نقض شود، ناسازگاری
دائمی است؛ صبرکردن و دوبارهتلاشکردن در بیشترِ موارد خرابیِ پایگاه داده را درست نمیکند و چک و
تعمیرِ صریح لازم است. در دنیای تراکنشِ ACID، «consistency» معمولاً همین مفهومِ integrity خاصِ
اپلیکیشن فهمیده میشود؛ و atomicity و durability ابزارهای مهمِ حفظِ آناند.
The absence of corruption — no lost data, no contradictory or wrong data. Especially when a derived dataset is maintained as a view over underlying data, derivation must be correct: the database's index must faithfully reflect the database's content — an index missing records is useless. Violated integrity means permanent inconsistency; waiting and retrying won't fix database corruption in most cases — explicit checking and repair is needed. In the ACID-transaction world, “consistency” is usually understood as this application-specific integrity notion; and atomicity and durability are key tools for preserving it.
به زبانِ شعاری: نقضِ timeliness زیرِ eventual consistency مجاز است؛ ولی نقضِ integrity یعنی ناسازگاریِ
دائمی. در بیشترِ اپها integrity خیلی مهمتر از timeliness است: نقضِ timeliness آزاردهنده و گیجکننده
است، ولی نقضِ integrity میتواند فاجعه باشد. مثلاً در صورتحسابِ کارتِ اعتباری عجیب نیست اگر تراکنشی که در
۲۴ ساعتِ گذشته کردی هنوز نیامده — عادی است که این سیستمها lag داشته باشند؛ میدانیم بانکها تراکنشها را
ناهمگام reconcile و settle میکنند و timeliness اینجا خیلی مهم نیست. ولی خیلی بد میشد اگر بالانسِ صورتحساب
با جمعِ تراکنشها بهعلاوهٔ بالانسِ قبلی برابر نبود (خطا در جمعها)، یا تراکنشی به تو شارژ میشد ولی به
فروشنده پرداخت نمیشد (پولِ گمشده) — اینها نقضِ integrity سیستم بودند.
As a slogan: timeliness violations are permitted under eventual consistency; but an integrity violation means permanent inconsistency. In most apps integrity matters far more than timeliness: timeliness violations are annoying and confusing, but integrity violations can be catastrophic. E.g. in a credit-card statement it's unsurprising if a last-24-hours transaction hasn't arrived yet — these systems normally lag; we know banks reconcile and settle transactions asynchronously and timeliness matters little here. But it would be very bad if the statement balance didn't equal the transactions' sum plus the previous balance (summing errors), or a transaction charged you without paying the merchant (lost money) — those would violate the system's integrity.
تراکنشهای ACID معمولاً هر دو تضمینِ timeliness (مثلِ linearizability) و integrity (مثلِ atomic commit) را
میدهند؛ پس اگر از زاویهٔ تراکنشِ ACID به درستیِ اپلیکیشن نگاه کنی، تمایزِ timeliness و integrity چندان
مهم نیست. ولی ویژگیِ جالبِ سیستمهای dataflow رویدادمحوری که در این فصل دیدیم این است که timeliness و
integrity را از هم جدا میکنند: موقعِ پردازشِ ناهمگامِ جریانِ رویداد، هیچ تضمینِ timelinessای نیست،
مگر اینکه صریح مصرفکنندهای بسازی که قبل از برگشتن منتظرِ رسیدنِ پیام بماند (مثلاً کاربر پرداخت را
درخواست میکند و بعد state حسابش را میخواند قبل از اینکه پردازنده درخواست را اجرا کرده باشد — پرداختی
را که تازه خواسته نمیبیند). ولی integrity در واقع محورِ سیستمهای جریانی است: semantics
دقیقاً-یکبار یا عملاً-یکبار مکانیزمِ حفظِ integrity است — اگر رویدادی گم شود یا دو بار اثر کند، integrity
سیستم میتواند نقض شود؛ پس تحویلِ fault-tolerant پیام و سرکوبِ تکراری (عملیاتِ idempotent) برای حفظِ
integrity در برابرِ خطا مهماند. و همانطور که در بخشِ قبل دیدیم، سیستمهای پردازشِ stream قابلاعتماد
میتوانند integrity را بدونِ تراکنشِ توزیعشده و پروتکلِ commit اتمیک حفظ کنند — یعنی به درستیِ
قابلمقایسه با عملکرد و قویبودنِ عملیاتیِ خیلی بهتر برسند. این integrity را با ترکیبی از مکانیزمها گرفتیم:
ACID transactions usually give both timeliness (like linearizability) and integrity (like atomic commit) guarantees; so viewed from the ACID-transaction angle, the timeliness/integrity distinction matters little. But the interesting property of this chapter's event-driven dataflow systems is that they separate timeliness from integrity: in asynchronous event-flow processing there is no timeliness guarantee, unless you explicitly build a consumer that waits for the message before returning (e.g. the user requests a payment then reads their account state before the processor executed the request — not seeing the just-requested payment). But integrity is actually the streaming systems' focus: exactly-once or effectively-once semantics is an integrity-preserving mechanism — if an event is lost or applied twice, system integrity can break; so fault-tolerant message delivery and duplicate suppression (idempotent operations) matter for preserving integrity against failure. And as the previous section showed, reliable stream processors can preserve integrity without distributed transactions and atomic-commit protocols — i.e. reach comparable correctness with far better performance and operational robustness. That integrity was taken with a combination of mechanisms:
- نشاندادنِ محتوای عملیاتِ نوشتن بهصورتِ یک پیامِ واحد که میشود اتمیک نوشت — رویکردی که خیلی خوب با event sourcing جفت میشود؛
- مشتقکردنِ بقیهٔ بهروزرسانیهای state از همان یک پیام با تابعِ مشتقِ قطعی، شبیهِ stored procedure؛
- ردکردنِ request ID تولیدشدهٔ کلاینت از همهٔ این لایههای پردازش، که سرکوبِ تکراری و idempotence سرتاسری را ممکن میکند؛
- immutable کردنِ پیامها و اجازهٔ بازپردازشِ گاهبهگاهِ دادهٔ مشتق، که بازیابی از باگ را سادهتر میکند.
- Showing a write operation's content as a single message that can be written atomically — an approach pairing very well with event sourcing;
- Deriving the rest of the state updates from that one message with a deterministic derivation function, like a stored procedure;
- Passing the client-generated request ID through all these processing layers, enabling end-to-end duplicate suppression and idempotence;
- Making messages immutable and allowing occasional reprocessing of derived data, simplifying bug recovery.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: جمعِ سریع اما غلط
وضعیت اولیه: پیام فروش e7 مبلغ ۲۰۰ دو بار به جمع اولیهٔ ۱۰۰۰ اضافه شده است.
اجرای مرحلهبهمرحله: مصرفکننده تا آخر لاگ رسیده، ولی جمع ۱۴۰۰ است؛ dedup اتمیک میتوانست نتیجه را ۱۲۰۰ نگه دارد.
نتیجه و دلیل: backlog صفر درستی را ثابت نمیکند؛ خطای integrity میتواند پس از تمام شدن پردازش هم باقی بماند.
نمودار یک شکلِ ساختاریِ UML-گونه است. بالا جعبهٔ «ACID consistency (the
overloaded word)» است که با دو پیکان («up-to-date?» و
«uncorrupted?») به دو جعبهٔ کلاس میشکند. جعبهٔ Timeliness
(سبز): «کاربران state بهروز میبینند»؛ خصیصهها: linearizability قویترین شکل است، read-after-write
شکلِ ضعیفتر؛ نقض → موقتی (با صبر و retry درست میشود، زیرِ eventual consistency خودش خوب میشود)؛
و مثال: «تراکنشِ کارت که ۲۴ ساعت در صورتحساب نیست — آزاردهنده ولی عادی». جعبهٔ
Integrity (آبی): «نه گمشدن، نه تناقض، مشتقسازیِ درست»؛ ایندکس باید جدول را
منعکس کند؛ نقض → دائمی (چک و تعمیرِ صریح میخواهد)؛ و مثال: «بالانس ≠ جمعِ تراکنشها، یا پولِ
گمشده — فاجعه».
وسط، بنرِ شعاری با قابِ خطچین است: «نقضِ timeliness زیرِ eventual consistency مجاز است؛ نقضِ integrity
یعنی ناسازگاریِ دائمی». پایین دو جعبه نشان میدهند dataflow هرکدام را چطور میدهد:
timeliness in dataflow — «بهطورِ پیشفرض هیچ تضمینی نیست (مصرفکنندههای
ناهمگام)؛ ولی کلاینت میتواند منتظرِ رویدادِ خروجیِ خودش بماند (outgoing-payment/declined) — و چکِ
بالانس هیچوقت منتظر نمیماند: اعلان از اثر جداست»؛ و integrity in dataflow —
همان چهار مکانیزمِ شمارهدارِ کتاب: ① یک پیامِ اتمیک (event sourcing) ② تابعِ مشتقِ قطعی ③ شناسهٔ
کلاینت در همهٔ stageها (dedup سرتاسری) ④ پیامِ immutable + بازپردازش. نوارِ پایین هم حکم میدهد:
«حفظِ integrity بدونِ تراکنشِ توزیعشده → درستیِ قابلمقایسه، با عملکرد و قویبودنِ خیلی بهتر».
The diagram is a UML-like structural figure. On top, the “ACID consistency (the overloaded word)” box splits with two arrows (“up-to-date?” and “uncorrupted?”) into two class boxes. The Timeliness box (green): “users see up-to-date state”; attributes: linearizability is the strongest form, read-after-write the weaker; violation → temporary (fixed by waiting and retry, self-heals under eventual consistency); and the example: “a card transaction missing from the statement for 24 hours — annoying but normal”. The Integrity box (blue): “no loss, no contradiction, correct derivation”; the index must reflect the table; violation → permanent (wants explicit check and repair); and the example: “balance ≠ transaction sums, or lost money — a catastrophe”.
In the middle, a dashed-frame slogan banner: “timeliness violations are permitted under eventual consistency; integrity violations mean permanent inconsistency.” Below, two boxes show how dataflow gives each: timeliness in dataflow — “by default no guarantee (asynchronous consumers); but the client can wait for its own output event (outgoing-payment/declined) — and the balance check never waits: notification is separate from effect”; and integrity in dataflow — the book's same four numbered mechanisms: ① one atomic message (event sourcing) ② a deterministic derivation function ③ the client ID in every stage (end-to-end dedup) ④ immutable messages + reprocessing. The bottom strip rules: “preserving integrity without distributed transactions → comparable correctness, with far better performance and robustness.”
۵.۵. نقطهٔ بهینه: هماهنگیِ کمتر، عذرِ بیشتر5.5. The sweet spot: less coordination, more apology
هماهنگی را جایی مصرف کنیم که نقضِ قید هزینهٔ غیرقابلقبول دارد. اگر جبران مجاز است، باید روش، سقفِ خطا و تجربهٔ کاربر از قبل طراحی شود.
مثال فنی: انتخاب محل هماهنگی
وضعیت اولیه: برای آخرین صندلی شمارهدار دو خریدار داریم، ولی برای شمارش بازدید محدودیت مشابهی نیست.
اجرای مرحلهبهمرحله: فروش صندلی روی منبع مالکیت رقابت میکند؛ شمارندهٔ بازدید میتواند غیرهمزمان ادغام شود.
نتیجه و دلیل: هزینهٔ هماهنگی را به قید لازم میدهیم؛ جبران فقط وقتی معنی دارد که محصول آن نتیجه را واقعاً بپذیرد.
Enforcing a uniqueness constraint needs consensus, which usually means funnelling one shard's every event through one node — and if you want the traditional form of the uniqueness constraint, that limit is unavoidable and stream processing doesn't bypass it. But many real apps have a business need that permits violating what you think of as a “hard constraint”:
- اگر مشتری بیشتر از موجودیِ انبار سفارش داد، میشود بیشتر سفارش داد، برای تأخیر عذر خواست و تخفیف داد — همان کاری که اگر لیفتراک روی کالاهای انبار میرفت و موجودی از تصور کمتر میشد هم باید میکردی. پس workflow عذرخواهی بههرحال برای چنین حادثههایی باید بخشی از فرآیندِ کسبوکار باشد و قیدِ سختِ روی تعدادِ موجودی انبار شاید لازم نباشد.
- خیلی ایرلاینها با فرضِ اینکه بعضی مسافرها به پرواز نمیرسند overbook میکنند، و خیلی هتلها اتاق را overbook میکنند با فرضِ کنسلیِ بعضی مهمانها. اینجا قیدِ «یک نفر برای هر صندلی» عمداً برای دلیلِ کسبوکاری نقض میشود و فرآیندِ جبران (refund، ارتقا، اتاقِ رایگان در هتلِ همسایه) برای تقاضای بیش از عرضه گذاشته میشود. حتی بدونِ overbook هم فرآیندِ عذر و جبران برای رویدادهایی مثلِ کنسلیِ پرواز بهخاطرِ هوای بد یا اعتصاب لازم بود — بازیابی از این مسائل فقط بخشیِ عادیِ کسبوکار است.
- اگر کسی بیشتر از موجودیِ حسابش برداشت کرد، بانک میتواند کارمزدِ overdraft بگیرد و بخواهد بدهی را پس بدهد؛ و با محدودکردنِ کلِ برداشتِ روزانه، ریسکِ بانک محدود میماند.
- در سیستمهایی که داده را بینِ سازمانها یکپارچه میکنند، ناسازگاری ناگزیر پیش میآید و مکانیزمِ اصلاح لازم است؛ تسویهٔ پرداخت بینِ بانکها (فصل ۱۱) نمونهاش است.
- If a customer orders beyond warehouse stock, you can oversell, apologize for the delay and discount — the same thing you'd do if a forklift drove into the warehouse goods and stock fell short of imagination. So the apology workflow must be part of the business process for such incidents anyway, and a hard constraint on stock count may be unnecessary.
- Many airlines overbook assuming some passengers miss the flight, and many hotels overbook rooms assuming some guests cancel. Here the “one person per seat” constraint is deliberately violated for business reasons, with a compensation process (refunds, upgrades, a free room at the neighboring hotel) laid on for over-demand. Even without overbooking, apology-and-compensation processes were needed for events like weather- or strike-cancelled flights — recovering from such issues is just an ordinary part of business.
- If someone withdraws beyond their account balance, the bank can charge an overdraft fee and ask the debt repaid; and by capping total daily withdrawals the bank's risk stays bounded.
- In systems integrating data across organizations, inconsistency inevitably arises and a correction mechanism is needed; inter-bank payment settlement (Chapter 11) is an example.
در خیلی زمینههای کسبوکاری پس قابلِ قبول است که قید را موقتاً نقض کنی و بعد با عذرخواهی درستش کنی.
این جنس اصلاحِ اشتباه compensating transaction نامیده میشود. هزینهٔ عذرخواهی
(پول یا اعتبار) فرق میکند ولی اغلب کم است: ایمیلِ فرستادهشده را نمیشود پس گرفت، ولی میشود ایمیلِ
اصلاحیِ بعدی فرستاد؛ اگر کارت را دوبار شارژ کردی، میشود یکیش را refund کرد و هزینه فقط کارمزدِ پردازش و
شاید شکایتِ مشتری است. (پولی که از ATM بیرون رفته را مستقیم نمیشود پس گرفت — هرچند در اصل میشود برای
حسابِ منفی مانده مأمورِ وصول فرستاد.) اینکه هزینهٔ عذرخواهی قابلِ قبول است یا نه، تصمیمِ کسبوکاری
است. اگر قابلِ قبول باشد، مدلِ سنتیِ «چککردنِ همهٔ قیدها قبل از حتی نوشتنِ داده» بیجهت محدودکننده است؛
کاملاً ممکن است معقول باشد که خوشبینانه بنویسی و قید را بعداً چک کنی. هنوز میشود validation را
قبل از actionهایی گذاشت که بازیابیشان گران است — ولی این به این معنا نیست که باید قبل از حتی نوشتنِ
داده validate کنی. این اپها integrity میخواهند (نمیخواهی رزرو گم شود یا پول با بده-بستانِ نامتطابق
ناپدید شود) ولی timeliness در enforce قید نمیخواهند: اگر بیشتر از موجودی فروختهای، میشود بعداً
درستش کرد — شبیه همان رویکردهای حلِ تعارضِ «برخورد با نوشتنِ متعارض» (فصل ۶).
In many business contexts it's thus acceptable to temporarily violate a constraint and fix it later with an apology. This kind of mistake-fixing is called a compensating transaction. The apology's cost (money or reputation) varies but is often small: a sent email can't be unsent, but a corrective follow-up email can be sent; if you double-charged a card, one charge can be refunded at only the processing fee plus perhaps a customer complaint. (Cash out of an ATM can't directly be taken back — though in principle a collector can be sent for a negative balance.) Whether the apology cost is acceptable is a business decision. If acceptable, the traditional “check every constraint before even writing data” model is needlessly restrictive; it may be perfectly reasonable to write optimistically and check the constraint later. Validation can still sit before actions whose recovery is expensive — but that doesn't mean validating before even writing data. These apps want integrity (you don't want a reservation lost or money vanished with mismatched debit-credit) but no timeliness in constraint enforcement: oversold stock can be fixed later — like Chapter 6's “handling conflicting writes” conflict-resolution approaches.
حالا دو مشاهدهٔ جالب کنار هم مینشینند: (۱) سیستمهای dataflow میتوانند تضمینِ integrity روی دادهٔ مشتق
را بدونِ commit اتمیک و linearizability و هماهنگیِ همگامِ بین-shardی نگه دارند؛ (۲) با اینکه قیدِ
سختِ یکتایی timeliness و هماهنگی میخواهد، خیلی اپها با قیدِ شلی که ممکن است موقتاً نقض و بعد درست شود
راحتاند، تا وقتی integrity همیشه حفظ شود. با هم یعنی سیستمهای dataflow میتوانند برای خیلی اپها
سرویسِ مدیریتِ داده بدهند بدونِ اینکه هماهنگی لازم داشته باشند، ولی همچنان تضمینِ integrity قوی
بدهند. چنین سیستمهای coordination-avoiding جذابیتِ زیادی دارند: عملکرد و تحملِ خطای بهتری از
سیستمهایی دارند که باید هماهنگیِ همگام کنند. مثلاً چنین سیستمی میتواند توزیعشده روی چند دیتاسنتر در
پیکربندیِ multi-leader کار کند و بینِ regionها ناهمگام replicate کند؛ هر دیتاسنتری میتواند مستقل از
بقیه کار کند چون هماهنگیِ همگامِ بین-regionای لازم نیست. چنین سیستمی تضمینِ timeliness ضعیفی دارد
(بدونِ هماهنگی linearizable نمیشود) ولی همچنان میتواند تضمینِ integrity قوی داشته باشد. در این فضا،
تراکنشِ serializable هنوز بهعنوانِ بخشی از نگهداریِ state مشتق مفید است، ولی میشود در scope کوچکی
اجرایش کرد که خوب جواب میدهد؛ تراکنشِ توزیعشدهٔ ناهمگنِ سبکِ XA لازم نیست. هماهنگیِ همگام را هنوز
میشود جاهایی که لازم است آورد (مثلاً enforce قیدِ سخت قبل از عملیاتی که بازیابی از آن ممکن نیست)، ولی
لازم نیست همهچیز هزینهٔ هماهنگی را بدهد اگر فقط بخشِ کوچکی از اپ به آن نیاز دارد. نگاهِ دیگر به هماهنگی
و قیدها این است که تعدادِ عذرخواهیها برای ناسازگاری را کم میکنند ولی بالقوه عملکرد و دسترسپذیریِ
سیستم را کم میکنند و پس بالقوه تعدادِ عذرخواهیها برای outage را زیاد میکنند. نمیشود تعدادِ عذرها را
به صفر رساند، ولی میشود بهترین بدهبستان را برای نیازت پیدا کنی — نقطهٔ بهینهای که نه ناسازگاریِ زیاد
دارد نه مشکلِ دسترسپذیریِ زیاد.
Now two interesting observations sit side by side: (1) dataflow systems can hold integrity guarantees on derived data without atomic commit, linearizability or synchronous cross-shard coordination; (2) although hard uniqueness constraints want timeliness and coordination, many apps are comfortable with a loose constraint that may temporarily break and later heal, as long as integrity always holds. Together they mean dataflow systems can serve data management for many apps with no coordination required, yet still give strong integrity guarantees. Such coordination-avoiding systems are highly attractive: better performance and fault tolerance than systems needing synchronous coordination. E.g. such a system can run distributed across several datacenters in a multi-leader configuration, replicating asynchronously between regions; each datacenter can work independently of the rest since no synchronous cross-region coordination is needed. Such a system has weak timeliness guarantees (no linearizability without coordination) but can still have strong integrity guarantees. In this space, serializable transactions remain useful as part of derived-state maintenance, but can run in a small scope where they perform well; heterogeneous XA-style distributed transactions aren't needed. Synchronous coordination can still be brought where needed (e.g. enforcing a hard constraint before an irrecoverable action), but not everything must pay coordination's price if only a small app part needs it. Another view of coordination and constraints is that they reduce apology counts for inconsistency but potentially cut system performance and availability, thus potentially raising apology counts for outages. Apology counts can't reach zero, but you can find the best tradeoff for your needs — a sweet spot with neither much inconsistency nor many availability problems.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: انتخاب محل هماهنگی
وضعیت اولیه: برای آخرین صندلی شمارهدار دو خریدار داریم، ولی برای شمارش بازدید محدودیت مشابهی نیست.
اجرای مرحلهبهمرحله: فروش صندلی روی منبع مالکیت رقابت میکند؛ شمارندهٔ بازدید میتواند غیرهمزمان ادغام شود.
نتیجه و دلیل: هزینهٔ هماهنگی را به قید لازم میدهیم؛ جبران فقط وقتی معنی دارد که محصول آن نتیجه را واقعاً بپذیرد.
نمودار یک نمودارِ تعادلِ x-y است. محورِ افقی «هماهنگیِ همگام» است: از چپ (هیچ) تا راست (کامل —
رهبرِ واحد، consensus، XA برای هر عملیات). محورِ عمودی «عذرخواهیهایی که باید بکنی» است. دو منحنی روی
نمودارند که با هم یک U میسازند: منحنیِ نزولیِ «عذر برای ناسازگاری (نقضِ قید، بهروزرسانیِ
گمشده)» که سمتِ چپ بالاست و به راست میافتد؛ و منحنیِ صعودیِ «عذر برای outage (دردسترسنبودن،
توقفِ بین-regionی، سرایتِ خطا)» که سمتِ چپ پایین است و به راست بالا میرود. منحنیِ سومِ خطچین، جمعِ
آن دوست — «TOTAL عذرها» به شکلِ U — با نقطهٔ مینیممِ ستارهدار: «نقطهٔ بهینه: نه ناسازگاریِ زیاد، نه
مشکلِ دسترسپذیریِ زیاد»؛ و از همان نقطه خطچینِ عمودی به محور میرسد با برچسبِ «هماهنگیِ همگام فقط
همانقدر که لازم است؛ فقط بخشِ کوچکی از اپ زیرِ آن میرود».
روی نمودار پنج نقطهٔ نمونه هست: «انبارِ oversold (عذر + تخفیف)» و «پروازِ overbook (refund/ارتقا)»
سمتِ چپاند — عذرِ ارزان، پس نقض را تحمل میکنیم؛ «overdraft خودپرداز (کارمزد، سقفِ روزانه ریسک را
محدود میکند)» و «تسویهٔ بینسازمانی (مکانیزمِ اصلاح ناگزیر است)» وسطاند؛ و «پرداختِ نقدِ ATM (قابلِ
بازیابی نیست)» سمتِ راست است — اینجا باید سخت هماهنگ کرد. پنلهای کنار هم دو درس را میگویند:
«هماهنگی عذرِ ناسازگاری را کم ولی عذرِ outage را زیاد میکند» و «نقطهٔ صفر عذر وجود ندارد — بهترین
بدهبستان را پیدا کن: بعداً جبران کن، integrity را نگه دار، timeliness را شل بگیر».
The diagram is an x-y tradeoff chart. The horizontal axis is “synchronous coordination”: from left (none) to right (total — single leader, consensus, XA for every operation). The vertical axis is “apologies you must make”. Two curves form a U: the falling “apology for inconsistency (constraint violations, lost updates)” curve, high on the left falling rightward; and the rising “apology for outage (unavailability, cross-region stalls, error spread)” curve, low on the left rising rightward. A third dashed curve sums the pair — “TOTAL apologies” U-shaped — with a starred minimum: “the sweet spot: neither much inconsistency nor many availability problems”; and from that point a vertical dashed line reaches the axis, labelled “synchronous coordination only as much as needed; only a small app part goes under it”.
Five sample dots sit on the chart: “oversold warehouse (apology + discount)” and “overbooked flight (refund/upgrade)” on the left — cheap apologies, so we tolerate violation; “ATM overdraft (fees, daily caps bound the risk)” and “cross-organization settlement (correction machinery inevitable)” in the middle; and “ATM cash payout (irrecoverable)” on the right — coordinate hard here. The side panels state two lessons: “coordination cuts inconsistency apologies but raises outage apologies” and “no zero-apology point exists — find the best tradeoff: compensate later, keep integrity, relax timeliness.”
۵.۶. اعتماد کن، ولی راستیآزمایی کن5.6. Trust, but verify
علاوه بر جلوگیری از خطا، باید بتوانیم داده را بررسی و ناسازگاری را کشف کنیم. تطبیقِ مستقل و امکان بازسازی، خطاهایی را آشکار میکند که از فرضهای طراحی عبور کردهاند.
مثال فنی: تطبیق بر اساس شناسه، نه فقط جمع
وضعیت اولیه: مرجع سفارشهای 42:100 و 43:200 دارد؛ گزارش مجموع ۳۰۰ نشان میدهد.
اجرای مرحلهبهمرحله: ممکن است گزارش ۴۲ را حذف و سفارش دیگری را اشتباهاً اضافه کرده باشد؛ مقایسهٔ شناسه، نسخه و مبلغ اختلاف واقعی را پیدا میکند.
نتیجه و دلیل: یکسان بودن مجموع شاهد کافی برای یکسان بودن داده نیست؛ ممیزی و مسیر ترمیم باید از مرجع معتبر تغذیه شوند.
All the correctness, integrity and fault-tolerance discussion assumed some things can break and some can't — called the system model (Chapter 9). E.g. we assume processes crash, machines lose power, networks delay or drop messages arbitrarily; and assume disk-written data after fsync isn't lost, memory data doesn't corrupt, and the CPU's multiply always answers right. These assumptions are often true, and if we constantly worried about computer mistakes nothing would get done. But the traditional system model has a binary view of faults (some happen, some never); in reality it's mostly probability: some likelier, some less likely; and the question is whether assumption violations happen enough to meet them in practice. We saw data can corrupt in memory (Chapter 1), on disk (Chapter 6) and on the network (Chapter 9) — should we attend to it more? If you work at large enough scale, even the very unlikely happen.
گذشته از سختافزار، همیشه ریسکِ باگِ نرمافزاری هست که checksumهای سطحپایینِ شبکه و حافظه و
فایلسیستم نمیگیرندش. حتی پایگاههای داده پراستفاده باگ دارند — نسخههای قدیمیِ MySQL قیدِ یکتایی را درست
نگه نمیداشتند و سطحِ serializable در PostgreSQL ناهنجاریِ write skew نشان داده، با اینکه هر دو
پایگاههای داده قوی و battle-testedاند؛ در نرمافزارِ کمبلوغتر وضع احتمالاً خیلی بدتر است. با همهٔ تلاشِ
طراحی و تست و بازبینی، باگ میخزد تو؛ نادر است و سرانجام پیدا و درست میشود، ولی یک دورهای هست که میتواند
داده را خراب کند. و دربارهٔ کدِ اپلیکیشن باید باگِ خیلی بیشتری فرض کنی، چون بیشترِ اپها بهاندازهٔ کدِ
پایگاه داده بازبینی و تست نمیبینند؛ خیلیهاشان حتی ویژگیهای حفظِ integrity پایگاه داده (کلیدِ خارجی، قیدِ یکتایی)
را درست به کار نمیبرند. «سازگاری» به معنای ACID بر این ایده است که پایگاه داده از حالتِ سازگار شروع میکند و
تراکنش آن را از یک حالتِ سازگار به حالتِ سازگارِ دیگری میبرد — پس انتظار داری پایگاه داده همیشه سازگار باشد؛
ولی این حرف فقط وقتی معنا دارد که فرض کنی تراکنش بیباگ است. اگر اپ از پایگاه داده غلط استفاده کند (مثلاً
isolation ضعیف را ناامن به کار ببرد)، integrity پایگاه داده تضمینشدنی نیست.
Past hardware, there's always software-bug risk that low-level network, memory and filesystem checksums don't catch. Even heavily used databases have bugs — old MySQL versions didn't uphold uniqueness correctly and PostgreSQL's serializable level showed write-skew anomalies, though both are strong battle-tested databases; in less mature software it's probably far worse. Despite all design, test and review effort, bugs creep in; rare, eventually found and fixed, but there's a window where they can corrupt data. And about application code you must assume far more bugs, since most apps see less review and testing than database code; many don't even correctly use the database's integrity-preserving features (foreign keys, uniqueness constraints). “Consistency” in the ACID sense rests on the idea that the database starts from a consistent state and transactions move it from one consistent state to another — so you expect the database always consistent; but that only makes sense assuming bug-free transactions. If the app misuses the database (e.g. unsafely using weak isolation), database integrity can't be guaranteed.
کورکورانه به وعدهها اعتماد نکنDon't blindly trust promises
وقتی نه سختافزار نه نرمافزار همیشه به ایدهآل عمل نمیکنند، خرابیِ داده دیر یا زود ناگزیر بهنظر میرسد؛
پس دستکم باید راهی داشته باشیم بفهمیم داده خراب شده تا درستش کنیم و منشأ خطا را پیدا کنیم. چککردنِ
integrity داده همان حسابرسی (auditing) است. حسابرسی فقط برای مالی نیست، ولی در مالی خیلی مهم است
دقیقاً چون همه میدانند اشتباه رخ میدهد و باید بتوان مشکل را دید و درست کرد. سیستمهای بالغ هم همینطور
احتمالِ رخدادنِ نامحتملها را در نظر میگیرند و ریسکش را مدیریت میکنند: مثلاً سیستمهای ذخیرهسازیِ
بزرگ مثلِ HDFS و S3 کورکورانه به دیسک اعتماد نمیکنند؛ پروسههای پسزمینه دارند که مدام فایلها را
میخوانند، با replicaهای دیگر مقایسه میکنند و فایل را از دیسکی به دیسکِ دیگر میبرند تا ریسکِ خرابیِ
خاموش را کم کنند. اگر میخواهی مطمئن شوی دادهات هنوز هست، باید بخوانیاش و چک کنی؛ بیشترِ وقتها هست،
ولی اگر نبود میخواهی زود بفهمی نه دیر. به همین استدلال، مهم است گاهبهگاه restore از backup را هم
امتحان کنی — وگرنه ممکن است وقتی بفهمی backup خراب است که دیر شده و داده را از دست دادهای.
کورکورانه اعتماد نکن که همهچیز دارد کار میکند. سیستمهایی مثلِ HDFS و S3 هنوز باید فرض کنند
دیسکها بیشترِ وقتها درست کار میکنند — فرضِ معقولی است، ولی با فرضِ «همیشه درست کار میکنند» فرق دارد.
ولی خیلی سیستمهای فعلی این رویکردِ «اعتماد، ولی راستیآزمایی» و حسابرسیِ مداومِ خودشان را ندارند؛ خیلیها
فرض میکنند تضمینهای درستی مطلقاند و برای احتمالِ خرابیِ نادرِ داده هیچ تمهیدی ندارند. در آینده ممکن است
سیستمهای خوداعتبارسنج بیشتری ببینیم که مدام integrity خودشان را چک میکنند بهجای اعتمادِ کور.
When neither hardware nor software always meets the ideal, data corruption sooner or later seems inevitable; so at minimum we need a way to learn data corrupted, to fix it and find the error's source. Checking data integrity is auditing. Auditing isn't only for finance, but it matters hugely in finance precisely because everyone knows mistakes happen and problems must be seen and fixed. Mature systems likewise account for unlikely-event probability and manage its risk: e.g. large storage systems like HDFS and S3 don't blindly trust disks; they run background processes constantly reading files, comparing with other replicas and moving files disk to disk to cut silent-corruption risk. To be sure your data still exists, you must read and check it; mostly it's there, but if not you want to learn early, not late. By the same argument, it's important to occasionally test restoring from backup — otherwise you may learn the backup is broken only when it's too late and data is lost.
Don't blindly trust that everything is working. Systems like HDFS and S3 must still assume disks work correctly most of the time — a reasonable assumption, but different from “always work correctly”. Yet many current systems lack this “trust, but verify” approach and continuous self-auditing; many assume correctness guarantees are absolute, with no provision for rare data-failure probability. In the future we may see more self-validating systems continuously checking their own integrity instead of blind trust.
طراحی برای حسابرسیپذیری: اگر تراکنشی چند آبجکت را در پایگاه داده mutate کند، فهمیدنِ دلیلِ زیرین بعداً
سخت است؛ حتی اگر لاگِ تراکنش را بگیری، insert و update و delete در جدولهای مختلف لزوماً تصویرِ روشنی از
«چرا این mutationها انجام شدند» نمیدهند — فراخوانیِ منطقِ اپلیکیشن که آن mutationها را تصمیم گرفت گذرا
است و قابلِ بازتولید نیست. در مقابل، سیستمهای رویدادمحور حسابرسیپذیریِ بهتری میدهند: در رویکردِ event
sourcing، ورودیِ کاربر به سیستم بهصورتِ یک رویدادِ immutable واحد نشان داده میشود و هر بهروزرسانیِ
stateای از همان رویداد مشتق میشود. مشتقسازی را میشود قطعی و تکرارپذیر کرد، طوری که اجرای همان لاگِ
رویدادها با همان نسخهٔ کدِ مشتق، همان بهروزرسانیهای state را بدهد. صریحبودن دربارهٔ dataflow، provenance
داده را خیلی روشنتر میکند و چککردنِ integrity را شدنیتر: برای لاگِ رویداد میشود با hash چک کرد
ذخیرهٔ رویداد خراب نشده؛ و برای هر state مشتق میشود پردازندههای batch و streamای که از لاگِ رویداد
مشتقش کردند را دوباره اجرا کرد و دید همان نتیجه میآید یا نه — یا حتی مشتقِ افزونه را موازی اجرا کرد.
dataflow قطعی و خوشتعریف، debug و trace اجرای سیستم را هم سادهتر میکند تا بفهمی چرا کاری کرد —
اگر چیزِ غیرمنتظرهای رخ داد، ارزشمند است که بتوانی دقیق همان شرایطی که به رویدادِ غیرمنتظره رسید را
بازتولید کنی — نوعی قابلیتِ debug سفر-در-زمان.
Designing for auditability: if a transaction mutates several objects in the database, understanding the underlying reason later is hard; even capturing the transaction log, inserts, updates and deletes across tables needn't paint a clear picture of “why these mutations happened” — the application-logic call deciding those mutations is transient and irreproducible. Event-driven systems, by contrast, give better auditability: in event sourcing, user input to the system shows as one immutable event, and every state update derives from that event. Derivation can be made deterministic and repeatable, so running the same event log with the same derivation-code version yields the same state updates. Being explicit about dataflow makes data provenance far clearer and integrity checking feasible: for an event log, hashing can check event storage isn't corrupted; and for each derived state, the batch and stream processors that derived it from the event log can be rerun to see if the same result comes — or even run a redundant derivative in parallel. Deterministic, well-defined dataflow also simplifies debugging and tracing system execution to learn why it did something — valuable when something unexpected happens: the ability to reproduce exactly the conditions that led to the unexpected event — a kind of time-travel debugging.
دوباره آرگومانِ سرتاسری: اگر نتوانیم کاملاً اعتماد کنیم که هر جزءِ سیستم عاری از خرابی است — که هر
سختافزاری بیایراد و هر نرمافزاری بیباگ است — پس دستکم باید دورهای integrity دادهمان را چک کنیم؛ اگر
چک نکنیم، از خرابی خبردار نمیشویم تا وقتی که دیر شده و خسارتِ پاییندستی زده و ردیابیاش خیلی سختتر و
گرانتر شده. چککردنِ integrity سیستمهای داده بهتر است سرتاسری انجام شود: هرچه سیستمهای بیشتری را در
چکِ integrity بگنجانیم، فرصتِ کمتری هست که خرابی در مرحلهای از فرآیند دیدهنشده بماند. اگر بتوانیم چک
کنیم که کلِ خطِ لولهٔ دادهٔ مشتق سرتاسر درست است، آنوقت همهٔ دیسکها و شبکهها و سرویسها و الگوریتمهای
سرِ راه ضمناً در چک گنجانده شدهاند. چکِ مداومِ integrity سرتاسری اعتماد به درستیِ سیستمها را بیشتر
میکند و این بهنوبهٔ خود اجازه میدهد سریعتر حرکت کنی (مثلِ تستِ خودکار، حسابرسی شانسِ زود پیداشدنِ باگ
را بیشتر میکند و ریسکِ اینکه تغییری در سیستم یا فناوریِ ذخیرهسازیِ تازه خسارت بزند را کم میکند)؛ اگر
از تغییر نترسی، خیلی بهتر میتوانی اپ را برای نیازِ در حالِ تغییر تکامل بدهی.
The end-to-end argument again: if we can't fully trust every system part failure-free — that all hardware is flawless and all software bug-free — then we must at least periodically check our data's integrity; unchecked, we won't learn of corruption until too late, when it has caused downstream damage and tracing is far harder and pricier. Checking data-system integrity is better done end to end: the more systems included in the integrity check, the less chance for failure to pass unseen at some pipeline stage. If we can check that the whole derived-data pipeline is correct end to end, then every disk, network, service and algorithm along the way is implicitly included. Continuous end-to-end integrity checking raises trust in system correctness, which in turn lets you move faster (like automated tests, auditing raises the chance of early bug discovery and cuts the risk that a change or a fresh storage technology causes damage); if you don't fear change, you can evolve the app far better for changing needs.
ابزارهای سیستمِ دادهٔ حسابرسیپذیر: در حالِ حاضر خیلی سیستمهای داده حسابرسیپذیری را دغدغهٔ
سطحِ اول نمیکنند. بعضی اپها مکانیزمِ حسابرسیِ خودشان را پیاده میکنند (مثلاً لاگکردنِ همهٔ تغییرها در
جدولِ audit جدا) ولی تضمینِ integrity لاگِ حسابرسی و state پایگاه داده هنوز سخت است. لاگِ تراکنش را میشود با
امضای دورهای با ماژولِ امنیتیِ سختافزاری tamper-proof کرد، ولی این تضمین نمیکند که اصلاً تراکنشهای
درست واردِ لاگ شدهاند. بلاکچینهایی مثلِ Bitcoin و Ethereum لاگهای append-only مشترک با چکِ سازگاریِ
رمزنگاریاند؛ تراکنشهایی که ذخیره میکنند رویدادند و قراردادهای هوشمند اساساً پردازندهٔ streamاند؛ و
پروتکلِ اجماعشان مطمئن میشود همهٔ نودها روی همان دنبالهٔ رویدادها توافق دارند. فرقشان با پروتکلهای
اجماعِ فصل ۱۰ این است که بلاکچینها Byzantine fault-tolerantاند — یعنی حتی اگر بعضی نودهای شرکتکننده
دادهٔ خراب داشته باشند کار میکنند چون replicaها مدام integrity همدیگر را چک میکنند. برای بیشترِ اپها
بلاکچینها overhead زیادی دارند که مفید باشند؛ ولی بعضی ابزارهای رمزنگاریشان را میشود در زمینهٔ
سبکتر هم به کار برد: مثلاً Merkle treeها درختهای hashاند که میشود با آنها
کارا ثابت کرد رکوردی در dataset هست (و چند چیزِ دیگر). Certificate transparency
از لاگهای append-only تأییدشدهٔ رمزنگاری و درختهای Merkle برای چککردنِ اعتبارِ گواهیهای TLS/SSL
استفاده میکند؛ و با داشتنِ یک رهبر برای هر لاگ، از نیاز به پروتکلِ اجماع پرهیز میکند. الگوریتمهای
چکِ integrity و حسابرسی، مثلِ certificate transparency و ledgerهای توزیعشده، ممکن است در آینده در
سیستمهای داده بهطورِ کلی بیشتر استفاده شوند؛ کمی کار لازم است تا بهمقیاسِ سیستمهای بدونِ حسابرسیِ
رمزنگاری برسند و جریمهٔ عملکرد را پایین نگه دارند، ولی بههرحال جالباند.
Auditable data-system tools: currently few data systems make auditability a first-class concern. Some apps implement their own audit machinery (e.g. logging every change in a separate audit table) but the audit log's and the database state's integrity guarantees remain hard. The transaction log can be made tamper-proof by periodic signing with a hardware security module, but that doesn't guarantee the right transactions entered the log at all. Blockchains like Bitcoin and Ethereum are shared append-only logs with cryptographic consistency checks; the transactions they store are events and smart contracts are basically stream processors; and their consensus protocol ensures all nodes agree on the same event sequence. Their difference from Chapter-10 consensus protocols is that blockchains are Byzantine fault-tolerant — they work even with some participants holding corrupt data, since replicas constantly check each other's integrity. For most apps blockchains carry too much overhead to be useful; but some of their cryptographic tools apply in lighter contexts too: e.g. Merkle trees are hash trees that can efficiently prove a record's presence in a dataset (and several other things). Certificate Transparency uses cryptographically verified append-only logs and Merkle trees for checking TLS/SSL certificate validity; and by having one leader per log it avoids needing a consensus protocol. Integrity-checking and auditing algorithms, like certificate transparency and distributed ledgers, may see wider data-system use in future; some work is needed to scale them to non-cryptographically-audited systems' scale while keeping the performance penalty low — but they're interesting regardless.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: یک تغییر، چند نمای عقبمانده
وضعیت اولیه: قیمت کالای P7 در مرجع با نسخهٔ ۱۲ به ۱۲۰ تغییر کرده است.
اجرای مرحلهبهمرحله: ایندکس جستوجو تا نسخهٔ ۱۱ و کش تا ۱۰ پیش رفتهاند؛ هر مصرفکننده با لاگ معتبر به ۱۲ میرسد.
نتیجه و دلیل: lag را میتوان با نسخه و موقعیت پیشرفت سنجید؛ اگر جریان کامل و قابلبازپخش نباشد، «بالاخره درست میشود» تضمین مفیدی نیست.
گرهٔ میانی DERIVED DATA است: یک مبنا، چند نما. ردیفِ بالا نظم است: یک مبنا (ورودی فقط یکبار وارد میشود و با CDC بیرون میرود)، ترتیب یعنی اجماع (ترتیبِ کامل رهبر میخواهد)، و علّیت (فقط همان ترتیبی را نگه دار که لازم است). دو گرهِ کناری ساختناند: کنار هم بودنِ بچ و استریم (بازپردازش و مهاجرتِ تدریجی) و dataflow بهجای RPC (مشترک شو و محلی بخوان). ردیفِ پایین درستی است: شناسههای سرتاسری (UNIQUE و dedup در همهٔ hopها)، زمان در برابر integrity (موقتی در برابرِ دائمی)، و نقطهٔ بهینه (فقط همانقدر هماهنگی که لازم است).
یالها داستان را میگویند: ورودی را قیف کن، هزینهٔ ترتیب را بدان، علّی نگه دار، بازپخش کن، محلی بخوان، شناسایی کن، جدا بسنج و فقط جایی عذر بخواه که ارزان است. خطِ پایینی تزِ فصل است: ورودی را از یک مبنا قیف کن، همهچیز را مشتق کن، سرتاسر راستیآزمایی کن.
The center node is DERIVED DATA: one record, many views. The top row is order: one record (input enters once, CDC flows out), order means consensus (total order wants a leader), and causality (keep only the order you need). The two side nodes are building: batch beside stream (replay, gradual migration) and dataflow over RPC (subscribe, read locally). The bottom row is correctness: end-to-end IDs (UNIQUE with dedup at every hop), time vs integrity (temporary vs permanent), and the sweet spot (coordinate just enough).
The edges tell the story: funnel the input, know ordering's cost, stay causal, replay, read locally, identify, judge separately, and apologize only where cheap. The bottom line is the chapter's thesis: funnel input through one record, derive everything, verify end to end.