DDIA·مطالعهSTUDY

نقشه راهRoadmap / فصل 7Chapter 7 / حل تمرین‌هاSolutions

حل آموزشی تمرین‌های فصل 7Chapter 7 exercise solutions, explained

هر حل از سؤال مصاحبه شروع می‌کند، با یک شبیه‌سازی کوچک جاوا می‌سنجد، گام‌به‌گام استدلال می‌کند و نمودارهایی دارد که خواندنشان را یاد می‌گیری. هر حل به تمرین خودش برمی‌گردد. Each solution starts from the interview question, measures with a small Java simulation, reasons step by step, and shows diagrams you learn to read aloud. Every solution links back to its exercise.

33 حل تشریحیworked solutions· متناظر با بخش تمرین‌های فصل 7Companion to Chapter 7 exercises

11 تمرین ۱ — طرحِ sharding برای دادهٔ زمان‌سریExercise 1 — a sharding scheme for time-series data

L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
برای دادهٔ یک ماههٔ سنسورها چهار طرح (بازهٔ timestamp، بازهٔ sensor:timestamp، hash(sensor_id) و hash کلید کامل) را مقایسه کن. بار لحظه‌ای و مجموع ماه را جدا بسنج، مسیر کوئری «یک ماهِ یک سنسور» و fan-out آن را مشخص کن و اثر سنسور پرنرخ و راه repartitioning را توضیح بده.
Compare four schemes (timestamp ranges, sensor:timestamp ranges, hash(sensor_id), hash(full key)) for one month of sensor data. Measure instantaneous and monthly load separately, pin down the 'one sensor, one month' query path and its fan-out, and explain the hot-sensor effect and repartitioning.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

کلید sensor:timestamp را انتخاب کن چون کوئری غالب («یک ماهِ یک سنسور») با یک range scan تک‌شاردی جواب می‌گیرد (fan-out یک)؛ سنسور داغ را با پسوند ۰۰ تا ۹۹ می‌شکنی و ماه‌های قدیم را با retention whole-shard می‌اندازی. بها: سنسور ۴۰٪ حدود نیمی از یک شارد را می‌گیرد و خواندن چندسنسوری چند کوئری جدا می‌خواهد.
Pick sensor:timestamp keys because the dominant query ('one sensor, one month') resolves with a single-shard range scan (fan-out 1); split the hot sensor with a 00..99 suffix and drop old months whole-shard by retention. Price: the 40% sensor owns about half a shard, and multi-sensor reads need several separate queries.

صورت مسئله و فرض‌هاProblem and assumptions

  • ۸ شارد و ۱۲۰هزار نوشتن آموزشی روی ۳۰ روز از ۵۱ سنسور؛ سنسور صفر ۴۰٪ نوشتن‌ها را دارد (seed ثابت ۷).
  • بار لحظه‌ای یعنی سهم یک شارد از نوشتن‌های یک روز؛ بار ماهانه یعنی سهمش از کل ماه. fan-out یعنی تعداد شاردهای درگیر در یک کوئری.
  • عددها آموزشی‌اند، هرگز بنچمارک نیستند؛ الگو (کدام شارد داغ می‌شود) مهم است نه رقم دقیق.
  • 8 shards and 120k teaching writes over 30 days from 51 sensors; sensor-0 holds 40% of writes (fixed seed 7).
  • Instantaneous load is one shard's share of one day's writes; monthly load is its share of the whole month. Fan-out is the shards touched by one query.
  • Numbers are teaching aids, never benchmarks; the pattern (which shard heats) matters, not the exact figure.

چرا این روش و این عددها؟Why this method and these numbers?

کلید partition مسیر query و hotspot را تعیین می‌کند: hash(sensor_id) همهٔ writeهای sensor داغ را روی یک shard نگه می‌دارد، پس ۴۰٪ سهم یک حسگر با اضافه‌کردن shard خودبه‌خود پخش نمی‌شود. ترکیب sensor و time bucket قابلیت تقسیم می‌دهد ولی خواندن بازهٔ بلند fan-out بیشتری دارد. هشت shard و seed=7 برای تکرار آزمایش‌اند. سهم بار را از تعداد writeهای shard÷کل writeها می‌گیریم تا مقایسه از اندازهٔ مطلق نمونه مستقل باشد.

Partition keys determine query fan-out and hotspots. Hash(sensor_id) keeps a hot sensor’s writes together, so a 40% sensor does not spread merely by adding shards. Sensor/time buckets permit splitting at the cost of long-range fan-out. Eight shards and seed 7 make the experiment repeatable. Load share is shard writes / total writes, normalizing comparisons across sample sizes.

مدل ذهنیMental model

دو ساعت داری: ساعتِ ماه (مجموع) و ساعتِ همین ثانیه (لحظه‌ای). طرحی که ماه را قشنگ پخش می‌کند ممکن است همین ثانیه همه‌چیز را روی یک شارد بریزد. بعد یک سؤال داری: «ماهِ کدام سنسور را می‌خوانی؟» جواب همین سؤال کلید تقسیم را انتخاب می‌کند.
You own two clocks: the month clock (totals) and the this-second clock (instantaneous). A scheme that spreads the month beautifully may dump this very second onto one shard. Then one question: 'whose month do you read?' That answer picks the partition key.

حل گام‌به‌گامStep by step

  1. اول کوئری را نام ببر کوئری غالب «یک ماهِ یک سنسور» است، پس خانوادهٔ بازه‌ای می‌خواهیم: نزدیکی کلیدها باید بماند تا range scan ممکن شود. همین یک تصمیم، hash کلید کامل را از میز حذف می‌کند مگر نوشتن بر خواندن بچربد.
  2. هر دو ساعت را بسنج بازهٔ timestamp در ماه متعادل است (بیشینه ۱۳٫۵٪) ولی ۱۰۰٪ روز پانزدهم روی یک شارد می‌نشیند — شارد امروز همیشه داغ است. sensor:timestamp در هر دو ساعت حدود ۴۷٪ روی شارد سنسور داغ می‌نشیند: صادقانه داغ ولی قابل‌پیش‌بینی.
  3. بهای هش را بشمار hash(sensor_id) خواندن را تک‌شاردی نگه می‌دارد ولی ترتیب زمانی را به اسکن کامل شارد می‌سوزاند. hash کلید کامل نوشتن را کاملاً پخش می‌کند (بیشینه ۱۲٫۶٪) ولی همان کوئری را به ۸ شارد می‌کشاند. هیچ‌کدام بار یک کلید داغ را درمان نمی‌کند.
  4. نگهبان و بازنشستگی سنسور ۴۰٪ را با پسوند ۰۰ تا ۹۹ به ۱۰۰ زیرکلید بشکن: نوشتن پخش می‌شود و خواندن ۱۰۰ تکه را جمع می‌کند. بازهٔ داغ را split کن (هرگز rehash کل دنیا نه) و ماه‌های قدیم را whole-shard با retention بینداز. سیم‌خاردار: هشدار وقتی سهم لحظه‌ای یک شارد از ۳ برابر میانگین گذشت.
  1. Name the query first The dominant query is 'one sensor, one month', so we want the range family: key closeness must survive for a range scan to work. That single call removes full-key hashing from the table unless writes outweigh reads.
  2. Measure both clocks Timestamp ranges look even monthly (max 13.5%) yet 100% of day 15 lands on one shard — today's shard is always hot. sensor:timestamp sits near 47% on the hot sensor's shard on both clocks: honestly hot, but predictable.
  3. Price the hash options hash(sensor_id) keeps reads single-shard but burns time order into a full-shard scan. Full-key hashing spreads writes perfectly (max 12.6%) but drags the same query across 8 shards. Neither cures one hot key's load.
  4. Guard and retire Split the 40% sensor with a 00..99 suffix into 100 sub-keys: writes spread, reads merge 100 pieces. Split the hot range (never rehash the world) and drop old months whole-shard by retention. Tripwire: alert when one shard's instantaneous share passes 3x the mean.

طراحی مشخصConcrete design

کلید sensor_id:timestamp؛ مالک = sensor_id % 8 (نگاشت آموزشی). مسیر نوشتن: الصاق به شارد مالک. مسیر خواندن «یک ماهِ یک سنسور»: یک range scan روی همان شارد. رجیستری سنسور داغ برای سنسورهای بالای ۱۰٪ پسوند ۰۰ تا ۹۹ می‌گذارد. retention شاردهای whole-shard ماه قدیم را می‌اندازد، هرگز سطر‌به‌سطر نه.

Key sensor_id:timestamp; owner = sensor_id % 8 (teaching mapping). Write path: append to the owning shard. Read path for 'one sensor, one month': a single range scan on that shard. A hot-sensor registry adds the 00..99 suffix to sensors above 10%. Retention drops whole old-month shards, never row by row.

مثال محاسبه‌شدهWorked example

خروجی واقعی دمو (ShardSim): بیشینهٔ ماهانه ۱۳٫۵ / ۴۷٫۲ / ۴۸٫۵ / ۱۲٫۶٪ و بیشینهٔ روز پانزدهم ۱۰۰ / ۴۷٫۰ / ۴۸٫۱ / ۱۳٫۱٪ در چهار طرح؛ fan-out «یک ماهِ یک سنسور»: ۸ / ۱ / ۱ / ۸. بازهٔ زمانی ماه را قشنگ و ثانیه را خراب پخش می‌کند.

Actual demo output (ShardSim): monthly maxima 13.5 / 47.2 / 48.5 / 12.6% and day-15 maxima 100 / 47.0 / 48.1 / 13.1% across the four schemes; 'one sensor, one month' fan-out 8 / 1 / 1 / 8. Timestamp ranges spread the month beautifully and the second terribly.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · انتخاب طرح سری‌زمانی: چهار نامزد، یک برندهٔ نگهبان‌دارTime-series scheme choice: four candidates, one guarded winner · گردش‌کارworkflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
یک ماه دادهٔ سنسور داری و باید کلید تقسیم را انتخاب کنی. درخت را از جعبهٔ شروع بخوان: هر پرسش یک ویژگی بار را می‌سنجد و هر برگ یک طرح با برچسب قیمتش است.
You hold one month of sensor data and must pick the partition key. Read the tree from the start box: each question measures one load property, each leaf is one scheme with its price tag.
  1. از شروع تا پرسش اول: «اسکن یک ماهِ یک سنسور؟» — جواب بله مستقیم به برگ بازه‌های sensor:timestamp می‌رسد (یک سنسور، یک شارد، range scan تک‌شاردی).
  2. جواب نه به «یک سنسور بالای ۱۰٪؟» می‌رسد: بله روی خط نگهبان می‌نشیند — سنسور داغ را با پسوند ۰۰ تا ۹۹ بشکن و بدان خواندن باید ۱۰۰ تکه را جمع کند.
  3. جواب نه به «خواندن پراکنده قابل‌قبول است؟» می‌رسد: بله یعنی hash کلید کامل با fan-out هشت و نوشتن یکنواخت؛ نه یعنی hash(sensor_id) با خواندن تک‌شاردی و اسکن کامل شارد.
  1. Start to the first question: 'One-sensor month scans?' — a yes drops straight to the sensor:timestamp ranges leaf (one sensor, one shard, 1-shard range scan).
  2. A no reaches 'One sensor above 10%?': a yes lands on the guard lane — split the hot sensor with a 00..99 suffix, knowing reads must merge 100 pieces.
  3. A no reaches 'Scatter reads OK?': yes means hash(full key) with fan-out 8 and even writes; no means hash(sensor_id) with single-shard reads and a full-shard scan.

نقطهٔ تصمیم: نقطهٔ تصمیم برچسب هر برگ است: fan-out و رفتار hotspot روی خود برگ نوشته شده نه در پانوشت.

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

تضمین نشان‌داده‌شده: شکل ترتیب تصمیم و بهای هر طرح را نشان می‌دهد؛ عددهای دمو را ثابت نمی‌کند — آن‌ها در جدول اسمبل‌اند.

