این مقاله درباره چارچوب حذف دادهها است، چارچوبی که فیسبوک (یا متا همانطور که اکنون نامیده میشود) برای پایبندی به تعهدات خود در زمینه حذف دادههای کاربران ایجاد کرده است. این تیم مسئول ساخت زیرساختهای مورد نیاز برای اجرای حذفها است. به عبارت دیگر، چارچوب حذف دادهها چارچوبی است که مسئول حذفهای گرافی است. بنابراین تصور کنید میخواهید حساب فیسبوک خود را حذف کنید؛ ما باید زیرگراف منطقی که دادههای شما را نمایندگی میکند شناسایی کنیم و این دادهها در ماشینها و مخازن داده متعدد پراکنده شدهاند. هدف ما اطمینان از این است که پس از مدت زمان مشخصی هیچ اثری از شما باقی نماند. از نظر مقیاس، چارچوبهای حذف روزانه ۵۰۰ میلیارد حذف گراف جدید را مدیریت میکنند که معادل حدود یک تریلیون شیء منفرد است که روزانه حذف میکنیم و ما یک دوجین مخزن داده متصل را مدیریت میکنیم که کل گراف فیسبوک را در بر میگیرند. در نهایت، این مقاله نمایی کلی از بسیاری از سیستمها ارائه میدهد و قصد ندارد وارد جزئیات عمیق شود.
مروری بر طراحی
بدون مقدمه بیشتر، بیایید با مرور طراحی چارچوب حذف دادهها شروع کنیم. ابتدا میخواهم درباره اینکه چگونه به زبان تعریف داده (Data Definition Language) وابستهایم صحبت کنم. این یک درس اولیه در طول عمر این تیم بود؛ زیرا نمیتوانیم از توسعهدهندگان انتظار داشته باشیم که به حریم خصوصی یا مقررات توجه کنند. در ابتدا، وقتی شروع به انجام این کار کردیم، از توسعهدهندگان خواستیم منطق حذف خود را بنویسند. این امر منجر به مشکلات زیادی شد، اولین مشکل این بود که افراد در منطق حذف باگ نوشتند که میتواند به سرعت مشکلساز شود، زیرا میخواهید آن منطق بسیار قابل اطمینان باشد تا بدون هیچ مشکلی اجرا شود. منطق حذف و تعریف دادهها نیز به مرور از هم جدا میشد، زیرا تیمهای محصول محصولات خود و دادههایی که جمعآوری و نگه میداشتند را بهروزرسانی میکردند، اما منطق حذف را بهروزرسانی نمیکردند. بنابراین ما به تولید کد (Code Generation) منتقل شدیم. فیسبوک دارای یک زبان تعریف داده است که ما روی آن سوار شدیم و حاشیهنویسیهای حذف را اضافه کردیم. حاشیهنویسی که ما استفاده میکنیم پیکربندی ذخیرهسازی است که اساساً به ما میگوید دادهها کجا ذخیره میشوند. این مورد پیش از ما وجود داشت اما ما حاشیهنویسیهای لبه حذف (Deletion Edge Annotations) را اضافه کردیم، که به ما میگویند چگونه روی لبههای مشخص (کمعمق / عمیق / شمارش شده) در گراف حرکت کنیم و سپس محدودیتهای حذف (Deletion Constraints) که روشی برای محافظت از دادهها توسط توسعهدهندگان است.

