۲۲ مرداد ۲۵۸۵ · 7 دقیقه مطالعه
کانفیگ byte به byte عین production بود. همین چیز اشتباهی رو ثابت کرد.
داشتیم یه OpenTelemetry Collector رو codify میکردیم که دستی نصب شده بود و هیچوقت تو هیچ repo ای ثبت نشده بود. راه امنش این نیست که یه config جدید از خودت دربیاری، اینه که همونی که الان داره کار میکنه رو دوباره بسازی. پس دوباره ساختیمش، و بعد کارمون رو به سختگیرانهترین شکلی که بلد بودیم چک کردیم: نتیجه رو byte به byte با config زندهی روی production مقایسه کردیم. مو نمیزد. YAML اش parse شد. terraform validate تمیز بود، lint تمیز بود، و چند تا review مستقل هم بدون هیچ ایرادی برگشتن.
بعد start شد و افتاد تو crash-loop.
هر چیزی که قرار بود یه config خراب رو بگیره اجرا شده بود و pass شده بود، و config بازم خراب بود. فاصلهی بین این دو تا واقعیت، کل حرف این پسته، و مخصوص collector یا Kafka هم نیست. حرف سر اینه که «byte-identical» واقعاً چی رو ثابت میکنه، که معلوم میشه یه چیز دیگهست غیر از اون چیزی که همهی آدمای توی اتاق فکر میکردن ثابت میکنه.
«byte-identical» چی رو verify کرد
مقایسه واقعی بود، و مفید هم بود. ثابت کرد که transformation از config در حال اجرا به نسخهی codify شده، وفادار بوده. چیزی نیفتاد، چیزی از نو نوشته نشد، هیچ key ای تو ترجمه drift نکرد. اگه دغدغهت اینه که «آیا source رو دقیق بازتولید کردیم»، یه diff از نوع byte به byte یه جواب کامل و صادقانهست.
ولی این یه حرفیه دربارهی کپی. نسخهی جدید رو به نسخهی قدیمی ربط میده و میگه یکیان. اصلاً هیچی نمیگه دربارهی یه خاصیت دومِ کاملاً جدا: اینکه این config برای اون چیزی که حالا باید روش اجرا بشه معتبره یا نه. اینا دو تا سؤال متفاوتن. وفاداریِ کپی، و اعتبار برای target. اتفاقی همون تهرنگِ سبز رو تولید میکنن، واسه همین راحته که اولی رو چک کنی و باور کنی دومی رو جواب دادی.
اینجا این دو تا بدجوری از هم فاصله گرفته بودن.
اون شکافی که کپی نمیتونست ببینتش
config واسه یه version خاص از collector نوشته شده بود. اون image ای که unit پین کرده بود حدود دو دوجین minor release جدیدتر بود. یه binary که config-driven ـه و config اش با هم یه contract ان، و این contract زیر یه config که یخزده مونده بود، جابهجا شده بود. سه تا چیز تو این release ها خراب شده بود:
- یه فیلد telemetry که config هنوز بهش reference میداد، حذف شده بود. واسه binary جدیدتر این فقط یه key ناشناختهست، و یه key ناشناخته سر startup کشندهست، نه اینکه نادیده گرفته بشه.
- اون
topicتوی Kafka receiver از یه string تکی تبدیل شده بود به یه list، در حالی که Kafka exporter همون شکل مفردش رو نگه داشته بود. این عدمتقارن بدترین نوعشه: یه find-and-replace ساده روی فایل، هر دو جای receiver رو «درست» میکنه و هر هشت جای exporter رو میشکنه، یا برعکس، و هر دو حالت هم مرتب به نظر میرسه. - یه تنظیم مربوط به اندازهی پیام producer، حالا از یه سقفِ نوشتنِ جدیدترِ سمتِ broker رد میشد، طوری که حتی یه start تمیز هم اولین باری که میخواست send کنه fail میکرد.
هیچکدوم اینا drift توی کپی ما نبود. تکتکشون وفادارانه و درست از یه source بازتولید شده بودن که خودش دیگه برای target معتبر نبود. diff دقیقاً کارش رو داشت انجام میداد و هیچی از اون چیزایی که لازم بود بشنویم بهمون نمیگفت.
چرا هر چک سبزی سبز بود
این همون قسمتیه که باید ملکهی ذهنت بشه، چون چکها بیدقت نبودن. داشتن سؤالهای خودشون رو درست جواب میدادن.
terraform validate چک میکنه که configuration و plan ات well-formed باشن. اون کامپوننتی که config توصیفش میکنه رو instantiate نمیکنه، اون image پینشده رو download نمیکنه، و اصلاً خبر نداره binary این image چه فیلدهایی رو قبول میکنه. validation مربوط به YAML، ساختار رو چک میکنه: این یه map ـه؟ اون یه list ـه؟ quote ها بالانس ان؟ نمیتونه بدونه که topic تو این version باید list باشه در حالی که تو version قبلی string بوده، چون «کدوم version» اصلاً تو YAML نیست. review ها چک کردن که config کدیفایشده با production یکی باشه، که بود، بینقص.
و عمیقترین دلیل، یه لایه پایینتره. این config رو binaryِ collector، سرِ startup میخونه، نه یه admission webhook و نه هیچ چکِ control-plane ای. پس یه config که واسه اون binary نامعتبره، بازم تمیز apply میشه. resource ساخته میشه، object قبول میشه، همهچی success گزارش میده، و بعد process یه ثانیه بعد config خودش رو میخونه و میمیره. این fail از هر gate ای که شانس داشت جلوش رو بگیره رد میشه و اونورش زندگی میکنه، چون هیچکدوم اون gate ها اون کامپوننتِ واقعاً پینشده رو start نمیکنن.
byte-identical، YAML معتبر، plan تمیز، review تأییدشده. چهار تا جملهی درست، که هیچکدومشون «این start میشه» نیست.
تنها چکی که یه معنایی داره
راهحل یه linter بهتر نبود. این بود که خودِ اون چیز واقعی رو اجرا کنی: اون binaryِ واقعاً پینشده رو بیاری، درست همون distribution (یه build دیگه از همون collector یه مجموعهی متفاوت از کامپوننتها میاره، پس اشتباهش یه pass ِ با اعتمادبهنفسِ الکی بهت میده)، بذاریش رو config واقعی با environment واقعی، و بذاری start بشه. این اولین چک تو کل زنجیرهست که کامپوننتهایی که config نامشون رو برده instantiate میکنه و اون contract ای که version پینشده واقعاً تعریفش میکنه رو اعمال میکنه. اون فیلد حذفشده، اون تغییرِ list شدنِ receiver، و اون خطاهای زبانِ config که validate از نظر ساختاری نمیتونه بگیره رو میگیره، چون validate هیچوقت یه کامپوننت رو build نمیکنه.
پس «binaryِ پینشده رو با این config start کن» شد یه قدمِ اجباری قبل از هر apply، و واسه بیشتر از codify کردنِ بارِ اول کاربرد داره. هر بار که image رو بالا میبری، contract رو جابهجا کردی و config رو یخ زدی. هر بار که یه فایل spec رو از یه جا کپی میکنی به یه جای دیگه، یه config رو از رو یه مرزِ version ای رد کردی که شاید اصلاً حواست بهش نبوده. هر دوش همون setupِ همون باگ اصلیه.
یه نسخهی کوچیکترش هم هست که به شکل warning ظاهر میشه نه crash. وقتی binary جدیدتر log میده که یه فیلد یا اسمِ کامپوننت به نفعِ شکلِ فعلیش deprecated شده، این نویز نیست که ازش رد شی. این همون fail ـه، یه release زودتر، که داره خودش رو اعلام میکنه در حالی که هنوز بیخطر میشه درستش کرد. وقتی اون چیز idle ـه rename ـش کن و یه restart هیچ هزینهای نداره؛ ولش کنی، اون release ای که بالاخره alias رو حذف میکنه، یه config که هیچکس دست بهش نزده رو تبدیل میکنه به یه config که دیگه start نمیشه.
چیزی که واقعاً باید ازش برداشت کنی
یه version رو pin کن و یه config رو کپی کن، و بیسروصدا دو تا چیز ساختی که باید با هم بخونن و جداگانه verify شدن: یه کپی که به source اش وفاداره، و یه source که شاید دیگه برای target اش معتبر نباشه. یه diff از نوع byte به byte، یه validate که pass میشه، یه lint سبز، و یه review تمیز، همگی میتونن اولی رو تأیید کنن، در حالی که تکتکشون نسبت به دومی کورن.
verify کن در برابر اون چیزی که pin کردی، نه اون چیزی که ازش کپی کردی. قانعکنندهترین چراغ سبزِ کل pipeline، یه جواب صادقانه بود به سؤالی که هیچکس تو اتاق واقعاً نمیپرسیدش.