تضمین نشان‌نداده‌شده: دربارهٔ خرابی شارد و تکثیر چیزی نمی‌گوید — آن فصل ۶ است نه این انتخاب.

Decision point: The decision point is each leaf's tag: fan-out and hotspot behavior are written on the leaf itself, not in a footnote.

Failure trace: Skip the hot-sensor question and the split leaf is never chosen: the hot sensor's shard silently saturates while the month totals still look fine.

Guarantee shown: The figure shows the decision order and each scheme's priced trade-off. It does not prove the demo numbers — those live in the assembly table.

Not shown: It says nothing about shard failure and replication — that is Chapter 6, not this choice.

خودآزماییSelf-check

کوئری «یک ماهِ یک سنسور» کدام برگ را انتخاب می‌کند و چرا؟

پاسخ: بازه‌های sensor:timestamp: کل ماه روی یک شارد است پس کوئری یک range scan با fan-out یک است — به قیمت اینکه سنسور داغ نیمی از همان شارد را می‌گیرد.

Which leaf does the 'one sensor, one month' query pick, and why?

Answer: sensor:timestamp ranges: the whole month sits on one shard, so the query is one range scan with fan-out 1 — at the price of the hot sensor owning half that shard.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · یک سنسور، یک ماه: fan-out یک در مسیر منتخب و هشت در مسیر پراکندهOne sensor, one month: fan-out 1 on the chosen path, 8 on the scattered one · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
دو خط را بالا‌به‌پایین بخوان: خط بالا مسیر منتخب sensor:timestamp است و خط پایین جایگزین پراکندهٔ hash کلید کامل برای همان نوشتن‌ها.
Read the two lines top to bottom: the upper line is the chosen sensor:timestamp path, the lower line the scattered hash(full key) alternative for the same writes.
  1. خط بالا: هر خوانش با کلید سنسورش الصاق می‌شود، نگاشت مالک را sensor % 8 می‌دهد و ماه whole روی یک شارد می‌نشیند.
  2. کوئری‌اش یک range scan با fan-out یک است — ولی ۴۰٪ سنسور داغ هم سوار همان شارد می‌شود.
  3. خط پایین: هش کلید کامل هر نوشتن را یکنواخت می‌پاشاند، ترتیب گم می‌شود و همان ماه ۸ دسته lookup می‌شود که باید ادغام شوند.
  1. Upper line: each reading appends with its sensor key, the map assigns owner = sensor % 8, and the month lands whole on one shard.
  2. Its query is one range scan with fan-out 1 — but the hot sensor's 40% rides along into the same shard.
  3. Lower line: hashing the full key scatters every write evenly, order is lost, and the same month becomes 8 point-lookup batches to merge.

نقطهٔ تصمیم: نقطهٔ تصمیم مرحلهٔ نگاشت است: آنچه کلید نگه می‌دارد (همسایگی سنسور) fan-out مرحلهٔ کوئری را تعیین می‌کند.

رد خرابی: فقط ستون ماهانه را بخوان و بازهٔ timestamp برنده به نظر می‌رسد — تا شارد امروز زیر ۱۰۰٪ نوشتن‌های همین ثانیه آب شود.

تضمین نشان‌داده‌شده: شکل مسیر نوشتن و خواندن هر طرح و محل پرداختش را نشان می‌دهد؛ عرض پسوند و پنجرهٔ نگهداری را تعیین نمی‌کند — آن‌ها تصمیم عملیاتی‌اند.

تضمین نشان‌نداده‌شده: دربارهٔ تکثیر و کوئری‌های چندسنسوریِ چندشاردی ساکت است.

Decision point: The decision point is the Map stage: what the key keeps (sensor locality) decides the Query stage's fan-out.

Failure trace: Read only the monthly column and timestamp-range looks like the winner — until today's shard melts under 100% of this second's writes.

Guarantee shown: The figure shows both write and read paths per scheme and where each pays. It does not set the suffix width or the retention window — those are operational calls.

Not shown: It is silent on replication and on multi-sensor queries spanning many shards.

خودآزماییSelf-check

چرا hash کلید کامل با وجود پخش عالی نوشتن، برای این کوئری بد است؟

پاسخ: نوشتن یکنواخت و fan-out هشت: ماه باید از همهٔ شاردها جمع و ادغام شود پس کوئری غالب سر هر خواندن پول می‌دهد.

Why is hash(full key) bad for this query despite its perfect write spread?

Answer: Even writes, fan-out 8: the month must be gathered from all shards and merged, so the dominant query pays on every read.

نمودار اسمبل نهاییFinal assembly diagram · چهار طرح اندازه‌گیری‌شده: سهم ماهانه، سهم روز پانزدهم، fan-out تک‌سنسورFour schemes measured: monthly share, day-15 share, one-sensor fan-out · جدول اندازه‌گیریmeasurement tableباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
جدول را سطر‌به‌سطر بخوان: هر سطر یک طرح است و هر ستون یک ساعت — یا همان یک کوئری که مهم است.
Read the table row by row: each row is one scheme, each column is one clock — or the one query that matters.
  1. ستون ماهانه: بازهٔ زمانی و hash کامل نزدیک ۱۳٪ متعادل‌اند؛ sensor:ts و hash(sensor) سنسور داغ را نزدیک ۴۸٪ حمل می‌کنند.
  2. ستون روز پانزدهم: بازهٔ زمانی روی شارد امروز به ۱۰۰٪ فرومی‌ریزد — همان سلول صورتی — و بقیه شکل ماهانه‌شان را تکرار می‌کنند.
  3. ستون fan-out: دو طرح همسایه‌دار در ۱ شارد جواب می‌دهند و دو طرح پراکنده به هر ۸ شارد سر می‌زنند.
  1. Monthly column: timestamp-range and hash(full) look even near 13%; sensor:ts and hash(sensor) carry the hot sensor near 48%.
  2. Day-15 column: timestamp-range collapses to 100% on today's shard — the pink cell — while the others repeat their monthly shape.
  3. Fan-out column: the two range-local schemes answer in 1 shard; the two scattered schemes knock on all 8.

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

رد خرابی: سطر سلول صورتی را چپ‌به‌راست دنبال کن: ماهِ قشنگ، روز ۱۰۰٪ و fan-out هشت. هر انتخاب طرحی که فقط ستون اول را بخواند دقیقاً همین شکست را تکرار می‌کند.

تضمین نشان‌داده‌شده: شکل شمارش‌های شبیه‌سازی هر چهار طرح در هر دو ساعت را گزارش می‌کند؛ فراتر از همین بار تعمیم نمی‌دهد — سنسور داغ را عوض کن و دوباره اجرا کن.

تضمین نشان‌نداده‌شده: دربارهٔ تاخیر و دوام چیزی نمی‌گوید — فقط شمارش.

Decision point: The pink cell is the chapter's lesson: monthly totals hide instantaneous heat — always read both columns before picking.

Failure trace: Trace the pink cell's row left to right: an even-looking month, a 100% day, fan-out 8. Any scheme choice that reads only the first column repeats this exact failure.

Guarantee shown: The figure reports the simulation's counts for all four schemes on both clocks. It does not generalize beyond this workload — change the hot sensor, rerun.

Not shown: It says nothing about latency or durability — counts only.

خودآزماییSelf-check

کدام طرح در ماه متعادل و در روز فاجعه است؟

پاسخ: بازهٔ timestamp: ماهانه ۱۳٫۵٪ ولی روز پانزدهم ۱۰۰٪ — ماهِ متعادل، ذوب امروز را پنهان می‌کند.

Which scheme looks even monthly and melts daily?

Answer: Timestamp ranges: 13.5% monthly, 100% on day 15 — the even month hides today's meltdown.

گزینه‌های جایگزینAlternatives

  • اگر خواندن «یک ماهِ یک سنسور» نادر و نوشتن سیل‌آسا باشد، hash کلید کامل را ببر: نوشتن کاملاً یکنواخت و خواندن ۸ شاردی. بهای ادغام را علنی بپذیر نه اینکه در ریویو کشفش کنی.
  • اگر کوئری غالب «همهٔ سنسورها در یک روز» باشد، بازهٔ timestamp برنده است: یک روز روی یک شارد. بها: شارد امروز همیشه داغ است — برایش ظرفیت کنار بگذار.
  • If one-sensor-month reads are rare and writes flood in, take full-key hashing: perfectly even writes, 8-shard reads. Accept the merge price openly instead of discovering it in review.
  • If the dominant query is 'all sensors in one day', timestamp ranges win: one day lives on one shard. Price: today's shard is always hot — roster capacity for it.

سناریوهای خرابیFailure scenarios

  • سنسور از ۴۰٪ به ۷۰٪ می‌رسد و ۱۰۰ زیرکلید هم یک شارد را پر می‌کند. نگهبان: شارد اختصاصی برای همان سنسور (کلید داغ، شارد تنها) یا سهمیهٔ نوشتن. بدون نگهبان کل شارد می‌خوابد.
  • دادهٔ دیررس سنسور بعد از بسته شدن ماهش می‌رسد. نگهبان: شارد ماه قبل در پنجرهٔ کوتاه (آموزشی: چند روز) باز می‌ماند و بعد فقط خواندنی می‌شود؛ دادهٔ دیرتر دور انداخته و شمرده می‌شود، هرگز بی‌صدا گم نه.
  • افتادن یک شارد ماهِ سنسورهایش را ناپدید می‌کند. این طرح تکثیر را جدا می‌خواهد (فصل ۶): هر شارد چند نسخه نگه می‌دارد و خواندن به نسخهٔ سالم می‌رود.
  • The sensor grows from 40% to 70% and 100 sub-keys still fill one shard. Guard: a dedicated shard for that sensor (hot key, lonely shard) or a write quota. Without a guard the whole shard sleeps.
  • A sensor's late data arrives after its month closed. Guard: last month's shard stays writable for a short grace window (teaching: a few days), then read-only; later data is dropped and counted, never silently lost.
  • Losing a shard hides its sensors' months. This scheme needs replication separately (Chapter 6): each shard keeps several copies and reads go to a healthy one.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

import java.util.*;
// Teaching demo: 4 sharding schemes over one synthetic month of sensor writes.
// 8 shards, 51 sensors (sensor-0 is hot: 40% of all writes), 30 days.
public class ShardSim {
    static int fnv(String s) {
        int h = 0x811c9dc5;
        for (int i = 0; i < s.length(); i++) { h ^= s.charAt(i); h *= 0x01000193; }
        return h & 0x7fffffff;
    }
    public static void main(String[] a) {
        int SH = 8, DAYS = 30, N = 120000;
        Random r = new Random(7);
        int[] sen = new int[N]; int[] day = new int[N];
        for (int i = 0; i < N; i++) {
            sen[i] = (r.nextInt(100) < 40) ? 0 : 1 + r.nextInt(50); // hot sensor = 40%
            day[i] = 1 + r.nextInt(DAYS);
        }
        int[][] cnt = new int[4][SH];
        int[][] day15 = new int[4][SH];
        for (int i = 0; i < N; i++) {
            int s = sen[i], d = day[i];
            int A = (d - 1) * SH / DAYS;          // timestamp-range
            int B = s % SH;                        // sensor:timestamp range
            int C = fnv("s" + s) % SH;             // hash(sensor_id)
            int D = fnv("s" + s + ":" + d + ":" + i) % SH; // hash(full key)
            cnt[0][A]++; cnt[1][B]++; cnt[2][C]++; cnt[3][D]++;
            if (d == 15) { day15[0][A]++; day15[1][B]++; day15[2][C]++; day15[3][D]++; }
        }
        String[] names = {"ts-range      ", "sensor:ts     ", "hash(sensor)  ", "hash(full key)"};
        // query fan-out for "all of sensor-5 in one month": shards touched
        int[] fan = new int[4];
        Set<Integer> t = new HashSet<>();
        for (int d = 1; d <= DAYS; d++) t.add((d - 1) * SH / DAYS);
        fan[0] = t.size(); fan[1] = 1; fan[2] = 1; fan[3] = SH;
        System.out.println("scheme        | monthly max-shard% | day15 max-shard% | one-sensor-month fan-out");
        for (int k = 0; k < 4; k++) {
            int mx = 0, dx = 0;
            for (int j = 0; j < SH; j++) { mx = Math.max(mx, cnt[k][j]); dx = Math.max(dx, day15[k][j]); }
            int dtot = 0; for (int j = 0; j < SH; j++) dtot += day15[k][j];
            System.out.printf("%s | %6.1f%% (%6d) | %6.1f%% (%5d) | %d shards%n",
                names[k], 100.0 * mx / N, mx, 100.0 * dx / dtot, dx, fan[k]);
        }
        System.out.println("hot sensor-0 share of all writes = 40.0% (by construction)");
        System.out.println("OK");
    }
}

