۱1 خرابی جزئی و شبکهی غیرقابلاعتمادPartial failure and the unreliable network
۱.۱. چرا توزیعشده ذاتاً سخت است1.1 Why distributed is inherently hard
در سیستم توزیعشده ممکن است بعضی اجزا سالم باشند و بعضی نباشند. گرهی که پاسخ نمیدهد الزاماً خاموش نیست؛ شاید شبکه یا زمانبندیِ اجرای آن مشکل دارد.
مثال فنی: نیمی از سیستم هنوز کار میکند
وضعیت اولیه: A نمیتواند با B ارتباط بگیرد، اما هر دو از کلاینتهای محلی درخواست میگیرند.
اجرای مرحلهبهمرحله: restart کردنِ A یا B الزاماً قطع ارتباط بین آنها را حل نمیکند؛ ممکن است دو طرف هنوز نوشتن محلی انجام دهند.
نتیجه و دلیل: partial failure با خاموشی یک فرایند فرق دارد؛ پروتکل باید رفتار هر طرف در زمان جدایی را مشخص کند.
A program on a healthy single computer is deterministic: every run of an operation gives the same result. Broken hardware usually kills everything at once (kernel panic) — no ambiguity. But with several machines joined by a network, part of the system may be broken while the rest is fine; that's partial failure, and worse, it's non-deterministic: the same operation sometimes succeeds, sometimes not.
خبر خوب اینکه همین تحملِ خرابی باعث میشود بتوانیم rolling upgrade کنیم،
یکبهیک نودها را ریاستارت کنیم و سرویس قطع نشود. یعنی میتوان از اجزای غیرقابلاعتماد،
سیستمِ قابلاعتماد ساخت — به شرط آنکه اول بدانیم دقیقاً چه چیزهایی ممکن است خراب شوند.
سیستمهایی که در این کتاب بررسی میکنیم عموماً shared-nothing هستند:
چند ماشین با حافظه و دیسکِ مستقل که فقط از راهِ شبکه حرف میزنند.
The good news: this same fault tolerance enables rolling upgrades — restarting nodes one by one without downtime. Unreliable parts can build a reliable system — provided you first know exactly what can break. The systems in this book are mostly shared-nothing: machines with independent memory and disk that talk only over the network.
۱.۲. وقتی پاسخی نمیآید، هیچچیز نمیدانی1.2 When no reply comes, you know nothing
وقتی پاسخ نمیرسد، چند احتمال داریم: درخواست نرسیده، هنوز درحال اجراست یا اجرا شده و پاسخ گم شده است. سکوتِ شبکه این حالتها را از هم جدا نمیکند.
مثال فنی: سه سرنوشت همان درخواست
وضعیت اولیه: CreateOrder(r7) ارسال شده و پاسخ timeout شده است.
اجرای مرحلهبهمرحله: حالت اول: نرسیده و هیچ سفارش نیست؛ دوم: در حال اجراست؛ سوم: سفارش ۴۲ ثبت شده اما پاسخ گم شده است.
نتیجه و دلیل: retry با همان هویت باید این حالتها را امن پوشش دهد؛ ساخت هویت تازه احتمالِ دو اثر را بالا میبرد.
Ethernet and internet networks are asynchronous packet networks: no guarantee about delivery time — or delivery at all. Send a request, get no reply, and six indistinguishable things may have happened:
- درخواستِ تو در شبکه گم شده (مثلاً کسی کابل را کشیده).
- درخواست در صف مانده و بعداً تحویل میشود (شبکه یا گیرنده شلوغ است).
- نودِ مقصد کرش کرده یا خاموش شده.
- نود موقتاً پاسخ نمیدهد (مثلاً یک GC pause طولانی) ولی بعداً برمیگردد.
- نود درخواست را انجام داده ولی پاسخ در شبکه گم شده.
- نود انجام داده و پاسخ هم در راه است، فقط دیر رسیده.
- Your request was lost in the network (someone pulled the cable).
- The request is queued and arrives later (network or receiver busy).
- The destination node crashed or is powered off.
- The node is temporarily unresponsive (e.g. a long GC pause) but comes back later.
- The node applied the request but the reply was lost in the network.
- The node applied it and the reply is on its way — just late.
تنها اطلاعاتِ تو این است: «هنوز پاسخی نیامده.» حتی خودِ فرستنده هم نمیداند بسته رسیده یا نه؛
فقط گیرنده میتواند خبر دهد و آن خبر هم ممکن است گم شود. پس timeout
یک حدس است، نه اثباتِ خرابی — و درخواستِ «تسلیمشده» هنوز ممکن است بعداً به مقصد برسد!
Your only information: “no reply yet.” Even the sender can't know whether the packet arrived; only the receiver can report back, and that report may be lost too. So a timeout is a guess, not proof of failure — and the “abandoned” request may still arrive later!
روایت مدرس — نمودار ۱ را با صدای بلند بخوانیمTeacher narration — read Diagram 1 aloud
مثال فنی برای خواندنِ نمودار: سه سرنوشت همان درخواست
وضعیت اولیه: CreateOrder(r7) ارسال شده و پاسخ timeout شده است.
اجرای مرحلهبهمرحله: حالت اول: نرسیده و هیچ سفارش نیست؛ دوم: در حال اجراست؛ سوم: سفارش ۴۲ ثبت شده اما پاسخ گم شده است.
نتیجه و دلیل: retry با همان هویت باید این حالتها را امن پوشش دهد؛ ساخت هویت تازه احتمالِ دو اثر را بالا میبرد.
بالای نمودار سه جمله میبینی: «اگر درخواستی بفرستی و پاسخی نیاید، نمیتوانی بفهمی چرا.» زیر آن سه ردیف مستقل است که هر کدام یک تایملاین کوچکاند. ردیف (a): فلش request از Client به Remote Node وسط راه به ضربدر صورتی dropped میرسد — بسته هرگز نرسیده. ردیف (b): درخواست میرسد ولی کنار نود ضربدر crashed / powered off است — شاید رسیده، شاید نه. ردیف (c): خطرناکترین حالت — request — applied successfully ولی فلش پاسخ وسط راه lost میشود: اثر اعمال شده ولی کلاینت بیخبر است.
در هر سه ردیف جعبهٔ خاکستری یک جمله دارد: no response yet — علتها متفاوت، مشاهده یکسان. جعبهٔ زرد جمعبندی میکند: timeout فقط یک حدس است و در حالت (c) یک retry کورکورانه کار را دوبار انجام میدهد.
Above the diagram three lines read: “if you send a request and no reply comes, you cannot tell why.” Below sit three independent rows, each a small timeline. Row (a): the request arrow from Client to Remote Node hits a pink cross mid-way, dropped — the packet never arrived. Row (b): the request arrives, but the node carries a crashed / powered off cross — maybe delivered, maybe not. Row (c): the most dangerous case — request — applied successfully, yet the reply arrow is lost mid-way: the effect applied, the client uninformed.
In all three rows the grey box says one line: no response yet — different causes, identical observation. The yellow box concludes: a timeout is only a guess, and in case (c) a blind retry performs the work twice.
۱.۳. محدودیتهای TCP1.3 The limits of TCP
TCP انتقالِ مرتبِ بایتها را در اتصال مدیریت میکند، اما نمیگوید منطقِ برنامهٔ مقصد با موفقیت تمام شده است. تأییدِ شبکه با تأییدِ عملیاتِ کسبوکاری یکی نیست.
مثال فنی: TCP ack، commit نیست
وضعیت اولیه: بایتهای درخواست به TCP stack سرور رسیده و دریافت شبکه تأیید شده است.
اجرای مرحلهبهمرحله: برنامه ممکن است پیش از خواندن بایتها crash کند یا پس از commit پاسخ ندهد.
نتیجه و دلیل: تضمین انتقالِ بایت، اجرای دستور یا پایداری تغییرِ برنامه را تأیید نمیکند؛ تأیید دامنه باید در لایهٔ برنامه تعریف شود.
TCP is famous for “reliable delivery”: it retransmits lost packets, reorders shuffled ones, catches data corruption with checksums and throttles send rate with congestion control. But that “reliability” doesn't fix our problem:
- TCP نمیداند بستهی رفت گم شده یا acknowledgment برگشتی؛ فقط دوباره میفرستد و اگر کابل کشیده شده باشد، کاری از دستش برنمیآید.
- تشخیص تکرار فقط درونِ یک اتصال است؛ اگر اتصال قطع شود و برنامه دوباره وصل و دوباره بفرستد، داده ممکن است تکرار شود.
- اگر اتصال با خطا بسته شود، نمیدانی مقصد دقیقاً تا کجا پردازش کرده.
- حتی ACK یعنی «هستهی سیستمعاملِ مقصد گرفت»، نه «برنامه انجام داد»؛ برای اطمینان باید پاسخِ صریحِ خودِ برنامه را بگیری.
- تأخیرِ retransmission در لایهی کاربر به شکلِ latency دیده میشود، نه خطای صریح.
- TCP can't tell a lost outbound packet from a lost returning acknowledgment; it just resends, and a pulled cable defeats it entirely.
- Dedup works only inside one connection; if the connection drops and the app reconnects and resends, data may duplicate.
- If a connection closes with an error, you can't know exactly how far the destination processed.
- Even an ACK only means “the destination OS kernel got it”, not “the application did it”; for certainty you need the application's own explicit reply.
- Retransmission delay shows up to the user as latency, not an explicit error.
TCP در برابر UDPTCP vs UDP
در برنامههای حساس به تأخیر (مثل VoIP) گاهی UDP انتخاب میشود،
چون دادهی دیررسیده بیارزش است و بهجای retransmit، جای بسته را با سکوت پر میکنند
و «retry» را به لایهی انسان میسپارند («ببخشید صدا قطع شد، دوباره بگو»).
این یک تریدآفِ صریحِ قابلیتاطمینان در برابر ثباتِ تأخیر است.
Latency-sensitive apps (like VoIP) sometimes choose UDP, because late data is worthless: dropped packets are filled with silence and “retry” is delegated to humans (“sorry, you cut out — say again”). An explicit reliability-vs-stable-latency trade-off.
۱.۴. خرابی شبکه در عمل و عاملِ اصلیِ تأخیر: صف1.4 Network failure in practice, and the main source of delay: queues
تأخیر شبکه فقط زمانِ حرکت سیگنال نیست؛ انتظار در صفِ شبکه، سیستمعامل و برنامه هم اثر دارد. افزایش بار ممکن است بدون خرابیِ کامل، پاسخها را بسیار دیر کند.
مثال فنی: تأخیر در صف شبکه
وضعیت اولیه: لینک 100 MB/s دارد و یک انتقال بزرگ صف خروجی را پر کرده است.
اجرای مرحلهبهمرحله: پیام heartbeat کوچک پشت دادههای دیگر منتظر میماند؛ بعد از 500 ms ممکن است failure detector آن را دیررس بداند.
نتیجه و دلیل: نود میتواند زنده باشد و پاسخ دیر برسد؛ timeout تنها به خرابی CPU یا فرایند اشاره نمیکند.
You'd think decades made networks reliable; studies say otherwise. One mid-size datacenter logged ~12 network failures a month (half killing one machine, half a whole rack). Adding redundant gear helps less than expected, because human error (misconfigured switches) is the main failure path. WAN fiber cuts were dealt by cows, beavers, even sharks (!), and cross-region cloud round-trips reached minutes at high percentiles.
مواردِ عجیبتر: ارتباطِ نامتقارن (A با B حرف میزند، B با C، ولی A با C نه)،
کارتِ شبکهای که فقط بستههای ورودی را بیصدا میریزد، و اینکه اثرِ یک قطعیِ کوتاه معمولاً
خیلی بیشتر از خودِ قطعی باقی میماند. پس هر ارتباطِ شبکهای ممکن است شکست بخورد؛
راهِ گریزی نیست. نکتهی مهم اینکه «تحمل» لزوماً یعنی «ادامهی کار» نیست:
نشاندادنِ یک پیامِ خطا به کاربر هم استراتژیِ معتبری است — به شرط آنکه بدانی نرمافزارت چه واکنشی نشان میدهد
و بعد از رفعِ مشکل بازیابی میشود.
Stranger cases: asymmetric links (A talks to B, B to C, but A not to C), NICs silently dropping only inbound packets, and short outages whose effects linger far longer than the outage. So any network link can fail; no escape. And “tolerating” needn't mean “keeping going”: showing the user an error is a valid strategy — provided you know how your software reacts and that it recovers once fixed.
عاملِ اصلیِ تغییرپذیریِ تأخیر، صفکشیدن (queueing) است — دقیقاً مثل ترافیکِ خیابان:
The main source of delay variance is queueing — exactly like street traffic:
- وقتی چند ماشین همزمان به یک مقصد میفرستند، سوییچ بستهها را صف میکند؛ اگر صف پر شود، بسته ریخته میشود و باید دوباره ارسال شود.
- وقتی بسته به ماشینِ مقصد میرسد، اگر همهی CPUها مشغول باشند، درخواست در صفِ هسته میماند — تأخیرِ نامعلوم.
- در محیطِ مجازی، ماشینِ مجازی ممکن است دهها میلیثانیه pause شود و بستهها در بافرِ hypervisor جمع شوند.
- خودِ TCP هم برای حفظِ شبکه، ارسال را محدود میکند (backpressure).
- در ابرِ چندمستاجری، همسایهی پرسروصدا (noisy neighbor) منابعِ مشترک را میگیرد و تأخیر را نوسانی میکند.
- Several machines sending to one destination at once make the switch queue packets; a full queue drops packets, which must be resent.
- Arriving at the destination with all CPUs busy, the request waits in the kernel queue — unknown delay.
- In virtual environments a VM may pause tens of milliseconds while packets pile in the hypervisor buffer.
- TCP itself throttles sending to protect the network (backpressure).
- On multi-tenant clouds a noisy neighbor grabs shared resources and jitters latency.
۱.۵. تشخیص خرابی و انتخاب timeout1.5 Failure detection and choosing a timeout
Timeout یک مرزِ انتظار است، نه مدرکِ قطعی خرابی. مقدار خیلی کوتاه هشدار و بازتلاشِ بیمورد میسازد؛ مقدار خیلی بلند تشخیص و بازیابی را عقب میاندازد.
مثال فنی: timeout کوتاه، خطای تشخیص بیشتر
وضعیت اولیه: معمولاً پاسخ در 50 ms میرسد، ولی گاهی pause یا صف آن را به 800 ms میرساند.
اجرای مرحلهبهمرحله: timeout برابر 100 ms بازیابی را زودتر شروع میکند، اما همین پاسخهای دیررس را هم خرابی میشمارد.
نتیجه و دلیل: آستانه از توزیع تأخیر و هزینهٔ تشخیص غلط انتخاب میشود؛ timeout پایان عمر نود را اثبات نمیکند.
Many systems must detect dead nodes automatically (a load balancer cutting it out, a follower taking the leader's place). Sometimes you get explicit feedback: an RST/FIN reply when nothing listens, a crash-notification script (like HBase's), switch management, or an ICMP Destination Unreachable. But none is reliable; in practice assume no reply ever comes and declare the node “dead” after a timeout.
| timeout | مزیت | خطر |
| خیلی کوتاه | تشخیصِ سریعِ خرابی | مثبتِ کاذب: نودِ زنده «مرده» اعلام میشود و بار روی بقیه میافتد → cascading failure |
| خیلی بلند | خطای کاذبِ کمتر | کاربر منتظر میماند، failover کُند میشود |
| تطبیقی (مثل Phi Accrual) | بر اساسِ توزیعِ واقعیِ RTT تنظیم میشود | پیادهسازی سختتر؛ هنوز «قطعیت» نمیدهد |
| timeout | Benefit | Risk |
| Too short | Fast failure detection | False positives: live nodes “die”, load shifts → cascading failure |
| Too long | Fewer false alarms | Users wait, failover slows |
| Adaptive (e.g. Phi Accrual) | Follows the real RTT distribution | Harder to build; still no “certainty” |
فرضِ خیالیِ کتاب: اگر شبکه حداکثرِ تأخیرِ d و پردازشِ حداکثرِ r را تضمین میکرد،
timeoutِ معقول 2d + r بود. اما شبکههای واقعی تأخیرِ نامحدود دارند و سرورها هم سقفِ زمانی تضمین نمیکنند؛
پس timeout مقدارِ «درست» ندارد و باید تجربی اندازهگیری شود: توزیعِ RTT را در طولِ زمان بسنج، سپس بینِ سرعتِ تشخیص و خطرِ اعلامِ زودهنگام تعادل برقرار کن.
The book's fantasy assumption: if the network guaranteed max delay d and processing max r, a sane timeout would be 2d + r. But real networks have unbounded delay and servers guarantee no time ceiling; so timeouts have no “correct” value and must be measured empirically: observe the RTT distribution over time, then balance detection speed against premature-declaration risk.
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: timeout کوتاه، خطای تشخیص بیشتر
وضعیت اولیه: معمولاً پاسخ در 50 ms میرسد، ولی گاهی pause یا صف آن را به 800 ms میرساند.
اجرای مرحلهبهمرحله: timeout برابر 100 ms بازیابی را زودتر شروع میکند، اما همین پاسخهای دیررس را هم خرابی میشمارد.
نتیجه و دلیل: آستانه از توزیع تأخیر و هزینهٔ تشخیص غلط انتخاب میشود؛ timeout پایان عمر نود را اثبات نمیکند.
سه کارت را بخوان: TOO SHORT (تشخیص سریع ولی مثبت کاذب که بار را جابهجا میکند و زنجیرهٔ cascading failure میسازد)، TOO LONG (کمتر گرگنمایی ولی failover کند و سوختن بودجهٔ SLO)، و ADAPTIVE (دنبال کردن RTT واقعی مثل Phi Accrual — بهترین پیشفرض، ولی هنوز حدس است نه اثبات).
نوار پایین حکم نهایی است: مقدار «درست» وجود ندارد — توزیع RTT را اندازه بگیر و تریدآف را صریح انتخاب کن.
Read the three cards: TOO SHORT (fast detection, but false positives that shift load and chain into cascading failure), TOO LONG (rarely cries wolf, but slow failover and a burning SLO budget), and ADAPTIVE (tracking real RTT like Phi Accrual — the best default, yet still a guess, not a proof).
The bottom bar is the final verdict: no “correct” value exists — measure the RTT distribution and pick the tradeoff explicitly.
اعلامِ زودهنگامِ مرگ خطرناک استPremature death announcements are dangerous
اگر نودِ «مردهاعلامشده» واقعاً زنده باشد و وسطِ انجامِ کاری (مثلاً ارسال ایمیل) باشد،
نودِ جایگزین همان کار را دوباره انجام میدهد → اثرِ دوباره. اگر سیستم از قبل شلوغ باشد،
منتقلکردنِ بار میتواند زنجیرهای از اعلامِ مرگ بسازد تا جایی که همه، همه را مرده بدانند و کلِ سیستم بایستد.
If the “declared-dead” node is actually alive and mid-task (e.g. sending an email), the replacement performs the same task again → duplicate effect. And the shifted load can chain death declarations until everyone deems everyone dead and the whole system halts.
۲2 نتیجهٔ نامشخص و بازتلاشِ امنUnknown outcomes and safe retry
۲.۱. ماشینِ وضعیتِ یک درخواست2.1 A request’s state machine
درخواست میتواند درحال اجرا، موفق، ناموفقِ قطعی یا دارای نتیجهٔ نامشخص باشد. ثبت و نمایش این تفاوت به تصمیمِ درستِ کاربر و برنامه کمک میکند.
مثال فنی: حالتِ unknownِ عملیات
وضعیت اولیه: کلاینت درخواست را فرستاده و تایمر منقضی شده است.
اجرای مرحلهبهمرحله: حالت رابط کاربر به «در حال بررسی» میرود؛ استعلام GET /requests/r7 نتیجهٔ committed را به سفارش ۴۲ وصل میکند.
نتیجه و دلیل: نمایش «قطعاً ناموفق» پیش از حل وضعیت میتواند کاربر را به ثبت دوباره وادار کند.
Combining “timeout means nothing” with “the service must return” gives every network request a dangerous state: outcome unknown. Three choices there: retry, give up, or learn the state another way. The diagram below is one request's full state machine.
روایت مدرس — نمودار ۲ را با صدای بلند بخوانیمTeacher narration — read Diagram 2 aloud
مثال فنی برای خواندنِ نمودار: حالتِ unknownِ عملیات
وضعیت اولیه: کلاینت درخواست را فرستاده و تایمر منقضی شده است.
اجرای مرحلهبهمرحله: حالت رابط کاربر به «در حال بررسی» میرود؛ استعلام GET /requests/r7 نتیجهٔ committed را به سفارش ۴۲ وصل میکند.
نتیجه و دلیل: نمایش «قطعاً ناموفق» پیش از حل وضعیت میتواند کاربر را به ثبت دوباره وادار کند.
سه لین افقی: Client request (بالا)، Retry path (وسط) و Outcomes (پایین)؛ زمان چپبهراست. مسیر اصلی: Sent با «in flight» به Awaiting Reply میرسد؛ از آنجا یا «reply» به Confirmed (یک اثر، پایان خوش) یا «timeout» قرمز به لوزی Timed Out با برچسب unknown outcome.
از Timed Out سه فلش همان سه انتخاباند: «maybe applied» به Applied, Unconfirmed، «retry» به Retry (که با «try again» از کانال بالا به Awaiting برمیگردد — حلقهٔ retry)، و از آنجا «idempotent» به Deduplicated سبز یا «not idempotent» قرمز به Duplicate Effect با برچسب corruption. نکتهٔ طلایی: retry بدون idempotency از حالت وسطی به فاجعهٔ سومی میافتد.
Three horizontal lanes: Client request (top), Retry path (middle) and Outcomes (bottom); time flows left to right. The main path: Sent reaches Awaiting Reply via “in flight”; from there either “reply” to green Confirmed (one effect, happy end) or red “timeout” to the Timed Out diamond tagged unknown outcome.
Three arrows leave Timed Out — the three famous choices: dashed “maybe applied” down to yellow Applied, Unconfirmed, “retry” to Retry (looping back via “try again” through the top channel to Awaiting — the retry loop), and from there “idempotent” to green Deduplicated or red “not idempotent” to Duplicate Effect tagged corruption. The golden point: retry without idempotency falls from the middle state into the third disaster.
۲.۲. درخت تصمیم: حالا چه کنیم؟2.2 Decision tree: what now?
تصمیم برای بازتلاش به نوع عملیات و اطلاعاتِ موجود بستگی دارد. اگر اجرای دوباره اثر اضافه دارد، ابتدا باید نتیجه را استعلام کنیم یا از شناسهٔ ثابت برای جلوگیری از تکرار استفاده کنیم.
مثال فنی: عملیاتِ مناسبِ تکرار
وضعیت اولیه: خواندن قیمت timeout شده، ولی ثبت سفارش هم میتواند timeout شود.
اجرای مرحلهبهمرحله: خواندن را میتوان طبق سیاست بار تکرار کرد؛ برای ثبت، هویت ثابت و استعلام نتیجه لازم است و backoff جلوی retry storm را میگیرد.
نتیجه و دلیل: یک سیاست retry برای همهٔ عملیات مناسب نیست؛ بیاثر بودن تکرار و محدودیت بار باید بررسی شوند.
After a timeout, decide structurally instead of guessing. The tree below shows the thinking order: first ask whether the operation is safely repeatable at all, then whether you can read its state, and if not, whether your write needs a guard.
روایت مدرس — نمودار ۳ را با صدای بلند بخوانیمTeacher narration — read Diagram 3 aloud
مثال فنی برای خواندنِ نمودار: عملیاتِ مناسبِ تکرار
وضعیت اولیه: خواندن قیمت timeout شده، ولی ثبت سفارش هم میتواند timeout شود.
اجرای مرحلهبهمرحله: خواندن را میتوان طبق سیاست بار تکرار کرد؛ برای ثبت، هویت ثابت و استعلام نتیجه لازم است و backoff جلوی retry storm را میگیرد.
نتیجه و دلیل: یک سیاست retry برای همهٔ عملیات مناسب نیست؛ بیاثر بودن تکرار و محدودیت بار باید بررسی شوند.
یک ردیف تصمیم (بالا) و سه ردیف نتیجه (پایین). از Request Timed Out (outcome unknown) شروع کن. تصمیم ۱ — Operation Idempotent?: اگر بله، Retry with Idem Key؛ اگر نه، تصمیم ۲ — Outcome Queryable? (میتوانی بپرسی «انجام دادی؟»): اگر بله، Read Back State؛ اگر نه، تصمیم ۳ — Write Needs Guard?: اگر بله، Fencing Token Write (storage rejects stale)؛ وگرنه Fail the Request — خطای صریح بهتر از حدس است.
الگوی بصری: فلشهای افقی پررنگ مسیر اصلیاند و عمودیهای کمرنگ شاخههای yes. در مصاحبه حفظ باش: «اول idempotency، بعد خواندن وضعیت، بعد محافظت نوشته، و آخر شکست صریح.»
One decision row (top) and three outcome rows (bottom). Start at Request Timed Out (outcome unknown). Decision 1 — Operation Idempotent?: yes means Retry with Idem Key; no leads to decision 2 — Outcome Queryable? (can you ask “did you do it?”): yes means Read Back State; no leads to decision 3 — Write Needs Guard?: yes means Fencing Token Write (storage rejects stale); otherwise Fail the Request — an explicit error beats a guess.
The visual pattern: bold horizontal arrows are the main path, faint vertical ones the “yes” branches. Memorize for interviews: “idempotency first, then read the state, then guard the write, and finally fail explicitly.”
جمعبندیِ بخشSection recap
- idempotency key: هر درخواستِ تغییردهنده یک کلیدِ یکتا دارد و سرور تکرارِ آن کلید را به یک اثر فرو میکاهد.
- بازتلاش باید محدود باشد و با backoff + jitter همراه شود تا مشتریها همزمان حمله نکنند (thundering herd).
- خواندنِ وضعیت (read-back) گاهی ارزانتر از ایمنسازیِ کاملِ نوشتن است.
- idempotency key: every mutating request carries a unique key, and the server collapses repeats of that key into one effect.
- Retries must be bounded with backoff + jitter so clients don't attack in sync (thundering herd).
- Reading the state back (read-back) is sometimes cheaper than fully bulletproofing the write.
۳3 ساعتهای غیرقابلاعتمادUnreliable clocks
ساعتِ دستگاهها همیشه دقیق و همقدم نیست. بعضی ساعتها برای گفتنِ زمانِ تقویمیاند و بعضی برای اندازهگیریِ مدت یا وابستگیِ رویدادها؛ جایگزین یکدیگر نیستند.
مثال فنی: ساعت عقب میرود
وضعیت اولیه: فرایند شروع را با ساعت دیواری 12:00:10 ثبت میکند؛ NTP زمان را به 12:00:05 برمیگرداند.
اجرای مرحلهبهمرحله: تفاضل دو ساعت دیواری ممکن است مدت منفی بدهد؛ سنجش elapsed time با monotonic clock مناسبتر است.
نتیجه و دلیل: ساعتِ مناسبِ نمایش تاریخ با ساعتِ مناسبِ سنجش مدت یکی نیست؛ monotonic محلی هم ترتیب جهانی نمیسازد.
Programs use clocks for two kinds of questions: durations (how many seconds did this request take?) and points in time (when was this article published?). In a distributed system each machine has its own quartz clock differing from the rest, and inter-machine links aren't instant; so “event order” scrambles easily.
۳.۱. چهار نوع ساعت3.1 Four kinds of clock
ساعت دیواری زمان تقویمی میدهد، ساعت یکنواخت برای فاصلهٔ زمانی مناسب است، ساعت منطقی وابستگی را دنبال میکند و ساعت با بازهٔ عدمقطعیت میزان ابهام را هم بیان میکند.
مثال فنی: چهار ساعت، چهار سؤال
وضعیت اولیه: برای درخواست هم تاریخ ثبت میخواهیم، هم مدت انتظار، هم ترتیب علّی و هم تشخیص همزمانی.
اجرای مرحلهبهمرحله: wall clock تاریخ میدهد؛ monotonic مدت محلی؛ Lamport ترتیب سازگار با علت؛ vector clock میتواند concurrent بودن را آشکار کند.
نتیجه و دلیل: جایگزین کردن همهٔ این نیازها با timestampِ دیواری یک قرارداد درست برای توزیع نمیسازد.
Modern computers have at least two clocks: time-of-day (wall clock, e.g. System.currentTimeMillis) and monotonic (stopwatch, e.g. System.nanoTime). But ordering in distributed systems offers two more options: the logical clock and the uncertainty-interval clock. The diagram below lines all four up like a UML class diagram.
روایت مدرس — نمودار ۴ را با صدای بلند بخوانیمTeacher narration — read Diagram 4 aloud
مثال فنی برای خواندنِ نمودار: چهار ساعت، چهار سؤال
وضعیت اولیه: برای درخواست هم تاریخ ثبت میخواهیم، هم مدت انتظار، هم ترتیب علّی و هم تشخیص همزمانی.
اجرای مرحلهبهمرحله: wall clock تاریخ میدهد؛ monotonic مدت محلی؛ Lamport ترتیب سازگار با علت؛ vector clock میتواند concurrent بودن را آشکار کند.
نتیجه و دلیل: جایگزین کردن همهٔ این نیازها با timestampِ دیواری یک قرارداد درست برای توزیع نمیسازد.
یک نمودار کلاس UML است نه جریان. بالای وسط کلاس انتزاعی Clock با سه ویژگی (read()، نوع instant/duration، و قطعیت known/hidden) و زیرش bus آبی با فلش تعمیم: چهار زیرکلاس. از راست به چپ: UncertaintyClock (نارنجی، خروجی [earliest, latest])، LogicalClock (بنفش، فقط ترتیب، بدون ثانیه)، MonotonicClock (فیروزهای، هرگز به عقب نمیپرد، بین نودها قابل مقایسه نیست) و TimeOfDayClock (آبی، با NTP همگام، میتواند بپرد).
دو جعبهٔ خطچین حکم عملیاند: «خواندن ساعت یک نقطه نیست، یک بازه است» و «کدام ساعت برای کدام کار: timeout با monotonic، deadline با time-of-day، ترتیب با logical، و snapshot سراسری با uncertainty». جملهٔ پایانی: ارثبری دربارهٔ تضمینهاست نه پیادهسازی.
This is a UML class diagram, not a flow. Top center sits the abstract Clock class with three attributes (read(), instant/duration kind, known/hidden certainty) and below it a blue bus with a generalization arrow: four subclasses. Right to left: UncertaintyClock (orange, [earliest, latest] output), LogicalClock (purple, order only, no seconds), MonotonicClock (teal, never jumps back, not comparable across nodes) and TimeOfDayClock (blue, NTP-synced, can jump).
The two dashed boxes give the practical verdicts: “a clock reading is not a point but an interval”, and “which clock for which job: timeouts with monotonic, deadlines with time-of-day, ordering with logical, global snapshots with uncertainty”. Closing line: the inheritance is about guarantees, not implementation.
۳.۲. همگامسازی، drift و جهشهای ساعت3.2 Synchronization, drift and clock jumps
- کوارتزِ معمولی تا حدود 200 ppm خطا دارد (≈ ۱۷ ثانیه در روز اگر روزی یکبار sync شود)؛ دما هم روی drift اثر میگذارد.
- اگر ساعتِ ماشین زیاد با سرورِ NTP فاصله بگیرد، ممکن است اجباراً ریست شود و برنامه ببیند که زمان به عقب پرید!
- یک فایروالِ اشتباه میتواند NTP را ببُرد و کسی مدتها نفهمد؛ drift هم آرام آرام جمع میشود.
- خطای NTP روی اینترنت عمومی حدود دهها میلیثانیه است و در شلوغی ممکن است به بالای 100ms برسد؛ بعضی سرورهای NTP هم اشتباه گزارش میدهند.
- leap second دقیقهای ۵۹ یا ۶۱ ثانیهای میسازد و سیستمهای آمادهنشده را میشکند (راهِ عملی: «smearing» یا پخشِ تدریجیِ اصلاح). از ۲۰۳۵ دیگر leap second نخواهیم داشت.
- در ماشینِ مجازی، pause شدنِ VM از دیدِ برنامه مثلِ جهشِ ناگهانیِ زمان است و کلاینتِ NTP داخلِ VM هم نمیفهمد.
- روی دستگاههای کنترلنشده (موبایلِ کاربر) اصلاً به ساعتِ سختافزار اعتماد نکن — بعضی کاربران عمداً ساعت را جابهجا میکنند!
- Ordinary quartz drifts up to ~200 ppm (≈ 17 seconds a day at one sync per day); temperature affects drift too.
- A machine clock far from the NTP server may be forcibly reset, and the app sees time jump back!
- One wrong firewall rule can cut NTP and nobody notices for ages; drift quietly accumulates.
- Public-internet NTP error is tens of milliseconds, over 100ms when busy; some NTP servers misreport too.
- A leap second makes a 59- or 61-second minute and breaks unprepared systems (practical fix: “smearing” the correction gradually). No more leap seconds after 2035.
- On a VM, a pause looks to the app like a sudden time jump, and the in-VM NTP client never learns.
- On unmanaged devices (the user's phone) never trust the hardware clock — some users shift it on purpose!
خطای ساعت بیصدا استClock error is silent
ساعت سختافزاری کمکم جلو یا عقب میافتد و همگامسازی ممکن است آن را اصلاح کند. بنابراین زمانِ دیواری میتواند جهش کند یا نسبت به گره دیگر متفاوت باشد.
مثال فنی: اختلاف ساعت دو ماشین
وضعیت اولیه: ساعت A سه ثانیه جلو و ساعت B دو ثانیه عقب است.
اجرای مرحلهبهمرحله: رخداد بعدی روی B میتواند timestamp کوچکتری از رخداد قبلی A داشته باشد؛ اصلاح ساعت هم ممکن است جهش یا تغییر سرعت ایجاد کند.
نتیجه و دلیل: timestamp خام دو میزبان را بدون مدل خطا به ترتیب واقعی تبدیل نمیکنیم؛ عدم قطعیت بخشی از داده است.
A broken CPU stops the system outright and you notice instantly. But a broken quartz clock or NTP leaves everything silently “healthy” while data corrupts quietly. If your software depends on synchronized clocks, monitor the offset between cluster clocks and cut out drifting nodes.
۳.۳. ترتیبدادن با timestamp و فاجعهی LWW3.3 Ordering with timestamps and the LWW disaster
در Last Write Wins، نسخهای با زمانِ بزرگتر انتخاب میشود. اگر ساعتها دقیق نباشند، زمانِ بزرگتر الزاماً نشاندهندهٔ تغییرِ واقعاً جدیدتر نیست و داده ممکن است بیصدا حذف شود.
مثال فنی: LWW با ساعت جلو افتاده
وضعیت اولیه: A مقدار v1 را با timestamp=105 ثبت میکند؛ B در زمان واقعیِ بعدی v2 را با timestamp=102 ثبت میکند.
اجرای مرحلهبهمرحله: حل تعارضِ بزرگترین timestamp، v1 را نگه میدارد.
نتیجه و دلیل: رویداد جدیدتر میتواند حذف شود؛ LWW فقط ترتیب timestampها را اجرا میکند، نه معنای درستِ آخرین ویرایش را.
The most dangerous temptation: ordering writes by timestamp. In last-write-wins (LWW), the write with the greatest timestamp wins. If two nodes' clocks skew slightly, a “newer” write may carry a smaller timestamp and vanish forever.
روایت مدرس — نمودار ۵ را با صدای بلند بخوانیمTeacher narration — read Diagram 5 aloud
مثال فنی برای خواندنِ نمودار: LWW با ساعت جلو افتاده
وضعیت اولیه: A مقدار v1 را با timestamp=105 ثبت میکند؛ B در زمان واقعیِ بعدی v2 را با timestamp=102 ثبت میکند.
اجرای مرحلهبهمرحله: حل تعارضِ بزرگترین timestamp، v1 را نگه میدارد.
نتیجه و دلیل: رویداد جدیدتر میتواند حذف شود؛ LWW فقط ترتیب timestampها را اجرا میکند، نه معنای درستِ آخرین ویرایش را.
بالا سه جعبهٔ شیء: Node 1 · clock 42.004 («client A writes x = 1»)، Node 3 · clock 42.003 («client B increments x = 2» با یادداشت «skew under 3 ms — still wrong»)، و Node 2 · receives both که قاعدهٔ LWW را اجرا میکند: «x = 1 (42.004) wins» و «x = 2 (42.003) is discarded». فلشهای سبز تکثیر را نشان میدهند.
قلب شکل دو بخش پایین است. باند ① «Real time»: دایرهٔ آبی «A writes x=1» چپ و سبز «B increments to x=2» راست (B مقدار A را خوانده، پس علّی دیرتر است). باند ② «Timestamp order»: همان دو رویداد جابهجا — x=2 با مهر کوچکتر چپ («LWW drops this one») و x=1 راست («LWW keeps this one»). دو ترتیب ناسازگارند و جعبهٔ صورتی میگوید «increment lost» بدون هیچ خطایی. جعبهٔ زرد راهحلها: مهر بالاتر از timestamp فعلی، logical clock، یا صبر Spanner تا سپری شدن بازهٔ عدم قطعیت.
Three object boxes on top: Node 1 · clock 42.004 (“client A writes x = 1”), Node 3 · clock 42.003 (“client B increments x = 2”, noted “skew under 3 ms — still wrong”), and Node 2 · receives both, which executes LWW: “x = 1 (42.004) wins”, “x = 2 (42.003) is discarded”. Green arrows show replication.
The heart is the two bands below. Band ① “Real time”: blue circle “A writes x=1” left, green “B increments to x=2” right (B read A's value first, so B is causally later). Band ② “Timestamp order”: the same two events swapped — x=2 with the smaller stamp left (“LWW drops this one”), x=1 right (“LWW keeps this one”). The two orders disagree, and the pink box says “increment lost” with no error raised. The yellow box lists fixes: stamping above the current timestamp, logical clocks, or Spanner's wait past the uncertainty interval.
۳.۴. بازهی عدم قطعیت و راهحلِ Spanner3.4 Uncertainty intervals and the Spanner fix
اگر زمان همراه با بازهٔ عدمقطعیت بیان شود، میتوان دربارهٔ تقدم بعضی رویدادها دقیقتر استدلال کرد. در طراحیهایی مثل Spanner، انتظارِ کنترلشده بخشی از راهِ تضمین ترتیب واقعی است.
مثال فنی: بازهٔ زمان بهجای نقطهٔ قطعی
وضعیت اولیه: API زمان را بهشکل بازهٔ [100,106] برمیگرداند و timestamp کامیت 106 انتخاب شده است.
اجرای مرحلهبهمرحله: در الگوی commit-wait، تأیید تا وقتی عقب میافتد که کران پایین زمان از ۱۰۶ عبور کند.
نتیجه و دلیل: این ایدهٔ Spanner عدم قطعیتِ محدود را در قرارداد وارد میکند؛ ساعت دقیق بدون انتظار و replication بهتنهایی کافی نیست.
If a clock returns an interval instead of a number ([earliest, latest]), we can reason: non-overlapping intervals of two events mean their order is certain; only overlapping intervals leave us unsure which came first. Google Spanner's TrueTime and Amazon's ClockBound give exactly this. So a transaction's timestamp can't violate causality, Spanner waits out the interval's width (commit wait) — with GPS and atomic clocks in every datacenter shrinking the width to ~7ms.
روایت مدرس — نمودار ۶ را با صدای بلند بخوانیمTeacher narration — read Diagram 6 aloud
مثال فنی برای خواندنِ نمودار: بازهٔ زمان بهجای نقطهٔ قطعی
وضعیت اولیه: API زمان را بهشکل بازهٔ [100,106] برمیگرداند و timestamp کامیت 106 انتخاب شده است.
اجرای مرحلهبهمرحله: در الگوی commit-wait، تأیید تا وقتی عقب میافتد که کران پایین زمان از ۱۰۶ عبور کند.
نتیجه و دلیل: این ایدهٔ Spanner عدم قطعیتِ محدود را در قرارداد وارد میکند؛ ساعت دقیق بدون انتظار و replication بهتنهایی کافی نیست.
چهار ستون (Time Source، Time Service، Local Clock، Using Time) و دو مسیر موازی. مسیر بالا («best-effort»): GPS Receiver ← satellite time ← NTP Servers ← NTP corrections ← Wall Clock ← wall-clock stamps ← LWW Timestamps قرمز (unsafe if skewed) — همان مسیر خطرناک نمودار ۵.
مسیر پایین («bounded»): Atomic Clock ← disciplined signal ← PTP + ClockBound ← reported error bound ← Uncertainty (earliest .. latest) ← wait out the width ← Commit Wait (Spanner snapshots). درس: مسیر بالا خطا را پنهان میکند، مسیر پایین خطا را اندازه میگیرد و گزارش میدهد — به بهای سختافزار و صبر.
Four stages (Time Source, Time Service, Local Clock, Using Time) and two parallel paths. The upper (“best-effort”) path: GPS Receiver → satellite time → NTP Servers → NTP corrections → Wall Clock → wall-clock stamps → red LWW Timestamps (unsafe if skewed) — the same dangerous path as Diagram 5.
The lower (“bounded”) path: Atomic Clock → disciplined signal → PTP + ClockBound → reported error bound → Uncertainty (earliest .. latest) → wait out the width → Commit Wait (Spanner snapshots). Lesson: the upper path hides error, the lower measures and reports it — at the price of hardware and patience.
۴4 توقفِ فرآیند، lease و fencingProcess pauses, leases and fencing
۴.۱. هر نخی ممکن است در هر لحظه pause شود4.1 Any thread may pause at any point
یک فرایند میتواند بیخبر برای مدتی متوقف شود و بعد ادامه دهد. اجاره یا lease مجوزی زماندار است؛ ادامهٔ اجرای کد بعد از مکث ثابت نمیکند مجوز هنوز معتبر است.
مثال فنی: Pause در جای حساس
وضعیت اولیه: worker مجوز ۵ثانیهای گرفته و پیش از نوشتن ۱۰ ثانیه متوقف میشود.
اجرای مرحلهبهمرحله: worker دیگری پس از انقضای مجوز کار را میگیرد؛ worker اول از pause برمیگردد و هنوز خط بعدی کد را اجرا میکند.
نتیجه و دلیل: چک کردن مجوز پیش از pause تضمین اعتبار هنگام اثر نیست؛ GC، تعلیق VM یا زمانبندی میتواند بین این دو رخ دهد.
Suppose each shard has one leader and only the leader may write. How does it stay sure it's still leader? The usual answer is a lease: a lock with a timeout that must be renewed before expiry. Now look at this code:
while (true) {
request = getIncomingRequest();
// Ensure that the lease always has at least 10 seconds remaining
if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}
if (lease.isValid()) {
process(request);
}
}
دو اشکال دارد: اول اینکه به ساعتِ همگام تکیه کرده (انقضا را ماشینِ دیگری تنظیم کرده و با ساعتِ محلی مقایسه میشود)؛
دوم و مهمتر اینکه فرض کرده بینِ بررسیِ ساعت و پردازشِ درخواست، زمانِ کمی میگذرد.
حالا تصور کن همان نخ ۱۵ ثانیه pause شود: lease منقضی شده، نودِ دیگری leader شده،
و وقتی نخ بیدار میشود، اصلاً نمیداند خواب بوده — و همان درخواستِ ناامن را اجرا میکند.
It has two flaws: first it leans on a synchronized clock (expiry set by another machine, compared against the local clock); second and more importantly, it assumes little time passes between checking the clock and processing the request. Now imagine that thread pauses for 15 seconds: the lease expires, another node becomes leader, and when the thread wakes it never knew it slept — executing the same unsafe request.
دلایلِ واقعیِ چنین pauseهایی کم نیستند:
Real causes of such pauses are far from rare:
- GC: توقفِ «stop-the-world» در JVMهای قدیمی تا چند دقیقه هم گزارش شده؛ GCهای مدرن بهترند اما هنوز محسوساند.
- مهاجرتِ زندهی VM (live migration): توقفِ دلخواه با مدتِ نامعلوم.
- context switch و steal time در hypervisor؛ در بارِ سنگین، صفِ نخها طولانی است.
- I/O سنکرون، page fault و swap؛ در حالتِ فشارِ حافظه، سیستم وارد thrashing میشود.
- سیگنالِ SIGSTOP (مثلاً Ctrl-Z اشتباهِ یک مهندسِ عملیات).
- GC: “stop-the-world” pauses of old JVMs reached minutes; modern GCs are better but still noticeable.
- Live VM migration: arbitrary stalls of unknown length.
- context switch and hypervisor steal time; under heavy load thread queues run long.
- Synchronous I/O, page faults and swap; under memory pressure the system thrashes.
- A SIGSTOP signal (e.g. an ops engineer's mistaken Ctrl-Z).
ابزارهای thread-safe به دادِ توزیعشده نمیرسندThread-safe tools won’t save the distributed
mutex، شمارندهی اتمیک و صفِ بلاکشونده روی یک ماشین عالیاند، چون حافظهی مشترک داریم.
در سیستم توزیعشده فقط پیام داریم؛ پس باید فرض کنی اجرای هر نخ میتواند در هر نقطهای متوقف شود،
در حالی که بقیهی دنیا جلو میرود و ممکن است تو را «مرده» اعلام کنند.
(سیستمهای hard real-time مثل کیسهی هوا این توقفها را با هزینهی بالا حذف میکنند؛ برای سرویسهای معمولی بهصرفه نیست.)
Mutexes, atomic counters and blocking queues are great on one machine, where memory is shared. Distributed systems have only messages; so assume any thread's execution can stall at any point while the rest of the world moves on and may declare you “dead”. (Hard real-time systems like airbags remove these stalls at great cost; not economical for ordinary services.)
۴.۲. زامبی، split brain و راهحلِ fencing4.2 Zombies, split brain and the fencing fix
گرهِ قدیمی ممکن است پس از پایانِ اجاره هنوز خودش را صاحب کار بداند. Fencing با بررسیِ مجوز در مقصدِ نوشتن، مانعِ اثرگذاریِ چنین نویسندهای میشود.
مثال فنی: زامبی با مجوز قدیمی
وضعیت اولیه: A توکن ۴۱ داشته و متوقف شده؛ B با توکن ۴۲ فایل را بهروز کرده است.
اجرای مرحلهبهمرحله: A برمیگردد و با ۴۱ مینویسد؛ مقصدی که آخرین توکن ۴۲ را نگه میدارد آن را رد میکند.
نتیجه و دلیل: fencing باید در همان منبعِ اثر اعمال شود؛ صرفِ انتخاب رهبر تازه، درخواستِ درراهِ رهبر قدیمی را خنثی نمیکند.
An old lease holder that hasn't realized its lease ended and keeps writing is called a zombie. Two famous book scenarios (both really happened in HBase): (1) client 1 pauses, the lease expires, client 2 writes, then client 1 returns and writes the same file; (2) client 1 dies but its write request sticks in the network and arrives only after the lease moved on. Both leave two writers interleaving and corrupt data.
راهحلِ سادهاندیشانه (خاموشکردنِ زامبی از راهِ شبکه یا hypervisor) کافی نیست:
در برابرِ تأخیرِ بلندِ حالتِ (۲) محافظت نمیکند، ممکن است همه، همزمان همدیگر را بکُشند،
و تا زامبی کشف شود داده خراب شده. راهحلِ درست fencing token است:
هر بار که lock service قفل را میدهد، یک عددِ افزایشی هم میدهد و سرویسِ ذخیرهسازی فقط نوشتههایی را میپذیرد
که tokenشان از آخرین tokenِ پذیرفتهشده بزرگتر باشد.
The naive fix (killing the zombie over the network or via the hypervisor) isn't enough: it doesn't guard against scenario (2)'s long delay, everyone may kill everyone at once, and by the time the zombie is found the data is corrupt. The right fix is a fencing token: every lock grant carries an increasing number, and the storage service accepts only writes whose token exceeds the last accepted one.
روایت مدرس — نمودار ۷ را با صدای بلند بخوانیمTeacher narration — read Diagram 7 aloud
مثال فنی برای خواندنِ نمودار: زامبی با مجوز قدیمی
وضعیت اولیه: A توکن ۴۱ داشته و متوقف شده؛ B با توکن ۴۲ فایل را بهروز کرده است.
اجرای مرحلهبهمرحله: A برمیگردد و با ۴۱ مینویسد؛ مقصدی که آخرین توکن ۴۲ را نگه میدارد آن را رد میکند.
نتیجه و دلیل: fencing باید در همان منبعِ اثر اعمال شود؛ صرفِ انتخاب رهبر تازه، درخواستِ درراهِ رهبر قدیمی را خنثی نمیکند.
چهار ردیف: Lease holder، Lock service، Storage guard و Outcome. از Lease Held (token 33) شروع کن: «renew» به Renewals (شمارش معکوس TTL) و «TTL ends» به Lease Expires؛ همزمان دارنده با pause به Process Paused (GC / VM suspend) میرود. از آنجا سه راه است: «resumes» سبز به Renewals، «pause beats TTL» قرمز به Lease Expires، و «wakes up» به Zombie Writer (stale token 33).
از Lease Expires «new owner» به New Holder (token 34) و «writes 34» به Write Token 34 میرسد؛ ولی نوشتهٔ زامبی («delayed write 33») از کانال بالای نمودار — استعارهٔ درخواست دیررسیده — وارد Write Token 33 میشود. دو پایان: «rejected 33» به Zombie Fenced سبز (33 < 34 rejected) یا «no token check» به Split Brain (interleaved writes) — همان فاجعهٔ HBase.
Four rows: Lease holder, Lock service, Storage guard and Outcome. Start at green Lease Held (token 33): “renew” down to Renewals (TTL countdown), “TTL ends” to Lease Expires; meanwhile the holder pauses into Process Paused (GC / VM suspend). Three exits leave it: green “resumes” back to Renewals (the normal return), red “pause beats TTL” to Lease Expires (the stall outlived the lease), and red “wakes up” to Zombie Writer (tagged “stale token 33”).
From Lease Expires, “new owner” reaches New Holder (client 2, token 34) and “writes 34” lands in Write Token 34; but the zombie's write (“delayed write 33”) travels the diagram's top channel — the metaphor of a late network request — into Write Token 33. Two endings: red “rejected 33” to green Zombie Fenced (“33 < 34 rejected”, the token mechanism worked), or red “no token check” to Split Brain (“interleaved writes”) — exactly the HBase disaster without token checks.
۴.۳. fencing token در عمل4.3 fencing tokens in practice
توکنِ fencing عددی افزایشی برای دورههای مالکیت است. مقصد، بزرگترین توکنِ پذیرفتهشده را نگه میدارد و درخواستِ قدیمیتر را نمیپذیرد؛ صرف تولید توکن کافی نیست.
مثال فنی: بررسی توکن اتمیک است
وضعیت اولیه: storage بیشترین توکن پذیرفتهشده را 42 نگه میدارد.
اجرای مرحلهبهمرحله: نوشتن با ۴۱ رد میشود؛ پذیرفتن ۴۳ باید همراه بهروزرسانیِ بیشترین توکن و اعمال اثر، اتمیک باشد.
نتیجه و دلیل: اگر بررسی و نوشتن جدا شوند، رقابت دوباره ایجاد میشود؛ توکن افزایشی بدون اجرای صحیح در مقصد کافی نیست.
The rule is simple: every storage write must carry the writer's current token, and storage remembers the greatest token seen. The diagram below retells Figure 9-6 of the book.
روایت مدرس — نمودار ۸ را با صدای بلند بخوانیمTeacher narration — read Diagram 8 aloud
مثال فنی برای خواندنِ نمودار: بررسی توکن اتمیک است
وضعیت اولیه: storage بیشترین توکن پذیرفتهشده را 42 نگه میدارد.
اجرای مرحلهبهمرحله: نوشتن با ۴۱ رد میشود؛ پذیرفتن ۴۳ باید همراه بهروزرسانیِ بیشترین توکن و اعمال اثر، اتمیک باشد.
نتیجه و دلیل: اگر بررسی و نوشتن جدا شوند، رقابت دوباره ایجاد میشود؛ توکن افزایشی بدون اجرای صحیح در مقصد کافی نیست.
یک sequence با چهار شرکتکننده (Client 1، Lock Service، Client 2، Storage) و سه پرده. پردهٔ اول: «acquire lease» و پاسخ «lease + token 33»، بعد «write x=1, token 33» با «200 OK, stored 33» (اولین نوشته دارندگان کهنه را fence میکند). پردهٔ دوم: نوار Client 1 قطع است («client 1 here paused — a 15 s GC pause»)؛ Client 2 «lease + token 34» میگیرد و «write x=2, token 34» را موفق مینویسد.
پردهٔ سوم: نوار Client 1 برمیگردد و «write x=3, token 33» — نوشتهٔ زامبی — با «409 rejected: 33 < 34» قرمز رد میشود. نکته: امنیت را ترتیب tokenها میسازد نه ساعت — به همین دلیل همین مفهوم همهجا نامهای مختلف دارد (sequencer در Chubby، epoch در Kafka، ballot/term در Paxos/Raft).
A sequence diagram with four participants (Client 1, Lock Service, Client 2, Storage) in three acts. Act one: “acquire lease” answered with “lease + token 33”, then “write x=1, token 33” answered “200 OK, stored 33” (the first write fences stale holders). Act two: Client 1's bar is cut (“client 1 here paused — a 15 s GC pause”); Client 2 takes “lease + token 34” (a bigger number!) and successfully writes “write x=2, token 34”.
Act three: Client 1's bar resumes with “write x=3, token 33” — the zombie write — rejected in red: “409 rejected: 33 < 34”. The reading point: token order creates the safety, not clock time — which is why the same idea wears many names (sequencer in Chubby, epoch in Kafka, ballot/term in Paxos/Raft).
جمعبندیِ بخشSection recap
امنیتِ یک lease فقط به «یکی بودنِ دارنده» نیست — چنین چیزی را نمیتوان تضمین کرد.
آنچه میتوانی تضمین کنی این است که نوشتههای کهنه بیاثر شوند. fencing دقیقاً همین است:
یک ترتیبِ قطعیِ افزایشی که storage آن را اجرا میکند، حتی اگر زامبیها هنوز زنده باشند.
A lease's safety isn't about “having exactly one holder” — that can't be guaranteed. What you can guarantee is that stale writes take no effect. That's exactly fencing: a strictly increasing order enforced by storage, even while zombies still walk.
۵5 دانش، حقیقت و دروغKnowledge, truth and lies
۵.۱. quorum و حکومتِ اکثریت5.1 quorum and majority rule
برای بعضی تصمیمها، نظرِ یک گره کافی نیست و تأییدِ اکثریت لازم میشود. در پروتکلِ درست، مجموعههای رأی همپوشاناند تا دو گروهِ جدا نتوانند تصمیمهای متعارضِ معتبر بسازند.
مثال فنی: اکثریت هنگام جدایی شبکه
وضعیت اولیه: پنج رأیدهنده به گروههای {A,B,C} و {D,E} تقسیم شدهاند.
اجرای مرحلهبهمرحله: فقط گروه سهعضوی میتواند اکثریت بگیرد؛ گروه دوعضوی برای عملیاتِ نیازمند quorum منتظر میماند.
نتیجه و دلیل: هر دو گروه اکثریت جدا نمیسازند؛ پروتکل epoch و رأیدهی هنوز باید مانع پذیرش همزمانِ رهبران نامعتبر شود.
The book's nightmare scenario: a node can receive every message while its outbound messages get lost. Everyone declares it dead after timeouts while it screams “I'm not dead!” — and nobody hears. Or: a node pauses a minute, everyone buries it, then it returns healthy and surprised!
درس این است که هیچ نودی نمیتواند به قضاوتِ خودش تکیه کند. به همین دلیل الگوریتمهای توزیعشده به
quorum (رأیگیری بینِ نودها) تکیه میکنند: تصمیمها به حداقلِ آرا نیاز دارند.
معمولاً اکثریتِ مطلق (بیش از نصف) — با ۳ نود، ۱ خرابی قابلتحمل است و با ۵ نود، ۲ خرابی.
و مهمتر: نمیتواند دو اکثریتِ متناقض همزمان وجود داشته باشد. اگر quorum تو را مرده اعلام کند،
حتی اگر زنده باشی باید کنار بروی.
The lesson: no node may trust its own judgment. So distributed algorithms lean on quorum (voting among nodes): decisions need a minimum of votes. Usually an absolute majority (over half) — 3 nodes tolerate 1 failure, 5 nodes tolerate 2. And crucially: two contradictory majorities can't exist at once. If a quorum declares you dead, step aside even if you're alive.
۵.۲. خرابیهای بیزانسی5.2 Byzantine faults
خرابی بیزانسی یعنی گره ممکن است اطلاعات غلط یا متناقض بفرستد. این با گرهِ خاموش یا کند فرق دارد و تحمل آن معمولاً پروتکل و فرضهای متفاوتی میخواهد.
مثال فنی: نودِ دروغگو در برابر نودِ خاموش
وضعیت اولیه: نود A به B میگوید نتیجه x و به C میگوید نتیجه y.
اجرای مرحلهبهمرحله: این رفتار با crash یا تأخیر متفاوت است؛ الگوریتمِ صرفاً crash-tolerant لزوماً چنین تناقضی را تحمل نمیکند.
نتیجه و دلیل: مدل اعتماد تعیینکننده است؛ checksum خطای تصادفی و امضای پیام، بهتنهایی همهٔ رفتار بیزانسی را حل نمیکنند.
So far we assumed nodes are unreliable but honest: they may be slow or silent, but their answers never lie. If nodes can lie (forged messages, conflicting votes), the problem becomes the “Byzantine generals” — far harder.
- جایی که واقعاً لازم است: کنترلِ پرواز (تخریبِ حافظه با پرتو)، سیستمهای چندحزبی با طرفهای متخاصم (بلاکچین).
- جایی که لازم نیست: دیتاسنترِ خودت با نودهای تحتِ کنترلت — پروتکلهای بیزانسی گراناند و معمولاً عملی نیستند.
- اگر یک باگ در نرمافزار باشد و همهی نودها نسخهی یکسان داشته باشند، تحملِ بیزانسی هم نجاتت نمیدهد؛ برای آن به چند پیادهسازیِ مستقل نیاز است.
- در برابرِ مهاجم، همچنان authentication، رمزنگاری و firewall اصلیترین سپرند.
- شکلهای ضعیفِ دروغگویی را جدی بگیر: checksum در لایهی برنامه، اعتبارسنجیِ ورودی، و کلاینتِ NTP چندسروره که outlier را حذف میکند.
- Where truly needed: flight control (memory damage by radiation), multi-party systems with hostile sides (blockchain).
- Where not: your own datacenter with controlled nodes — Byzantine protocols are expensive and usually impractical.
- If the bug is in the software and all nodes run the same version, Byzantine tolerance won't save you; that needs several independent implementations.
- Against attackers, authentication, encryption and firewalls remain the main shield.
- Take weak forms of lying seriously: app-layer checksums, input validation, and multi-server NTP clients dropping outliers.
۵.۳. مدلِ سیستم، safety و liveness5.3 System models, safety and liveness
مدل سیستم فرضها را روشن میکند. Safety یعنی اتفاقِ ممنوع رخ ندهد؛ liveness یعنی تحت شرایط مشخص، سیستم سرانجام پیشرفت کند. برای ارزیابیِ درستی، هر دو لازماند.
مثال فنی: درستی حتی هنگام توقف پیشرفت
وضعیت اولیه: قانون سیستم «یک صندلی به دو نفر فروخته نشود» است و شبکه دوپاره شده است.
اجرای مرحلهبهمرحله: توقف فروش در اقلیت میتواند safety را حفظ کند، ولی تکمیل درخواستهای آن بخش را موقتاً عقب بیندازد.
نتیجه و دلیل: safety میگوید چه نتیجهٔ بدی هرگز رخ ندهد؛ liveness میگوید تحت فرضهای پیشرفت، کار بالاخره انجام شود.
To talk about an algorithm's “correctness” we must first write down its assumed world: the system model. The model has two dimensions: timing assumptions (synchronous, partially synchronous, asynchronous) and failure assumptions (crash-stop, crash-recovery, partial slowness, Byzantine). The diagram below maps this space like a class diagram.
روایت مدرس — نمودار ۹ را با صدای بلند بخوانیمTeacher narration — read Diagram 9 aloud
مثال فنی برای خواندنِ نمودار: درستی حتی هنگام توقف پیشرفت
وضعیت اولیه: قانون سیستم «یک صندلی به دو نفر فروخته نشود» است و شبکه دوپاره شده است.
اجرای مرحلهبهمرحله: توقف فروش در اقلیت میتواند safety را حفظ کند، ولی تکمیل درخواستهای آن بخش را موقتاً عقب بیندازد.
نتیجه و دلیل: safety میگوید چه نتیجهٔ بدی هرگز رخ ندهد؛ liveness میگوید تحت فرضهای پیشرفت، کار بالاخره انجام شود.
دو درخت تعمیم UML. چپ کلاس انتزاعی TimingModel با سه زیرکلاس: Synchronous (سقف مشخص برای همهچیز)، PartiallySynchronous (قاب برجسته: بیشتر اوقات سقف دارد، گاهی نه) و Asynchronous (هیچ فرض زمانی). راست FaultModel با چهار زیرکلاس: CrashStop، CrashRecovery (قاب برجسته)، Limping (fail-slow) (از خرابی تمیز سختتر) و Byzantine (میتواند دروغ بگوید). جعبهٔ زرد وسط انتخاب عملی است: partially synchronous + crash-recovery.
ردیف پایین دو خاصیت درستی: Safety («هیچ اتفاق بدی نیفتد» — نقضش یک لحظهٔ مشخص و غیرقابل جبران است) و Liveness («بالاخره اتفاق خوبی بیفتد» — نشانهاش کلمهٔ eventually؛ eventual consistency هم liveness است). safety باید در همهٔ شرایط بماند ولی liveness میتواند فرض بازیابی داشته باشد. و جملهٔ آخر: مدلها سادهسازیاند — یک دیسک پاکشده میتواند فرض stable storage را بشکند.
Two UML generalization trees. Left, the abstract TimingModel with three subclasses: blue Synchronous (a stated ceiling for everything), highlighted orange PartiallySynchronous (usually bounded, sometimes not) and teal Asynchronous (no timing assumption, not even a clock). Right, the abstract FaultModel with four subclasses: CrashStop, highlighted orange CrashRecovery (returns but lost memory), Limping (fail-slow) (answers health checks yet does no work — harder than a clean crash) and Byzantine (may lie).
The dashed yellow box in the middle is the practical pick: partially synchronous + crash-recovery. The bottom row defines the two correctness properties. Blue Safety: “nothing bad ever happens” — its violation has a specific moment and is irreparable (a replayed token). Teal Liveness: “something good eventually happens” — possibly false now, true later; “eventually” in a definition marks it (eventual consistency is liveness too). Safety must hold in all conditions, but liveness may assume recovery (“if the network returns and a majority survives, answers come”). And never forget the line under the diagram: models simplify — a wiped disk can break the model itself.
روایت مدرس از نقشهٔ فصلTeacher narration of the chapter map
مثال فنی برای خواندنِ نمودار: timeout فقط یک مشاهده است
وضعیت اولیه: درخواست r7 به سرور میرود و کلاینت پس از 2 s پاسخی ندارد.
اجرای مرحلهبهمرحله: ممکن است درخواست نرسیده، در صف مانده، اجرا شده یا فقط پاسخ گم شده باشد.
نتیجه و دلیل: از سکوت، شکست قطعیِ اثر را نتیجه نمیگیریم؛ شناسهٔ عملیات و مسیر استعلام برای حل ابهام لازماند.
گرهٔ وسط (Distributed Trouble) را مرکز بگیر: بالا منابع سردرگمیاند (partial failure، timeout، ساعتها)، دو طرف سپرها (fencing در برابر زامبی، quorum در برابر قضاوت فردی)، و پایین خطرها و ابزارها (LWW، دو خاصیت safety/liveness، و اعتبارسنجی).
یک جملهٔ کل فصل: شبکه و ساعت و pauseی متوقفشده «شک» میسازند؛ quorum و fencing و اثبات «اطمینان» را برمیگردانند.
Take the center node (Distributed Trouble): confusion sources on top (partial failure, timeouts, clocks), shields on the sides (fencing against zombies, quorum against self-judgment), and dangers plus tools below (LWW, the two safety/liveness properties, and validation).
The whole chapter in one sentence: network, clocks and stalled pauses manufacture “doubt”; quorum, fencing and proofs restore “certainty”.