Kian
Kian
خواندن ۶ دقیقه·۴ سال پیش

تجربه‌ی شکست‌خورده‌ای در پایشِ خطا

هر کدی پُر است از حالت‌های «نباید رخ دهد»: این ورودی تابع نباید null باشد؛ اندازه‌ی این لیست و آن لیست باید همیشه برابر باشد؛ ... برخی از این حالت‌ها معمول‌اند و خوش‌خیم، و برخی نه.

  • مثال خوش‌خیم: در ورودی که از طرف کاربر می‌آید، هر چیزی ممکن است. برنامه تنها باید آن را هضم کند، مثلا رد کردن درخواست. چنین حالتی نشان‌گرِ bug در سیستم ما نیست.
  • مثال بدخیم: در داده‌ای که سِرور الف از سرور ب می‌گیرد، شما که کُد الف را نوشته‌اید مطمئنید که فلان field داده، در هر شرایطی نوشته خواهد شد. پس نبودِ آن در ب، یعنی bugای در سیستم. یا مثلا «سازگاری» بین داده‌ها، مثلا اگر فلان متغیر بزرگتر از صفر است پس حتما باید فلان متغیر دیگر هم set شده باشد (چنین وابستگی‌هایی بسیار مکروه است)، قرار نیست به هم بخورد مگر bugای در جایی از سیستم باشد.

برای این حالت‌های بدخیم، یعنی حالت‌های «منطقا هرگز نباید رخ دهد» چه می‌کنید؟ مشخصا این یادداشت درباره‌ی «کشف و خبررسانی» این حالت‌هاست، یعنی از پیش حالت خطا را پیش‌بینی و برایش در کُد چاره کرده‌اید (مثلا رد کردن درخواست)، ولی مهم است که از bug خبردار شوید تا به آن رسیدگی کنید. برای این بخش دوم چه می‌کنید؟

  • آیا با مشت آهنین assert با آن‌ها برخورد می‌کنید تا jobها بمیرند و خبردار شوید؟ در ابزار (tool) شاید، ولی در یک سرویس سَرپا نه. سرورها باید همیشه زنده بمانند، وگرنه ممکن است همه «هم‌زمان» پایین بروند: تصور حالتی که ماشه‌ی چنین فاجعه‌ای را می‌چکاند سخت نیست.
  • آیا log error می‌زنید؟ ولی log برای پایش (monitoring) نیست. log برای ذخیره‌ی اطلاعات اضافی است که اگر (و تنها اگر) به روش‌های دیگر از خطا باخبر شدید به آن مراجعه کنید؛ باید بدیهی باشد چرا. آیا exception می‌زنید؟ آن هم مانند log error.
  • آیا در کنار log زدن، یک شاخص (metric) شمارنده هم می‌گذارید که به بیرون از سرور صادر شده و سپس سیستم پایش مثلا Prometheus آن را تجمیع کند و هر وقت بزرگتر از صفر شد هشدار دهد؟

این آخری، تقریبا درست‌ترین کار است. ولی این روش به اندازه‌ی کافی «رادَست» نیست. باید کُدش را بنویسید و سپس در سیستم پایش برایش تجمیع و هشدار تعریف کنید در حالی که بیشترِ توسعه‌دهندگان اصلا بخش دوم را بلد نیستند و قرار هم نیست همه بلد باشند. همین باعث بی‌میلی و تنبلی افراد در این کار می‌شود و حالت‌های مهم خطا، مسکوت می‌ماندند؛ دست کم برای ما چنین بود.

ما چه کردیم

حل این یک مساله‌ی از دید تکونولوژی بسیار ساده است، ولی از دید رویه‌ای و فرایندی، نکته‌هایی دارد: هشداردهی درباره‌ی شرط‌هایی که نقضشان در کد حاکی از یک bug است در سیستمی با هزاران توسعه‌دهنده و ده‌ها تیم featureای و زیرساختی.

