سرعت لود عکس های یک سایت تاثیر زیادی روی تجربهی کاربر داره. مدتی بود که بحث سرعت لود عکس ذهنم رو مشغول کرده بود و دوست داشتم در یک محیط آزمایشی یه سری تست و workaround حول این مسئله داشته باشم. بالاخره در یک آخر هفته ی بارونی اواخر بهار این فرصت پیش آمد.
به طور خلاصه من میخواهم با استفاده از یک سرویس جداگانه عکس ها را کراپ کنم و فایل های مربوط را به صورت توزیع شده در minio نگه میدارم (عکس های اصلی نیز در minio نگهداری میشوند). در این بین برای لود کردن عکس های کراپ شده سمت کلاینت، از یک لایهی cache مجزا بهره می برم (در اینجا از memcached استفاده می کنم). تاکیدی که خودم دارم این است که این پیاده سازی تا جای ممکن در پروداکشن و محیط عملیاتی قابل استفاده باشد و ابعاد مختلف آن را در نظر بگیرم. لینک ریپوی این مقاله:
https://github.com/sadeghmohebbi/nginx-minio-memcached-imagecaching
برای نوشتن این مقاله از منابع و داکیومنتیشن های مختلفی استفاده شده ولی می توان گفت که این مقاله، یک محتوای ارگانیک بدون دخالت ای آی است :)
خب بریم که شروع کنیم...
قابلیت جدید ورژن انقلابی ۲ به بعد آبجکت استورج MinIO این امکان را میدهد که بتوانیم یک Server Pool داشته باشیم. ما سناریو کلاستر Multi Node, Multi Drive را پیاده سازی کردیم تا هم در مقابل اختلالات دیسک و سرور مقاوم باشیم و هم بار ترافیک را بین آن ها تقسیم کنیم (مطالعه ی بیشتر) در فایل داکر کامپوز زیر، ما ۲ درایو و سه سرور minio داریم که با آن ها یک Server Pool روی پورت ۹۰۰۰ بالا آورده ایم. پنل آن را نیز در پورت ۹۰۰۱ پابلیش کردیم.
همانطور که در تنظیمات بالا مشخص است، سه تا instance از minio بالا آوردیم که با کامند minio server به هم وصل شدند یعنی در نتیجه ما سه تا node و دو تا volume داریم. الزاما در پروداکشن بایستی مسیر های data در دیسک های متفاوتی mount شده باشند و حتی می توان از سه vm جداگانه در سرور های جدا از هم نیز بهره برد به شرط اینکه شبکه ی بین سرور ها ظرفیت و سرعت کافی را داشته باشد. محیط آزمایشی دست ما را بسته است :)
در آخر instance های minio در کانفیگ nginx به عنوان upstream تعریف شدند و nginx در پورت های ۹۰۰۰ و ۹۰۰۱ Listen میکند تا ترافیک را به سمت نود های minio لود بالانس و reverse proxy کند. (فایل کانفیگ minio.conf nginx را در ریپو ببینید)
با کامند docker compose up -d --build سرویس هامون سریع بالا اومد و پنل زیر مشاهده شد. امکانات پنل minio در این ورژن واقعا فوق العاده است. ?
در اینجا میتوانیم توکن هایی را تعریف کنیم و از طریق سرویس بک اند مان به وسیلهی minio sdk لایبرری به سرور minio واقع در پورت ۹۰۰۰ متصل شویم تا مثلا عکس پست ها، محصولات،بنر ها یا آواتار کاربران در این آبجکت استورج ذخیره یا به اصطلاح putObject شوند. ولی ما در آزمایشگاه مون چنین سرویس ای رو توسعه ندادیم. پس از طریق همین پنل پیش فرض، یک باکت برای عکس ها می سازیم و چند تا عکس آپلود می کنیم.
در ادامه در بخش تنظیمات باکت ، با افزودن دسترسی خواندن برای anonymous ، توانستیم عکس ها را از مسیر تعریف شده ببینیم. خیلی شیک و خفن!
خب تا اینجای کار، عکس ها به صورت توزیع شده ذخیره و از مسیر لود بالانسر nginx در دسترس قرار گرفتند. حال تنها کافی است که از این عکس ها در سمت کلاینت استفاده کنیم. سمت کلاینت یا فرانت اند یک نیازمندی وجود دارد و آن کراپ عکس هاست با توجه با اینکه کاربر با دیوایس های متنوع ای به سایت ما مراجعه می کند و سایت نیز در اندازه های متفاوتی عکس هارا به کاربر نشان میدهد.
برای پیاده سازی این قابلیت، اول از همه ماژول image_filter خود nginx را امتحان کردم. یکپارچه سازی این ماژول با minio اینقدر دردسر داشت که بیخیالش شدم!
پس از قدری گشتن، یه ابزار خوب و پیشرفته به اسم imgproxy پیدا کردم. جزو معدود ابزار هایی بود که با s3 amazon (بخونید minio) کار می کرد و به همین دلیل رفتم سراغش. یک بنچ مارک با siege هم ازش دیدم برق از سرم پرید! در همزمانی ۱۰۰ و ۵۰ ریکوئست هر کدام (جمعا ۵۰۰۰ هزار ریکوئست) تنها ۲۰۰ مگابایت رم استفاده کرده. از بقیه ی امکاناتش هم نگم براتون مثلا مانیتورینگ با Prometheus و هلث چک ، انواع مختلف image proccessing و transformation (البته یه سری از موارد خفن اش پولی بود) ، افزودن واترمارک (لوگوی سایت گوشه ی عکس) و تبدیل فرمت و کم حجم سازی و غیره.
به گفته ی خودش سریع و ساده و امنه ولی به نظرم فقط سریع و امنه و ابدا ساده نبود :(
برای اینکه بتوانید عکس ای را به صورت کراپ شده از imgproxy دریافت کنید، بایستی آدرسی را با فرمتی خاص تشکیل و رمزنگاری کنید. فایده ی این کار این است که فرد هکر با ابزار هایی در دسترس نتواند لود غیر واقعی روی سیستم از طریق پراسس کردن تعداد زیادی عکس ایجاد کند. اینجای قضیه کمی طولانی و پیچیده است و پیشنهاد می کنم به این مقاله مراجعه کنید که یک ابزار ایجاد لینک imgproxy را معرفی کرده است. من با استفاده از این ابزار، لینک زیر را ایجاد کردم و نتیجه این شد:
همه ی آن پیچیدگی که در بخش قبل برای ساختن آدرس عکس دیدیم را در اینجا باید در سرویس بک اندی پیاده سازی کنیم. با یک جستجوی ساده به لایبرری imgproxy برای node js رسیدم. ازش استفاده کردم و خیلی تمیز و خوب کار کرد، برای زبان های دیگر مثل روبی و گو هم لایبرری ها و کدبلاک هایی وجود داشت. در اینجا باید اسم عکس ها در دیتابیس ذخیره و در سرویس بک اند، آدرس های عکس (کراپ شده و تنظیم شده) ایجاد شود (که ما از پیاده سازی این بخش صرف نظر کردیم)
استفاده از کش برای شرایط ترافیک بالا ضروری است و برای عکس ها همیشه
در این مرحله سیل عظیمی از ابزار ها و کانسپت ها وارد می شود. یکی از ابزار های قدیمی و معروف کش memcahced است که به شکل یک دیتابیس in-memory توزیع شده عمل می کند. با کانفیگ زیبای زیر توانستیم عکس ها را کش کنیم، برای تست آن، من یک عکس جدید را در مرورگر بدون کش براوزر باز کردم و رفرش کردم، لود دفعه ی اول عکس با دفعات بعدی ۲ تا ۳ برابر سریع تر شد. در لاگ ها هم به طور کامل مشخص بود که فقط اولین ریکوئست به imgproxy رسیده و سایر درخواست ها توسط memcached جواب داده شده است.
جهت یادآوری، تمامی کد های این مقاله (حتی دو تا عکس استفاده شده در مثال ها) در ریپازیتوری زیر موجود است:
https://github.com/sadeghmohebbi/nginx-minio-memcached-imagecaching
در مسیر نوشتار این مقاله، بار ها گیر خوردم و اشتباه کردم تا کار به اینجا رسید. در همین وضعیت هم می توان اشکالات و بهبود های فراوانی در آن دید. به هر حال دعوت می کنم که اگر ذوق زده شدید، راه مشارکت در بهبود این مقاله و کد ها فراهم است. Your Contribution is Welcome