اجرا: javac ShardSim.java && java ShardSim — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: بیشینهٔ ماهانه ۱۳٫۵ / ۴۷٫۲ / ۴۸٫۵ / ۱۲٫۶٪ و بیشینهٔ روز پانزدهم ۱۰۰ / ۴۷٫۰ / ۴۸٫۱ / ۱۳٫۱٪ برای چهار طرح؛ fan-out کوئری تک‌سنسور: ۸ / ۱ / ۱ / ۸؛ OK. (خروجی واقعی همین محیط)

Run: javac ShardSim.java && java ShardSim — stdlib Java only, instant.

Expected output: Monthly max shares 13.5 / 47.2 / 48.5 / 12.6% and day-15 max shares 100 / 47.0 / 48.1 / 13.1% for ts-range, sensor:ts, hash(sensor), hash(full); one-sensor-month fan-out 8 / 1 / 1 / 8; OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

شمارندهٔ per-shard برای نوشتن؛ هشدار وقتی سهم لحظه‌ای از ۳ برابر میانگین گذشت؛ بررسی fan-out برای هر نوع کوئری (تک‌سنسور باید ۱ باشد)؛ تمرین دوره‌ای: انداختن یک ماه و راستی‌آزمایی ناپدید شدنش از همهٔ مسیرهای خواندن.

Per-shard write counters; alert past 3x-mean instantaneous share; assert fan-out per query type (single-sensor must be 1); periodic drill: drop one month and verify it vanishes from every read path.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): یک سنسور ۴۰٪ نوشتن‌ها را تولید کند؛ چه تغییری در کلید می‌دهی؟Follow-up (Senior): One sensor generates 40% of all writes. How do you change the partition key?

کلید تقسیم را فقط برای همان سنسور عوض کن: پسوند ۰۰ تا ۹۹ بچسبان تا نوشتن‌هایش روی ۱۰۰ زیرکلید در همهٔ شاردها پخش شود. خواندن آن سنسور حالا هر ۱۰۰ تا را می‌خواند و ادغام می‌کند (جمع برای شمارنده‌ها). رجیستری سنسور داغ بچرخان: بالای ۱۰٪ شناسایی، تبدیل قالب کلید، و بازنشستگی پسوند وقتی سنسور خنک شد — چون فقط نوشتن پخش می‌شود و خواندن برای همیشه پول می‌دهد.

Change the partition key for that sensor only: append a 00..99 suffix so its writes spread over 100 sub-keys across all shards. Reads of that sensor now fetch all 100 and merge them (a sum for counters). Run a hot-sensor registry: detect past 10%, convert the key format, and retire the suffix when the sensor cools — because only writes spread, reads pay forever.

پیگیری (سینیور): ماه قدیم را دقیقاً چطور بازنشست می‌کنی که <span class="ltr">delete</span> گران نشود؟Follow-up (Senior): How exactly do you retire an old month without an expensive delete?

هرگز حذف سطر‌به‌سطر نه: شاردها را هم‌تراز ماه نگه دار (یا فایل‌های time-partitioned داخلشان) و شارد whole ماه قدیم را با retention بینداز. بعد تمرینش کن: یک ماه را در staging بازنشست کن و پیش از اعتماد به ران‌بوک، ناپدید شدنش از همهٔ مسیرهای خواندن را راستی‌آزمایی کن.

Never row-by-row deletes: keep shards month-aligned (or time-partitioned files inside them) and drop the whole old shard by retention. Then drill it: retire one month on staging and verify its readings vanish from every query path before trusting the runbook.

پیگیری (استف): بازهٔ داغ را وسط ترافیک چطور بدون قطعی <span class="ltr">split</span> می‌کنی؟Follow-up (Staff): How do you split a hot range mid-traffic with no downtime?

بازهٔ داغ را بدون قطعی split کن: شاردهای مقصد را از پیش بساز، بازه را کپی کن (اول bulk بعد جبران نوشتن‌های تازه تا lag نزدیک صفر)، نگاشت شارد را اتمی بچرخان، درخواست‌های در پرواز را از مالک قدیم forward کن و کپی مبدأ را فقط بعد از تأیید سرویس مالک تازه پاک کن. split ناموفق به نگاشت قدیم برمی‌گردد — چیزی گم نمی‌شود چون نگاشت هرگز نچرخیده بود.

Split the live range without downtime: pre-create the target shards, copy the range (bulk, then catch-up on fresh writes until lag is near zero), flip the shard map atomically, forward in-flight requests from the old owner, and delete the source copy only after the new owner confirms serving. A failed split aborts back to the old map — nothing is lost because the map never flipped.

اشتباهات رایجCommon mistakes

  • مجموع ماهانه برای ظرفیت کافی است. روز پانزدهم با ۱۰۰٪ روی یک شارد ردش می‌کند — ساعت لحظه‌ای را هم همیشه بسنج.
  • hash(sensor) با hash کلید کامل یکی است. fan-out یک در برابر هشت خلافش را می‌گوید: ورودی هش سرنوشت کوئری را تعیین می‌کند نه خود هش.
  • پیشوند تصادفی خواندن را هم پخش می‌کند. فقط نوشتن را پخش می‌کند؛ خواندن باید هر ۱۰۰ زیرکلید را بخواند و ادغام کند — هر دو نیمه را بگو.
  • retention یعنی حذف سطر از جدول. یعنی انداختن whole شاردهای قدیم — حذف سطری در این مقیاس همان مسیر گران است.
  • Monthly totals suffice for sizing. Day 15 at 100% on one shard disproves it — always measure the instantaneous clock too.
  • hash(sensor) equals hash(full key). Fan-out 1 versus 8 says otherwise: the hash input, not the hash itself, decides the query.
  • A random prefix spreads reads too. It spreads writes only; reads must fetch and merge all 100 sub-keys — say both halves.
  • Retention means deleting rows from the table. It means dropping whole old shards — row deletes at this scale are the expensive path.

منابع:Sources: Microsoft · technical interviewing · Amazon · SDE III interview preparation

22 تمرین ۲ — consistent hashing را خودت بسازExercise 2 — build consistent hashing yourself

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

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
برای اضافه/حذف node، modulo و rendezvous و شاردهای منطقی ثابت با جدول مالکیت صریح را مقایسه کن. درصد جابه‌جایی را برای نگاشت مشخص محاسبه کن و دربارهٔ کجی، وزن گره، خرابی، تکثیر و تحویل مرحله‌ای تصمیم بگیر.
Compare modulo, rendezvous and fixed logical shards with an explicit ownership map for node joins and leaves. Compute the moved percentage for a stated mapping and decide on skew, node weights, failure, replication and staged handoff.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

حلقه را با دست بساز (نقطهٔ هش، قدم ساعت‌گرد، ۱۰۰ vnode per-node) با نگاشت مالکیت نسخه‌دار: عضو تازه حدود ۹٪ کلیدها را می‌برد در برابر ۹۱٪ mod-N، گره ۲ برابری ۲ برابر سهم می‌گیرد بدون جابه‌جایی سراسری، و هر چرخش مرحله‌ای است — استریم، جبران، چرخش نگاشت و forward در پرواز. بها: خود نگاشت نسخه‌بندی می‌خواهد و کلاینت‌ها باید تازه‌اش کنند.
Build the ring by hand (hash point, clockwise walk, 100 virtual nodes per node) with a versioned ownership map: a join moves ~9% of keys versus ~91% for mod-N, a 2x node takes 2x shares with no global move, and every flip is staged — stream, catch up, flip the map, forward in flight. Price: the map itself needs versioning and clients must refresh it.

صورت مسئله و فرض‌هاProblem and assumptions

  • ۱۰هزار کلید، خوشهٔ ۱۰ به ۱۱ و ۱۰ به ۹؛ هش پایدار FNV با نهایی‌ساز fmix (انتخاب آموزشی، تست‌شده، نه استاندارد).
  • حلقه ۱۰۰ vnode per-node دارد؛ طرح ثابت ۶۴ شارد منطقی با جدول مالکیت صریح دارد.
  • درصد جابه‌جایی کلید‌به‌کلید شمرده می‌شود؛ کجی (کمینه تا بیشینهٔ بار) و رفتار وزن کنارش گزارش می‌شود، هرگز پنهان نه.
  • 10k keys, cluster 10 to 11 and 10 to 9; a stable FNV hash with an fmix finalizer (teaching choice, tested, not a standard).
  • Ring carries 100 virtual nodes per physical node; fixed scheme uses 64 logical shards with an explicit ownership table.
  • Movement percent is measured key by key; skew (min..max load) and weight behavior are reported beside it, never hidden.

چرا این روش و این عددها؟Why this method and these numbers?

با توزیع ایدئال، اضافه‌شدن node یازدهم تقریباً ۱/۱۱ کلیدها را به عضو تازه می‌دهد؛ این امیدِ توزیع یکنواخت است، نه تضمین ring محدود. صد vnode پراکندگی سهم‌ها را کاهش می‌دهد ولی metadata را زیاد می‌کند؛ عدد بهینه با skew و وزن nodeها سنجیده می‌شود. ۶۴ shard منطقی هویت داده را از مالک فعلی جدا می‌کند؛ جابه‌جایی ownership هنوز انتقال داده، replica و fencing لازم دارد.

Under ideal uniform distribution, an eleventh node takes roughly 1/11 of keys; this is an expectation, not a finite-ring guarantee. One hundred vnodes reduce share variability at metadata cost; choose the count from skew and node weights. Sixty-four logical shards separate data identity from ownership, but moving ownership still needs data transfer, replication and fencing.

مدل ذهنیMental model

سه کار جدا: نگاشت (کدام کلید کجا می‌نشیند)، جابه‌جایی (در تغییر چه چیزی روی سیم می‌پرد) و مسیریابی (چه کسی حقیقت جاری را باور دارد). mod-N هر سه را قاطی می‌کند؛ حلقه به‌علاوهٔ نگاشت نسخه‌دار هر کار را صادق نگه می‌دارد.
Three separate jobs: mapping (which key sits where), movement (what flies over the wire on change), routing (who believes the current truth). Mod-N confuses all three; the ring plus a versioned map keeps each job honest.