اگر به مثال اینجا نگاه کنیم، سه طرح داریم: کاربر، پستها و کامنتها و میتوانیم ببینیم که بین آنها لبههایی وجود دارد. لبه “از کاربر به پست” چیزی است که ما آن را لبه عمیق مینامیم. این بدان معناست که وقتی این لبه را میبینیم، میخواهیم روی آن حرکت کنیم و بهصورت بازگشتی هر چیزی که در سمت دیگر آن لبه است را حذف کنیم. برعکس، لبه “از پست به کاربر” یک لبه کمعمق است، زیرا وقتی پست حذف میشود، ما در واقع نمیخواهیم نویسنده حذف شود. این حاشیهنویسیهای لبه هستند که به ما میگویند چگونه کل گراف را پیمایش کنیم. ما میتوانیم سایر حاشیهنویسیها روی طرحها را ببینیم، مانند ذخیرهسازی، که در این مورد Tao است، بزرگترین پایگاه داده گراف ما در فیسبوک، و سپس محدودیتها. اگر به مثال نگاه کنیم، محدودیت کاربر “هرگز بهصورت عمیق حذف نشود” است، که اساساً به این معناست که کاربر هرگز به عنوان بخشی از حذف شیء دیگر حذف نمیشود. کاربران همیشه شیء سطح بالا خواهند بود، بنابراین هرگز نمیخواهید هنگام حذف یک پست، کاربر را حذف کنید. این اساساً از پیکربندی نادرست جلوگیری میکند. در مورد طرح “پست”، گفته شده است “بهصورت عمیق توسط هر نهاد دیگر حذف شود”، که به این معناست که طرح پست لبههای عمیق را از هر کسی قبول میکند، اما حداقل یکی لازم است. کامنتها نوع دیگری از محدودیتها هستند که “توسط پست حذف میشوند”، که اساساً به توسعهدهندگان اجازه میدهد نوع اشیایی که میتوانند دادههایشان را حذف کنند محافظت کنند.
ما از همه این محدودیتها برای تولید کد استفاده میکنیم. {UserDeleter} و {PostDeleter} بهطور کامل تولید شدهاند. حذفکنندهها عمدتاً کار را به سایر بخشهای زیرساخت واگذار میکنند و در این مورد، کار را به چیزی که آن را حذفکنندههای اولیه (primitive deleters) مینامیم واگذار میکنند؛ نقاط تعامل بین چارچوب حذف و مخازن مختلفی که با آنها ادغام میکنیم. این حذفکنندههای اولیه توسط تیم چارچوب حذف نوشته شدهاند تا پوشش چارچوب حذف را گسترش دهند. اساساً، این حذفکنندههای تولید شده روی گراف حرکت میکنند و هنگامی که به یک برگ رسیدیم، یک حذفکننده اولیه را اجرا میکنیم که دستور حذف را به مخزن داده میفرستد.
پیمایش گراف
حال بیایید ببینیم چگونه واقعاً روی گراف حرکت میکنیم. ما از الگوریتم DFS استفاده میکنیم زیرا الگوریتم سادهای برای پیادهسازی است و اجرای آن ارزان تمام میشود. ما باید قابلیت ورود مجدد (reentrant) داشته باشیم زیرا گرافهای بسیار بزرگی را حذف میکنیم که نمیتوانیم در یک اجرا حذف کنیم. و ما نیاز به معنای حداقل یک بار (at least once) داریم، بنابراین هر شیء باید حداقل یک بار حذف شود.

