۲2سازگاری و ارتقای تدریجیCompatibility and gradual upgrades
در ارتقای تدریجی، نسخههای قدیم و جدید همزمان کار میکنند. سازگاری عقبرو یعنی کد جدید دادهٔ قدیم را بخواند؛ سازگاری جلورو یعنی کد قدیم بتواند با دادهٔ جدید طبق قرارداد کنار بیاید.
مثال فنی: چهار ترکیب در rolling upgrade
وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.
اجرای مرحلهبهمرحله: نسخهٔ ۲ رکورد قدیمی را میخواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.
نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش میدهد.
Changes in program features usually mean changes in the data it stores: a fresh field, a fresh record type, or a fresh rendering of old data. But code never changes in one instant:
- در سمت سرور: rolling upgrade (یا staged rollout) — نسخهٔ جدید چند node به چند node deploy میشود، پایش میشود و جلو میرود. بدون downtime، با releaseهای کوچک و مکرر.
- در سمت کلاینت: کاربر ممکن است مدتها اپ را بهروز نکند؛ کنترل دست تو نیست.
- Server side: rolling upgrades (or staged rollouts) — the new version deploys node by node, gets monitored, and moves on. No downtime, with small frequent releases.
- Client side: users may not update the app for a long time; that timing is out of your hands.
پس نسخههای قدیم و جدیدِ کد و قالبهای قدیم و جدیدِ داده باید کنار هم زندگی کنند:
So old and new versions of the code and old and new versions of the data formats must live side by side:
| نوع سازگاری | تعریف | یادآوری |
| Backward compatibility | کدِ جدیدتر بتواند دادهای را که کدِ قدیمیتر نوشته بخواند | «از گذشته خواندن» |
| Forward compatibility | کدِ قدیمیتر بتواند دادهای را که کدِ جدیدتر نوشته بخواند | «از آینده خواندن» — سختتر، چون کدِ قدیم باید افزودههای ناشناخته را نادیده اما حفظ کند |
| Compatibility type | Definition | Mnemonic |
|---|
| Backward compatibility | Newer code can read data written by older code | “Reading from the past” |
| Forward compatibility | Older code can read data written by newer code | “Reading from the future” — harder, since old code must ignore unknown additions but preserve them |
در APIها دقیقاً چه چیزی سازگار میخواهد؟What exactly needs compatibility in APIs?
اگر کلاینتِ قدیمی باید سرویسِ جدید را صدا بزند: روی request سازگاریِ backward و روی
response سازگاریِ forward لازم است. اگر کلاینتِ جدید با سرویسِ قدیمی حرف بزند، برعکس.
backward معمولاً آسان است (فرمتِ قدیم را میشناسی)؛ forward سختتر است چون کدِ قدیم باید چیزهای ناشناخته را بیصدا کنار بگذارد.
If an old client must call a new service: requests need backward compatibility, responses need forward compatibility. A new client talking to an old service is the mirror image. Backward is usually easy (you know the old format); forward is harder since old code must silently set aside unknown things.
نمودار مفهومی — ماتریس سازگاری: چه کسی چه چیزی را میخواند؟Concept diagram — the compatibility matrix: who reads what?
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: چهار ترکیب در rolling upgrade
وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.
اجرای مرحلهبهمرحله: نسخهٔ ۲ رکورد قدیمی را میخواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.
نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش میدهد.
جدول دو در دو را بخوانید: سطرها «دادهٔ قدیمی» و «دادهٔ جدید»، ستونها «کد قدیمی میخواند» و «کد جدید میخواند». خانهٔ «دادهٔ قدیمی + کد جدید» همان سازگاری backward است (جدید، قدیم را میخواند) و خانهٔ «دادهٔ جدید + کد قدیمی» همان forward (قدیم، جدید را میخواند و نگه میدارد). دو خانهٔ همنسخه بیدردسرند. نوار پایین حکم ارتقای تدریجی است: چون نسخهها قاطیاند، هر دو سازگاری لازم است.
Read the 2×2 grid: rows “Old data” and “New data”, columns “Old code reads” and “New code reads”. “Old data + new code” is backward compatibility (the new reads the old); “New data + old code” is forward (the old reads and preserves the new). The same-version cells are painless. The bottom bar states the rolling-upgrade rule: with mixed versions live, you need BOTH.
۲.۱ نمودار ۱ + روایت مدرس2.1 Diagram 1 — rolling upgrades and unknown-field loss
خطرِ معروفِ شکل ۵-۱ کتاب: کدِ قدیمی رکوردی را که کدِ جدید با فیلدِ تازه نوشته میخواند، بهروز میکند و
دوباره مینویسد — و اگر decoder عمداً فیلدهای ناشناخته را نگه ندارد، آن فیلد برای همیشه میرود.
The famous danger of the book’s Figure 5-1: old code reads a record the new code wrote with a fresh field, updates it, writes it back — and if the decoder doesn’t deliberately keep unknown fields, that field is gone forever.
روایت مدرس از نمودار ۱Teacher narration
مثال فنی برای خواندنِ نمودار: چهار ترکیب در rolling upgrade
وضعیت اولیه: نیمی از سرورها نسخهٔ ۱ و نیمی نسخهٔ ۲ هستند.
اجرای مرحلهبهمرحله: نسخهٔ ۲ رکورد قدیمی را میخواند؛ سپس نسخهٔ ۱ ممکن است رکوردی را بخواند که نسخهٔ ۲ نوشته است.
نتیجه و دلیل: backward مسیر خوانندهٔ جدید با دادهٔ قدیمی و forward مسیر خوانندهٔ قدیمی با دادهٔ جدید را پوشش میدهد.
سه شرکتکننده داریم که از چپ به راست نشستهاند:
«Service v2.2»
(زیرنویس:
«new code, new field» — نسخهٔ جدیدِ کد)، وسط
«Record Store» (زیرنویس:
«shared database»)، و سمت راست
«Service v2.1» (زیرنویس:
«old code still live» — همان نسخهٔ قدیمی که هنوز زنده است).
زمان از بالا به پایین میگذرد و سه نوارِ عنوان در سمت چپ، سه پردهٔ داستان را جدا میکنند.
Three participants, left to right: Service v2.2 (new code, new field), Record Store (shared database), and Service v2.1 (old code still live). Time flows downward through three titled bands.
پردهٔ اول، «Rolling upgrade»: فلشِ پررنگِ «write record with new field»
از v2.2 به Record Store میرود. بعد فلشِ معمولیِ «read record» از v2.1 (هنگام ارتقای تدریجی،
node قدیمی هنوز سرویس میدهد) و پاسخِ برگشتیِ «record incl. unknown field» — یعنی رکوردِ حاویِ فیلدِ ناشناخته به دستِ کدِ قدیمی رسید.
Act one, Rolling upgrade: bold write record with new field from v2.2 to the store; then plain read record from v2.1 (during gradual rollout the old node still serves) with the reply record incl. unknown field — the unknown-field record in old code’s hands.
پردهٔ دوم، «Unknown-field hazard»: فلشِ پررنگِ «update known fields, write back»
از v2.1 به Store — و یادداشتِ زیرِ همان فلش میگوید «decoded into a plain model object»
(رکورد در object سادهای decode شده که فیلدِ ناشناخته را نگه نمیدارد). v2.2 دوباره میخواند
(«read record again») و پاسخِ برگشتی، تلخ است:
«new field gone — data lost» با یادداشتِ «unknown field was not preserved».
این همان گمشدنِ بیصدای شکل ۵-۱ است.
Act two, Unknown-field hazard: bold update known fields, write back from v2.1 to the store — annotated decoded into a plain model object (the record decoded into a plain object that keeps no unknown field). v2.2 reads again (read record again) and the reply is bitter: new field gone — data lost (unknown field was not preserved). That is Figure 5-1’s silent loss.
پردهٔ سوم، «Preserving unknown fields»: فلشِ چینچینِ «write record with field tag 4»
— یعنی رکورد با شمارهٔ تگِ فیلد کد میشود نه با نام. کدِ قدیمی رکورد را round-trip میکند
(«old code round-trips the record») و اینبار پاسخِ برگشتی خبر خوب میدهد:
«field tag 4 still intact».
Act three, Preserving unknown fields: the dashed write record with field tag 4 — the record encoded with a field tag number, not a name. Old code round-trips the record (old code round-trips the record) and this time the reply is good news: field tag 4 still intact.
درسِ شکل: مشکل از «قدیمی بودنِ کد» نیست؛ از قراردادِ decoder است. قالبهای مبتنی بر schema
(مثل Protocol Buffers و Avro) به کدِ قدیمی میگویند چیزهای ناشناخته را نادیده اما بایتهایشان را دستنخورده نگه دارد.
The lesson: the problem isn’t “old code”; it’s the decoder contract. Schema-driven formats (like Protocol Buffers and Avro) tell old code to ignore unknown things but keep their bytes untouched.
۳3قالبهای کدگذاریEncoding formats
قالبِ تبادل مشخص میکند داده چگونه نمایش داده شود و خواننده از کجا ساختارش را بفهمد. انتخاب قالب، بر حجم، خوانایی، ارتباط میان زبانها و امکان تغییر آینده اثر دارد.
مثال فنی: مقایسهٔ قالب روی همان رکورد
وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب مینویسیم.
اجرای مرحلهبهمرحله: JSON نام فیلدها را متنی حمل میکند؛ قالب مبتنی بر schema میتواند شناسه یا جایگاه فیلد را بهکار گیرد.
نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن بهتنهایی تضمین کمترین حجم یا بهترین تکامل نیست.
Now that we know which compatibilities are needed, what are the options? This part surveys three families: language-native formats, textual formats (JSON/XML/CSV), and schema-driven binary formats (Protocol Buffers and Avro).
۳.۱ نمودار ۲ + روایت مدرس3.1 Diagram 2 — the encoding-format decision tree
روایت مدرس از نمودار ۲Teacher narration
مثال فنی برای خواندنِ نمودار: مقایسهٔ قالب روی همان رکورد
وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب مینویسیم.
اجرای مرحلهبهمرحله: JSON نام فیلدها را متنی حمل میکند؛ قالب مبتنی بر schema میتواند شناسه یا جایگاه فیلد را بهکار گیرد.
نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن بهتنهایی تضمین کمترین حجم یا بهترین تکامل نیست.
این نمودار چهار نوار افقی دارد که از بالا به پایین عنوانشان هست:
«Decision» (پرسشها)،
«Language-Native» (که با پسزمینهٔ استثنا مشخص شده)،
«Textual Formats» و
«Binary Schema-Driven».
در بالا هم دو بخشِ فاز داریم:
«Scope» روی دو ستونِ اول و
«Choose the format» روی بقیهٔ ستونها.
The chart has four horizontal lanes, top to bottom: Decision (questions), Language-Native (marked as the exception), Textual Formats, Binary Schema-Driven — with two phase bands above: Scope over the first two columns, Choose the format over the rest.
مسیرِ اصلی از جعبهٔ «Record to Encode» (زیرنویس: «object to bytes») شروع میشود؛
فلشِ پررنگِ «encode once» آن را به اولین پرسش میرساند:
«Transient + One Language?» (زیرنویس: «in-process handoff only»، برچسبِ decide).
The main path starts at Record to Encode (object to bytes); the bold encode once reaches the first question: Transient + One Language? (in-process handoff only).
از هر پرسش دو فلش بیرون میآید. فلشِ «بله» از پرسشِ اول مستقیم پایین میآید به جعبهٔ
«Language-Native» (زیرنویس: «Serializable / pickle»، برچسبِ هشدارِ «compat risk»)؛
فلشِ «خیر» به راست، به پرسشِ دوم: «Human-Readable?».
از آن «بله» پایین میرود به «JSON / XML / CSV» (زیرنویس: «textual, schema optional»،
برچسبِ «JSON Schema for validation») و «خیر» به راست به پرسشِ سوم:
«Schema-Driven Binary?».
Each question has two exits. “Yes” from the first drops straight to Language-Native (Serializable / pickle; warning tag compat risk); “no” goes right to the second question Human-Readable?. Its “yes” drops to JSON / XML / CSV (textual, schema optional; JSON Schema for validation), its “no” right to the third question Schema-Driven Binary?.
از پرسشِ سوم، فلشِ «tag-based» پایین میآید و از کنارِ چپ به «Protocol Buffers» میرسد
(زیرنویس: «field tags in the data»، برچسبِ «Thrift similar») و فلشِ پررنگِ
«name-based» به راست میپیچد و به «Avro» میرسد
(زیرنویس: «writer and reader schemas»، برچسبِ «no tag numbers»).
این دو جعبه داخل قابِ «Defined evolution rules» جمع شدهاند — یعنی پاداشِ انتخابِ این مسیر.
From the third question, tag-based drops down-left to Protocol Buffers (field tags in the data; Thrift similar), and bold name-based turns right to Avro (writer and reader schemas; no tag numbers). Both sit inside Defined evolution rules — the reward for taking this path.
درسِ شکل: ترتیبِ پرسشها مهم است. اول طول عمر و مخاطبِ بایتها را مشخص کن (موقت و تکزبانه؟)، بعد
انسانخوان بودن، و در آخر قواعد تکامل. هر «بله» شما را به خانوادهای میرساند و جزئیاتش را در بخشهای بعد میبینیم.
The lesson: question order matters. First pin down the bytes’ lifespan and audience (transient and single-language?), then human-readability, finally evolution rules. Each “yes” lands you in one family, detailed in the sections ahead.
۳.۲ قالبهای ویژهٔ زبان3.2 Language-native formats
قالبهای ویژهٔ زبان، ذخیرهٔ اشیای همان زبان را آسان میکنند، اما تبادل میان زبانها و نسخهها را دشوارتر میکنند. برخی روشهای بازسازی شیء برای ورودی نامطمئن خطرناکاند.
مثال فنی: نام کلاس، قرارداد پایدار نیست
وضعیت اولیه: فایل ذخیرهشده به کلاس OldUser در نسخهٔ قبلی برنامه وابسته است.
اجرای مرحلهبهمرحله: نسخهٔ جدید کلاس را حذف کرده یا زبان دیگری دارد؛ decoder ویژهٔ زبان ممکن است نتواند شیء را بسازد.
نتیجه و دلیل: داده از عمر یک نسخهٔ کد طولانیتر است؛ برای تبادل، schema مستقل و ورودیِ معتبر لازم است.
- اکثر زبانها کتابخانهٔ کدگذاریِ داخلی دارند: java.io.Serializable در Java، pickle در Python، Marshal در Ruby و کتابخانههایی مثل Kryo. راحتاند: object را با کمترین کد ذخیره و بازیابی میکنند.
- اما چهار مشکلِ بنیادین دارند:
- قفلِ زبانی: کدگذاری به یک زبان گره خورده؛ خواندن داده با زبانِ دیگر دشوار است و یکپارچهسازی با سازمانهای دیگر را ناممکن میکند.
- امنیت: برای بازسازیِ objectها، decoder باید بتواند class دلخواه instantiate کند — اگر مهاجم بایتهای دلخواه بفرستد، معمولاً به اجرای کدِ دلخواه (RCE) میرسد.
- نسخهبندیِ فرعی: این کتابخانهها برای «سریع و آسان» ساخته شدهاند و معمولاً به سازگاریِ forward/backward بیتوجهاند.
- کاراییِ فرعی: serialization داخلیِ Java به کندی و حجمِ متورم معروف است.
- نتیجه: جز برای مقاصدِ بسیار موقت، از کدگذاریِ داخلیِ زبان استفاده نکن.
- Most languages ship a built-in encoding library: java.io.Serializable in Java, pickle in Python, Marshal in Ruby, plus libraries like Kryo. They’re comfortable: store and restore objects with minimal code.
- But they carry four fundamental problems:
- Language lock-in: encoding is tied to one language; reading the data in another language is hard, and integrating with other organizations becomes impossible.
- Security: to rebuild objects the decoder must instantiate arbitrary classes — if an attacker sends arbitrary bytes, arbitrary code execution (RCE) usually follows.
- Versioning as an afterthought: these libraries are built for “fast and easy” and mostly ignore forward/backward compatibility.
- Performance as an afterthought: Java’s built-in serialization is famously slow and bloated.
- Verdict: never use language-internal encoding except for very transient purposes.
۳.۳ JSON، XML، CSV و JSON Schema3.3 JSON, XML, CSV and JSON Schema
JSON و XML ساختار نامدار دارند؛ CSV بیشتر برای جدول ساده است و جزئیاتش باید توافق شود. JSON Schema میتواند قواعدی مثل نوع فیلد و اجباری بودن آن را مشخص کند.
مثال فنی: شناسهٔ بزرگ در JSON
وضعیت اولیه: شناسهٔ 9007199254740993 بهصورت JSON number به JavaScript میرسد.
اجرای مرحلهبهمرحله: تبدیل به Number ممکن است آن را به 9007199254740992 گرد کند؛ قراردادِ رشتهای برای شناسه، رقمها را حفظ میکند.
نتیجه و دلیل: JSON بهتنهایی دقتِ همهٔ خوانندهها را تضمین نمیکند؛ نوع فیلد باید در دو طرف قرارداد روشن باشد.
Standard multi-language formats: JSON and XML (plus CSV, which holds only tabular data and doesn’t nest). They’re textual and fairly human-readable, but their problems are real:
- XML معمولاً طولانی و بیش از حد پیچیده نقد میشود.
- ابهامِ اعداد: در XML و CSV اصلاً نمیتوان عدد را از رشتهٔ عددی تشخیص داد (مگر با schema بیرونی). JSON رشته و عدد را جدا میکند اما integer و floating-point را نه و دقت هم مشخص نمیکند. عدد بزرگتر از 2^53 در IEEE 754 دقیق نمیماند؛ مثلاً X شناسهٔ ۶۴بیتیِ هر post را هم بهصورت عدد JSON و هم رشتهٔ متنی برمیگرداند تا JavaScript خرابش نکند.
- رشتههای باینری ندارند: JSON/XML از Unicode (متنِ انسانخوان) پشتیبانی میکنند نه دنبالهٔ بایتِ خام. راهحلِ رایج Base64 است که کار را «تقریبی» میکند و حدود یکسوم حجم را زیاد میکند.
- XML Schema و JSON Schema قدرتمند و در نتیجه پیچیدهاند؛ برنامهای که از آنها استفاده نکند باید منطقِ کدگذاری/رمزگشایی را hardcode کند.
- CSV اصلاً schema ندارد؛ معنای هر سطر و ستون را باید برنامه تعیین کند و escape کردنِ ویرگول و خطجدید هم مبهم است.
- با این همه، برای تبادلِ بین سازمانها همینها «بهاندازهٔ کافی خوب»اند: تا وقتی دو طرف بر سرِ قالب توافق کنند، زیبایی و کارایی اهمیت کمتری دارد.
- XML is usually criticized as verbose and over-complex.
- Number ambiguity: in XML and CSV you can’t tell a number from a numeric string at all (without an external schema). JSON separates strings from numbers but not integers from floats, and pins no precision. Numbers above 2^53 don’t survive IEEE 754 exactly; e.g. X returns each post’s 64-bit id both as a JSON number and as a text string so JavaScript won’t corrupt it.
- No binary strings: JSON/XML support Unicode (human-readable text), not raw byte sequences. The common workaround is Base64, which roughly works and inflates size by about a third.
- XML Schema and JSON Schema are powerful and hence complex; programs that skip them must hardcode the encode/decode logic.
- CSV has no schema at all; the program must define each row and column’s meaning, and escaping commas and newlines is murky.
- Still, for cross-organization exchange these are “good enough”: as long as both sides agree on the format, beauty and efficiency matter less.
JSON Schema در عملJSON Schema in practice
JSON Schema امروز در OpenAPI (سرویسهای وب)، registryهایی مثل Confluent Schema Registry و
Apicurio، و پایگاه دادهها (افزونهٔ pg_jsonschema در PostgreSQL و
$jsonSchema در MongoDB) رایج است. علاوه بر نوعهای پایه (string، number، integer، object،
array، boolean، null) قواعد اعتبارسنجی هم دارد (مثلاً port بین ۱ تا ۶۵۵۳۵).
JSON Schema is common today in OpenAPI (web services), registries like Confluent Schema Registry and Apicurio, and databases (the pg_jsonschema extension in PostgreSQL, $jsonSchema in MongoDB). Beyond base types (string, number, integer, object, array, boolean, null) it has validation rules (e.g. a port between 1 and 65535).
نکتهٔ ظریف: content model باز در برابر بسته. باز (که additionalProperties: true پیشفرضش است)
هر فیلدِ تعریفنشدهای را میپذیرد؛ یعنی JSON Schema معمولاً میگوید چه چیزی مجاز نیست تا اینکه بگوید چه چیزی مجاز است.
مثال کتاب برای نقشهٔ «کلیدِ عددی → مقدارِ رشتهای»:
The subtle point: an open vs closed content model. Open (whose default is additionalProperties: true) accepts any undefined field; so JSON Schema usually says what is not allowed rather than what is. The book’s example for a “numeric-key → string-value” map:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"patternProperties": {
"^[0-9]+$": { "type": "string" }
},
"additionalProperties": false
}
JSON Schema به شرطهای if/else، نوعهای نامدار و ارجاع به schemaهای دوردست هم مجهز است —
قدرتمند، ولی تکاملدادنش بهصورت سازگار کارِ دشواری است. XML Schema هم دغدغههای مشابه دارد.
JSON Schema also handles if/else conditions, named types and remote-schema references — powerful, but evolving it compatibly is hard work. XML Schema shares similar concerns.
باینریهای «JSON» فقط نصفِ راهاند“JSON” binaries are only half the road
قالبهایی مثل MessagePack، CBOR و BSON همان مدلِ JSON را
باینری میکنند (گاهی با چند نوعِ اضافه مثل رشتهٔ باینری). اما چون schema ندارند، هنوز نامِ همهٔ فیلدها داخل بایتهاست.
مثالِ کتاب: رکوردِ سهفیلدیِ {"userName": "Martin", "favoriteNumber": 1337, "interests": [...]}
با MessagePack ۶۶ بایت میشود در برابر ۸۱ بایتِ JSON متنیِ بدون فاصله — صرفهجوییِ اندک در برابر ازدستدادنِ انسانخوانی.
در بخشهای بعد همین رکورد را با Protocol Buffers (۳۳ بایت) و Avro (۳۲ بایت) میبینیم.
Formats like MessagePack, CBOR and BSON binarize the same JSON model (sometimes with a few extra types like binary strings). But schema-less, every field name still sits inside the bytes. The book’s example: the three-field record {"userName": "Martin", "favoriteNumber": 1337, "interests": [...]} becomes 66 bytes in MessagePack vs 81 bytes of spaceless textual JSON — a small saving for the loss of human-readability. Ahead we’ll see the same record at 33 bytes (Protocol Buffers) and 32 (Avro).
۳.۴ Protocol Buffers3.4 Protocol Buffers
در Protocol Buffers، فیلدها شماره دارند و همین شماره بخشی از هویت آنها روی شبکه است. تغییر نام با استفادهٔ دوباره از شمارهٔ یک فیلد حذفشده یکسان نیست.
مثال فنی: Tag هویت فیلد است
وضعیت اولیه: schema پیام شامل string name = 1 است و بعد string email = 2 اضافه میشود.
اجرای مرحلهبهمرحله: شمارهٔ ۱ همچنان به name تعلق دارد؛ پس از حذف email، شمارهٔ ۲ برای معنای دیگری دوباره استفاده نمیشود.
نتیجه و دلیل: تغییر اسم ظاهری با تغییر هویت wire فرق دارد؛ reuse شماره میتواند دادهٔ قدیمی را با معنای اشتباه بخواند.
Protocol Buffers (protobuf) is Google’s binary encoding library; Apache Thrift (from Facebook) is very similar. Every datum needs a schema — in the IDL language:
syntax = "proto3";
message Person {
string user_name = 1;
int64 favorite_number = 2;
repeated string interests = 3;
}
- ابزارِ codegen از این schema در زبانهای مختلف class میسازد؛ کدِ برنامه به همان کدِ تولیدشده صدا میزند. زبانِ schema بسیار سادهتر از JSON Schema است: فقط فیلد و نوع، نه قواعدِ اعتبارسنجیِ پیچیده.
- کدگذاریِ همان رکوردِ مثال فقط ۳۳ بایت است. برخلاف MessagePack، نامِ فیلدها در داده نیست؛ بهجایش شمارهٔ تگ (tag) میآید: همان ۱ و ۲ و ۳ که در schema دیدی — یک ناممستعارِ فشرده برای فیلد.
- نوعِ فیلد و تگ در یک بایت جا میشوند؛ اعداد طولمتغیر (varint)اند: ۱۳۷۱ در دو بایت (بیتِ بالاییِ هر بایت یعنی «هنوز ادامه دارد»). اعدادِ −64..63 در یک بایت و −8192..8191 در دو بایت جا میشوند.
- لیستِ صریح ندارد؛
repeated میگوید فیلد چند مقداری است و در بایتها، تکرارِ همان تگ نمایشدهندهٔ آیتمهاست.
- The codegen tool builds classes in many languages from this schema; application code calls into that generated code. The schema language is far simpler than JSON Schema: just fields and types, no complex validation rules.
- Encoding the same example record takes only 33 bytes. Unlike MessagePack, field names aren’t in the data; instead come tag numbers: the 1, 2, 3 you saw in the schema — a compact alias for each field.
- A field’s type and tag fit in one byte; numbers are variable-length (varint): 1337 fits in two bytes (each byte’s top bit means “more follows”). Numbers −64..63 fit in one byte, −8192..8191 in two.
- There is no explicit list;
repeated marks a multi-valued field, and in the bytes, repeating the same tag represents the items.
قواعد تکامل schema در protobufSchema-evolution rules in protobuf
- رکوردِ کدشده فقط الحاقِ فیلدهای کدشده است؛ فیلدِ تنظیمنشده اصلاً نوشته نمیشود. پس تگها معنای داده را میسازند.
- تغییر نامِ فیلد آزاد است (نام در داده نیست)؛ تغییر تگ ممنوع — همهٔ دادههای موجود نامعتبر میشوند.
- افزودنِ فیلدِ جدید با تگِ تازه = forward compatible: کدِ قدیمی تگِ ناشناخته را نادیده میگیرد و (اگر درست پیاده شود) بایتهایش را حفظ میکند. backward هم برقرار است چون دادههای قدیمی برای فیلدِ تازه، مقدارِ پیشفرض میگیرند.
- حذفِ فیلد آینهٔ افزودن است: تگِ قدیمی هرگز نباید دوباره استفاده شود (دادههایی هنوز با آن تگ نوشته شدهاند)؛ میتوان تگهای مستعمل را در schema reserve کرد.
- تغییر نوعِ فیلد برای بعضی نوعها ممکن است ولی خطر truncation دارد: ۳۲بیتی ← ۶۴بیتی برای کدِ جدید ساده است، اما کدِ قدیمی مقدار ۶۴بیتی را در متغیر ۳۲بیتی میریزد و بریده میشود.
- An encoded record is just concatenated encoded fields; an unset field is never written. So tags make the data’s meaning.
- Renaming a field is free (names aren’t in the data); changing a tag is forbidden — all existing data becomes invalid.
- Adding a field with a fresh tag = forward compatible: old code skips unknown tags and (if implemented right) preserves their bytes. Backward holds too, since old data takes the default for the fresh field.
- Removing a field mirrors adding: the old tag must never be reused (data is still written with it); retired tags can be reserved in the schema.
- Changing a field’s type is possible for some types but risks truncation: 32-bit → 64-bit is easy for new code, but old code pours a 64-bit value into a 32-bit variable and clips it.
۳.۵ Avro + نمودار ۳3.5 Avro + Diagram 3
در Avro، ساختارِ نویسنده و خواننده برای تفسیر داده کنار هم قرار میگیرند. نام فیلدها و قواعد سازگاری و مقدار پیشفرض، در تغییر ساختار اهمیت دارند.
مثال فنی: Resolution در Avro
وضعیت اولیه: writer فقط name دارد؛ reader تازه فیلد country با default برابر IR اضافه کرده است.
اجرای مرحلهبهمرحله: هنگام خواندن رکورد قدیمی، reader مقدارِ غایب country را از default میگیرد.
نتیجه و دلیل: default برای حلِ نبود فیلد در دادهٔ نویسنده است؛ جای الزام نوشتن فیلد در schema نویسنده را نمیگیرد.
Apache Avro (since 2009, a Hadoop subproject) differs from protobuf in interesting ways. It has two schema languages: Avro IDL for humans and a JSON rendering for machines. Example:
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
- هیچ شمارهٔ تگی در کار نیست. کدگذاریِ همان رکورد فقط ۳۲ بایت است — فشردهترینِ همه. در بایتها نه نامِ فیلد هست و نه نوعِ داده؛ فقط مقادیرِ بههمچسبیده (رشته = طول + بایتهای UTF-8). رمزگشاییِ درست فقط با دقیقاً همان schemaای که نوشتن با آن انجام شده ممکن است.
- پس هنگام خواندن، برنامه با دو schema کار میکند: writer's schema (همانِ موقعِ نوشتن) و reader's schema (آنچه کدِ فعلی انتظار دارد). Avro با مقایسهٔ این دو، داده را از نمایِ writer به نمایِ reader ترجمه میکند.
- No tag numbers at all. Encoding the same record takes only 32 bytes — the most compact of all. The bytes hold neither field names nor data types; just concatenated values (a string = length + UTF-8 bytes). Correct decoding is possible only with exactly the schema used at write time.
- So at read time the program works with two schemas: the writer’s schema (the one at write time) and the reader’s schema (what current code expects). Avro translates the data from the writer’s view to the reader’s view by comparing the two.
قواعد resolution (فرمولِ شکل ۵-۶ کتاب)Resolution rules (the book’s Figure 5-6 formula)
خواننده باید دادهٔ نوشتهشده با اسکیمای قبلی را طبق قواعدِ سازگاری به اسکیمای خودش نگاشت کند؛ مقدار پیشفرض برای فیلدِ غایب کاربرد دارد.
مثال فنی: فیلد اضافی و فیلد غایب
وضعیت اولیه: writer فیلدهای name, age دارد؛ reader فقط name, country میخواهد.
اجرای مرحلهبهمرحله: age موردنیاز reader نیست و کنار گذاشته میشود؛ country باید default معتبر داشته باشد، وگرنه resolution شکست میخورد.
نتیجه و دلیل: «نسخهها فرق دارند» همیشه قابلحل نیست؛ قواعد تطبیق schema تعیین میکند خواندن مجاز است یا نه.
- فیلدها بر اساس نام جفت میشوند؛ ترتیبِ فیلدها مهم نیست.
- فیلدِ موجود در writer ولی نه در reader ← نادیده گرفته میشود.
- فیلدِ موردِ انتظارِ reader که writer ننوشته ← با مقدارِ پیشفرضِ reader پر میشود.
- برای سازگاری فقط میتوان فیلدِ دارای مقدارِ پیشفرض را افزود یا حذف کرد. افزودنِ فیلدِ بدونِ default ← backward میشکند؛ حذفِ فیلدِ بدونِ default ← forward.
- در Avro، null پیشفرضِ همهچیز نیست: برای nullable بودن باید
union { null, ... } بنویسی و فقط وقتی میتوانی default برابرِ null بگذاری که null شاخهٔ اول union باشد.
- تغییر نامِ فیلد با alias در reader's schema ممکن است → backward هست ولی forward نیست. افزودنِ شاخه به union هم فقط backward است.
- Fields pair by name; field order doesn’t matter.
- A field present in the writer but not the reader is ignored.
- A field the reader expects but the writer never wrote is filled with the reader’s default.
- For compatibility you may only add or remove a field with a default. Adding a default-less field breaks backward; removing one breaks forward.
- In Avro, null isn’t everything’s default: for nullability you must write
union { null, ... }, and you may default to null only when null is the union’s first branch. - Renaming a field is possible with an alias in the reader’s schema → backward yes, but not forward. Adding a union branch is backward-only too.
اما writer's schema را از کجا بیاوریم؟But where do we get the writer’s schema?
برای تفسیرِ درستِ داده باید اسکیمای نویسنده در دسترس باشد؛ ممکن است همراه فایل یا با شناسهای در مخزنِ اسکیما معرفی شود.
مثال فنی: پیدا کردن schema نویسنده
وضعیت اولیه: پیام Avro شناسهٔ schema برابر 23 دارد و بایتهای رکورد را حمل میکند.
اجرای مرحلهبهمرحله: مصرفکننده schema ۲۳ را از registryِ مورد توافق میگیرد و همراه reader schema به decoder میدهد.
نتیجه و دلیل: داشتن فقط reader schema برای تفسیر همهٔ بایتها کافی نیست؛ شناسه و دسترسپذیری schemaهای قدیمی باید حفظ شود.
| زمینه | راهحل | نمونه |
| فایلِ بزرگِ پر از رکورد | schema یکبار در ابتدای فایل نوشته میشود (object container files) | dumpهای داده، فایلهای Avro |
| پایگاه داده با رکوردهای تکتک | شمارهٔ نسخه در ابتدای هر رکورد + فهرستِ schemaها در پایگاه داده | Confluent Schema Registry برای Kafka، LinkedIn Espresso |
| ارتباطِ شبکهای | مذاکرهٔ نسخهٔ schema هنگام باز شدنِ connection و استفاده تا پایانِ عمر آن | پروتکلِ Avro RPC |
| Context | Fix | Example |
|---|
| A big file full of records | The schema is written once at the file’s start (object container files) | Data dumps, Avro files |
| A database of individual records | A version number at each record’s start + a schema list in the database | Confluent Schema Registry for Kafka, LinkedIn Espresso |
| Network communication | Schema-version negotiation when the connection opens, used for its whole life | The Avro RPC protocol |
نگهداشتنِ یک «پایگاه دادهِ نسخههای schema» به هر حال مفید است: هم سند (documentation) است و هم میتوان سازگاریِ هر تغییر را
قبل از deploy بررسی کرد. شمارهٔ نسخه میتواند عددِ افزایشی یا hash خودِ schema باشد.
Keeping a “schema-versions database” is useful anyway: it is documentation, and every change’s compatibility can be checked before deploy. Version numbers can be increasing integers or the schema’s own hash.
نمودار ۳ + روایت مدرسDiagram 3 + teacher narration
روایت مدرس از نمودار ۳Teacher narration
مثال فنی برای خواندنِ نمودار: Resolution در Avro
وضعیت اولیه: writer فقط name دارد؛ reader تازه فیلد country با default برابر IR اضافه کرده است.
اجرای مرحلهبهمرحله: هنگام خواندن رکورد قدیمی، reader مقدارِ غایب country را از default میگیرد.
نتیجه و دلیل: default برای حلِ نبود فیلد در دادهٔ نویسنده است؛ جای الزام نوشتن فیلد در schema نویسنده را نمیگیرد.
چهار ستون داریم که عنوانشان از چپ به راست است:
«Write Side»،
«Encoded Bytes»،
«Read Side» و
«Resolution».
نوارِ بالا جریانِ اصلی و نوارِ پایین، «پشتیبانِ schema» است.
Four columns, left to right: Write Side, Encoded Bytes, Read Side, Resolution. The upper lane is the main flow; the lower lane is schema backstage.
در نوارِ بالا: «Writer App» (زیرنویس: «compiled with schema»، برچسبِ
«writer's schema») با فلشِ پررنگِ «encode values in schema order» به
«Encoded Record» میرود (زیرنویس: «values only, no names»، برچسبِ «32 bytes») —
دقت کنید هیچ نامی در بایتها نیست. از آن فلشِ پررنگِ «bytes + version number» به
«Reader App» (زیرنویس: «uses two schemas») میرسد.
Top lane: Writer App (compiled with schema; tag writer’s schema) goes via bold encode values in schema order to the Encoded Record (values only, no names; 32 bytes) — note no names in the bytes. From there bold bytes + version number reaches the Reader App (uses two schemas).
نوارِ پایین سه جعبهٔ حمایتی دارد. از «Writer's Schema» (زیرنویس: «fields in write order»،
برچسبِ «defaults declared») دو فلشِ چینچین میآید: یکی بالا به Writer App با برچسبِ
«writer's schema» (قراردادِ compile-time) و دیگری به راست به «Schema Registry»
با برچسبِ «register schema version». از Registry هم فلشِ چینچینِ «fetch writer's schema»
بالا میرود و به Reader App میرسد — همان «شمارهٔ نسخه ← schema» که در جدولِ بالا دیدید.
The lower lane holds three support boxes. From Writer’s Schema (fields in write order; defaults declared) come two dashed arrows: one up to Writer App tagged writer’s schema (the compile-time contract), one right to the Schema Registry tagged register schema version. From the Registry the dashed fetch writer’s schema climbs to Reader App — the “version number → schema” trick from the table above.
سومین جعبهٔ پایینی «Reader's Schema» (زیرنویس: «fields code expects»، برچسبِ
«may differ») است که با فلشِ چینچینِ «reader's schema» به Reader App میرود.
بعد Reader App با فلشِ پررنگِ «two schemas to resolve» به «Schema Resolution»
(زیرنویس: «match by field name»، برچسبِ «Avro spec rules») و از آن فلشِ
«translate to reader's schema» به «Decoded Record»
(زیرنویس: «defaults filled in»، برچسبِ «extras ignored»).
The third lower box, Reader’s Schema (fields code expects; may differ), goes via dashed reader’s schema to Reader App. Then Reader App takes bold two schemas to resolve to Schema Resolution (match by field name; Avro spec rules), and translate to reader’s schema on to the Decoded Record (defaults filled in; extras ignored).
درسِ شکل: خواندنِ داده در Avro یعنی ترجمه بین دو schema، نه فقط رمزگشاییِ بایتها. writer's schema را یا خودِ فایل
دارد، یا registry با شمارهٔ نسخه میدهد؛ reader's schema همان چیزی است که کدِ امروزِ تو انتظار دارد.
و دو قاعدهٔ طلایی resolution در برچسبِ آخر خلاصه شده: extras ignored و defaults filled in.
The lesson: reading data in Avro means translating between two schemas, not just decoding bytes. The writer’s schema comes either inside the file itself or from a registry via the version number; the reader’s schema is whatever today’s code expects. And the two golden resolution rules sit in the last tag: extras ignored and defaults filled in.
۳.۶ مزایای schema و اسکیماهای پویا3.6 Schema benefits and dynamic schemas
اسکیما قراردادِ شکل و معنای داده را روشن میکند. داشتن فایل اسکیما کافی نیست؛ باید نسخهها، قواعد تغییر و دسترسیِ خواننده به ساختار مناسب هم مدیریت شوند.
مثال فنی: قرارداد پیش از ورود داده
وضعیت اولیه: پیام ورودی فیلد age را با متن old میفرستد، اما قرارداد عدد میخواهد.
اجرای مرحلهبهمرحله: validation پیام را رد یا قرنطینه میکند و مسیرِ خطا را ثبت میکند؛ صرف تولید کلاس از schema کافی نیست.
نتیجه و دلیل: schema اختلاف نوع را زود آشکار میکند؛ درستی قواعد دامنه مثل سن منفی هنوز کنترل جدا میخواهد.
- اسکیماهای پویا با Avro راحتترند: چون تگی در schema نیست، میتوان مستقیم از schema رابطهای، Avro schema تولید کرد (هر جدول ← یک record، هر ستون ← یک فیلد) و dump کاملِ پایگاه داده را نوشت. اگر schema پایگاه داده عوض شود، همان مبدل دوباره اجرا میشود؛ چون فیلدها با نام شناخته میشوند، reader's schema قدیمی هنوز جفت میشود. در protobuf باید تگها را دستی تخصیص دهی و مراقب باشی تگی تکراری استفاده نشود.
- قالبهای باینریِ مبتنی بر schema چند مزیتِ بزرگ دارند: فشردهتر از «JSONهای باینری»اند (نامِ فیلدها حذف میشود)؛ خودِ schema سندِ زنده است (برخلاف مستنداتِ دستی، همیشه بهروز است چون برای decode لازم است)؛ با نگهداشتنِ registry میتوان سازگاریِ تغییرات را قبل از deploy سنجید؛ و برای زبانهای statically typed کد تولید میکند (بررسیِ نوع در compile-time).
- این ایدهها تازه نیستند: ASN.1 از ۱۹۸۴ با تگها تکاملِ schema داشت (هنوز در گواهیهای SSL/X.509 با کدگذاریِ DER استفاده میشود) اما پیچیده و بدمستند است. بیشتر پایگاههای داده رابطهای هم پروتکلِ باینریِ اختصاصی خودشان را دارند که driver (JDBC/ODBC) آن را decode میکند.
- قاعدهٔ عملی: تعدادِ قالبهای همزمان در سیستم را کم نگه دار؛ هر قالبِ اضافه یعنی عملیاتِ بیشتر برای نگهداشتنِ سازگاری.
- Dynamic schemas are easier with Avro: with no tags in the schema you can generate an Avro schema straight from a relational schema (each table → one record, each column → one field) and write a full database dump. If the database schema changes, re-run the same converter; since fields are known by name, the old reader’s schema still pairs up. In protobuf you must allocate tags by hand and watch none get reused.
- Schema-driven binary formats carry big wins: denser than “JSON binaries” (field names drop out); the schema itself is living documentation (unlike hand docs, always current since decoding needs it); with a registry you can test a change’s compatibility before deploy; and they generate code for statically typed languages (compile-time type checks).
- These ideas aren’t new: ASN.1 has had tag-based schema evolution since 1984 (still used in SSL/X.509 certificates with DER encoding) but is complex and badly documented. Most relational databases also have their own proprietary binary protocol that the driver (JDBC/ODBC) decodes.
- Practical rule: keep the number of simultaneous formats in the system small; each extra format means more work keeping compatibility.
| محور | JSON / XML | MessagePack و دوستان | Protocol Buffers | Avro |
| schema | اختیاری (JSON Schema) | ندارد | اجباری (IDL با تگ) | اجباری (IDL/JSON بدون تگ) |
| شناسهٔ فیلد در داده | نامِ کامل | نامِ کامل | شمارهٔ تگ | هیچ (فقط ترتیبِ schema) |
| حجمِ رکوردِ مثال | ۸۱ بایت | ۶۶ بایت | ۳۳ بایت | ۳۲ بایت |
| سازگاری | بستگی به قراردادِ شما دارد | مثل JSON | با تگهای ثابت: روشن و قابلاتکا | با defaultها و resolution: روشن و قابلاتکا |
| انسانخوان | بله | نه | نه (ابزار میخواهد) | نه (ابزار میخواهد) |
| بهترین کاربرد | تبادلِ بین سازمانها، APIهای عمومی | وقتی فقط میخواهی JSON کوچکتر شود | RPC (gRPC)، لاگها، پیامها | فایلهای بزرگ، dump، pipelineهای داده |
نمودار مفهومی — یک رکورد، چهار کدگذاریConcept diagram — one record, four encodings
روایت مدرس از نمودارTeacher narration
مثال فنی برای خواندنِ نمودار: مقایسهٔ قالب روی همان رکورد
وضعیت اولیه: رکورد user_id=17, active=true را در چند قالب مینویسیم.
اجرای مرحلهبهمرحله: JSON نام فیلدها را متنی حمل میکند؛ قالب مبتنی بر schema میتواند شناسه یا جایگاه فیلد را بهکار گیرد.
نتیجه و دلیل: برای مقایسه باید همان داده، کتابخانه و تنظیمات سنجیده شوند؛ باینری بودن بهتنهایی تضمین کمترین حجم یا بهترین تکامل نیست.
چهار میله را از بالا بخوانید: JSON با ۸۱ بایت (نام فیلدها داخل داده سفر میکنند)، MessagePack با ۶۶ بایت (همان مدل، باینری، ولی هنوز بیschema)، پروتوباف با ۳۳ بایت (نامها با شمارهٔ تگ جایگزین شدهاند) و Avro با ۳۲ بایت (فقط مقادیر؛ ترتیب از schema میآید). جملهٔ پایین درس است: schema نامها را حذف میکند و ۸۱ بایت را ۳۲ میکند.
Read the four bars top-down: JSON at 81 bytes (field names travel inside the data), MessagePack at 66 (same model, binary, still schema-less), Protobuf at 33 (names replaced by tag numbers) and Avro at 32 (values only; order comes from the schema). The bottom line is the lesson: the schema drops the names, turning 81 bytes into 32.
| Aspect | JSON / XML | MessagePack & friends | Protocol Buffers | Avro |
|---|
| Schema | Optional (JSON Schema) | None | Mandatory (tagged IDL) | Mandatory (tagless IDL/JSON) |
| Field ID in data | Full name | Full name | Tag number | None (schema order only) |
| Example-record size | 81 bytes | 66 bytes | 33 bytes | 32 bytes |
| Compatibility | Depends on your conventions | Like JSON | With stable tags: clear and reliable | With defaults + resolution: clear and reliable |
| Human-readable | Yes | No | No (needs tools) | No (needs tools) |
| Best use | Cross-org exchange, public APIs | When you just want smaller JSON | RPC (gRPC), logs, messages | Big files, dumps, data pipelines |
۳.۷ چرخهٔ حیاتِ یک فیلدِ اسکیما + نمودار ۶3.7 A schema field’s lifecycle + Diagram 6
تغییر یک فیلد بهتر است مرحلهای انجام شود: خوانندهها را آماده کنیم، نوشتن شکل تازه را شروع کنیم و پس از کنار رفتن وابستگیهای قدیمی، شکل قبلی را حذف کنیم.
مثال فنی: چرخهٔ حذف یک فیلد
وضعیت اولیه: سرویس از full_name به given_name, family_name مهاجرت میکند.
اجرای مرحلهبهمرحله: ابتدا خوانندهها هر دو شکل را تحمل میکنند؛ writerها مرحلهای عوض میشوند؛ حذف نهایی وقتی است که مصرفکننده و دادهٔ قدیمی پوشش داده شدهاند.
نتیجه و دلیل: حذف فیلد فقط تغییر یک فایل schema نیست؛ عمر داده و همهٔ readerها بخشی از مهاجرتاند.
So far we’ve seen how writer’s and reader’s schemas get along via resolution rules: the writer’s extra field is ignored, the writer’s missing field is filled with the reader’s default. But these rules only save us if schema change arrives step by step: in a live system, during a rolling upgrade old and new code versions run side by side for hours or days, and every message must stay readable to both. So “adding one field” isn’t one deploy; it’s a multi-stage cycle, each step guaranteeing one compatibility.
قانونِ طلاییِ این چرخه readers first است: اول کدی را deploy کنید که فیلدِ تازه را میخواند و اگر نبود
تحملش میکند، بعد کدی را که آن را مینویسد. ارزانترینِ تغییرها، افزودنِ فیلدِ اختیاری با مقدارِ پیشفرض است —
در Avro یعنی union { null, ... } با default مناسب — چون خوانندههای قدیمی نادیدهاش میگیرند و خوانندههای
جدید با پیشفرضِ خودشان پرش میکنند. اما تغییرِ نام یا نوعِ یک فیلد، در حکمِ «حذف + افزودن» است: اگر در یک گام انجام شود،
یکی از دو سازگاریِ backward/forward میشکند.
This cycle’s golden rule is readers first: first deploy code that reads the fresh field and tolerates its absence, then code that writes it. The cheapest changes add an optional field with a default — in Avro a union { null, ... } with a proper default — since old readers ignore it and new readers fill it with their own default. But renaming or retyping a field equals “delete + add”: done in one step, one of the two backward/forward compatibilities breaks.
و چرا حذف همیشه آخرین گام است؟ چون داده از کد عمرش بیشتر است: پیامهای قدیمیِ مانده در صفها، backupها و
فایلهای آرشیوی برای همیشه با همان اسکیمای قدیمی میمانند. پس اول نوشتنِ فیلد را متوقف میکنیم (deprecated)
اما خوانندهها هنوز باید رکوردهای حاویِ آن را بخوانند، و فقط وقتی هیچ خوانندهای به آن نیاز نداشت، از اسکیما حذفش میکنیم.
نمودار ۶ همین چرخه را بهصورتِ یک ماشینِ وضعیت میکشد: پنج حالتِ اصلیِ فیلد، دو «دروازهٔ سازگاری» برای تصمیمهای سخت،
و یک وضعیتِ بنبست برای وقتی که ترتیب را اشتباه رفتهایم.
And why is deletion always the last step? Because data outlives code: old messages stuck in queues, backups and archive files keep their old schema forever. So first stop writing the field (deprecated) while readers must still read records containing it, and only drop it from the schema once no reader needs it. Diagram 6 draws this cycle as a state machine: five main field states, two compatibility gates for the hard calls, and one dead-end state for wrong ordering.
روایت مدرس از نمودار ۶Teacher narration
مثال فنی برای خواندنِ نمودار: چرخهٔ حذف یک فیلد
وضعیت اولیه: سرویس از full_name به given_name, family_name مهاجرت میکند.
اجرای مرحلهبهمرحله: ابتدا خوانندهها هر دو شکل را تحمل میکنند؛ writerها مرحلهای عوض میشوند؛ حذف نهایی وقتی است که مصرفکننده و دادهٔ قدیمی پوشش داده شدهاند.
نتیجه و دلیل: حذف فیلد فقط تغییر یک فایل schema نیست؛ عمر داده و همهٔ readerها بخشی از مهاجرتاند.
این نمودار سه نوارِ (
lane) افقی دارد که عنوانشان از بالا به پایین این است:
«Field states» (زندگیِ یک فیلد)،
«Compatibility gates»
(دروازههای تصمیمِ سازگاری) و
«Terminal» (وضعیتِ بنبست). پنج حالتِ نوارِ بالا شمارهٔ گام دارند —
«01» تا
«05» — و مسیرِ عادیِ زندگیِ یک فیلد را میسازند؛ دو حالتِ
لوزیشکلِ نوارِ میانی «دروازه»اند و حالتِ قرمزِ نوارِ پایین، مقصدِ مسیرِ شکست.
The diagram has three horizontal lanes, top to bottom: Field states (a field’s life), Compatibility gates (compatibility decision gates), Terminal (dead end). The top lane’s five states are numbered 01–05 and form a field’s normal life; the middle lane’s two diamonds are gates, the bottom lane’s red state the failure destination.
از چپِ نوارِ بالا شروع کنید: «Proposed» (گامِ «01») با زیرنویسِ
«schema change drafted» و برچسبِ «not deployed» —
یعنی تغییر هنوز فقط روی کاغذ است و به هیچ سروری نرسیده. فلشِ پررنگِ «deploy readers first»
آن را به «Added Optional» (گامِ «02») میبرد — زیرنویسِ
«new field, default or null» و برچسبِ «writers may omit it»:
فیلد در اسکیما هست و خوانندهها تحملش میکنند، اما هنوز هیچ نویسندهای پرش نمیکند. فلشِ بعدی،
«writers fill it»، به «In Use» (گامِ «03»)
میرسد — زیرنویسِ «new writers fill it» و برچسبِ «old readers ignore it» —
تنها حالتی از این پنجتا که فیلد واقعاً داده حمل میکند.
Start left on the top lane: Proposed (step 01; schema change drafted; not deployed) — the change is still only on paper. The bold deploy readers first leads to Added Optional (step 02; new field, default or null; writers may omit it): the field is in the schema and readers tolerate it, but no writer fills it yet. Next, writers fill it reaches In Use (step 03; new writers fill it; old readers ignore it) — the only one of the five where the field really carries data.
از اینجا داستانِ جمعوجور کردن است. فلشِ «stop writing it» به
«Deprecated» (گامِ «04») میرود — زیرنویسِ
«no new writes» و برچسبِ «readers still tolerate it»:
نوشتنِ تازه متوقف شده، اما رکوردهای قدیمی هنوز فیلد را دارند و خوانندهها باید بتوانند همانها را بخوانند.
سرانجام فلشِ «drop the field» به «Removed»
(گامِ «05») میرسد — زیرنویسِ «gone from the schema» و برچسبِ
اطمینانبخشِ «safe to drop». دقت کنید این پنج گام فقط به همین ترتیب امناند؛ برای هر
جابهجاییِ ترتیب، یکی از دو دروازهٔ نوارِ میانی باید جوابِ «بله» بدهد.
From here it’s the winding-down story. stop writing it goes to Deprecated (step 04; no new writes; readers still tolerate it): fresh writes stop, but old records still carry the field and readers must keep reading them. Finally drop the field reaches Removed (step 05; gone from the schema; reassuringly safe to drop). These five steps are only safe in this order; any reordering must pass one of the middle lane’s two gates.
حالا دروازهها. از «Added Optional» فلشِ چینچینِ «need it now?»
به پایین میآید و به نوارِ «Compatibility gates» میرسد: حالتِ لوزیِ
«All Writers Upgraded?» با زیرنویسِ «old code still running» و برچسبِ
«gate». اگر همهٔ نویسندهها بالا رفته باشند، فلشِ «yes, all upgraded»
مستقیم به «In Use» برمیگردد — یعنی نوشتنِ فیلد فقط بعد از عبور از این دروازه شروع میشود.
اما اگر نسخهای قدیمی هنوز در حال اجرا باشد، فلشِ قرمزِ «no, mixed fleet» به نوارِ
«Terminal» میرود: «Breaking Change» با زیرنویسِ
«old code crashes or drops data» و برچسبِ «roll back» —
یعنی یا کدِ قدیمی میشکند یا داده را میبلعد، و تنها راه، برگرداندنِ deploy است.
Now the gates. From Added Optional the dashed need it now? descends to the Compatibility gates lane: the diamond All Writers Upgraded? (old code still running; gate). If all writers are up, yes, all upgraded returns straight to In Use — writing the field starts only past this gate. But if an old version still runs, the red no, mixed fleet drops to the Terminal lane: Breaking Change (old code crashes or drops data; roll back) — either old code breaks or it swallows data, and the only way out is rolling the deploy back.
دروازهٔ دوم سمتِ راستِ همان نوار است: از «Deprecated» فلشِ چینچینِ
«can we drop it?» پایین میآید به «All Readers Upgraded?»
با زیرنویسِ «ignore unknown fields» و برچسبِ «gate».
جوابِ مثبت — فلشِ «yes, all upgraded» — فیلد را به «Removed» میرساند؛
اما جوابِ منفی — فلشِ «not yet» — درست به خودِ «Deprecated»
برمیگردد: حلقهای که ممکن است هفتهها و ماهها تکرار شود تا آخرین خوانندهای که هنوز به فیلد نیاز دارد بازنشسته شود.
حذفِ فیلد فقط وقتی «safe to drop» است که این دروازه برای همیشه بسته شده باشد.
The second gate sits right on the same lane: from Deprecated the dashed can we drop it? descends to All Readers Upgraded? (ignore unknown fields; gate). A yes — yes, all upgraded — carries the field to Removed; a no — not yet — loops right back to Deprecated: a loop that may repeat for weeks and months until the last reader still needing the field retires. Dropping is safe to drop only once this gate closes for good.
درسِ اولِ شکل را در یک ترتیب جمع کنید: readers first، writers second، drop last. اول کدِ خواننده را بالا ببرید
تا فیلدِ ناشناخته را تحمل کند، بعد کدِ نویسنده را تا فیلد را پر کند، و حذف را فقط آخرِ کار و پس از هر دو دروازه انجام دهید.
در یک rolling upgrade واقعی یعنی deployها را باید با همین ترتیب برنامهریزی کنید:
نسخهای که فقط «میخواند» همیشه ایمن است، نسخهای که «مینویسد» فقط بعد از آن.
Sum the figure’s first lesson into one order: readers first, writers second, drop last. First raise reader code to tolerate the unknown field, then writer code to fill it, and delete only at the very end, past both gates. In a real rolling upgrade that means scheduling deploys in exactly this order: a version that only “reads” is always safe; one that “writes” only after it.
و درسِ دوم: داده از کد عمرش بیشتر است، و هر تغییرِ «ظاهراً کوچک» ممکن است در لباسِ «حذف + افزودن» باشد —
تغییرِ نامِ فیلد، تغییرِ نوع، یا حتی عوض کردنِ مقدارِ پیشفرض. قیمتِ بههمریختنِ ترتیب همان یک فلشِ قرمزِ
«no, mixed fleet» است: «Breaking Change» و بعد
«roll back». اگر هر تغییرِ اسکیما را با همین پنج گام و دو دروازه بسنجید،
deployِ بدونِ downtime دیگر شانس نیست؛ نتیجهٔ منطقیِ ترتیبِ درست است.
And the second lesson: data outlives code, and every “seemingly small” change may wear a “delete + add” costume — renaming a field, retyping it, even changing its default. The price of scrambling the order is that one red arrow no, mixed fleet: Breaking Change, then roll back. Weigh every schema change against these five steps and two gates, and downtime-free deploy stops being luck — it becomes the logical result of the right order.
۴4حالتهای جریان دادهDataflow modes
داده میتواند از راه ذخیرهسازی، درخواست مستقیم، صف پیام یا گردشکار میان برنامهها منتقل شود. در هر مسیر باید بدانیم چه کسی تولید میکند، چه کسی میخواند و چه مدت بعد.
مثال فنی: سه مسیر همان داده
وضعیت اولیه: تغییر نام کاربر میتواند از رکورد پایگاه داده، پاسخ API یا پیام رویداد خوانده شود.
اجرای مرحلهبهمرحله: پایگاه داده نسخههای قدیمی را نگه میدارد؛ API با کلاینتهای قدیمی سروکار دارد؛ صف ممکن است پیام قدیمی را دوباره تحویل دهد.
نتیجه و دلیل: هر مسیر پنجرهٔ سازگاری خودش را دارد؛ پایان انتشار سرورها لزوماً پایان دادهٔ قدیمی نیست.
Compatibility is a relation between one process that encodes and another that decodes. But data travels between processes in assorted ways. This part looks at four common modes.
۴.۱ نمودار ۴ + روایت مدرس4.1 Diagram 4 + teacher narration
روایت مدرس از نمودار ۴Teacher narration
مثال فنی برای خواندنِ نمودار: سه مسیر همان داده
وضعیت اولیه: تغییر نام کاربر میتواند از رکورد پایگاه داده، پاسخ API یا پیام رویداد خوانده شود.
اجرای مرحلهبهمرحله: پایگاه داده نسخههای قدیمی را نگه میدارد؛ API با کلاینتهای قدیمی سروکار دارد؛ صف ممکن است پیام قدیمی را دوباره تحویل دهد.
نتیجه و دلیل: هر مسیر پنجرهٔ سازگاری خودش را دارد؛ پایان انتشار سرورها لزوماً پایان دادهٔ قدیمی نیست.
شکل از چپ شروع میشود: جعبهٔ
«Application Process» با زیرنویسِ
«in-memory objects». فلشِ پررنگِ
«encode object to bytes»
آن را به قلبِ شکل میرساند: جعبهٔ بزرگِ
«Encoder / Decoder»
(زیرنویس:
«objects to bytes and back»، برچسبِ
«textual or schema-driven»).
The figure starts left: Application Process (in-memory objects). The bold encode object to bytes carries it to the figure’s heart: the big Encoder / Decoder box (objects to bytes and back; textual or schema-driven).
از راستِ این جعبه، چهار فلش به چهار حالتِ جریان داده میرود و همه داخل قابِ
«Bytes cross a process boundary — backward + forward compatibility required» جمع شدهاند:
From this box’s right, four arrows run to four dataflow modes, all gathered inside Bytes cross a process boundary — backward + forward compatibility required:
- بالاترین: «Database» (زیرنویس: «writer encodes, later reader decodes»، برچسبِ «data outlives code») با فلشِ پررنگِ «persist and read records».
- دوم (همترازِ Encoder): «Services: REST / RPC» (زیرنویس: «client encodes request, server replies»، برچسبِ «evolve APIs independently») با فلشِ «request and response payloads».
- سوم: «Workflow Engine» (زیرنویس: «steps logged and replayed»، برچسبِ «durable execution») با فلشِ «step results to durable log».
- پایینی: «Broker / Actors» (زیرنویس: «sender encodes, subscribers decode»، برچسبِ «async messages») با فلشِ چینچینِ «publish events and messages».
- Top: Database (writer encodes, later reader decodes; data outlives code) via bold persist and read records.
- Second (level with the Encoder): Services: REST / RPC (client encodes request, server replies; evolve APIs independently) via request and response payloads.
- Third: Workflow Engine (steps logged and replayed; durable execution) via step results to durable log.
- Bottom: Broker / Actors (sender encodes, subscribers decode; async messages) via dashed publish events and messages.
درسِ شکل: در هر چهار حالت، کارِ یکسان است — object به بایت و بایت به object — اما چه کسی encode میکند،
چه کسی decode، و کِی فرق میکند. همین تفاوتها تعیین میکند هر حالت چه سازگاریهایی لازم دارد؛
بقیهٔ این بخش تکتکِ همین چهار شاخه را باز میکند.
The lesson: in all four modes the job is identical — object to bytes and bytes to object — but who encodes, who decodes, and when differ. Those very differences decide what compatibilities each mode needs; the rest of this part unpacks these four branches one by one.
۴.۲ جریان داده از طریق پایگاه داده4.2 Dataflow via databases
نوشتن در پایگاه داده، نوعی تحویل داده به خوانندهٔ آینده است. ممکن است نسخهٔ بعدی برنامه رکوردهایی را بخواند که سالها قبل ساخته شدهاند.
مثال فنی: ویرایش نام، حذف email
وضعیت اولیه: نسخهٔ ۲ رکورد {name:Sara,email:a@b} نوشته است؛ نسخهٔ ۱ فقط name را میشناسد.
اجرای مرحلهبهمرحله: نسخهٔ ۱ کل شیء را میخواند، name را عوض میکند و بدون email بازمینویسد؛ update محدود به name یا حفظ فیلد ناشناخته جلوی حذف را میگیرد.
نتیجه و دلیل: یک read-modify-write قدیمی میتواند دادهٔ جدید را پاک کند، حتی وقتی decode بدون خطا بوده است.
- در پایگاه داده، processِ نویسنده encode میکند و processِ خواننده decode. اگر فقط یک process داشته باشی، خواننده همان برنامه در آینده است: ذخیره کردن یعنی پیام فرستادن به خودِ آیندهات — پس backward compatibility قطعاً لازم است.
- اما معمولاً چند process همزمان به پایگاه داده دسترسی دارند (سرویسهای مختلف یا چند instance از یک سرویس) و در میانهٔ rolling upgrade بعضیها جدید و بعضیها قدیمیاند؛ پس forward compatibility هم لازم است.
- data outlives code: ممکن است مقداری ۵ میلیثانیه پیش و مقداری ۵ سال پیش نوشته شده باشد. کد در چند دقیقه عوض میشود؛ داده تا بازنویسیِ صریح با همان فرمتِ قدیمی میماند.
- بازنویسی (migration) داده به schema جدید ممکن است ولی روی دادهٔ بزرگ گران است؛ پس موتورها آن را بهتعویقانداخته و best-effort انجام میدهند: موتورهای LSM هنگام compaction با جدیدترین فرمت بازنویسی میکنند؛ پایگاههای داده رابطهای با افزودنِ ستونِ nullable بدونِ rewrite کار میکنند (هنگام خواندنِ ردیفِ قدیمی، null پر میشود). نتیجه: کلِ پایگاه داده «انگار» با یک schema کد شده، هرچند ذخیرهسازی زیرین مخلوطی از نسخههاست.
- تغییراتِ پیچیدهتر (تکمقداری ← چندمقداری، جابهجایی به جدولِ جدا) هنوز به بازنویسیِ سطحِ برنامه نیاز دارند و نگهداشتنِ سازگاری در چنین migrationهایی هنوز پژوهشی است.
- بایگانی (archival): snapshot یا dump معمولاً با جدیدترین schema و بهصورت یکدست نوشته میشود (چون اصل داده در حال کپی است). چون dump یکجا نوشته و تغییرناپذیر میشود، Avro object container files عالیاند و فرصتِ خوبی است برای کدگذاریِ ستونی مثل Parquet (فصل ۱۱).
- In a database the writer process encodes and the reader process decodes. With a single process the reader is the same program in the future: storing means messaging your future self — so backward compatibility is definitely needed.
- But usually several processes touch the database at once (different services, or several instances of one service), and mid-rolling-upgrade some are new and some old; so forward compatibility is needed too.
- Data outlives code: one value may be 5 milliseconds old, another 5 years. Code changes in minutes; data keeps its old format until explicitly rewritten.
- Rewriting (migrating) data to a new schema is possible but expensive on big data; so engines do it deferred and best-effort: LSM engines rewrite in the newest format during compaction; relational databases add nullable columns without rewrites (reading an old row fills null). Result: the whole database looks “as if” encoded with one schema, though the underlying storage mixes versions.
- Trickier changes (single- → multi-valued, moving to a separate table) still need application-level rewrites, and keeping compatibility through such migrations is still research territory.
- Archival: a snapshot or dump is usually written uniformly with the newest schema (since the actual data is being copied). Since a dump is written at once and frozen, Avro object container files are ideal — and a good moment for columnar encodings like Parquet (Chapter 11).
۴.۳ سرویسها: REST و RPC4.3 Services: REST and RPC
- الگوی رایج: کلاینت و سرور. سرور API را در شبکه میگذارد و کلاینت به آن درخواست میفرستد. وب همینطور کار میکند (GET برای گرفتن HTML/CSS/JS، POST برای فرستادن داده) با مجموعهای استاندارد از پروتکلها (HTTP، URL، TLS، HTML).
- کلاینت فقط مرورگر نیست: اپهای موبایل و دسکتاپ و اپهای JavaScript مرورگر هم با HTTP به سرور وصل میشوند و معمولاً JSON میگیرند.
- سرویسها شبیه پایگاه دادهاند (داده میپذیرند و میدهند) اما بهجای زبانِ کوئریِ عمومی، API ویژهٔ برنامه دارند با ورودی/خروجیهای ازپیشتعیینشده — همین، کپسولهسازی میآورد.
- هدفِ معماری microservices این است که هر سرویس مستقل deploy و تکامل یابد (معمولاً یک تیم، یک سرویس، releaseهای مکرر). پس نسخههای قدیم و جدیدِ سرور و کلاینت همزماناند و فقط تا وقتی APIها سازگار بمانند، تیمها آزادند زیرساخت را عوض کنند.
- web service یعنی سرویسی که زیرینش HTTP است؛ در سه زمینه به کار میرود: کلاینتِ روی دستگاهِ کاربر (اینترنتِ عمومی)، سرویسبهسرویسِ درونِ یک سازمان (شبکهٔ خصوصی)، و سازمانبهسازمان (APIهای عمومی مثل پردازشِ پرداخت یا OAuth).
- REST محبوبترین فلسفهٔ طراحی است: فرمتهای ساده، URL برای شناساییِ منابع، و امکاناتِ HTTP برای cache، احراز هویت و مذاکرهٔ نوعِ محتوا. اما کلاینت هنوز باید بداند کدام endpoint و چه فرمتی دارد؛ برای همین IDL مینویسند: OpenAPI (Swagger) برای سرویسهای JSON و Protocol Buffers برای gRPC. ابزارهای IDL مستندات میسازند، سازگاریِ تغییرات schema را بررسی میکنند و GUI برای آزمایش میدهند.
- The common pattern: client and server. The server puts an API on the network and the client sends it requests. The web works this way (GET for HTML/CSS/JS, POST for sending data) with a standard protocol set (HTTP, URL, TLS, HTML).
- Clients aren’t just browsers: mobile and desktop apps and browser JavaScript apps also reach servers over HTTP, usually taking JSON.
- Services resemble databases (they take and give data) but instead of a general query language they expose a program-specific API with predefined inputs/outputs — which brings encapsulation.
- The microservices goal is for each service to deploy and evolve independently (usually one team, one service, frequent releases). So old and new server and client versions coexist, and teams are free to change internals only while APIs stay compatible.
- A web service is a service over HTTP; used in three settings: user-device clients (the public internet), service-to-service inside one organization (private network), and org-to-org (public APIs like payment processing or OAuth).
- REST is the most popular design philosophy: simple formats, URLs for resource identity, and HTTP facilities for caching, auth and content negotiation. But the client must still know which endpoint and format; hence IDLs: OpenAPI (Swagger) for JSON services, Protocol Buffers for gRPC. IDL tools build docs, check schema-change compatibility, and give testing GUIs.
چرا «فراخوانی از راه دور» مثل فراخوانیِ محلی نیست؟Why isn’t a “remote call” like a local call?
در REST یا RPC، برنامه از سرویس دیگری کاری میخواهد. این فراخوانی روی شبکه است و ممکن است دیر برسد، پاسخ آن گم شود یا نتیجهاش نامشخص بماند.
مثال فنی: RPC ناموفق یا پاسخ نامعلوم؟
وضعیت اولیه: کلاینت CreateOrder(request_id=r7) را میفرستد؛ سرور سفارش را ثبت میکند، ولی پاسخ نمیرسد.
اجرای مرحلهبهمرحله: retry با همان شناسه به همان نتیجه وصل میشود؛ retry با شناسهٔ تازه میتواند سفارش دوم بسازد.
نتیجه و دلیل: برخلاف فراخوانی محلی، timeout شبکه نتیجهٔ عملیات دوردست را مشخص نمیکند؛ API باید این ابهام را مدیریت کند.
The RPC model wants a network request to look like calling a function (location transparency) — but the differences are fundamental:
- فراخوانیِ محلی قابلپیشبینی است؛ درخواستِ شبکهای غیرقابلپیشبینی (گمشدن، کندیِ ماشینِ دور).
- نتیجهٔ فراخوانیِ محلی یا مقدار است یا exception؛ درخواستِ شبکهای یک پایانِ سوم هم دارد: timeout بدون نتیجه — نمیدانی اصلاً اجرا شد یا نه.
- تلاشِ مجدد ممکن است عمل را دوبار اجرا کند (اگر فقط پاسخ گم شده باشد)؛ مگر قراردادِ idempotence و شناسههای یکتا داشته باشی (فصل ۱۲).
- latency شبکه متغیر است: از کمتر از یک میلیثانیه تا چند ثانیه، برخلافِ تابعِ محلی.
- نمیتوانی pointer بدهی؛ همهچیز باید encode شود — برای دادههای بزرگ و objectهای mutable دردسرساز است.
- کلاینت و سرور ممکن است زبانهای متفاوت داشته باشند و ترجمهٔ نوعها (مثل مشکلِ اعدادِ بزرگتر از 2^53 در JavaScript) زشت شود.
- A local call is predictable; a network request is unpredictable (lost packets, a slow far machine).
- A local call’s outcome is a value or an exception; a network request has a third ending: timeout with no result — you don’t know if it ran at all.
- Retrying may run the action twice (if only the response was lost); unless you hold an idempotence contract and unique IDs (Chapter 12).
- Network latency varies: from under a millisecond to seconds, unlike a local function.
- You can’t pass pointers; everything must be encoded — painful for big data and mutable objects.
- Client and server may speak different languages, and type translation gets ugly (like the bigger-than-2^53 numbers problem in JavaScript).
بخشی از جذابیتِ REST همین است که انتقالِ state روی شبکه را فراتر از صدازدنِ تابع میفهمد.
تاریخچه هم پر از شکستهای پرطمطراق است: EJB و RMI فقط Java، DCOM فقط مایکروسافت، CORBA پیچیده و بدون سازگاری، SOAP/WS-* پیچیده.
Part of REST’s appeal is understanding state transfer over the network as beyond function calls. History is full of grandiose failures too: EJB and RMI for Java only, DCOM for Microsoft only, CORBA complex and incompatible, SOAP/WS-* complex.
load balancing، service discovery و service meshLoad balancing, service discovery and service mesh
کشف سرویس مقصدهای موجود را معرفی میکند؛ متعادلساز درخواستها را پخش میکند؛ مشِ سرویس بخشی از ارتباطِ میان سرویسها را مدیریت میکند.
مثال فنی: کشف سرویس تا انتخاب مقصد
وضعیت اولیه: سرویس انبار سه instance دارد و یکی جابهجا شده است.
اجرای مرحلهبهمرحله: discovery نشانیهای معتبر را میدهد؛ load balancer یکی را انتخاب میکند؛ mesh میتواند سیاست اتصال و مشاهدهپذیری مشترک اجرا کند.
نتیجه و دلیل: هیچکدام idempotency عملیات را خودکار نمیسازند؛ retry لایهٔ شبکه باید با معنای درخواست هماهنگ باشد.
| راهحل | چطور کار میکند | نقطهٔ ضعف / نکته |
| Load balancer سختافزاری | تجهیزِ ویژه در datacenter؛ کلاینت به یک host:port وصل میشود و اتصال به سرورها مسیردهی میشود | تشخیصِ خرابی و جابهجاییِ ترافیک؛ هزینهٔ سختافزار |
| Load balancer نرمافزاری (NGINX، HAProxy) | همان کار، بهصورت برنامه روی سختافزارِ معمولی | ساده و رایج برای deploymentهای متوسط |
| DNS | یک نامِ دامنه ← چند IP؛ لایهٔ شبکهٔ کلاینت یکی را انتخاب میکند | تغییرات کند منتشر میشوند و cache میشوند؛ با start/stop مکررِ سرورها IPهای کهنه میبینی |
| سیستمِ service discovery (etcd، ZooKeeper) | instance خودش را با host:port و metadata (مالکیتِ shard، دیتاسنتر) ثبت میکند و heartbeat میفرستد؛ کلاینت فهرستِ endpointها را میگیرد | محیطهای پویاتر؛ تصمیمِ load-balancingِ هوشمندانهتر (فصل ۷) |
| service mesh (Istio، Linkerd) | load balancer نرمافزاری + discovery، بهصورت کتابخانهٔ درونprocess یا sidecar در دو طرف؛ رمزنگاری در سطحِ load balancer | پیچیده ولی observability قوی؛ رایج در محیطهای Kubernetes |
| Fix | How it works | Weakness / note |
|---|
| Hardware load balancer | Special datacenter gear; the client connects to one host:port and connections route to servers | Failure detection and traffic shifting; hardware cost |
| Software load balancer (NGINX, HAProxy) | The same job as a program on commodity hardware | Simple and common for mid-size deployments |
| DNS | One domain name → several IPs; the client’s network layer picks one | Changes propagate slowly and get cached; with frequent server start/stops you see stale IPs |
| Service discovery (etcd, ZooKeeper) | Each instance registers itself with host:port and metadata (shard ownership, datacenter) and heartbeats; the client fetches the endpoint list | More dynamic environments; smarter load-balancing calls (Chapter 7) |
| Service mesh (Istio, Linkerd) | Software load balancer + discovery as an in-process library or sidecar on both sides; encryption at the load-balancer level | Complex but strong observability; common in Kubernetes environments |
سازگاریِ RPC دقیقاً چه میخواهد؟What exactly does RPC compatibility need?
چون میتوان فرض کرد اول همهٔ سرورها و بعد کلاینتها بهروز میشوند: روی requestها فقط backward و روی
responseها فقط forward لازم است. gRPC و Avro RPC قواعدِ همان قالبِ کدگذاریشان را ارث میبرند؛ در REST/JSON
معمولاً «افزودنِ پارامترِ اختیاریِ request» و «افزودنِ فیلدِ response» سازگار شمرده میشوند.
اما چون RPC اغلب از مرزِ سازمان میگذرد، کنترلی روی کلاینتها نیست و سازگاری باید شاید تا ابد نگه داشته شود
(یا چند نسخهٔ API کنار هم نگه داشته شود). برای نسخهبندی هم توافقِ جهانی نیست: شماره در URL، هدرِ Accept، یا نسخهٔ هر کلاینت در سرور.
Since we may assume servers upgrade first, clients after: requests need only backward, responses only forward. gRPC and Avro RPC inherit their encoding format’s rules; in REST/JSON “adding an optional request parameter” and “adding a response field” usually count as compatible. But since RPC often crosses org boundaries there is no control over clients, and compatibility may need keeping forever (or several API versions side by side). Versioning has no global agreement either: a number in the URL, the Accept header, or each client’s version on the server.
۴.۴ workflow و اجرای پایدار (Durable Execution) + نمودار ۵4.4 Workflows and durable execution + Diagram 5
گردشکار پایدار، وضعیت مراحل را نگه میدارد تا پس از توقف برنامه بتوان ادامه داد. اثر بیرونیِ هر مرحله همچنان باید در برابر اجرای دوباره محافظت شود.
مثال فنی: ادامهٔ workflow بعد از crash
وضعیت اولیه: workflow مرحلهٔ رزرو و سپس صدور رسید دارد؛ رزرو با شناسهٔ r7 موفق شده است.
اجرای مرحلهبهمرحله: engine پیشرفت را پایدار میکند؛ بعد از crash از تاریخچه ادامه میدهد، اما فعالیتی که نتیجهاش ثبت نشده ممکن است دوباره فراخوانی شود.
نتیجه و دلیل: اثر بیرونی رزرو باید با r7 تکرارپذیرِ امن باشد؛ دوام workflow بهمعنی اجرای فیزیکی یکبارهٔ همهٔ APIها نیست.
The book’s example: a payment that must charge the card and deposit money to the bank account — separate services for fraud detection, card connectivity and bank connectivity. Running one payment = several service calls; this sequence is a workflow, each step a task (Temporal calls them activities; some say durable functions). Workflows are usually graphs of tasks, defined in a general language, a DSL, or BPEL.
- workflow engine تصمیم میگیرد هر task کِی و روی کدام ماشین اجرا شود، با خرابی چه کند و چقدر موازیسازی کند؛ از یک orchestrator (زمانبندی) و یک یا چند executor (اجرا) تشکیل شده. اجرا با trigger شروع میشود: زمانبندی، سرویسِ وب، یا انسان.
- خانوادهها: ابزارهای دادهای مثل Airflow / Dagster / Prefect (هماهنگسازیِ ETL)، ابزارهای گرافیکی مثل Camunda / Orkes با BPMN برای غیرمهندسها، و چارچوبهای durable execution مثل Temporal / Restate.
- A workflow engine decides when and on which machine each task runs, what to do on failure, and how much to parallelize; it is an orchestrator (scheduling) plus one or more executors (running). Runs start with a trigger: a schedule, a web service, or a human.
- Families: data tools like Airflow / Dagster / Prefect (ETL orchestration), graphical tools like Camunda / Orkes with BPMN for non-engineers, and durable execution frameworks like Temporal / Restate.
durable execution چه مشکلی را حل میکند؟What problem does durable execution solve?
در معماری سرویسی، نمیتوانی دو فراخوانیِ کارت و بانک را در یک تراکنشِ پایگاهدادهای بپیچی؛ ضمناً با درگاههای پرداختِ شخصِ ثالث هم کار میکنی.
اما میخواهی هر پرداخت دقیقاً یکبار پردازش شود. راهحل: چارچوب، همهٔ فراخوانیها و تغییراتِ state را در
ذخیرهسازیِ پایدار (مثل write-ahead log) ثبت میکند. اگر taskی شکست بخورد، دوباره اجرا میشود اما
گامهایی که قبلاً موفق بودهاند رد میشوند و نتیجهٔ قبلیشان برمیگردد — گویی فراخوانی تکرار نشده.
In a service architecture you can’t wrap the card and bank calls in one database transaction; you also work with third-party payment gateways. But you want each payment processed exactly once. The fix: the framework records every call and state change in durable storage (like a write-ahead log). If a task fails it re-runs, but previously successful steps are skipped with their old results returned — as if the call never repeated.
نمودار ۵ + روایت مدرسDiagram 5 + teacher narration
روایت مدرس از نمودار ۵Teacher narration
مثال فنی برای خواندنِ نمودار: ادامهٔ workflow بعد از crash
وضعیت اولیه: workflow مرحلهٔ رزرو و سپس صدور رسید دارد؛ رزرو با شناسهٔ r7 موفق شده است.
اجرای مرحلهبهمرحله: engine پیشرفت را پایدار میکند؛ بعد از crash از تاریخچه ادامه میدهد، اما فعالیتی که نتیجهاش ثبت نشده ممکن است دوباره فراخوانی شود.
نتیجه و دلیل: اثر بیرونی رزرو باید با r7 تکرارپذیرِ امن باشد؛ دوام workflow بهمعنی اجرای فیزیکی یکبارهٔ همهٔ APIها نیست.
شش شرکتکننده از چپ به راست:
«Checkout» (زیرنویس:
«workflow trigger»)،
«Workflow Engine» (زیرنویس:
«workflow runtime»)،
«Durable Log» (زیرنویس:
«write-ahead log»)،
«Fraud Check» (زیرنویس:
«internal service»)،
«Card Network» و
«Bank Service» (هر دو زیرنویسِ
«third-party API»).
دو نوارِ عنوان در چپ:
«First execution» و
«Crash recovery and replay».
Six participants, left to right: Checkout (workflow trigger), Workflow Engine (workflow runtime), Durable Log (write-ahead log), Fraud Check (internal service), Card Network and Bank Service (both third-party API). Two title bands at left: First execution and Crash recovery and replay.
در اجرای اول: «start payment workflow» از Checkout به موتور میآید. موتور گامها را یکییکی میفرستد:
«activity: check fraud» به Fraud Check و پاسخِ «not fraudulent»؛
بعد «append step result» به Durable Log (فلشِ چینچین — یعنی هر گامِ موفق اول ثبت میشود)؛
بعد «activity: debit card» به Card Network و پاسخِ «charged» و دوباره ثبت در لاگ.
گامِ سوم «activity: deposit funds» به Bank Service است که یادداشتِ زیرش میگوید:
«worker crashes mid-step» — همینجا worker میمیرد.
First run: start payment workflow arrives from Checkout. The engine sends steps one by one: activity: check fraud to Fraud Check, reply not fraudulent; then dashed append step result to the Durable Log (every successful step is recorded first); then activity: debit card to Card Network, reply charged, recorded in the log again. The third step, activity: deposit funds to Bank Service, is annotated worker crashes mid-step — the worker dies right there.
نوارِ دوم، قصهٔ نجات است: «re-execute workflow» (فلشِ چینچین از Checkout)،
بعد «replay recorded steps» از موتور به Durable Log — موتور میبیند check fraud و debit card قبلاً
انجام و ثبت شدهاند. پس فقط گامِ گمشده را میفرستد:
«deposit funds (only missing step)» به Bank Service، پاسخِ «deposited»،
و در نهایتِ خوش، «payment complete, exactly once» به Checkout.
Band two is the rescue story: re-execute workflow (dashed arrow from Checkout), then replay recorded steps from the engine to the Durable Log — the engine sees fraud-check and debit-card already done and recorded. So it sends only the missing step: deposit funds (only missing step) to Bank Service, reply deposited, and the happy end payment complete, exactly once to Checkout.
درسِ شکل: «دقیقاً یکبار» با تراکنشِ سنتیِ پایگاهدادهای به دست نمیآید؛ با لاگِ پایدار + بازپخش به دست میآید.
و البته بهایی دارد — همان چیزی که در بخشِ بعد میخوانید.
The lesson: “exactly once” doesn’t come from a traditional database transaction; it comes from a durable log + replay. And of course it has a price — which the next part covers.
چالشهای durable executionDurable-execution challenges
اجرای پایدار باید با تکرار مرحله، تغییر نسخهٔ کد و نتیجهٔ نامشخصِ سرویس بیرونی کنار بیاید.
مثال فنی: Replay با کد عوضشده
وضعیت اولیه: workflow قدیمی در تاریخچه ابتدا «رزرو» و سپس «رسید» دارد.
اجرای مرحلهبهمرحله: کد تازه اگر بدون نسخهبندی ترتیب تصمیمها را عوض کند، replay ممکن است با تاریخچه ناسازگار شود.
نتیجه و دلیل: دادهٔ تصمیم و نسخهٔ منطق باید قابلتفسیر بمانند؛ ساعت و تصادف و I/O نباید بیقاعده داخل replay تصمیمساز شوند.
- سرویسهای بیرونی (مثل درگاهِ پرداختِ شخصِ ثالث) باید هنوز idempotent باشند؛ تو باید با شناسههای یکتا جلوی اجرای تکراری را بگیری.
- چارچوب فراخوانیها را به ترتیب ثبت میکند و انتظار دارد اجرای بعدی دقیقاً همان فراخوانیها را به همان ترتیب بکند؛ پس جابهجاییِ دو خطِ کد میتواند رفتارِ بازپخشِ invocationهای در جریان را مبهم کند. راهحلِ امن: تغییرِ کدِ workflow را بهصورت نسخهٔ جدا deploy کنی تا invocationهای قدیمی با منطقِ قدیم بازپخش شوند.
- بازپخش باید قطعی (deterministic) باشد: همان ورودی ← همان خروجی. تولیدِ عددِ تصادفی یا خواندنِ ساعتِ سیستم ممنوع؛ چارچوبها نسخههای قطعیِ این توابع را میدهند و ابزارهایی مثل Workflow Check تخلف را پیدا میکنند. (بازگشت به بحثِ قطعیت در فصل ۹.)
- Outside services (like third-party payment gateways) must still be idempotent; you must block repeat runs with unique IDs.
- The framework records calls in order and expects the next run to make exactly the same calls in the same order; so swapping two code lines can blur the replay behavior of in-flight invocations. The safe fix: deploy workflow-code changes as a separate version so old invocations replay with old logic.
- Replay must be deterministic: same input → same output. Random numbers and system-clock reads are banned; frameworks give deterministic versions of these functions, and tools like Workflow Check catch violations. (Back to the determinism debate in Chapter 9.)
۴.۵ معماری event-driven4.5 Event-driven architecture
در معماری رویدادمحور، تولیدکننده خبرِ وقوع یک اتفاق را منتشر میکند و مصرفکنندهها مستقل به آن واکنش نشان میدهند. شکل و نسخهٔ رویداد همچنان یک قرارداد مشترک است.
مثال فنی: رویداد مستقل از مصرفکننده
وضعیت اولیه: سفارش 42 رویداد OrderPlaced را منتشر میکند.
اجرای مرحلهبهمرحله: گروه ارسال و گروه تحلیل هر دو آن را میگیرند؛ چند worker در گروه ارسال کار همان گروه را تقسیم میکنند.
نتیجه و دلیل: pub/sub تکثیر بین گروههاست و queue تقسیم کار داخل گروه؛ دریافت پیام هنوز به کنترل تکرار و خطا نیاز دارد.
- در این معماری، درخواست یک event یا message است و فرستنده معمولاً منتظر پردازشِ گیرنده نمیماند؛ پیام از یک message broker (event broker، message queue، MOM) میگذرد که پیام را موقتاً نگه میدارد.
- مزایای broker نسبت به RPCِ مستقیم: بافر بودن وقتی گیرنده در دسترس نیست یا overload است؛ تحویلِ مجددِ خودکار بعد از crash؛ بینیازی از service discovery؛ ارسالِ یک پیام به چند گیرنده؛ و جداییِ منطقیِ فرستنده و گیرنده (فرستنده فقط publish میکند).
- دو الگوی توزیع: queue (یک مصرفکننده از چند مصرفکننده پیام را میگیرد) و topic / pub-sub (همهٔ subscriberها نسخهٔ خودشان را میگیرند).
- brokerها مدلِ داده تحمیل نمیکنند: پیام = بایتها + metadata. رایج: Protobuf/Avro/JSON بههمراهِ یک schema registry؛ و AsyncAPI معادلِ OpenAPI برای پیام. نمونهها: RabbitMQ، ActiveMQ، NATS، Redpanda، Kafka (متنباز) و Kinesis، Azure Service Bus، Cloud Pub/Sub (ابری) — مقایسه در فصل ۱۲.
- دوامِ پیام: بسیاری روی دیسک مینویسند تا با crash از بین نروند؛ برخلافِ پایگاه داده، بسیاری بعد از مصرف خودکار حذف میکنند. برای event sourcing به broker نیاز داری که پیامها را تا ابد نگه دارد (فصل ۳).
- اگر مصرفکنندهای پیام را دوباره در topic دیگری publish کند، باید فیلدهای ناشناخته را حفظ کند — وگرنه همان بلای شکل ۵-۱ اینبار وسطِ pipeline میآید.
- actor model: بهجای thread و قفل و deadlock، منطق در actorها کپسوله میشود؛ هر actor state محلیِ خودش را دارد، با پیامِ ناهمزمان حرف میزند، یک پیام در هر لحظه پردازش میکند و تحویلِ پیام تضمینشده نیست. در چارچوبهای توزیعشده مثل Akka، Orleans و Erlang/OTP همان مکانیزم، محلی یا دور، یکسان است (پیام شفاف encode و در آنطرف decode میشود). location transparency اینجا بهتر از RPC جواب میدهد چون مدل از اول «پیام ممکن است گم شود» را میپذیرد. برای rolling upgrade باز هم به کدگذاریِ سازگار نیاز داری.
- In this architecture a request is an event or message and the sender usually doesn’t wait for the receiver’s processing; the message passes through a message broker (event broker, message queue, MOM) holding it temporarily.
- Broker advantages over direct RPC: buffering when the receiver is down or overloaded; automatic redelivery after a crash; no service discovery needed; sending one message to several receivers; and logical sender/receiver decoupling (the sender only publishes).
- Two distribution patterns: queue (one of several consumers takes the message) and topic / pub-sub (every subscriber gets its own copy).
- Brokers impose no data model: a message = bytes + metadata. Common: Protobuf/Avro/JSON with a schema registry; AsyncAPI as the message-world OpenAPI. Examples: RabbitMQ, ActiveMQ, NATS, Redpanda, Kafka (open source) and Kinesis, Azure Service Bus, Cloud Pub/Sub (cloud) — compared in Chapter 12.
- Message durability: many write to disk to survive crashes; unlike databases, many auto-delete after consumption. For event sourcing you need a broker keeping messages forever (Chapter 3).
- If a consumer republishes a message to another topic it must preserve unknown fields — otherwise Figure 5-1’s curse strikes mid-pipeline.
- Actor model: instead of threads, locks and deadlocks, logic is encapsulated in actors; each actor owns its local state, talks over async messages, processes one message at a time, and delivery isn’t guaranteed. In distributed frameworks like Akka, Orleans and Erlang/OTP the same mechanism works locally or remotely (messages transparently encode here, decode there). Location transparency works better here than in RPC because the model accepts “messages may be lost” from the start. Rolling upgrades still need compatible encoding.
| حالتِ جریان | چه کسی encode میکند | چه کسی decode میکند | سازگاریِ لازم |
| پایگاه داده | processِ نویسنده | processِ خواننده (شاید نسخهای قدیمیتر یا «خودِ آینده») | backward + forward (data outlives code) |
| REST / RPC | کلاینت (request) و سرور (response) | سرور (request) و کلاینت (response) | backward روی request، forward روی response (سرورها اول) |
| workflow / durable execution | هر task هنگام تکمیل | orchestrator هنگام replay | قالبِ لاگ باید برای بازپخشِ آینده پایدار بماند؛ کدِ workflow قطعی |
| broker / actor | فرستنده | subscriberها (شاید با نسخههای مختلف) | backward + forward؛ حفظِ فیلدِ ناشناخته هنگام republish |
نقشهٔ فصل — هفت ایده در یک نگاهChapter map — seven ideas at a glance
روایت مدرس از نقشهٔ فصلChapter-map narration
مثال فنی برای خواندنِ نمودار: رکورد قدیمی زیر کد جدید
وضعیت اولیه: نسخهٔ ۱ رکورد {name: "Sara"} نوشته و نسخهٔ ۲ فیلد email اضافه کرده است.
اجرای مرحلهبهمرحله: خوانندهٔ تازه برای رکورد قدیمی باید رفتارِ نبود email را بداند؛ خوانندهٔ قدیمی هم باید فیلد تازه را طبق قرارداد تحمل کند.
نتیجه و دلیل: آزمون سازگاری باید هر دو جهت خواندن را پوشش دهد؛ صرفِ موفقیت encode/decode با نسخهٔ یکسان کافی نیست.
گرهٔ میانی «کدگذاری و تکامل» است: از بالا «backward + forward» (با یال «خواندن»)، «قالبهای متنی» («بایتها») و «پروتوباف + Avro» («تگها»)؛ در میانه «رجیستری schema» («بررسی») و «ارتقاهای تدریجی» («انتشار»)؛ و در پایین «سرویسها + RPC» («فراخوانی») و «رویدادها + workflowها» («جریان»). پیام نقشه: قالب، قراردادِ خواندنِ «کدِ هنوزنوشتهنشده» از «دادهٔ دیروز» است.
The hub is “Encoding & Evolution”: on top “Backward + forward” (“reads”), “Text formats” (“bytes”) and “Protobuf + Avro” (“tags”); mid-row the “Schema registry” (“checks”) and “Rolling upgrades” (“ships”); below “RPC + services” (“calls”) and “Events + workflows” (“streams”). The map’s message: a format is the contract letting “code not yet written” read “yesterday’s data.”
| Dataflow mode | Who encodes | Who decodes | Needed compatibility |
|---|
| Database | Writer process | Reader process (maybe older, or “future self”) | Backward + forward (data outlives code) |
| REST / RPC | Client (request) and server (response) | Server (request) and client (response) | Backward on requests, forward on responses (servers first) |
| Workflow / durable execution | Each task on completion | Orchestrator on replay | Log format must stay stable for future replays; deterministic workflow code |
| Broker / actor | Sender | Subscribers (maybe mixed versions) | Backward + forward; preserve unknown fields on republish |