حل گام‌به‌گامStep by step

  1. حلقه را با دست بکش هر کلید را به نقطه‌ای روی حلقه هش کن و ساعت‌گرد تا اولین vnode قدم بزن — ۱۰۰ عدد per-node شمار کلید را در ۱۰ گره به ۸۴۰ تا ۱۱۶۴ هموار می‌کند. عضو تازه دقیقاً کمان میان همسایه‌ها را می‌گیرد.
  2. جابه‌جایی را بشمار کلید‌به‌کلید بشمار: mod-N در ۱۰ به ۱۱ نود ممیز هشت درصد می‌برد درحالی‌که حلقه و rendezvous و شارد ثابت حدود ۹٪؛ ترک‌ها آینهٔ عضویت‌اند. سلول صورتی کل استدلال علیه «تعداد گره داخل نگاشت» است.
  3. بدون rehash وزن بده گره ۲ برابری ۲ برابر vnode (یا ۲ برابر شارد منطقی) می‌گیرد: فقط سهم خودش رشد می‌کند و بقیهٔ کلیدها می‌مانند. FNV خام ۴۰٪ را برای گره n10 دزدید تا نهایی‌ساز fmix avalanche را درست کرد — اول هش را تست کن بعد به سهم‌ها اعتماد کن.
  4. مسیریابی را از جابه‌جایی جدا کن جابه‌جایی و مسیریابی دو رویداد جدایند: اول داده استریم می‌شود بعد نگاشت نسخه‌دار می‌چرخد و کلاینت‌های کهنه از مالک قدیم forward می‌شوند. ترک خوشه فقط بازه‌های خودش را جابه‌جا می‌کند و نسخه‌ها محلی promote می‌شوند.
  1. Draw the ring by hand Hash each key to a ring point and walk clockwise to the first virtual node — 100 per physical node smooths key counts to 840..1164 at 10 nodes. A join takes exactly the arc between its neighbors.
  2. Count the moves Count key by key: mod-N moves 90.8% on 10 to 11 while ring, rendezvous and fixed shards all move ~9%; leaves mirror joins. The pink cell is the whole argument against node-count-inside-mapping.
  3. Weigh without rehash A 2x node takes 2x virtual nodes (or 2x logical shards): only its portion grows, every existing key stays put. Raw FNV stole 40% for node n10 until an fmix finalizer fixed the avalanche — test the hash, then trust the shares.
  4. Split routing from movement Movement and routing are separate events: data streams first, the versioned map flips second, stale clients forward through the old owner. A leave rehomes only its own ranges while replicas promote locally.

طراحی مشخصConcrete design

حلقه با ۱۰۰ vnode per-node؛ نگاشت مالکیت نسخه‌دار است (v41 و v42 و ...). پروتکل عضویت: استریم بازه‌های گره تازه، جبران نوشتن‌های تازه تا lag نزدیک صفر، چرخش اتمی نگاشت و forward درخواست‌های در پرواز از مالک قدیم. ترک خوشه آینهٔ عضویت است. هر بازه دو replica دارد که محلی promote می‌شوند، پس یک گره مرده هرگز بازه‌هایش را قفل نمی‌کند.

Ring with 100 virtual nodes per node; the ownership map is versioned (v41, v42, ...). Join protocol: stream the joining node's ranges, catch up on fresh writes until lag is near zero, flip the map atomically, forward in-flight requests from the old owner. A leave mirrors the join. Each range keeps two replicas that promote locally, so a single dead node never blocks its ranges.

مثال محاسبه‌شدهWorked example

خروجی واقعی دمو (HashRing): mod-N نود ممیز هشت / هشتادونه ممیز نه درصد، حلقه ۸٫۸ / ۸٫۷٪ با بار ۸۴۰ تا ۱۱۶۴، rendezvous ۹٫۲ / ۹٫۹٪ و شارد ثابت ۸٫۷ / ۹٫۳٪ برای ۱۰ به ۱۱ / ۱۰ به ۹. یافتهٔ اضافه: FNV خام ۴۰٪ کلیدها را به n10 داد — نهایی‌ساز fmix سهم‌ها را به حدود یک‌یازدهم برگرداند.

Actual demo output (HashRing): mod-N 90.8 / 89.9%, ring+vnodes 8.8 / 8.7% at load 840..1164, rendezvous 9.2 / 9.9%, fixed-64 8.7 / 9.3% for 10->11 / 10->9. Bonus finding: raw FNV let n10 win 40% of keys — the fmix finalizer restored ~1/11 each.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · حلقه با دست: n10 می‌آید، یک کمان می‌رود و بقیه می‌مانندThe ring by hand: n10 joins, one arc moves, the rest stays · نقشهٔ حلقهring plotباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
ده گره حلقه را نگه داشته‌اند و n10 می‌آید. از گره سبز شروع کن، کمان قرمز را دنبال کن و بعد بررسی کن بقیهٔ نقطه‌ها رنگشان را نگه داشته‌اند.
Ten nodes hold the ring and n10 joins. Start at the green node, follow the red arc, then check that every other dot kept its color.
  1. هر کلید به یک نقطه هش می‌شود و ساعت‌گرد تا اولین vnode قدم می‌زند: کل قاعدهٔ lookup همین است، در پنل آبی.
  2. n10 میان n9 و n0 می‌نشیند و دقیقاً همان کمان را می‌گیرد: نقطه‌کلید قرمز می‌رود و همسایه‌ها نه.
  3. پنل سبز قاعدهٔ عضویت را می‌گوید: حدود ۹٪ می‌رود و بقیه می‌مانند — با ۹۱٪ قرمز mod-N در پایین مقایسه کن.
  1. Each key hashes to a point and walks clockwise to the first virtual node: that is the whole lookup rule, in the blue panel.
  2. n10 lands between n9 and n0 and takes exactly that arc: the red key-dot moves, its neighbors do not.
  3. The green panel states the join rule: near 9% move, the rest stay — compare the red 91% of mod-N at the bottom.

نقطهٔ تصمیم: نقطهٔ تصمیم کمان قرمز است: دقیقاً کلیدهای میان n9 و n10 مالک عوض می‌کنند — بقیه طبق ساختار دست‌نخورده‌اند.

رد خرابی: کمان قرمز را «بیشتر کلیدها می‌روند» بخوان و تفکر mod-N را روی حلقه سوار کرده‌ای: کل نکته — جابه‌جایی کمینه — با یک بدخوانی گم می‌شود.

تضمین نشان‌داده‌شده: شکل جایابی حلقه و تک‌کمان جابه‌جاشده در عضویت را نشان می‌دهد؛ دربارهٔ وزن و مسیریابی چیزی ثابت نمی‌کند — آن‌ها گام‌های بعدند.

تضمین نشان‌نداده‌شده: دربارهٔ تکثیر هر بازه و کلیدهای داغ چیزی نمی‌گوید — حلقه کلید را پخش می‌کند، هرگز بار را نه.

Decision point: The decision point is the red arc: exactly the keys between n9 and n10 change owners — everything else is untouched by construction.

Failure trace: Read the red arc as 'most keys move' and you rebuild mod-N thinking on top of a ring: the whole point — minimal remap — is lost in one misread.

Guarantee shown: The figure shows ring placement and the single remapped arc on join. It proves nothing about weights or routing — those are the next steps.

Not shown: It says nothing about replication of each range or about hot keys — the ring spreads keys, never load.

خودآزماییSelf-check

گره تازه می‌آید: کدام کلیدها می‌روند و حدوداً چندتا؟

پاسخ: فقط کلیدهای کمان میان همسایه‌ها: حدود یک‌یازدهم فضای کلید، ۹٪ — کل وعدهٔ حلقه.

A new node joins: which keys move, and roughly how many?

Answer: Only the keys on the arc between its neighbors: about one eleventh of the keyspace, ~9% — the ring's entire promise.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · عضویت گره: جابه‌جایی کمینه روی سیم و مسیریابی نسخه‌دار کنارشNode join: minimal remap on the wire, versioned routing beside it · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
خط دادهٔ بالا را چپ‌به‌راست برای آنچه روی سیم می‌پرد بخوان و خط مسیریابی پایین را برای آنچه کلاینت‌ها باور دارند.
Read the upper data line left to right for what flies over the wire, and the lower routing line for what the clients believe.
  1. خط بالا: نگاشت پایدار فقط بازه‌های تازه (حدود ۹٪ کلیدها) را می‌دهد که استریم می‌شوند و تا lag نزدیک صفر جبران می‌کنند.
  2. چرخش یک گام اتمی نسخهٔ نگاشت است (v41 به v42)؛ درخواست‌های در پرواز از مالک قدیم forward می‌شوند به‌جای سرگردانی.
  3. خط پایین: کلاینت‌ها نگاشت را کوتاه کش می‌کنند، خواندن‌های کهنه forward می‌شوند، سهم‌های وزنی به گره ۲ برابری ۲ برابر بازه می‌دهند و ترک خوشه فقط بازه‌های خودش را جابه‌جا می‌کند.
  1. Upper line: the stable map yields only the new ranges (~9% of keys), which stream over and catch up until lag is near zero.
  2. The flip is one atomic map version step (v41 to v42); in-flight requests forward from the old owner instead of stranding.
  3. Lower line: clients cache the map briefly, stale reads forward, weighted shares give the 2x node 2x ranges, and a leave rehomes only its own ranges.

نقطهٔ تصمیم: نقطهٔ تصمیم گره چرخش مالکیت است: یک تغییر اتمی نسخهٔ نگاشت و هر پیکان بعدش دنیای تازه را می‌خواند.

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

تضمین نشان‌داده‌شده: شکل تحویل مرحله‌ای و مسیریابی/نسخه‌بندی کنارش را نشان می‌دهد؛ promotion نسخه‌ها داخل بازه را پوشش نمی‌دهد.

تضمین نشان‌نداده‌شده: کرانی برای زمان جبران زیر نوشتن سنگین نمی‌دهد — آن محدودسازی سرعت و سیم‌خاردار lag می‌خواهد.

Decision point: The decision point is the Flip ownership node: one atomic map version change, and every arrow after it reads the new world.

Failure trace: Flip the map before the data arrives and reads land on an empty owner: movement and routing are separate events in a fixed order — data first, map second.

Guarantee shown: The figure shows the staged handoff and the routing/versioning beside it. It does not cover replica promotion inside a range.

Not shown: It promises no bound on catch-up time under heavy writes — that needs throttling and a lag tripwire.

خودآزماییSelf-check

گره تازه دو برابر ظرفیت دارد: سهمش را چطور می‌دهی؟

پاسخ: ۲ برابر vnode (یا ۲ برابر شارد منطقی) به آن بده: فقط سهم خودش رشد می‌کند و بقیهٔ کلیدها می‌مانند — بدون جابه‌جایی سراسری.

The new node has twice the capacity: how do you size its share?

Answer: Give it 2x virtual nodes (or 2x logical shards): only its portion grows, every other key stays — no global move.

نمودار اسمبل نهاییFinal assembly diagram · جابه‌جایی عضویت و ترک اندازه‌گیری‌شده: mod-N نزدیک ۹۰٪ و حلقه نزدیک ۹٪Join and leave moves measured: mod-N near 90%, ring and rendezvous near 9% · جدول اندازه‌گیریmeasurement tableباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
جدول را سطر‌به‌سطر بخوان: هر سطر یک روش نگاشت است و هر ستون شکل بار یا جابه‌جایی در تغییر عضویت.
Read the table row by row: each row is one mapping method, each column is load shape or movement on membership change.
  1. ستون بار: حلقه در ۱۰ گره ۸۴۰ تا ۱۱۶۴ کلید per-node می‌دهد؛ mod-N «شانسی متعادل» است — یکنواخت ولی تصادفی.
  2. ستون عضویت: حلقه و rendezvous و شارد ثابت همه حدود ۹٪ می‌برند؛ mod-N نود ممیز هشت درصد — همان سلول صورتی.
  3. ستون ترک آینهٔ عضویت است نزدیک ۹ تا ۱۰٪ برای هر روش سالم؛ پانوشت وزن‌ها و انضباط تست هش را اضافه می‌کند.
  1. Load column: the ring spreads 840..1164 keys per node at 10 nodes; mod-N is 'balanced by luck' — even, but accidental.
  2. Join column: ring, rendezvous and fixed shards all move ~9%; mod-N moves 90.8% — the pink cell.
  3. Leave column mirrors join near 9-10% for every sane method; the footer adds weights and the hash-testing discipline.

نقطهٔ تصمیم: سلول صورتی تله است و پانوشت انضباط: هش را تست کن (FNV خام ۴۰٪ دزدید) بعد به سطرهای ۹٪ اعتماد کن.

رد خرابی: فقط ستون‌های جابه‌جایی را بخوان و سلول «شانسی متعادل» mod-N نامرئی می‌شود: جایابی متعادل امروز دربارهٔ عضویت فردا هیچ نمی‌گوید.