اگر به مثال گراف نگاه کنیم که یک درخواست حذف روی یک پست داریم، چارچوب ابتدا دادهای را که میخواهیم حذف کنیم میخواند، در این مورد، پست. ما روی یک پشته پایدار (persistent stack) که در DFS مشاهده میکنید نقطه بازرسی میگذاریم، تنها تفاوت این است که این پشته در یک blob storage قرار دارد و این امکان ورود مجدد را فراهم میکند. فرض کنید کار نیمهتمام متوقف شود، میتوانیم حذف را از سر بگیریم، پشته را بخوانیم و دقیقاً بدانیم در کجای گراف بودیم. دادههایی که باید روی گراف حرکت کنیم نیز در این پشته پایدار حفظ میشوند. سپس قبل از هر حذف، لاگهای بازیابی ثبت میشوند تا بتوان دادهها را بازگرداند. سپس حذف خود شیء و پس از آن حذف گراف انجام میشود. ما تعریف طرح را میخوانیم و همه زیرنوعهایی که باید دریافت کنیم لیست میکنیم. اینجا، ارتباطی که به نویسنده اشاره دارد پیمایش نمیکنیم زیرا لبه کمعمق است. اگر در مورد کامنتها صحبت کنیم، آنها لبههای عمیق هستند، بنابراین ابتدا آنها را پیمایش میکنیم، محتوای سمت دیگر را میخوانیم، روی پشته پایدار قرار میدهیم، لاگ بازیابی ثبت میکنیم، شیء را حذف میکنیم و بازگشتی از آنجا ادامه میدهیم. در این مثال، فقط نویسنده وجود دارد که به عنوان نوع کمعمق حذف میشود. پس از پایان کار حذفکننده، آن را از پشته خارج میکنیم، به بالا بازمیگردیم، لاگهای بازیابی را ثبت میکنیم و در نهایت مسیر منجر به حذف آن اشیاء را حذف میکنیم. پس از آن، ما به تکرار ادامه میدهیم، اشیاء را هنگام حرکت به پایین حذف میکنیم، همه چیز را روی پشته قرار میدهیم، هنگام حرکت به بالا شیءها را خارج میکنیم و هنگامی که حذفکننده از پشته خارج شود، نشانه آن است که همه چیز حذف شده است. این اساساً نحوه عملکرد است. همانطور که دیدهاید، گردشهای زیادی درگیر است، بنابراین ما مفهوم batching را معرفی کردیم. تمام این اقدامات به ترتیبی انجام میشوند: چارچوب ابتدا مقدار زیادی داده میخواند و تا زمانی که داده کافی جمعآوری شود یا به حذفکنندهای برسیم که به ترتیب حذفها اهمیت میدهد، متوقف نمیشود. اکثریت حذفکنندهها به ترتیب اهمیت نمیدهند، اما بعضی اهمیت میدهند. سپس همه چیز روی پشته نقطه بازرسی میشود. این اجازه میدهد حذف را در صورت شکست هر حذف دوباره امتحان کنیم. سپس لاگهای بازیابی در یک رکورد بزرگتر ثبت میشوند، همه چیز بهصورت یکجا حذف میشود، ممکن است برخی شکست بخورند، در این صورت حذف متوقف شده و دوباره شروع میشود، سپس همه چیز از پشته خارج شده و ادامه مییابد. این اساساً به ما امکان میدهد خیلی سریعتر از حالت عادی عمل کنیم.
زمانبندی حذفها در آینده
اکنون سریع نگاه کنیم که چگونه حذفها را برای آینده زمانبندی میکنیم. این برای فعال کردن قابلیت گذرا بودن (ephemerality) در فیسبوک ضروری است. این همان چیزی است که داستانها یا دوره مهلت حذف حساب را امکانپذیر میکند (وقتی میخواهید حساب خود را حذف کنید، ۳۰ روز برای ورود مجدد برای لغو حذف فرصت دارید). ما از زمانبندی سالها در آینده پشتیبانی میکنیم، که برای سوابق مالی بسیار مفید است. همچنین از منطق TTL سفارشی پشتیبانی میکنیم که به ما امکان میدهد به عنوان مثال حذف یک پست را ۹ روز پس از آخرین کامنت زمانبندی کنیم. این نیاز به بررسی مداوم دارد و از نظر مقیاس، ما روزانه ۱۶۰ میلیارد رویداد را پردازش میکنیم. این بسیار سریعتر از بقیه عملیات حذف رشد کرده است.

