Author: امین مصباحی
Site: امین مصباحی
Source: https://mesbahi.net/fa/blog/1405/05/24/16years-bug-sqlite-tailscale
⚠️ Article content is partial (Telegram Instant View delivered in segments).
باگ ۱۶ سالهی SQLite، و شش ماه زمان که Tailscale صرف پیدا کردنش کرد!
این ماجرای جالبیه از مشکل SQlite که سالها وجود داشته ولی بین این همه کاربر SQLite که از پروژه کوچیک شخصی تا زیرساختهای بزرگ ازش استفاده کردن؛ گریبانگیری Tailscale شد. داستان مهندسی پشتش و پیدا کردن دلیلش اینقدر جالب بود که به صورت کاملتر (با توضیحات اضافه که برای دوستانی که جزئیات دیتابیسها رو نمیدونن هم بار آموزشی داشته باشه و مبهم نمونه) 🔗 اینجا نوشتم.
من در مورد دیتابیس حداقل ۲ مورد مشابه رو تجربه کردم که باگ جدی گریبانگیر یه شرایط خیلی خاص شد؛ یکیش حتی به مکاتبه با Kimberly کشید و اونم گفت نمیشه کاری کرد؛ ولی چند ماه تعطیل کردن اکثر کارها برای تمرکز روی یک باگ و بارها سفر به جنوب منجر به حلش شد. و دومی ۳۶ ساعت کار بدون وقفه (در حد غذا و...) و جمع کردن حجم عظیمی از لاگها نتونست کمک کنه تا اینکه از جایی که عقل جن هم بهش نمیرسید ایراد رو پیدا کردم. متاسفانه در شرایط فعلی ترجیح میدم توضیح بیشتری ندم درحالیکه دوست دارم روزی با جزئیات، داستان و مسیر حلشون رو بنویسم.
این دو تا باگ؛ جزو تجربیاتی بود که به «یادگیری عمیق» من از انجین دیتابیس و لایههای پایینتر خیلی کمک کردن؛ این که دقیقا خود انجین چجوری برخی کارها رو انجام میده (خوندن این دو تا فایل از pg 8.1 و دهها فایل دیگه ازش کمک کرد یه بخشی از انجین دیگهای که کدش بسته بود رو با حدس بفهمم و برای منظور خاص بخشی از رفتار انجین رو طوری بازنویسی کنم که رفتار مورد نظرم رو انجام بده تا مسئله رو بشه حل کرد فایل ۱ و فایل ۲)
Tailscale چیه/کیه؟
Tailscale یک سرویس ایجاد شبکه بر پایهی WireGuard است؛ یعنی شما میتونید با استفاده از پروتکل وایرگارد، شبکه دلخواه خودتون رو بسازین و مدیریت کنید. مثلا دستگاههای شما را، هرجای دنیا که باشن، توی یک شبکهی مش (mesh) خصوصی به هم وصل بشن؛ بدون درگیر شدن با پیکربندی VPNهای سنتی، فورواردینگ پورت، یا تنظیمات پیچیده فایروالها. ایدهی اصلیاش اینه که هر دستگاه مستقیماً به دستگاههای دیگه وصل بشه، نه از طریق یک سرور مرکزی که ترافیک رو رله کنه. اما برای اینکه این اتصالات نظیر-به-نظیر شکل بگیره، دستگاهها باید جایی رو داشته باشن که فهرست بقیهی اعضای شبکه، کلیدها، و تنظیمات دسترسی رو ازش بگیرن؛ که بهش control plane تیلاسکیل میگن.
از بیرون، این control plane یک اندپوینت عمومی واحده: controlplane.tailscale.com. اما پشت صحنه، به صورت shard شده نگهداری میشه؛ سرورهای هماهنگکنندهی مستقلی که هرکدام مسئول مجموعهای از تیلنتها هستن. هر شارد یک پایگاهدادهی SQLite داره که تمام اطلاعات تیلنتهای روی اون شارد رو نگهداری میکنه، و فقط یک پروسهی Go بهصورت انحصاری به اون پایگاهداده دسترسی نوشتن داره. این دقیقاً الگوی single-writer است که SQLite براش طراحی شده، و تیلاسکیل از سال ۲۰۲۲ ازش استفاده میکنه؛ نه برای اینکه SQLite جذاب یا خاصه، بلکه دقیقاً برعکس: چون خیلی پایدار و قابلاتکاست. با این توضیح میشه حدس زد چقدر حجم دیتای زیاده و چقدر پایداریش مهمه.
وقتی یه «boring technology» هم به مشکل میخوره!
از پاییز سال گذشته، uptime تیلاسکیل افت محسوسی داشته. و ماجرا از یک خطا در پایپلاین بکاپ شروع میشه: هر چند دقیقه یک اسنپشات کامل از پایگاهداده گرفته میشده و به S3 آپلود میشده؛ روالی که از اوایل ۲۰۲۳ بدون مشکل کار کرده بود. یک روز، PRAGMA integrity_check روی یکی از این بکاپها خطا میده و پایگاهداده corrupt شده بوده.
خرابی SQLite اتفاق نادریه، اما در مقیاس بزرگ حتی اتفاقات نادر هم تکرار میشن. توی شش ماه، این اتفاق ۱۹ بار میافته. نکتهی خوب ماجرا این بوده که control plane فقط متادیتای تیلنتها رو نگهداری میکنه، و کلید رمزنگاری خصوصی یا ترافیک شبکهی کسی رو هرگز اونجا ذخیره نمیکنن؛ پس این اتفاق ریسک نشت داده نداشته. اما نکتهی بد این بوده که با هر خرابی، پروسهی control plane روی اون شارد باید متوقف میشد تا پایگاهداده تعمیر یا بازیابی بشه. طی این پنجرهی زمانی، دستگاههای آنلاینِ همون تیلنت به هم متصل میموندن، اما دستگاههای جدید نمیتونستن فهرست بقیه رو بگیرن و وصل بشن، و کنسول ادمین و API هم در دسترس نبوده.
شکار بدون سرنخ
چیزی که این باگ رو واقعاً سخت میکرده، نبودِ هیچ الگوی قابلتکرار بوده. تیم مهندسی تغییرات اخیر کدهاشون رو بررسی میکردن؛ ولی چیزی پیدا نمیشده؛ بخشهایی از کد که با SQLite کار میکرده سالها دستنخورده مونده بودن. دنبال وجه مشترک بین حوادث میگشتن؛ ولی مشکلات نه به یک شارد خاص محدود بود، نه به یک مشتری، نه به یک ویژگی، نه به ساعت خاصی از روز. حوادث هم فاصلهی زمانی منظمی نداشتن؛ گاهی چند ساعت، گاهی چند هفته، و یک بار حتی شش هفته سکوت کامل قبل از بازگشت باگ درست موقع کریسمس!
بدون محرک مشخص، بازتولید مصنوعی باگ ممکن نبوده. و تنها راه، گذاشتن تلهی خطایابی توی محیط پروداکشن و صبر برای دفعهی بعد بوده؛ دقیقاً همون کاری که هیچ تیمی دوست نداره روی یک پایگاهدادهی زنده انجام بده.
نکتهی جالب توجه اینه که که تیلاسکیل بهجای اکتفا به تلاش داخلی، یک قرارداد پشتیبانی حرفهای با توسعهدهندگان اصلی SQLite بست. این تصمیم بعداً معلوم شد نقطهی عطف ماجراست، چون باگی که در نهایت پیدا شد، خارج از کدِ خودِ تیلاسکیل، بلکه توی دل SQLite بود.
WAL و checkpoint: پسزمینهای که باگ توش زندگی میکرد
برای فهم خود باگ، اول باید دو مفهوم را روشن کنم.
دیتابیس SQLite از واحدهایی به اسم page تشکیل شده؛ بلوکهای کوچیک اطلاعات. وقتی رکوردی رو آپدیت میکنیم، برخی از این صفحات باید با نسخهی جدید جایگزین بشن (بسته به حجم رکوردهای تغییر یافته). حالت پیشفرض SQLite اینه که این جایگزینی مستقیماً توی فایل اصلی پایگاهداده انجام بشه. اما برای عملکرد و همزمانی بهتر، تیلاسکیل از حالت Write-Ahead Logging یا WAL استفاده میکنه: توی این حالت، صفحات جدید به جای فایل اصلی دیتابیس، اول توی یک فایل جداگانه به اسم WAL نوشته میشن. این یعنی پروسههایی که میخوان دادهها رو بخونن، همزمان با نوشتن، نسخهی سازگار قدیمی رو از فایل اصلی بخونن، بدون اینکه بلاک بشن. (همون چیزی که توی MySQL و PostrgreSQL و SQL Server و… داریم)