تضمین نشان‌داده‌شده: شکل شمارش کلید‌به‌کلید جابه‌جایی هر چهار روش در عضویت و ترک را گزارش می‌کند؛ هیچ هشی را تأیید نمی‌کند — سطر هشدار صورتی چرایی‌اش را می‌گوید.

تضمین نشان‌نداده‌شده: جابه‌جایی را می‌سنجد نه زمان جبران و دسترس‌پذیری حین انتقال را.

Decision point: The pink cell is the trap and the footer is the discipline: test the hash (raw FNV stole 40%), then trust the ~9% rows.

Failure trace: Read only the movement columns and mod-N's 'balanced by luck' cell is invisible: even placement today says nothing about tomorrow's join.

Guarantee shown: The figure reports key-by-key movement counts for all four methods on join and leave. It does not bless any hash — the pink warning row explains why.

Not shown: It measures movement, not catch-up time or availability during the move.

خودآزماییSelf-check

mod-N در ۱۰ گره متعادل است — قابل‌قبول است؟

پاسخ: نه: جایابی متعادلش شانسی است و ستون عضویت ثابت می‌کند — ۹۰٫۸٪ کلیدها می‌روند. یکنواختی بدون پایداری تزئین است.

mod-N looks even at 10 nodes — is it acceptable?

Answer: No: its even placement is luck, and the join column proves it — 90.8% of keys move. Evenness without stability is decoration.

گزینه‌های جایگزینAlternatives

  • rendezvous (بیشترین وزن تصادفی) وقتی هیچ نگاشت مالکیتی نمی‌خواهی: جایابی محاسبه‌ای بدون حالت per-key است پس هر node می‌تواند مسیریابی کند. بها: O(nodes) هش per-lookup و وزن‌ها کپی مجازی صریح می‌خواهند.
  • شاردهای منطقی ثابت (مثلاً ۶۴ عدد) وقتی عملیات جابه‌جایی درشت و دیدنی می‌خواهد: عضو تازه شاردهای whole را می‌گیرد و جدول مالکیت خواناست. بها: تعداد را از اول با مقسوم‌علیه زیاد انتخاب کن — resharding بعدی همه‌چیز را بازنویسی می‌کند.
  • Rendezvous (highest random weight) when you want no ownership map at all: placement is a stateless computation per key, so any node can route. Price: O(nodes) hash computations per lookup and weights need explicit virtual copies.
  • Fixed logical shards (say 64) when operations wants coarse, visible moves: a join takes whole shards, and the ownership table is human-readable. Price: pick the count with many divisors up front — resharding later rewrites everything.

سناریوهای خرابیFailure scenarios

  • گره تازه وسط کپی می‌میرد. نگهبان: نگاشت هرگز نچرخیده پس مالک قدیم هنوز سرو می‌کند و کپی ناقص دادهٔ دورریختنی است — abort طبق ساختار امن است.
  • کلاینتی با نگاشت کهنه به مالک قدیم می‌نویسد. نگهبان: مالک قدیم در پنجره forward یا رد می‌کند و نوشتن در تلاش مجدد idempotent است — هرگز بی‌صدا جای اشتباه نمی‌نشیند.
  • همهٔ نسخه‌های یک بازه با هم می‌افتند. هیچ ترفند نگاشتی کمک نمی‌کند: بازه تا برگشتن یک کپی نادسترس است — همین را بگو به‌جای اختراع لحظه‌ای کپی سوم.
  • The joining node dies mid-copy. Guard: the map never flipped, so the old owner still serves and the partial copy is discarded data — abort is safe by construction.
  • A client with a stale map writes to the old owner. Guard: the old owner forwards or rejects during the window, and the write is idempotent on retry — never silently lands in the wrong place.
  • All replicas of one range go down together. No mapping trick helps: the range is unavailable until a copy returns — say so instead of inventing a third copy on the spot.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

import java.util.*;
// Teaching demo: consistent hashing by hand — mod-N vs ring+vnodes vs
// rendezvous vs fixed logical shards. 10k keys, stable FNV hash.
public class HashRing {
    // NOTE: raw FNV-1a has a weak final avalanche: for structured inputs like
    // "key-5@n10" the trailing chars dominate and node n10 wins ~40% of keys.
    // The fmix32 finalizer below decorrelates the bits (verified: ~1/11 each).
    // Lesson: sharding needs a *tested* hash, never an ad-hoc one.
    static int fnv(String s) {
        int h = 0x811c9dc5;
        for (int i = 0; i < s.length(); i++) { h ^= s.charAt(i); h *= 0x01000193; }
        h ^= h >>> 16; h *= 0x85ebca6b; h ^= h >>> 13; h *= 0xc2b2ae35; h ^= h >>> 16;
        return h & 0x7fffffff;
    }
    static List<String> nodes(int n) {
        List<String> l = new ArrayList<>();
        for (int i = 0; i < n; i++) l.add("n" + i);
        return l;
    }
    // ring with V virtual nodes per physical node
    static Map<Integer, String> ring(List<String> ns, int V) {
        TreeMap<Integer, String> m = new TreeMap<>();
        for (String n : ns) for (int v = 0; v < V; v++) m.put(fnv(n + "#" + v), n);
        return m;
    }
    static String onRing(Map<Integer, String> m, int h) {
        Map.Entry<Integer, String> e = ((TreeMap<Integer, String>) m).ceilingEntry(h);
        return (e != null ? e : ((TreeMap<Integer, String>) m).firstEntry()).getValue();
    }
    static String rendezvous(String key, List<String> ns) {
        String best = null; int bs = -1;
        for (String n : ns) { int s = fnv(key + "@" + n); if (s > bs) { bs = s; best = n; } }
        return best;
    }
    public static void main(String[] a) {
        int K = 10000, V = 100, LS = 64;
        String[] keys = new String[K];
        for (int i = 0; i < K; i++) keys[i] = "key-" + i;
        List<String> n10 = nodes(10), n11 = nodes(11), n9 = nodes(9);

        // --- mod N ---
        int modJoin = 0, modLeave = 0;
        for (String k : keys) {
            if (fnv(k) % 10 != fnv(k) % 11) modJoin++;
            if (fnv(k) % 10 != fnv(k) % 9) modLeave++;
        }
        // --- ring + vnodes ---
        Map<Integer, String> r10 = ring(n10, V), r11 = ring(n11, V), r9 = ring(n9, V);
        int ringJoin = 0, ringLeave = 0;
        Map<String, Integer> load = new HashMap<>();
        for (String k : keys) {
            int h = fnv(k);
            String o = onRing(r10, h);
            load.merge(o, 1, Integer::sum);
            if (!onRing(r11, h).equals(o)) ringJoin++;
            if (!onRing(r9, h).equals(o)) ringLeave++;
        }
        int mn = Collections.min(load.values()), mx = Collections.max(load.values());
        // --- rendezvous ---
        int rvJoin = 0, rvLeave = 0;
        for (String k : keys) {
            String o = rendezvous(k, n10);
            if (!rendezvous(k, n11).equals(o)) rvJoin++;
            if (!rendezvous(k, n9).equals(o)) rvLeave++;
        }
        // --- fixed logical shards + ownership map ---
        int[] own10 = new int[LS], own11 = new int[LS], own9 = new int[LS];
        for (int s = 0; s < LS; s++) own10[s] = s % 10;
        // join: new node takes ~LS/11 shards from the most-loaded owners
        int[] cnt10 = new int[10];
        for (int s = 0; s < LS; s++) cnt10[own10[s]]++;
        System.arraycopy(own10, 0, own11, 0, LS);
        int need = LS / 11 + 1;
        int moved = 0;
        outer: for (int n = 0; n < 10; n++)
            for (int s = 0; s < LS && moved < need; s++)
                if (own11[s] == n && cnt10[n] > LS / 11) { own11[s] = 10; cnt10[n]--; moved++; }
        int fxJoin = 0, fxLeave = 0;
        for (int s = 0; s < LS; s++) { own9[s] = own10[s] == 9 ? s % 9 : own10[s]; }
        for (String k : keys) {
            int s = fnv(k) % LS;
            if (own10[s] != own11[s]) fxJoin++;
            if (own10[s] != own9[s]) fxLeave++;
        }
        // weighted join: new node with 2x capacity takes 2x shards
        System.out.println("method            | load/node@10 (min..max) | moved 10->11 | moved 10->9");
        System.out.printf("mod-N             | balanced by luck        | %5d (%4.1f%%) | %5d (%4.1f%%)%n",
            modJoin, 100.0 * modJoin / K, modLeave, 100.0 * modLeave / K);
        System.out.printf("ring+100vnodes    | %d..%d keys            | %5d (%4.1f%%) | %5d (%4.1f%%)%n",
            mn, mx, ringJoin, 100.0 * ringJoin / K, ringLeave, 100.0 * ringLeave / K);
        System.out.printf("rendezvous        | even (max-weight)       | %5d (%4.1f%%) | %5d (%4.1f%%)%n",
            rvJoin, 100.0 * rvJoin / K, rvLeave, 100.0 * rvLeave / K);
        System.out.printf("fixed-64-shards   | %d shards/node approx   | %5d (%4.1f%%) | %5d (%4.1f%%)%n",
            LS / 10, fxJoin, 100.0 * fxJoin / K, fxLeave, 100.0 * fxLeave / K);
        System.out.println("weighted join (2x node): fixed-shards moves ~2x shards, others keep 1x shares; no full rehash");
        System.out.println("OK");
    }
}

اجرا: javac HashRing.java && java HashRing — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: جابه‌جایی mod-N نود ممیز هشت / هشتادونه ممیز نه درصد؛ حلقه ۸٫۸ / ۸٫۷٪ (بار ۸۴۰ تا ۱۱۶۴)، rendezvous ۹٫۲ / ۹٫۹٪ و شارد ثابت ۸٫۷ / ۹٫۳٪ برای ۱۰ به ۱۱ / ۱۰ به ۹؛ عضو وزین rehash سراسری نمی‌خواهد؛ OK. (خروجی واقعی همین محیط)

Run: javac HashRing.java && java HashRing — stdlib Java only, instant.

Expected output: mod-N moves 90.8 / 89.9%; ring+vnodes 8.8 / 8.7% (load 840..1164), rendezvous 9.2 / 9.9%, fixed-64 8.7 / 9.3% for 10->11 / 10->9; weighted join needs no full rehash; OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

کران جابه‌جایی در عضویت و ترک (هرکدام حدود 1/N)؛ کران بار per-node (کمینه تا بیشینه در ۱٫۵ برابر میانگین)؛ بررسی یکسانی نگاشت در restartها (هش پایدار)؛ بررسی نشستن سهم گره ۲ برابری نزدیک ۲ برابر میانگین.

Bound moved keys on join and leave (~1/N each); bound per-node load (min..max within ~1.5x mean); assert identical mapping across restarts (stable hash); assert a 2x node's share lands near 2x mean.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): node جدید دو برابر ظرفیت دارد؛ balancing را بدون جابه‌جایی همهٔ کلیدها چطور انجام می‌دهی؟Follow-up (Senior): A new node has twice the capacity. How do you rebalance without moving every key?

با سهم اندازه بزن نه rehash: روی حلقه ۲ برابر vnode، در شارد ثابت ۲ برابر شارد منطقی و در rendezvous ۲ برابر ورودی وزنی به آن بده. فقط سهم گره تازه رشد می‌کند — بقیهٔ کلیدها می‌مانند. با ستون بار راستی‌آزمایی کن: شمار کلیدهایش باید نزدیک ۲ برابر میانگین بنشیند.

Size by shares, not by rehash: on the ring give it 2x virtual nodes, in fixed shards 2x logical shards, in rendezvous 2x weight entries. Only the new node's portion grows — every existing key stays. Verify with the load column: its key count should land near 2x the mean.

پیگیری (سینیور): یک کلید سلبریتی ۳۰٪ ترافیک را می‌گیرد. حلقه نجاتت می‌دهد؟Follow-up (Senior): One celebrity key takes 30% of traffic. Does the ring save you?