این مروری سریع بر زیرساختها است. این واقعاً یک خط لوله داده بسیار بزرگ است اما برای جمعبندی، خط لوله تجمیع همه ایجادها را جمعآوری میکند، خط لوله وضعیت همه چیز در جریان را بررسی میکند، به موارد زمانبندی شده، شکستخورده و نیازمند زمانبندی توجه میکند. خطوط لوله retry و just in time مسئول انتقال موارد از خط لوله وضعیت و زمانبندی اجرای بعدی هستند.
تضمینها
اولین چیزی که درباره آن صحبت میکنم، زمانبندی است. حذف در دو مرحله اجرا میشود: یک بخش همگام (sync) که در درخواست وب اجرا میشود و یک بخش غیرهمگام (async) که روی گراف حرکت میکند. ما این را داریم زیرا اجرای کامل گراف در زمان درخواست وب ممکن نیست و در اکثر موارد بخش همگام فقط مسئول حذف اشیاء سطح بالا خواهد بود. برای انجام این کار، کافی است metadata را بنویسید، درخواست حذف را ثبت کنید، شیء سطح بالا را حذف کنید و سپس کار غیرهمگام را برای حذف بقیه زمانبندی کنید. اینگونه اطمینان حاصل میکنیم که میتوانیم درخواست حذف را بپذیریم و کمترین زمان ممکن را صرف کنیم تا کاربران متوقف نشوند. بخش همگام همچنین مسئول مخفی کردن زیرگراف است؛ بنابراین هر زمان پست را حذف میکنیم، میخواهیم همه کامنتها و پاسخها مخفی شوند، حتی اگر لینک مستقیم به محتوا وجود داشته باشد، میخواهیم چیزی باشد که مانع مشاهده شود. این کار را با استفاده از سیاستهای حریم خصوصی انجام میدهیم. آنها ویژگیای هستند که به لایه حریم خصوصی فیسبوک تعلق دارند، که توسط زبان تعریف داده ارائه میشوند. این لایه حریم خصوصی امکان میدهد فیسبوک بگوید عکسها مخفی هستند اگر شما دوست آن فرد نباشید. این لایه تمام بررسیهای حریم خصوصی را اجرا میکند که تعیین میکند موارد قابل مشاهده هستند یا خیر و ما از آن برای اعتبارسنجی زمان واقعی محتوا در حالی که منتظر حذف است، استفاده میکنیم. یک مسئله وجود دارد، و آن اینکه لایه حریم خصوصی از گراف حذف جدا است. ما در تلاش هستیم آنها را ترکیب کنیم تا با توجه به تعریف لبههای عمیق و حضور یا عدم حضورشان، بتوانیم مشخص کنیم آیا یک مورد باید روی پلتفرم قابل مشاهده باشد یا خیر. اما این یک مشکل بسیار پیچیده است و هنوز به اندازه کافی بالغ نیست. بنابراین، برای عملی کردن آن، هر بار که حذف را اجرا میکنیم، نمونهای از دادهها را بررسی میکنیم. سعی میکنیم دادهها را با زمینه کاربری فردی که حذف را زمانبندی کرده بارگذاری کنیم و اگر بتوانیم هر دادهای را بارگذاری کنیم، اساساً به این معناست که سیاستهای حریم خصوصی درست نیستند، زیرا ما نباید قادر به بارگذاری باشیم. سپس این موضوع را برای حل شدن به تیم داخلی منتقل میکنیم، اینگونه اطمینان حاصل میکنیم که دادهها به درستی مخفی شدهاند.
اتمام نهایی
این بخش مهم است، زیرا حذفها ممکن است با مشکلات زیادی مواجه شوند و حتی با وجود تضمینهای قابل اطمینان، کاربران ما میخواهند ما ۱۰۰٪ قابل اعتماد باشیم. برای انجام این کار، نیاز داریم تا شبکههای ایمنی ایجاد کنیم تا تمام مشکلات موجود را پوشش دهند. چه مشکلات زیرساختی، چه باگ در کد حذف، حذفهایی که توسط وابستگیها رها میشوند یا در نیمه راه شکست میخورند و در وضعیت ناسازگار باقی میمانند، همه اینها اهمیتی ندارند زیرا هر حذف شروع شده باید در نهایت تکمیل شود. بنابراین، ما هر رویداد از چرخه عمر حذف را در یک جدول بزرگ به نام تاریخچه حذف ثبت میکنیم و سپس خط لولهای داریم که وضعیت روزانه هر حذف در جریان را محاسبه میکند. از آنجا دو مجموعه داده استخراج میکنیم. اولین مجموعه، حذفهای غیر فعال (idle deletion) است که حذفهایی هستند که در مدتی اجرا نشدهاند. تمام کاری که باید انجام دهیم زمانبندی مجدد آنهاست. این اولین لایه دفاع است: سعی کنید مشکل را بهطور خودکار کاهش دهید. همچنین پایشهایی داریم که طولانیترین حذف اجرا نشده در کل چارچوب را شناسایی میکنند. این هشدارها وجود دارند زیرا حتی وقتی شبکههای ایمنی دارید، تمام اقدامات اصلاحی ممکن است شکست بخورند. این یک درس کلیدی است که ما آموختهایم. مجموعه دوم حذفهای گیر کرده (stuck deletions) است که اساساً همه حذفهایی را که با مشکلات مداوم زیرساخت مواجه هستند جمعآوری میکند. ما سعی میکنیم آنها را در واحدهای کاری جمعآوری کنیم تا پیشبینی کنیم چه کسی در شرکت بهترین فرد برای رسیدگی به این مشکل خواهد بود و وظیفهای به او اختصاص دهیم همراه با اطلاعات ابزار اشکالزدایی مورد نیاز برای اشکالزدایی آن حذف. همچنین یک برنامه کامل در بالای این داریم با SLAها تا مطمئن شویم مشکلات زیرساختی که مانع پیشرفت حذفها میشوند با SLA حل میشوند و این به نوبه خود اجازه میدهد ما بتوانیم SLAهایی در مورد زمان تکمیل برنامهها ارائه دهیم.
کامل بودن نهایی
این بخش دوم از کامل بودن نهایی است. کامل بودن نهایی اطمینان میدهد که کارها تمام شوند. کامل بودن نهایی اطمینان میدهد که همه دادهها حذف شدهاند، صرف نظر از اینکه کار قبلاً تمام شده باشد یا خیر. مسائل زیادی میتوانند منجر به دادههای یتیم، باگ در منطق حذف، شرایط رقابتی بین سیستمها یا پیکربندی نادرست حذف شوند و نیاز به پاکسازی دارند. راه حل ما، حذف مجدد اشیاء است. ما از همه اشیائی که از ابتدا حذف کردهایم اطلاع داریم، زیرا یک ستون auto-implement داریم که شناسه اشیاء است. از همه این اشیاء میتوانیم ارتباطاتی که به یا از آنها اشاره دارند را بازیابی کنیم و سپس منطق حذف را دوباره اعمال کنیم. اگر ارتباط عمیق باشد، گراف را پیمایش میکنیم و همه چیز سمت دیگر را دوباره حذف میکنیم. اگر کمعمق باشد، فقط آن را حذف میکنیم. ما هر دو هفته مهاجرتهایی روی هر نوع شیء در فیسبوک اجرا میکنیم و آنها تمام دادهها را مرور میکنند تا بهطور پیشگیرانه مسائل را کاهش دهند. به نظر میرسد یک آشفتگی بزرگ است اما بیش از آنچه انتظار دارید نیست، با توجه به سیستمهایی که داریم. این تنها عارضه داشتن سیستمهای متعدد و پیچیدهای است که هماهنگسازی آنها دشوار است.
بازیابیها
اگر سیستمهایی که حذفها را در مقیاس بزرگ انجام میدهند مدیریت کنید، از دست دادن داده اجتنابناپذیر است. ممکن است یک باگ محصول داشته باشید، مثلاً یک عنصر UI بهطور تصادفی حذف را زمانبندی کند یا مهندسینی داشته باشید که پیکربندی حذف خود را اشتباه کردهاند. برای کاهش این خسارت، قبل از هر حذف، بازیابیها را ثبت میکنیم و اینها کمی متفاوت از نسخه پشتیبان معمولی پایگاه داده هستند زیرا گراف شاخص دارد. یک گراف در این لاگها یک موجودیت است که میتوانید آن را بازیابی کنید و همچنین چندین منبع داده را شامل میشود، بنابراین میتوانید گرافی را بازیابی کنید که از چندین مخزن داده مختلف حذف شده است. نحوه کار به این شکل است که ما یک write-ahead log از جریان اجرای حذف داریم و یک tailer مسئول جمعآوری آنها در payloadهای بزرگتر است تا اقتصادی بودن نوشتن در مخزن داده منطقی باشد زیرا نمیتوانیم آنها را مستقیماً بنویسیم وقتی خیلی کوچک هستند. این منجر به یک مسئله جالب میشود که ما نسخههای پشتیبان دادههای حذف شده را نگه میداریم اما میخواهیم بسیار سختگیرانه مشخص کنیم که دادههای کاربران چه مدت نگه داشته شوند. اگر نسخه پشتیبان در یک مخزن داده باشد، آن مخزن ممکن است نسخه پشتیبان داشته باشد و سپس با مشکل منشأ داده مواجه میشویم. راه حل ما این است که از سرویس رمزگذاری استفاده کنیم که کلید رمزگذاری روزانه ارائه میدهد و تنها ما به آن دسترسی داریم. ما تمام دادهها را با کلید روز رمزگذاری میکنیم. این کلید TTL دارد، بنابراین پس از انقضای کلید، حذف از طریق رمزگذاری انجام میشود و هر کسی که ممکن است جایی دادهها را کپی یا لاگ کرده باشد، قادر به دسترسی نخواهد بود. اینگونه تضمین میکنیم که دادههای کاربران پس از مدت زمان مشخص حذف شوند.
ما روشهایی برای جلوگیری از از دست دادن داده داریم. هیچکدام کامل نیستند اما تحلیل ایستا روی گراف حذف انجام میدهیم، بررسیهای پویا هنگام اجرا بر اساس محدودیتهای حذف انجام میدهیم و پیشبینیهایی روی رفتار حذف لبهها اجرا میکنیم تا پیکربندیهای نادرست را که ممکن است بهطور دیگر دیده نشده باشند پیدا کنیم.
نظارت بر تضمینها
ما باید بتوانیم به کاربران و ارزیابان ثابت کنیم که واقعاً استانداردها را رعایت میکنیم. معیارهایی که پایش میکنیم شامل موارد زیر است:

ما یک مسیر موفق (happy path) داریم و باید قابلیت اطمینان آن را اندازهگیری کنیم، اما کافی نیست و باید بررسی کنیم چه میزان از مسیرها از دست میروند تا شبکههای ایمنی که بهطور خودکار مشکل را اصلاح میکنند، ایجاد شوند. این همچنین به این معناست که باید قابلیت اطمینان شبکههای ایمنی را بسنجیم و در صورت امکان، شبکههای ایمنی بیشتری پشت سر داشته باشیم. نکته مثبت این است که اگر روش اندازهگیری موفقیت کاملاً مستقل از سیستم شما باشد و چندین لایه شبکه ایمنی داشته باشید. در پایان، حذف واقعاً مسئله سختی است، مسیر موفق کافی نیست و باید انجام کار درست برای توسعهدهندگان آسانتر شود. این درس در امنیت نیز وجود دارد، جایی که میتوان مهندسان را آموزش داد اما با این حال اشکالات امنیتی رخ خواهند داد. ما باید حریم خصوصی را در لایه زیرساخت جاسازی کنیم، همانطور که امنیت را در لایه زیرساخت جاسازی میکنیم، تا وقتی مهندسان از این زیرساختها استفاده میکنند، به طور پیشفرض محصولات آگاه به حریم خصوصی یا امنیت ایجاد کنند.
بخش پرسش و پاسخ
س: آیا درباره حذف سخت صحبت میکنیم یا حذف یک محصول از سیستم تولید؟
ج: این حذف سخت است. و ما درباره حذف سخت هم از ترافیک تولید شده توسط کاربران (افراد با فشردن دکمه حذف) و هم سیستمهای تولید دیگر که ممکن است بخواهند کل نوع دادهها و گراف مرتبط با آنها را تکرار کنند، صحبت میکنیم.
س: سطح جزئیات حذف شما چگونه است؟ برخی کشورها و مقررات شرکتها را موظف میکنند برخی دادهها را نگه دارند. وقتی کاربری درخواست حذف دادهای میکند، شما باید برخی را حذف کنید، اما ممکن است برخی را نیز نگه دارید، بسته به کشور یا کاربر. چگونه این تفاوتها را مدیریت میکنید؟
ج: ما یک سیاست داخلی داریم که سندی است که توسط سیاستگذاران، وکلا و مهندسی نوشته شده و استانداردی را تعریف میکند که شرکت قصد دارد رعایت کند. وکلا مسئول هستند که اطمینان حاصل کنند این استاندارد با هر مقرراتی مطابقت دارد. بدین ترتیب، مهندسان نیازی به فهم مقررات ندارند، آنها فقط باید یک استاندارد را رعایت کنند.
