دیوار چگونه از میلیون‌ها عکس نگهداری می‌کند؟

این پست به قلم مهدی خراط زاده و مرتضی حسینی از توسعه‌دهندگان بکند دیوار، به جهت آشنایی مخاطبان با فضای کاری دیوار نوشته‌شده است. مهدی خراط زاده از حدود دو سال و اندی پیش به دیوار پیوسته و در این میان تجربه‌ی کار در تیم‌های مختلفی از جمله بررسی آگهی و زیرساخت را داشته است. مرتضی حسینی نیز از حدود دو سال پیش در جمع دیوار حضور دارد و تجربه‌‌ی کار در تیم‌های سرچ و سابمیت، زیرساخت و املاک در دیوار را داشته است. مهدی از زمان حضورش در دیوار به گفته‌‌ی خودش با تکنولوژی و ابزارهای بسیاری آشنا شده و در بالا نگه‌داشتن و افزایش اتکا‌پذیری سرویس‌های دیوار نقش داشته است. مهدی ارزشمند‌ترین تجربه‌ کاری ش را زمانی می‌داند که در شرایطی که بار سنگین روی سرویس‌های دیوار بوده، توانسته در توسعه و نگهداری سیستم‌ها نقش داشته‌باشد. به نظر مرتضی مهم‌ترین تجربه کاری او، کسب توانایی کار تیمی اثر بخش و رسیدن به یک سیستم مقاوم در برابر سختی‌هایی مثل بار و درخواست زیاد از سمت کاربران بوده‌است.این پست بیان تجربه از سری کارهایی است که به انجام رساندیم تا شرایط توسعه را برای ورتیکال‌ها راحت‌تر کنیم. مهم‌ترین جذابیت کار برای ما این بود توانستیم سرویسی را پیاده‌سازی کنیم که عکس‌ها را فارغ ازین‌که مربوط به کدام دسته بوده، به خوبی مدیریت کرده به صورتی که توسعه پذیری و reliability بالا را برای ما فراهم کند. در نهایت اینکه پیاده‌سازی و ریویوی این سرویس حدود یک ماه در کنار کارهای دیگر تیم از ما زمان برد. پیاده‌سازی این سرویس در تیم خودرو، و ریویوی آن با همکاری زیرساخت دیوار انجام گرفت. با این مقدمه‌، به سراغ شرح این نوشته می‌رویم.عکس‌ها در دیوار چگونه‌اند؟ در دیوار روزانه حدودا ۵۰۰ هزار آگهی سابمیت می‌شود. با توجه به اینکه هر آگهی می‌تواند دارای یک یا چند عکس باشد در مجموع به تعداد ۱ میلیون و ۳۰۰ هزار عکس در دیوار، روزانه بارگذاری انجام می‌شود. در عین حال با افزایش تعداد ورتیکال‌ها و تیم‌ها در دیوار نیاز به اینکه هر تیم بتواند بدون تاثیر روی سایر تیم‌ها، عکس‌های خود را مدیریت کند بیش از پیش احساس شده‌است. منظور از ورتیکال‌ تیم‌هایی هستند که با تمرکز بر یک دسته‌بندی خاص مانند خودرو، املاک و..سعی می‌‌کنند مشکلات کاربر‌های آن دسته‌بندی را حل نموده یا برای‌شان ارزش جدید تولید کنند.عکس‌های دیوار به صورت کلی شامل چند دسته‌ی زیر است:۱. عکس‌های صفحه مدیریت آگهی که فقط گذارنده‌ی آگهی به آن‌ها دسترسی دارد.۲. عکس‌های آگهی‌های منتشر شده۳. عکس‌های مختص سرویس‌های جانبی دیوار (مانند لوگوهای کسب و کار‌ها، عکس‌های کارشناسی خودرو، عکس املاک و ...)از جهت سیکل حضور عکس‌ها در دیوار به صورت کلی با ۲ دسته عکس روبرو هستیم:دسته اول شامل عکس‌هایی بوده که هنوز به حالت نهایی نرسیده‌اند و در چرخه‌های تکمیل نشده (سابمیت آگهی، اضافه کردن لوگو و ...) هستند و دسته دوم شامل عکس‌‌های نهایی شده (آگهی‌های سابمیت شده و ...) می‌باشد.برای اینکه بتوانیم در به کار بردن اصطلاحات راحت‌تر باشیم عکس‌های دسته اول را عکس‌های temp و عکس‌های دسته دوم را عکس‌های اصلی نام‌گذاری می‌کنیم.در ادامه رویکرد دیوار در مواجه با مساله مدیریت عکس‌ها را بررسی می‌‌‌کنیم.برخورد نزدیک از نوع «اول»: مدیریت عکس‌ها تیم‌های جدیدی در دیوار تشکیل دادیم که تجربه‌های تازه و ارزشمندی را با خود به همراه آوردند.حضور تیم‌های جدید باعث شد از لحاظ محصولی با محدودیت زمانی مواجه شویم چرا که قصد داشتند فیچرهای جدیدی را بر محصول پیاده‌سازی کنند. در نتیجه به این فکر افتادیم تعدادی از میکرو‌سرویس‌هایی که ما جزء میکروسرویس‌های هسته تلقی می‌کنیم را به روز رسانی کنیم تا علاوه بر‌ اینکه نیازمندی سرویس‌های جدید را برآورده کنند، در آینده بتوانند برای تیم‌ها محصولات جدیدی که در دیوار خلق می‌شوند، مورد استفاده قرار بگیرند.با تجربه‌ای که از نگهداری سرویس عکس دیوار داشتیم و با توجه به مشکلاتی که در محیط عملیاتی وجود داشت، تلاش کردیم تا در ابتدا مجموعه نیازمندی‌هایی تشکیل دهیم و سپس راهکارهای مختلف را بررسی کرده و راهکار مطلوب را انتخاب کنیم.پرسش بنیادین: چه می‌خواهیم؟ اگر بخواهیم نیازمندی های خود را به صورت کلی دسته بندی کنیم با موارد زیر روبرو خواهیم بود.بتوانیم عکس‌های ورتیکال‌های دیوار را به صورت جداگانه مدیریت کنیم به طوری که در یکدیگر اختلال ایجاد نکنند. لازم به ذکر است این روش باید در تمام ورتیکال‌ها یکپارچه انجام گیرد.عکس‌هایی که به یک آگهی مرتبط هستند (مانند عکس‌های کارشناسی یک خودرو که مربوط به آگهی همان خودرو در دیوار می‌شود) باید بتوانند مستقل از آن آگهی مدیریت شوند و با پاک شدن آن آگهی همچنان دسترسی مناسبی به آن عکس‌ها داشته باشیم.هنگامی که یک عکس توسط کاربر بارگذاری می‌شود قبل از اینکه آن آگهی ثبت شود یا به صورت کلی آن فرآیند تکمیل شود، در سیستم به صورت موقتی وجود دارد. اسم این‌گونه عکس ها را temp (موقتی) می‌گذاریم. باید بتوانیم اگر عکسی برای مدت زیادی temp باقی ماند آن را از سیستم حذف کنیم تا فضای اضافی مصرف نکنیم.بتوانیم عکس‌های اصلی (غیر temp) را در CDN ذخیره کنیم تا کاربر سریع‌تر بتواند به آن‌ها دسترسی داشته باشد.اگر عکسی از حالت موقتی خارج شد و در سیستم ماندگار و ذخیره شد، در پروسه انتقال این عکس، کاربر را منتظر تمام شدن عملیات نگذاریم و همیشه پاسخگوی درخواست عکس باشیم.آدرس عکس نهایی شده نباید قابل تولید باشد چون باعث می‌شود یک فرد خرابکار بتواند عکس‌های temp را وارد CDN کند و حجم آن اشغال شود.با دست‌های باز: انتخاب‌هاطبق نیازمندی ها، می‌خواهیم بتوانیم عکسهای temp و اصلی را موقع serve کردن تشخیص بدهیم (که به ترتیب باید در CDN، مورد cache قرار بگیرند یا نه). درخواست برای یک عکس temp یا اصلی با استفاده از Url درخواست داده شده، مشخص می‌شود. همچنین عکس‌ها را باید در Rados Gateway ذخیره سازی کنیم که API ای مشابه AWS S3 دارد و برای محل قرار گیری عکس‌ها می‌توانیم در آن bucket بسازیم. برای این منظور از سرویس کیسه زیرک استفاده می‌کنیم.برای انجام این کارها، چند راه حل وجود دارد:۱. دو باکت مختلف برای عکس‌های temp و اصلی داشته باشیم و درخواست‌های عکس های temp را به باکت خودش و عکس‌های اصلی را به باکت خودشان بزنیم (و cache را روی عکسهای اصلی بگذاریم).در این صورت پروسه‌ی جابجایی از حالت temp به اصلی نیاز به move کردن خواهد داشت و بنابراین این پروسه‌ی move کردن باید به صورت async (با کمک یک سیستم صف مانند RabbitMQ) انجام پذیرد. مشکلی که این موضوع به‌وجود می‌آورد این است که در بازه‌ای عکس‌ها حالت temp ندارند اما هنوز در باکت temp ها هستند. اتفاق مذکور به این سبب رخ می‌دهد که ما مجبور هستیم برای سریع‌تر کردن response time مان، جابجایی عکس‌ها را به صورت async انجام دهیم. برای حل این مشکل باید یک سرویس میانی وجود داشته باشد که اگر عکس در جای اصلی آن پیدا نشد، ریکوئست را به باکت temp ها نیز بفرستد. این مورد، نیازمند خواندن از دیتابیس خواهد بود چرا که ما باید مطمئن شویم عکسی که هنوز منتشر نشده را به اشتباهی منتشر نکنیم و آن را در CDN نگذاریم.راه حل اول. استفاده از دو باکت کاملا جدا ۲. یک باکت برای هر دو حالت temp و اصلی داشته باشیم و در آن صورت همه‌ی ریکوئست ها را به یک سرویس میانی بدهیم و برای هر ریکوئست دیتابیس را چک کنیم و بسته به حالت عکس، عکس را برگردانیم یا برنگردانیم (چرا که نباید روی URLای که عکس‌های اصلی serve می‌شوند عکس‌های temp را برگردانیم). این روش نیاز به move را حذف می‌کند اما db hit بیشتری خواهد داشت.راه حل دوم. استفاده از یک باکت مشترک به همراه یک پایگاه داده کمکی ۳. یک باکت برای هر دو حالت temp و اصلی داشته باشیم و منتشر بودن یا نبودن عکس‌ها را به جای دیتابیس با رمزنگاری متقارن اسم عکس‌ها انجام دهیم. در این حالت، تشخیص آن‌که یک عکس منتشر شده یا خیر را در یک سرویس میانی انجام می‌دهیم که وظیفه آن serve کردن عکس‌ها و reverse proxy کردن به موقعیت واقعی عکس‌هاست.این اتفاق به این صورت انجام می‌گیرد که وقتی یک عکس با اسم A از حالت temp به حالت اصلی تغییر می‌کند، با استفاده از یک کلید اسم جدید B را برای آن از روی A بسازیم به صورتی که بتوانیم از روی ‌‌B نیز اسم A را تشخیص بدهیم و فردی غیر از ما، این تبدیل را نتواند انجام دهد. این کار را می‌توانیم با استفاده از symmetric key encryption انجام دهیم.در این حالت اگر کسی درخواست عکس temp را داد، از داخل باکت serve می‌کنیم و اگر درخواست عکس اصلی با اسم B را فرستاد، اول اسم A را از روی B پیدا می‌کنیم و سپس از داخل باکت عکس A را serve می‌کنیم.راه حل سوم. استفاده از یک باکت مشترک بدون استفاده از پایگاه داده کمکی مسیر سبز: انتخاب نهایی راه حل اول و دوم reliability کمتری دارند. چرا که هر دو برای پاسخ به درخواست‌ها نیاز به خواندن دیتابیس داشته و این احتمال از دسترس خارج شدن سرویس را بیشتر می‌کند. از طرفی زمانی‌که صرف خواندن از دیتابیس می‌شود، موجب می‌گردد مدت زمان بیشتری در کل فرآیند صرف کنیم.در عین حال راه حل سوم فقط یک بار به AWS S3 درخواست می‌دهد و این response time را نسبت به حالت اول بسیار بهتر می‌کند. همچنین در راه حل سوم نیازی به جابجایی عکس‌ها از فولدری به فولدر دیگر یا عوض کردن اسم‌ آن‌ها دیگر نیست.از بین راه های بررسی شده مسیری که در نهایت انتخاب کردیم را در ادامه بیان می‌کنیم. برای هر سرویس جدیدی که می‌خواهد عکس‌های مدیریت شده داشته باشد یک باکت در استوریج S3 درست می‌کنیم. هر تیم می‌تواند چند سرویس داشته باشد. داشتن یک باکت جداگانه این کمک را به ما می‌کند که می‌توانیم عکس‌های هر سرویس را بدون اینکه روی هم تاثیر داشته باشند، مدیریت کنیم. از طرفی چون صاحب هر باکت، تیمی مشخص است؛ اگر اشکالی در هر باکت پیدا شود (مانند Leakage عکس‌ها)، عیب‌یابی آن راحت‌تر خواهد بود.برای اینکه بتوانیم داده‌های بیش‌تری از عکس‌های وارد شده به سیستم داشته باشیم و هنگام بروز مشکل در سیستم دید بیش‌تری به روند ورود عکس‌ها داشته باشیم و همچنین در صورت عدم موفقیت در فرآیند انتقال عکس از temp به حالت اصلی بتوانیم عملیات را از اول انجام دهیم، از یک دیتابیس SQL با یک جدول که شامل ستون‌های زیر است استفاده می‌کنیم.Name: اسم عکس ذخیره شده روی سرور استSource: اسم سرویس صاحب عکس استState: وضعیت موقتی بودن یا نبودن عکس را مشخص می‌کندCreated at: تاریخ آپلود عکس را ذخیره می‌کندبرای پیاده سازی نیاز به دو API داریم:الف) Upload: این api سرویس مربوطه و عکس را می‌گیرد و برای عکس یک نام یکتا درست می‌کند و به صورت temp آن را در باکت سرویس مربوطه ذخیره می‌کند و نام تولید شده را برمی‌گرداند. همچنین یک رکورد به ازای این عکس در دیتابیس تعریف شده ایجاد می‌کند.ب) Make Permanent: این api عکس‌ها را از حالت temp خارج می‌کند و اسم جدیدی برای آن‌ها از روی اسم قدیمی‌شان می‌سازد (با استفاده از الگوریتم توضیح داده شده AES) و اسم‌های جدید را برمی‌گرداند.برای اینکه بتوانیم با توجه به مسیری که کلاینت از ما درخواست می‌کند عکس مناسب با آن مسیر را برگردانیم نیاز به یک سرویس میانی داریم که مسیر را به صورت temp یا اصلی می‌گیرد سپس عکس مناسب را پیدا می‌کند و به کاربر برمی‌گرداند. اسم این سرویس میانی را Resolver می‌گذاریم.همچنین به یک Job نیاز داریم که هر‌چند وقت یک بار عکس‌هایی که مدت زیادی temp بودند را پاک کند.برای اینکه CDN نیز داشته باشیم، رکورد‌های مسیر اصلی را وارد CDN می‌کنیم که درخواست‌ها را به Resolver ارسال کند و Resolver عکس‌ها را به CDN باز می‌گرداند تا در آن ذخیره شود. به این ترتیب فقط عکس‌های اصلی را در CDN ذخیره می‌کنیم.اثر شگفت‌انگیز یک مدیریت کارآمد پیاده‌سازی این سرویس تأثیر بسیار مثبتی در بهبود فرایند توسعه سرویس‌های جانبی دیوار داشت. این تأثیر مهم در جایی خود را نشان می‌دهد که دیگر برای ذخیره‌سازی عکس‌های سرویس‌هایی مانند کارشناسی خودرو نیاز به اضافه کردن شرط‌های اضافه در جای به جای کد نبوده‌ایم. این شرط‌های اضافه اکنون از جای جای کد جمع آوری شده و به صورت یک فرایند مشخص و به خصوص برای نگهداری عکس‌های هر سرویس جانبی دیوار درآمده‌است.از نظر کارایی، سرویس Image Resolution که برای serve کردن عکس منتشر شده با زبان برنامه‌نویسی Go پیاده‌سازی کرده‌ایم، زمان پاسخگویی قابل قبولی دارد. نمودار زمان پاسخدهی این سرویس در شکل زیر نمایش داده شده:علت انتخاب زبان برنامه‌نویسی ‌Go کارایی خوب این زبان در هندل کردن درخواست‌های با IO زیاد و نیز سادگی پیاده‌سازی منطق رمزگشایی و proxy کردن عکس‌های دیوار در این زبان بوده است.همچنین به علت مستقل بودن عملکرد این سرویس نسبت به دیگر زیرسیستم‌های دیوار، این سرویس آپ‌تایم مستقلی از دیگر بخش‌های دیوار داشته و این کمک نموده تا ورتیکال‌های دیوار مانند خودرو باعث ایجاد اختلال در سرویس‌دهی زیرسیستم‌های اصلی دیوار نشوند.حالا چه خبر...؟با گذشتن مدت خوبی از پیاده‌سازی این مسیر، چالش‌ها و نیازمندی‌هایی را مشاهده کردیم که می‌توانستیم در ابتدای مسیر برای آن‌ها چاره‌ای بیاندیشیم. در ادامه به تعدادی از آن‌ها اشاره می‌کنیم:با توجه به اینکه در سمت ذخیره سازی فایل کلاستر‌های متفاوتی داریم که بعضی نسبت به بعضی دیگر سریع‌تر هستند، اما هزینه‌ی بیشتری دارند، می‌توان عکس‌های temp را ابتدا در کلاستر کم هزینه‌تر ذخیره کرد سپس با نهایی شدن عکس آن‌ها را در کلاستر‌های سریع‌تر ذخیره نمود.راهکار Atomicی برای ذخیره‌سازی چند عکس نداریم. برای مثال اگر صفحه‌ای داشته باشیم که چند گروه متفاوت از کاربر عکس ورودی دریافت کند، راهکاری نداریم که اطمینان حاصل کنیم که تمام عکس‌ها با هم ذخیره شده باشد و تعدادی ذخیره نشده نداشته باشیم.حرف آخر اینکه برای حل هر چه بیش‌تر از چالش‌های پیش‌رو به یاری از سمت افراد تازه‌نفس و مشتاق به یادگیری نیاز داریم. امیدواریم مطالعه این پست به شما در آشنایی با چالش‌های فنی دیوار و شناخت مقیاس فنی چالش‌های ما کمک کرده باشد.برای پیوستن به دیوار به صفحه فرصت‌های شغلی ما سر بزنید.