نمی‌کنی — حلقه کلید را پخش می‌کند، هرگز بار را نه. کلید داغ را روی شارد خودش تنها بگذار (یا ماشین اختصاصی) یا نوشتن‌هایش را با پسوند بشکن و در خواندن جمع کن. هر بار قیمت را نام ببر: جداسازی آشکارساز کلید داغ می‌خواهد و پسوندها از هر خواندن مالیات می‌گیرند.

You don't — the ring spreads keys, never load. Isolate the hot key on its own shard (or a dedicated machine), or split its writes with a suffix and merge on read. Name the price each time: isolation needs hot-key detection, suffixes tax every read.

پیگیری (استف): دو گره هم‌زمان مدعی یک بازه‌اند. طرحت چطور این را ناممکن می‌کند؟Follow-up (Staff): Two nodes both claim one range. How does your design rule that out?

از مسیر نگاشت نسخه‌دار نمی‌تواند پیش بیاید: مالکیت در یک گام اتمی نسخه می‌چرخد، پس دو مالک برای یک بازه یعنی کسی نگاشت را دور زده. فنس بگذار: نوشتن‌ها نسخهٔ نگاشت را حمل می‌کنند و نوشتن با نسخهٔ کهنه برای تلاش مجدد رد می‌شود — نگاشت تنها مرجع مالکیت است.

It can't happen through the versioned map: ownership flips in one atomic version step, so two owners for one range means someone bypassed the map. Fence it: writes carry the map version, and a stale-version write is rejected for retry — the map is the single ownership authority.

اشتباهات رایجCommon mistakes

  • «سازگار» یعنی consistency نسخه‌ها یا ACID. اینجا یعنی میل کلید به ماندن در شارد خودش — نام دربارهٔ پایداری است نه درستی.
  • hashCode جاوا (یا هر هش زبانی) برای sharding خوب است. ممکن است بین پروسه‌ها و نسخه‌ها عوض شود — sharding تابع منجمد و تست‌شده می‌خواهد و ماجرای FNV خام ثابت می‌کند حتی هش خوب هم بررسی avalanche می‌خواهد.
  • vnodeها کلید داغ را درست می‌کنند. فقط شمار کلید را هموار می‌کنند؛ یک کلید داغ whole روی یک گره می‌نشیند — بار جداسازی یا شکستن می‌خواهد نه vnode بیشتر.
  • درصد جابه‌جایی تنها معیار است. کجی و رفتار وزن و داستان مسیریابی به همان اندازه تصمیم می‌گیرند — هر سه را گزارش کن وگرنه مقایسه دروغ می‌گوید.
  • 'Consistent' means replica consistency or ACID. Here it means a key's tendency to stay in its shard — the name is about stability, not correctness.
  • Java's hashCode (or any language hash) is fine for sharding. It may change between processes and versions — sharding needs a frozen, tested function, and the raw-FNV episode proves even a decent hash needs its avalanche checked.
  • Virtual nodes fix hot keys. They smooth key counts only; one hot key still lands whole on one node — load needs isolation or splitting, not more vnodes.
  • Movement percentage is the only metric. Skew, weight behavior and the routing story decide as much — report all three or the comparison lies.

منابع:Sources: Microsoft · technical interviewing · Atlassian · engineering interview handbook

33 تمرین ۳ — local در برابر global: اندازه‌گیری، نه تئوریExercise 3 — local vs global: measure, don't theorize

L3 · Staff / PrincipalL3 · Staff / Principalوابسته به نقشRole-specific45 دقیقه بحث45 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
برای دادهٔ چهار شارد، ایندکس ثانویهٔ محلی و سراسری طراحی کن. مسیر نوشتن، تک‌شرط/دوشرط، scatter-gather، خرابی نصفه و lag را مقایسه کن. مدیریت تغییر چندtermی با outbox/نسخه را مشخص کن و تعداد RPC و p99 و هزینهٔ سازگاری را بگو.
Design local and global secondary indexes over four shards. Compare writes, one/two predicates, scatter-gather, partial failures and lag. Specify outbox/version handling, RPC counts and consistency cost.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

محلی یعنی نوشتن تک‌شاردی و خواندن پراکندهٔ ۴ شاردی؛ سراسری یعنی نوشتن چندشاردی نسخه‌دار از مسیر outbox و خواندن هدفمند (۱ شارد ایندکس بعد گرفتن رکورد). خواننده هر شناسه را با شارد مرجعش بازبینی می‌کند — حذف ثبت‌شده همیشه بر posting کهنه می‌چربد. بها: RPC و شارد per-operation را بشمار و هرگز زمان کرونومتر محلی را p99 شبکه جا نزن.
Local means 1-shard writes and 4-shard scatter reads; global means multi-shard versioned writes through an outbox and targeted reads (1 index shard, then fetch). The reader revalidates every id against its base shard — a recorded delete always beats a stale posting. Price: count RPCs and shards per operation, and never quote local stopwatch timings as networked p99.

صورت مسئله و فرض‌هاProblem and assumptions

  • ۴ شارد مرجع که رکوردها را با شناسه دارند و ۲ شارد ایندکس سراسری که postingها را با هش term دارند؛ هر رکورد ۲ term دارد.
  • RPC یعنی یک درخواست به یک شارد؛ واحد سنجش شاردهای درگیر per-operation است — هرگز میلی‌ثانیه نه، که دربارهٔ p99 شبکه هیچ پیش‌بینی نمی‌کند.
  • نوشتن‌های ایندکس از رلهٔ outbox نسخه‌دار می‌گذرند؛ خواننده‌ها همیشه برای بازبینی به شارد مرجع می‌رسند.
  • 4 base shards owning records by id, 2 global index shards owning postings by hashed term; each record carries 2 terms.
  • An RPC is one shard request; the measured unit is shards engaged per operation — never milliseconds, which predict nothing about networked p99.
  • Index writes travel through a versioned outbox relay; readers can always reach the base shard for revalidation.

چرا این روش و این عددها؟Why this method and these numbers?

برای query با term، local index باید هر چهار base shard را بپرسد چون محل سند از term معلوم نیست؛ global index term را مستقیم به shard index می‌برد ولی write را به outbox و index هم می‌رساند. شمار RPC را می‌شماریم چون هزینهٔ ساختاری قابل مقایسه است؛ زمان چهار SQLite روی یک لپ‌تاپ شبیه شبکهٔ واقعی نیست. validation از مرجع لازم است چون index ناهمگام ممکن است حذف یا ACL تازه را هنوز ندیده باشد.

A local term index queries all four base shards because the term does not identify record placement. A global term index routes directly but adds outbox/index maintenance. Count RPCs as structural cost: four local SQLite instances do not model network latency. Authoritative validation matters because asynchronous indexes may not yet reflect deletions or ACL changes.

مدل ذهنیMental model

ایندکس کپی مشتقی است با sharding خودش — و sharding آن به سؤالی متفاوت از سؤال مرجع جواب می‌دهد. هر خواندن ثانویه دو پرش است (شناسه‌ها را پیدا کن و رکوردها را بگیر) و دو پرش ممکن است اختلاف داشته باشند؛ کل طراحی یعنی تصمیم اینکه در اختلاف چه کسی می‌برد.
The index is a derived copy with a sharding of its own — and its sharding answers a different question than the base's. Every secondary read is two hops (find the ids, fetch the records) and the two hops may disagree; the whole design is deciding who wins the disagreement.

حل گام‌به‌گامStep by step

  1. هر دو مسیر نوشتن نوشتن محلی دقیقاً یک شارد مرجع را لمس می‌کند با ورودی‌اش در همان commit؛ نوشتن سراسری روی مرجع commit می‌شود به‌علاوهٔ یک رویداد نسخه‌دار outbox per-term که روی هر شارد مالک ایندکس اعمال می‌شود. شاردهای مرجع و ایندکس را جدا بشمار — سیگنال ممیزی همین را می‌خواهد.
  2. یک شرط بعد دو شرط بخوان کوئری‌های term محلی به هر ۴ شارد پراکنده و ادغام می‌شوند (tail latency دنبال کندترین)؛ تک‌شرط سراسری به ۱ شارد ایندکس می‌زند بعد رکوردها را با شناسه می‌گیرد. AND دوشرط postingها را روی شبکه اشتراک می‌گیرد — دقیقاً وقتی کند است که لیست‌ها بلندند.
  3. یک نوشتن را بشکن نوشتن گمشدهٔ ایندکس برند، کوئری AND را وادار می‌کند رکورد زنده را بی‌صدا بیندازد (منفی کاذب)؛ posting مانده رکورد حذف‌شده را سرو می‌کند (مثبت کاذب). هر دو شکل‌های خرابی نصفهٔ نوشتن‌های چندشاردی غیراتمی‌اند و دمو هرکدام را می‌شمارد.
  4. قرارداد و سیم‌خاردار خواننده هر شناسه را با شارد مرجعش بازبینی می‌کند و حذف ثبت‌شده همیشه می‌چربد؛ سیم‌خاردارها روی fan-out برابر تعداد شارد، lag per-term رله و هر پاک‌سازی ایندکس بدون tombstone قبلی آتش می‌کنند.
  1. Write both paths A local write touches exactly one base shard with its entry in the same commit; a global write commits to the base plus one versioned outbox event per term, applied to each owning index shard. Count base and index shards separately — the audit signal demands it.
  2. Read one term, then two Local term queries scatter to all 4 shards and merge (tail latency follows the slowest); global single-term reads hit 1 index shard, then fetch records by id. Two-term AND intersects postings over the network — slow exactly when the lists are long.
  3. Break one write A lost brand-index write makes the AND query silently drop a live record (false negative); a lingering posting serves a deleted record (false positive). Both are partial-failure shapes of non-atomic multi-shard writes, and the demo counts each.
  4. Contract and tripwires The reader revalidates every id against its base shard and the recorded delete always wins; tripwires fire on fan-out equal to shard count, per-term relay lag, and any index purge without a tombstone first.

طراحی مشخصConcrete design

شاردهای مرجع رکوردها را با id % 4 دارند و شاردهای ایندکس termها را با hash(term) % 2. نوشتن روی شارد مرجعش commit می‌شود به‌علاوهٔ یک رویداد نسخه‌دار outbox per-term؛ رله هر رویداد را با نسخهٔ per-record روی شارد مالک ایندکس اعمال می‌کند. خواندن: محلی به ۴ شارد پراکنده و ادغام می‌کند؛ سراسری هر term را روی ۱ شارد ایندکس می‌خواند و اشتراک می‌گیرد بعد با شناسه رکوردها را می‌گیرد. هر شناسهٔ گرفته‌شده پیش از رسیدن به صداکننده با شارد مرجعش بازبینی می‌شود.

Base shards own records by id % 4; index shards own terms by hash(term) % 2. A write commits to its base shard plus one versioned outbox event per term; the relay applies each event to the owning index shard with per-record versions. Reads: local scatters to 4 and merges; global looks up each term on 1 index shard, intersects, then fetches by id. Every fetched id is revalidated against its base shard before it reaches the caller.

مثال محاسبه‌شدهWorked example

خروجی واقعی دمو (IndexSim): ۱۲ رکورد روی ۴ شارد مرجع و ۲ ایندکس. تک‌شرط قرمز: محلی پراکندهٔ ۴ شاردی و سراسری ۱ ایندکس + ۳ مرجع. دوشرط قرمز و a: محلی ۴ پراکنده و سراسری ۲ ایندکس + گرفتن رکورد. نوشتن ناقص: کوئری AND شناسهٔ ۱۲ را گم می‌کند. ورودی کهنه: شناسهٔ حذف‌شدهٔ ۳ در بازبینی فیلتر شد و شبح‌های گرفته‌شده ۱ است.