ما آن را به این شکل مثلا حل کردیم: مشخصا حالت‌های «این فرض (invariant) هرگز نباید نقض شود و رخ دادنش ناموسی‌ست - درستیِ سرویس در خطر است و باید فورا رسیدگی شود» که مهم‌تر و حیاتی‌تر از بقیه‌ی حالت‌ها مانند «این فرض هرگز نباید نقض شود ولی حالا یکی دو بار اشکال ندارد» و «این فرض هرگز نباید نقض شود ولی حالا اتفاق خاصی هم نمی‌افتد» است جدا شد، و برای آن حالت ناموسی، کتابخانه‌ای و دفتر و دستکی تعریف شد به نام invariantها (به معنی پایا و دگرش‌ناپذیر). موارد کاربردش از نامش واضح است. برای استفاده، توسعه‌دهنده صرفا یک تابع فرامی‌خواند و شرطِ همیشه درست (مثلا برابری طول دو لیست که نقض این شرط حاکی از یک خطای ناموسی‌ست) و اطلاعات اضافی (برای پیغام خطا) و غیره را به آن می‌دهد و بقیه‌اش به عهده‌ی کتابخانه است - تجمیع در سامانه‌ی پایش و غیره. از نظر فنی، کتابخانه‌ی ساده‌ایست. پیچ و خمِ ماجرا، فرایندی است.

یک: چنین چیزی باید بین صدها/هزاران توسعه‌دهنده جا می‌افتاد، ولی تمایزی که در بالا یاد شد (میانِ حالت‌های هرگز-نباید مهم و نامهم)، خیلی بدیهی نیست و نیازمند فکر عمیق از سوی توسعه‌دهنده است - و همین یعنی خطازایی.

  • چنین چیزی از پیش توسط طراحان کتابخانه دیده شده بود، و مستندسازی‌های کافی و حتی یک شورای بازبینی (که اگر یک changelist کلا حاوی یک invariant بود حتما یک نفر از این شورا را به بازبینان/reviewerها در سیستم بازبینی کد (مثلا gerrit) اضافه شود) تعبیه شده بود تا فرهنگ این invariantها در سازمان جا بیفتد. فرهنگ یعنی چه؟ یعنی توسعه‌دهنده بفهمد که هر «نباید»ی لایق invariant بودن نیست؛ هر invariantای باید حاوی اطلاعات کافی برای واکنش از سوی شخص هشدارگیرنده باشد؛ و این دست استانداردها.

دو: خواننده کنجکاو باید تا اینجا برایش سوال شده باشد که «هشدارها به چه کسی می‌روند؟». در یک سرویس کوچک و یک تیمِ همه با هم آشنا و همه به همه‌ی سیستم آگاه، مشکلی نیست اگر جعفر یک invariant بنویسد و هشدارش به هم‌تیمی‌اش کامبیز که گوش‌به‌زنگ (oncall) است برود. ولی در یک سیستم بزرگ و برساخته از چندین سرویس و ده‌ها تیمِ زیرساختی و محصولی چه؟

  • فقط برخی تیم‌ها (تیم‌های زیرساختی‌ها بیشتر، تیم‌های محصولی کمتر) چرخه‌ی گوش‌به‌زنگ دارند. اگر من از تیمی که ندارد، یک invariant تعریف کردم، هشدارش به چه کسی برود؟ قرار شد به تیمِ نگهداری‌کننده‌ی سروری برود که کد روی آن اجرا می‌شود و invariant روی آن نقض شده - بی‌ربط هم نیست چون تیم‌های دیگر که در سرور الف کد اضافه می‌کنند همیشه توسط نگهدارندگان سرور الف بازبینی می‌شوند و نگهدارندگان الف کم و بیش در جریانند.
  • بسیاری از invariantها در نقطه‌ی الف شکار می‌شوند ولی ناشی از یک bug در جای دیگری پیش از رسیدن به الف هستند. کجا؟ از پیش معلوم نیست. پس هشدار به چه کسی برود؟ چاره‌ای نیست جز نگهدارندگان الف.

