۲۷ شهریور ۲۵۸۵ · 7 دقیقه مطالعه
۸۴ تا repository ناپدید شدن. راهحل mkdir بود.
سه روز بعد از اینکه یه GitLab ی self-hosted رو migrate کردیم، یه همکار پرسید که دو تا project ناپدید شدن یا نه. web UI صفحههای project رو نشون میداد، لیست issue ها رو، member ها رو. database هنوز برای یکیشون ۱۲۵ تا commit و ۵۸۴ KB ثبت کرده بود. ولی هر کاری از git میخواستی، جوابش این بود: «A repository for this project does not exist yet».
موضوع دو تا project نبود. ۸۴ تا از ۵۱۷ تا در کل instance بود، ۳۴ تاشون فعال، از جمله سرویسهای production. سه روز هیچکس متوجه نشده بود، چون repository های آسیبدیده، تقریباً بر اساس تعریف، همونهایی بودن که اخیراً هیچکس بهشون دست نمیزده.
نشونهای که میگه هیچکس چیزی رو پاک نکرده
اولین واکنش به شنیدن «repository ها ناپدید شدن» سراغ یه deletion گرفتن است. ولی داده چیز دیگهای میگفت، و split اونقدر تمیز بود که میشد حسابش کرد یه قانون.
record هر project سالم بود: issue ها، merge request ها، member ها، event ها، تعداد commit درست، حجم درست تو database. ولی layer ی git که زیرش بود، اصلاً نبود. وقتی metadata زنده میمونه و داده نه، فرضیهی deletion غلطه؛ یه چیزی تو storage layer اشتباه جابهجا شده. اگه metadata هم میرفت، معناش یه deletion بود. metadata زنده با دادهی گمشده یعنی یه transfer یا یه mount، و بهت مسیری که byte ها رفتن رو نشون میده، نه کارهای هیچکس رو.
همین split نذاشت investigation ساعت اولش رو دور کنه روی اینکه چه کسی چی کار کرده، و به جاش نشونش داد migration.
git واقعاً چی لازم داره
یه git repository «فایلها» نیست. یه graph از object هاست، بهعلاوهی یه مجموعه reference که به داخل همون graph اشاره میکنن. reference ها زیر refs/ زندگی میکنن، با branch tip ها تو refs/heads و tag ها تو refs/tags، به شکل سنتی یه فایل کوچیک برای هر reference.
همین که repository ها بزرگ میشن، housekeeping اون reference ها رو، همونجوری که object های loose پک میشن، به یه فایل واحد ی packed-refs پک میکنه. یه optimization ی روتینه و از دید هیچ کاربری از repository دیده نمیشه. ولی همین که packing تموم شد، توی refs/heads و refs/tags هیچ فایلی نیست. دو تا directory خالیان.
git همچنان قبل از اینکه به یه directory بگه repository، لازم داره refs/ وجود داشته باشه. پس repository هایی که کاملترین نگهداری رو داشتن، یعنی اونهای packed، دقیقاً همونهایی بودن که directory یی که git میخواستش توش خالی بود.
S3 چیزی به اسم directory خالی نداره
migration درخت storage ی Gitaly رو از طریق S3 stage کرده بود و با aws s3 sync برگردوندش. اون مسیر دلایل خوبی برای انتخابش داشت: هیچ تغییری تو routing نبود، هیچ تغییری تو security group نبود، هیچی تو مسیر network عوض نمیشد، و یه sync که تو هر دو جهت incremental ـه، پس اون شکل pre-seed بعدش delta از یه freeze window ی کوتاه جون سالم به در میبره.
ولی key های S3 flat ان. تو object model اصلاً directory وجود نداره؛ directory یه inference ایه که یه client از key prefix ها درش میکشه، و یه directory که هیچ فایلی زیرش نباشه هیچی نداره که اون inference ازش دربیاد. پس نمیرسه.
هیچی از این ماجرا تو یه byte count دیده نمیشه. حجم دو طرف رو باهم مقایسه کرده بودیم و دو طرف موافق بودن، چون directory خالی هیچ byte ای نگه نمیداره. تکتک object ها سالم منتقل شدن. اون دو تا directory خالی که git لازمشون داره سمت destination اصلاً وجود نداشتن، و git از پذیرفتن اون درخت بهعنوان repository سر باز میزد، در حالی که تکتک object های تکتک commit ها همونجا نشسته بودن.
این همون بخشیه که ارزش internalize کردن داره. کپی وفادار بود. دقیقاً همون چیزی رو برد که contract ش میگه حمل میکنه: object ها. اون contract با filesystem یکی نیست، و چیزی که معنی رو حمل میکرد filesystem بود.
حسابی که یه حدس رو تبدیل به root cause کرد
یه مدتی این انتخاب تصادفی به نظر میاومد؛ شکلی که آدم رو به فرضیهش شک میندازه. چرا این ۸۴ تا؟ بعضیهاشون سالها idle بودن، ولی یکیشون تا شش هفته پیش بهش push زده شده بود. تصادفی بودن همون شکلیه که یه تئوری غلط از درونش دیده میشه.
بعد شمردیم. ۵۱۷ تا repository با محتوا. ۴۳۳ تاشون فایل reference ی loose داشتن، پس directory های refs/ شون فایل داشت و به همین دلیل از sync جون سالم به در بردن. ۸۴ تا نداشتن. ۵۱۷ میشه ۴۳۳ بهعلاوهی ۸۴، بدون هیچ باقیماندهای.
آسیب کاملاً تابع این بود که housekeeping اون repository رو پک کرده بود یا نه. برای همینه که به سمت repository های قدیمی و idle خم شده بود و باز هم یه چیز اخیر رو گرفته بود: packing تابع history ـه، نه فعالیت فعلی. یه correlation که از شمردهشدن دقیق جون سالم به در ببره، root cause ـه؛ باقیمانده همون proof ـه.
check هایی که سبز موندن
اینجا اون قسمت ناخوشایندشه. exit criteria ی migration امضا و تأیید شده بود، و check هایی که توش اسمشون برده شده بودن پاس شده بودن. تکتک signal های سلامت هر سه روز سبز بودن.
| Check | واقعاً چی کار میکنه |
|---|---|
gitlab:gitaly:check | یه خط: سرویس رو probe میکنه، هیچوقت یه repository رو باز نمیکنه |
gitlab:check | حدود ۵۶۰ خط configuration، connectivity، namespace ها |
مورد گمراهکننده gitlab:check ـه. به ازای هر project یه خط چاپ میکنه که از هر جهتی شبیه یه check ی per-repository ـه. زیر «Projects have namespace» فایل شده. هیچوقت به یه repository دست نمیزنه. هر دو check موقعی که ۱۶ درصد repository های instance خوانا نبودن clean برگشتن، و فردا هم زیر همون fault باز clean برمیگردن.
task هایی که واقعاً چک میکنن git:fsck ـه، که integrity تکتک repository ها رو verify میکنه، و git:checksum_projects، که reference های هر project رو checksum میکنه. هیچکدوم جزء gitlab:check نیستن، و دقیقاً برای همینه که اجرا نشدن. exit criteria اسم دو تا task یی رو برده بود که اثباتاً نمیتونستن خرابیای که قرار بود gate ش کنن تشخیص بدن.
checksum_projects ابزار درست برای دور هر migration ایه، و برای گرفتنش لازم نیست بدونی چی خراب شده: قبل از جابهجایی سمت source اجراش کن، بعدش سمت target، و diff شون کن. یه refs/ افتاده فوراً خودش رو نشون میده. verification فقط چیزی رو proof میکنه که واقعاً تست میکنه، و ارزونترین موقع برای فهمیدن اینکه یه check چی تست میکنه، قبل از اونه که بهش تکیه کنی.
کجا generalize میشه، و اون blind spot
recovery برای هر repository چهار تا mkdir بود. نه restore، نه restart، و هیچ جایی هم دادهای از دست نرفت. همون لحظهای که directory ها وجود اومدن، برگشتن.
قانونها، به ترتیب اینکه چقدر خرجمون دادن:
directory خالی دادهست. هر درختی که structure ش معنی حمل کنه، یه storage root ی git، یه mail spool، یه application directory با یه lock directory توش، باید با ابزاری جابهجا بشه که contract ش filesystem رو هم شامل بشه. درخت رو با tar بریز توی S3، بهجای sync فایلبهفایل، و کل مشکل محو میشه، چون tar دایرکتوریها رو بهعنوان entry حمل میکنه، نه اینکه از key ها inference شون کنه.
یه check سبز فقط evidence دربارهی سوالیه که اون check میپرسه. exit criteria ی یه migration یه لیست از سوالهاست، و اگه هیچکس نچکسته باشه که check ها واقعاً چی تست میکنن، ممکنه اون لیست، لیست سوالهای غلط باشه.
و اون صادقانهش. قبل از این migration، مسیر sync بررسی شده بود و یه فهرست از چیزهایی که بیصدا drop میکنه نوشته شده بود: hard link ها، symlink ها، ownership. تکتک آیتمهاش اون موقع با دقت اندازه گرفته شده بود. directory خالی تو لیست نبود. اون note دقیقاً حین همون migration نوشته شده بود که بعداً از همین مسیرِ بعینه استفاده کرد، و اون یک آیتم غایب از یه فهرست که جز این یکی دقیق بود، همون آیتمیه که ۸۴ تا repository رو شکست.
یه فهرست از چیزهایی که یه ابزار drop میکنه فقط تا جایی خوبه که blind spot هاش خوب باشن، و لبهها از درون نامرئیان، قبل و بعد دقیقاً به همون یه شکل. check ی که میتونست بگیردش یه command ـه، و جاش جلوی هر جابهجاییای سمت object storage ـه: find <tree> -type d -empty | wc -l. اگه اون عدد صفر باشه، sync میتونه درخت رو حمل کنه. اگه نه، اون عدد لیست چیزهاییه که میخوان گم بشن، نوشتهشده قبل از اینکه برن، نه بعدش.