Actual demo output (IndexSim): 12 records over 4 base and 2 index shards. Single-term color=red: local 4-shard scatter, global 1 index + 3 base shards. Two-term red AND a: local 4 scatter, global 2 index + fetch. Partial write: the AND query misses id 12. Stale entry: deleted id 3 is filtered at revalidation, phantoms = 1 caught.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · مسیر نوشتن و خواندن: پراکندهٔ محلی در برابر lookup سراسریWrite and read paths: local scatter versus global term lookup · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
دو خط را بخوان: خط بالا یک بار می‌نویسد و سر هر خواندن پول می‌دهد؛ خط پایین سر هر نوشتن پول می‌دهد و دقیق می‌خواند.
Read the two lines: the upper line writes once and pays on every read; the lower line pays on every write and reads precisely.
  1. خط بالا: رکورد با ورودی محلی‌اش در یک تراکنش تک‌شاردی commit می‌شود؛ کوئری term بعد به هر ۴ شارد می‌رود و ادغام می‌کند.
  2. خط پایین: نوشتن به مرجع به‌علاوهٔ رلهٔ outbox می‌رود که هر term را نسخه‌دار روی شارد مالکش می‌نشاند.
  3. خواندن‌هایش per-term به ۱ شارد ایندکس می‌زنند بعد با شناسه رکورد می‌گیرند — و هرچه مرجع دیگر ندارد فیلتر می‌کنند.
  1. Upper line: the record commits with its local entry in one shard transaction; the term query then fans out to all 4 shards and merges.
  2. Lower line: the write fans out to the base plus the outbox relay, which versions each term onto its owning index shard.
  3. Its reads touch 1 index shard per term, then fetch by id — and filter whatever the base no longer holds.

نقطهٔ تصمیم: نقطهٔ تصمیم مرحلهٔ ایندکس است: مالک posting — همان شارد رکورد یا شارد دوری بر اساس term — بهای همهٔ پایین‌دست را تعیین می‌کند.

رد خرابی: ایندکس سراسری را حقیقت بدان و posting کهنه کالای ناموجود را روی صفحه می‌فروشد: ایندکس مشتق است و مرجع تصمیم می‌گیرد — گره ادغام دقیقاً برای همین است.

تضمین نشان‌داده‌شده: شکل هر دو مسیر نوشتن و خواندن و محل ورود کهنگی را نشان می‌دهد؛ کران lag تعیین نمی‌کند — مانیتور سیم‌خاردار در نمودار بعد مالک آن است.

تضمین نشان‌نداده‌شده: دربارهٔ خرابی شارد ایندکس و قطعی رله چیزی نمی‌گوید — آن‌ها سناریوهای خرابی پایین‌اند نه پیکان‌های اینجا.

Decision point: The decision point is the Index stage: who owns the posting — the same shard as the record, or a far shard by term — prices everything downstream.

Failure trace: Treat the global index as the truth and a stale posting becomes a sold-out product on the page: the index is derived, the base decides — the merge node exists for exactly this.

Guarantee shown: The figure shows both write and read paths and where staleness enters. It sets no lag bound — the tripwire monitor in the next diagram owns that.

Not shown: It says nothing about index-shard failure or relay outages — those are failure scenarios below, not arrows here.

خودآزماییSelf-check

کوئری فقط-term در مسیر محلی چند RPC خرج می‌کند؟

پاسخ: محلی: ۴ RPC به همهٔ شاردها به‌علاوهٔ ادغام، با tail latency تعیین‌شده توسط کندترین شارد.

How many RPCs does a term-only query cost on the local path?

Answer: Local: 4 RPCs to every shard plus a merge, with tail latency set by the slowest shard.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · چهار شارد مرجع، دو شارد ایندکس، یک رلهٔ outbox و یک مانیتور سیم‌خاردارFour base shards, two index shards, one outbox relay, one tripwire monitor · معماریarchitectureباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
از کلاینت شروع کن، هر پیکان توپر را برای خواندن و نوشتن برو و بعد پیکان‌های چین‌چین رله را برو که ایندکس را عقب نگه می‌دارد — ولی نسخه‌دار.
Start at the client, walk each solid arrow for reads and writes, then walk the dashed arrows for the relay that keeps the index behind — but versioned.
  1. کلاینت به مسیریاب به شارد مرجع مالک: مسیر درخواست کلیددار — یک شارد درگیر، ارزان‌ترین پیکان صفحه.
  2. مسیریاب به شاردهای ایندکس برای lookup termها و بعد برگشت به شاردهای مرجع برای گرفتن با شناسه: خواندن دوپرهٔ سراسری.
  3. مرجع به رلهٔ outbox به شاردهای ایندکس، چین‌چین: نوشتن‌های نسخه‌دار ایندکس که ناهمزمان جاری‌اند — به‌علاوهٔ پیکان متریک‌های مسیریاب به مانیتور سیم‌خاردار.
  1. Client to router to owning base shard: the keyed request path — one shard touched, the cheapest arrow on the page.
  2. Router to index shards for term lookups, then back to base shards to fetch by id: the two-hop global read.
  3. Base to outbox relay to index shards, dashed: versioned index writes flowing asynchronously — plus the router's metrics arrow to the tripwire monitor.

نقطهٔ تصمیم: نقطهٔ تصمیم مسیریاب است: دو نگاشت دارد (شناسه به مرجع و term به ایندکس) و هر مسیر — ارزان یا پراکنده — با اینکه کدام نگاشت را می‌پرسد شروع می‌شود.

رد خرابی: پیکان‌های چین‌چین رله را دنبال کن: اگر رلهٔ outbox بایستد، شاردهای ایندکس در گذشته منجمد می‌شوند درحالی‌که نوشتن‌های مرجع جلو می‌روند — متریک lag مانیتور تنها سیگنال صادق است و کارت‌ها همین را می‌گویند.

تضمین نشان‌داده‌شده: شکل همهٔ اجزا و گفت‌وگویشان در نوشتن و خواندن و رله را نشان می‌دهد؛ ظرفیت رله و آستانهٔ هشدار را تعیین نمی‌کند — ران‌بوک‌ها می‌کنند.

تضمین نشان‌نداده‌شده: فرض می‌کند نگاشت‌های مسیریاب تازه‌اند؛ رفتار نگاشت کهنه مال بخش مسیریابی فصل ۷ است نه این توپولوژی.

Decision point: The decision point is the router: it holds two maps (id to base, term to index) and every path — cheap or fanned-out — starts with which map it consults.

Failure trace: Trace the dashed relay arrows: if the outbox relay stalls, the index shards freeze in the past while base writes march on — the monitor's lag metric is the only honest signal, and the cards say so.

Guarantee shown: The figure shows every component and who talks to whom on writes, reads and relay. It does not size the relay or set alert thresholds — runbooks do.

Not shown: It assumes the router's maps are fresh; stale-map behavior belongs to Chapter 7's routing section, not this topology.

خودآزماییSelf-check

چرا رله نسخه حمل می‌کند نه posting لخت؟

پاسخ: رله رویدادهای per-term را با نسخهٔ per-record اعمال می‌کند، پس crash آن‌ها را به‌ترتیب بازپخش می‌کند به‌جای گم کردن نیمی از نوشتن چندtermی.

Why does the relay carry versions, not bare postings?

Answer: The relay applies per-term events with per-record versions, so a crash replays them in order instead of losing half a multi-term write.

نمودار اسمبل نهاییFinal assembly diagram · تعداد RPC و شارد per-operation به‌علاوهٔ دو حالت کهنگیRPC and shard counts per operation, plus the two staleness cases · جدول اندازه‌گیریmeasurement tableباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
جدول را سطر‌به‌سطر بخوان: هر سطر یک عملیات است و دو ستون دو خانوادهٔ ایندکس‌اند با صورت‌حساب شاردشان.
Read the table row by row: each row is one operation, the two columns are the two index families and their shard bills.
  1. سطر نوشتن: محلی ۱ شارد را لمس می‌کند؛ سراسری ۱ مرجع به‌علاوهٔ ۲ ایندکس — شاردهای مرجع و ایندکس را جدا بشمار.
  2. سطرهای خواندن: محلی همیشه به ۴ شارد پراکنده می‌خواند؛ سراسری per-term از ۱ شارد ایندکس می‌خواند بعد رکوردها را با شناسه می‌گیرد.
  3. سطرهای کهنگی: نوشتن ناقص رکورد زنده را از AND می‌اندازد (صورتی) و ورودی کهنه فیلتر می‌شود چون حذف می‌چربد.
  1. Write row: local touches 1 shard; global touches 1 base plus 2 index shards — count base and index shards separately.
  2. Read rows: local always scatters to 4; global reads 1 index shard per term, then fetches records by id.
  3. Staleness rows: the partial write drops a live row from AND (pink), the stale entry is filtered because the delete wins.

نقطهٔ تصمیم: سطر صورتی درس استف است: نوشتن‌های چندشاردی غیراتمی نصفه شکست می‌خورند و کوئری — نه نوشتن — جایی است که کاربر با شکست روبه‌رو می‌شود.

رد خرابی: سطر صورتی را بخوان: یک نوشتن گمشدهٔ ایندکس برند، کوئری AND را وادار می‌کند رکورد زنده را بی‌صدا بیندازد. بدون تمرین بخش آزمون‌ها، این سطر در staging سبز و در پروداکشن قرمز می‌ماند.

تضمین نشان‌داده‌شده: شکل شمار RPC و شارد اجرا را گزارش می‌کند به‌علاوهٔ هر دو نتیجهٔ کهنگی. هیچ زمانی گزارش نمی‌کند — عمداً.

تضمین نشان‌نداده‌شده: هیچ p99ای وعده نمی‌دهد: پانوشت تبدیل این شمارش‌ها به ادعای تاخیر را ممنوع می‌کند.

Decision point: The pink row is the staff lesson: non-atomic multi-shard writes fail partially, and the query — not the write — is where the user meets the failure.

Failure trace: Read the pink row: one lost brand-index write makes the AND query silently drop a live record. Without the drill in the tests section, this row stays green in staging and red in production.

Guarantee shown: The figure reports RPC and shard counts from the run, plus both staleness outcomes. It reports no timings — deliberately.

Not shown: It does not promise any p99: the footer forbids turning these counts into latency claims.

خودآزماییSelf-check

سطر صورتی دربارهٔ نوشتن‌های چندtermی چه چیزی را ثابت می‌کند؟

پاسخ: یک نوشتن گمشدهٔ term: کوئری AND رکورد را گم می‌کند درحالی‌که کوئری تک‌شرط هنوز پیدایش می‌کند — شکست نصفه نه کامل.

What does the pink row prove about multi-term writes?

Answer: One lost term write: the AND query misses the record while the single-term query still finds it — partial failure, not total.

گزینه‌های جایگزینAlternatives

  • وقتی کوئری‌ها کلید مرجع را دارند یا فقط چند نتیجه می‌خواهند محلی بمان: هر تک‌شاردی جواب می‌دهد و هیچ نوشتن چندشاردی هرگز نیست. بها: روزی که کسی «همهٔ تطابق‌ها» را بخواهد پراکنده می‌خوانی — آن روز را از پیش نام ببر.
  • global ناهمزمان (به سبک DynamoDB) وقتی کهنگی کران‌دار قابل‌تحمل است: خواندن هدفمند می‌ماند و نوشتن زود برمی‌گردد. بها: SLO صریح lag به‌علاوهٔ قرارداد بازبینی خواننده در پایین — کهنگی بدون قرارداد گزارش باگِ در راه است.
  • Stay local when queries carry the base key or want just a few results: any single shard answers, no cross-shard write ever exists. Price: the day someone asks for 'all matches', you scatter — name that day in advance.
  • Accept async global (DynamoDB-style) when bounded staleness is tolerable: reads stay targeted and writes return fast. Price: an explicit lag SLO plus the reader-revalidation contract below — staleness without a contract is a bug report waiting.