سه: تیم‌ها باید می‌پذیرفتند تا این تکنولوژی را در کدهایشان به کار گیرند. برخی تیم‌ها استقبال کرندند ولی برخی بی‌میل بودند؛ یا به کار گرفتند و سپس پشیمان شدند و در پیِ حذفش برآمدند. ولی خلاصه با حمایت و چانه‌زنی رهبران، در سازمان ماندنی شد.

چرا شکست خورده

  • علی‌رغم همه‌ی مستندسازی‌ها و شواری بازبینی و غیره، باز در ذهن توسعه‌دهندگان جا نیفتاد که invariantها صرفا یک سازوکارِ رادَست برای «خبررسانی» نیستند - یک ساز و کار برای «محافظت از سیستم» در برابر خطاهای مهم هستند. قرار نیست شما چون صرفا حدس می‌زنید فلان فرض نباید نقض شود، invariant تعریف کنید و با خود بگویید حالا اگر هم شد، شد و چه بهتر: شخص گوش‌به‌زنگشان خبر می‌شود و به من خبر می‌دهد. این تمایز در ذهن همه‌ی توسعه‌دهنده‌ها جا نیفتاد. آیا از آغاز معلوم بود که جا نمی‌افتد؟ نه چندان. به نظر این قدر سخت نمی‌رسید.
  • برای برخی، تمایز جا افتاد، ولی نمی‌شد از پیش اهمیت یک حالت «نبایدی» را دقیق دانست. تعریف «فرضی که حتی یک بار هم نباید نقض شود» در تئوری شیک است ولی در عمل نمی‌توان تمایز آن با «هرگز نباید نقض شود ولی اگر تک و توک در هزاران درخواست بود، سیستم در خطر نیست» را از پیش دانست (یعنی هنگام نوشتن کد). آیا از آغاز معلوم بود که نمی‌توان؟ بله و نه. دودستگی بود؛ برخی می‌گفتیم «بابا نمی‌شه» و برخی (از جمله رهبران وقت) می‌گفتند «چرا می‌شه».
  • نویسنده‌ی invariant و گیرنده‌ی هشدار، معمولا از دو تیم متفاوت‌اند. این هم در طراحی دیده شده بود و یکی از نکات مهم invariant نوشتن این بود که پیغام خطا حاوی اطلاعات لازم برای واکنش و debug باشد. ولی اصلا شدنی نبود که از پیش این اطلاعات را فرموله کرد. عمده‌ی پیغام خطاها بهتر از این نمی‌توانست باشد: «اگر این invariant نقض شد یعنی جایی پیش از این نقطه‌ی کد، یک اشکال هست»! آیا از آغاز معلوم بود که نمی‌شود اطلاعات debugی کافی را فرموله کرد؟ در برخی نمونه‌ها می‌شد و همان‌ها رهبران فنی را به سوی خوشبینی سودار/biased کرده بود.

نهایتا invariantها از حالت «فوریتی» و «همیشه page کن» پایین آمدند چون نویزِ هشدارها خیلی بالا بود. شدند صرفا ticket. ولی در همین حالت هم، گوش‌به‌زنگ‌ها شاکی بودند و هستند چون کارشان شده صرفا یادآوری و پی‌گیری از تیمِ صاحب feature. یک وضعیت خراب.

از طرفی همین invariantها با وجود این نابهینگی‌های فرایندی، کاملا هم بی‌خاصیت نبودند و مشکلات مهمی برای ما کشف شد که اگر سیریش شدنِ گوش‌به‌زنگ‌ها به تیم صاحب feature نبود شاید نمی‌شد. این شده که فعلا مانند ارّه‌ی گیرکرده هستند ولی چون قابل تحمل‌اند و خیلی درد ندارند، اصلاح این وضعیت بارها در جلسه‌های برنامه‌ریزی و OKR بحث شده و هر بار بی‌نصیب مانده.

فعال در مهندسی نرم‌افزار با اندکی تجربه از صنعت (عمدتا گوگل) و آکادمی (عمدتا اتلاف عمر). علاقمندم آموخته‌هامون رو رد و بدل کنیم.
شاید از این پست‌ها خوشتان بیاید