سناریوهای خرابیFailure scenarios

  • رلهٔ outbox می‌ایستد: نوشتن‌های مرجع جلو می‌روند درحالی‌که ایندکس در گذشته منجمد می‌شود. نگهبان: متریک lag per-term با آستانهٔ paging به‌علاوهٔ حالت خواندن کهنهٔ مستند — هرگز کهنگی بی‌صدا نه.
  • یک شارد ایندکس وسط کوئری می‌افتد: termهایش از نتایج AND ناپدید می‌شوند. نگهبان: حالت تنزل‌یافته را صریح اعلام کن (نتایج ناقص پرچم‌دار یا fail-closed برای کوئری‌های سخت‌گیر) — بدترین جواب، جواب ناقصِ مطمئن است.
  • حذف مرجع با خواندن دوشرط مسابقه می‌دهد: خواننده شناسه‌ای را می‌گیرد که مرجع تازه انداخته. نگهبان: هر شناسه را با مرجع بازبینی کن و بگذار حذف بچربد — به‌علاوهٔ نسخهٔ per-record تا رویداد قدیمی‌تر ایندکس هرگز زنده‌اش نکند.
  • The outbox relay stalls: base writes march on while the index freezes in the past. Guard: a per-term lag metric with a paging threshold, plus a documented stale-read mode — never silent staleness.
  • One index shard goes down mid-query: its terms vanish from AND results. Guard: declare the degraded mode explicitly (partial results flagged, or fail closed for strict queries) — the worst answer is a confident incomplete one.
  • A base delete races a two-term read: the reader fetches an id the base just dropped. Guard: revalidate every id against the base and let the delete win — plus per-record versions so an older index event can never resurrect it.

کد قابل اجراRunnable code

اجراشده و تأییدشده در همین محیطExecuted and verified in this environment

این کد آموزشی و ساده‌سازی‌شده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچ‌کس این‌شکلی کد پروداکشن نمی‌زند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.

import java.util.*;
// Teaching demo: local vs global secondary index over 4 base shards.
// Measures RPC counts and shards engaged (NOT latency: local timing does
// not predict networked p99) + a stale-index read the reader must filter.
public class IndexSim {
    static int fnv(String s) {
        int h = 0x811c9dc5;
        for (int i = 0; i < s.length(); i++) { h ^= s.charAt(i); h *= 0x01000193; }
        return h & 0x7fffffff;
    }
    record Prod(int id, String color, String brand) {}
    public static void main(String[] a) {
        int SH = 4, ISH = 2; // 4 base shards, 2 global index shards (by term)
        List<Prod> ps = List.of(
            new Prod(0, "red", "a"), new Prod(1, "blue", "a"), new Prod(2, "red", "b"),
            new Prod(3, "green", "a"), new Prod(4, "red", "a"), new Prod(5, "blue", "b"),
            new Prod(6, "green", "b"), new Prod(7, "red", "b"), new Prod(8, "blue", "a"),
            new Prod(9, "green", "a"), new Prod(10, "red", "a"), new Prod(11, "blue", "b"));
        // base placement: id % 4
        Map<Integer, Prod> base = new HashMap<>();
        for (Prod p : ps) base.put(p.id(), p);
        // global index: term -> ids, sharded by term
        Map<String, Set<Integer>> gidx = new HashMap<>();
        for (Prod p : ps) for (String t : List.of("color:" + p.color(), "brand:" + p.brand()))
            gidx.computeIfAbsent(t, k -> new HashSet<>()).add(p.id());

        // single-term query color=red (5 matches on shards {0,1,2,3})
        Set<Integer> reds = gidx.get("color:red");
        Set<Integer> redShards = new HashSet<>();
        for (int id : reds) redShards.add(id % SH);
        // two-term query color=red AND brand=a (3 matches)
        Set<Integer> redA = new HashSet<>(reds);
        redA.retainAll(gidx.get("brand:a"));
        Set<Integer> idxShards2 = new HashSet<>(Arrays.asList(fnv("color:red") % ISH, fnv("brand:a") % ISH));

        // partial failure: product 12 (red,a) written to base + color index, brand index write lost
        base.put(12, new Prod(12, "red", "a"));
        gidx.get("color:red").add(12); // brand:a entry missing -> false negative on AND
        Set<Integer> redA2 = new HashSet<>(gidx.get("color:red"));
        redA2.retainAll(gidx.get("brand:a"));
        // stale entry: product 3 deleted from base, index entry left behind
        base.remove(3);
        Set<Integer> stale = new HashSet<>(gidx.get("color:green"));
        int phantoms = 0;
        for (int id : stale) if (!base.containsKey(id)) phantoms++; // reader revalidates: delete wins

        System.out.println("op                              | local RPCs/shards | global RPCs/shards");
        System.out.println("write 1 record (2 terms)        | 1 / 1 base        | 1 base + 2 index / up to 3");
        System.out.printf("read  color=red (%d matches)      | 4 / 4 scatter     | 1 index + %d base shards%n",
            reds.size(), redShards.size());
        System.out.printf("read  red AND a (%d matches)      | 4 / 4 scatter     | %d index + fetch / intersect%n",
            redA.size(), idxShards2.size());
        System.out.println("partial brand-index write: AND query misses id 12 (false negative, count=" + (redA2.contains(12) ? 0 : 1) + ")");
        System.out.println("stale index entry: query returns deleted id 3; reader revalidates vs base, phantoms filtered=" + phantoms);
        System.out.println("tripwires: fan-out==shard-count alert; index-lag metric; delete-tombstone before index purge");
        System.out.println("OK");
    }
}

اجرا: javac IndexSim.java && java IndexSim — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: نوشتن ۱ رکورد: محلی ۱ شارد و سراسری ۱ مرجع + ۲ ایندکس. خواندن تک‌شرط: محلی پراکندهٔ ۴ شاردی و سراسری ۱ ایندکس + ۳ مرجع. دوشرط: محلی ۴ پراکنده و سراسری ۲ ایندکس + گرفتن رکورد. نوشتن ناقص: AND شناسهٔ ۱۲ را گم می‌کند. ورودی کهنه: شناسهٔ حذف‌شدهٔ ۳ فیلتر شد؛ OK. (خروجی واقعی همین محیط)

Run: javac IndexSim.java && java IndexSim — stdlib Java only, instant.

Expected output: Write 1 record: local 1 shard, global 1 base + 2 index. Single-term read: local 4-shard scatter, global 1 index + 3 base. Two-term: local 4 scatter, global 2 index + fetch. Partial write: AND misses id 12. Stale entry: deleted id 3 filtered, phantoms=1; OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

بررسی شاردهای درگیر per-operation (خواندن محلی همیشه ۴ و تک‌شرط سراسری ۱ ایندکس)؛ تمرین نوشتن کشته‌شدهٔ ایندکس و الزام شناسایی گم شدن AND با حسابرسی؛ تمرین شناسهٔ حذف‌شدهٔ ولی ایندکس‌شده و الزام صفر شبح روی سطح؛ رصد lag per-term رله با آستانهٔ paging؛ ممنوعیت ادعای تاخیر از شمارش‌های محلی.

Assert shards engaged per operation (local reads always 4, global single-term 1 index); drill a killed index write and require the AND miss to be detected by audit; drill a deleted-but-indexed id and require zero phantoms surfacing; track per-term relay lag with a paging threshold; forbid latency claims from local counts.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): index کالا را نشان می‌دهد ولی حذف آن در مرجع ثبت شده؛ خواننده چه می‌کند؟Follow-up (Senior): The index returns a product already deleted from the record. What does the reader do?

اول بازبینی بعد سرو: هر شناسهٔ آمده از ایندکس پیش از رسیدن به صداکننده با شارد مرجعش چک می‌شود و حذف ثبت‌شده می‌چربد — شبح فیلتر و شمرده می‌شود و هرگز نشان داده نمی‌شود. بعد بپرس چرا posting از رکورد بیشتر عمر کرد: tombstone گمشده یا عقب‌ماندگی رله، و همان علت را درست کن — قرارداد خواننده چیزی را پنهان نمی‌کند، فقط محافظت می‌کند.

Revalidate, then serve: every id from the index is checked against its base shard before reaching the caller, and the recorded delete wins — the phantom is filtered, counted, and never shown. Then ask why the posting outlived the record: a missing tombstone or a relay backlog, and fix that cause — the reader contract hides nothing, it only protects.

پیگیری (استف): یک AND دوشرط روی termهای داغ سر هر کوئری اجرا می‌شود. هزینهٔ چندشاردی کجا می‌رود؟Follow-up (Staff): A two-term AND over hot terms runs on every query. Where does the cross-shard cost go?

با RPC و اشتراک شبکه قیمت بگذار نه با صفت: هر term اضافه یک lookup شارد ایندکس اضافه می‌کند و لیست‌های بلند posting اشتراک را غالب می‌کنند. وقتی دو term داغ سر هر کوئری اشتراک می‌خورند، اشتراک را در زمان خواندن متوقف کن — عطف را به‌عنوان posting خودش پیش‌محاسبه کن (AND مادی‌شده) و به‌جایش سر نوشتن پول بده. تصمیم استف جابه‌جایی عمدی هزینه روی خط خواندن/نوشتن است با جدول RPC به‌عنوان مدرک.

Price it in RPCs and network intersections, not adjectives: each extra term adds one index-shard lookup, and long postings lists make the intersection dominate. When two hot terms intersect on every query, stop intersecting at read time — precompute the conjunction as its own posting (a materialized AND) and pay on write instead. The staff call is moving cost across the read/write line deliberately, with the RPC table as evidence.

پیگیری (استف): محلی چه چیزی را می‌سنجی و دربارهٔ <span class="ltr">p99</span> چه وعده‌ای می‌دهی؟Follow-up (Staff): What do you measure locally, and what do you promise about p99?

در staging بسنج و از پروداکشن وعده بده: شاردهای درگیر و lag رله را از لب گزارش کن، p50/p99 را برای خوشهٔ واقعی با هاپ‌های واقعی شبکه نگه دار و خط صداقت را در ریویو بنویس — «زمان‌های محلی هیچ پیش‌بینی نمی‌کنند». عادت استف جدا کردن نوشته‌شدهٔ آنچه شمردی (RPCها) از آنچه ادعا می‌کنی (تاخیر) است.

Measure in staging, promise from production: report shards-engaged and relay lag from the lab, keep p50/p99 for the real cluster with real network hops, and write the honesty line into the review — 'local timings predict nothing'. The staff habit is separating what you counted (RPCs) from what you claim (latency), in writing.

اشتباهات رایجCommon mistakes

  • ایندکس سراسری خواندن را رایگان می‌کند. lookup لیست posting یک شارد است ولی گرفتن رکوردها هنوز چند شارد را درگیر می‌کند و اشتراک چند term هنوز از شبکه می‌گذرد.
  • شارد بیشتر کوئری‌های ایندکس محلی را تند می‌کند. فضا را رشد می‌دهند نه throughput کوئری را — هر کوئری هنوز به همهٔ شاردها سر می‌زند و کندترین دم را تعیین می‌کند.
  • زمان‌های چهار SQLite محلی p99 خوشه را پیش‌بینی می‌کند. دربارهٔ p99 شبکه هیچ پیش‌بینی نمی‌کنند — محلی شمارش را گزارش کن و تاخیر را روی خوشهٔ واقعی بسنج.
  • ایندکس مرجع حقیقت است یا ایندکس دستی بدون تراکنش خوب است. مرجع حقیقت است و نوشتن‌های نصفه هر ایندکس بی‌نگهبان را جدا می‌کنند — تراکنش یا رلهٔ outbox بگذار، هرگز امید نه.
  • A global index makes reads free. The postings lookup is one shard, but fetching the records still spans shards and multi-term intersection still crosses the network.
  • More shards speed up local-index queries. They grow storage, not query throughput — every query still visits every shard and the slowest sets the tail.
  • Four local SQLite timings predict cluster p99. They predict nothing about networked p99 — report counts locally, measure latency on the real cluster.
  • The index is the source of truth, or a hand-rolled index without transactions is fine. The base is the truth and partial writes detach any unguarded index — use transactions or an outbox relay, never hope.

منابع:Sources: Debezium · outbox event router · Atlassian · engineering interview handbook