پست

طراحی اپلیکیشن اشتراک‌گذاری عکس مانند Instagram

بررسی گام‌به‌گام طراحی Instagram شامل ساخت پست با رسانه، دنبال‌کردن کاربران، تولید فید با Fan-out ترکیبی، آپلود تکه‌ای فایل‌های حجیم، تحویل رسانه با CDN و مقیاس‌دهی برای 500 میلیون کاربر فعال روزانه

طراحی اپلیکیشن اشتراک‌گذاری عکس مانند Instagram

درک مسئله

Instagram یک پلتفرم شبکه اجتماعی است که عمدتاً روی محتوای بصری تمرکز دارد و به کاربران اجازه می‌دهد عکس و ویدیو را با دنبال‌کنندگان خود به اشتراک بگذارند.

طراحی Instagram یکی از رایج‌ترین سؤال‌های مصاحبه طراحی سیستم است که نه‌فقط در Meta، بلکه در همه شرکت‌های بزرگ فناوری پرسیده می‌شود. شباهت‌های زیادی با تحلیل‌های «فید خبری Facebook» (FB News Feed) و «Dropbox» دارد، اما با توجه به محبوبیت و تقاضا، تصمیم گرفتیم این مسئله تحلیل مستقل خودش را داشته باشد.

نیازمندی‌های عملکردی

نیازمندی‌های اصلی:

  • کاربران بتوانند پست‌هایی شامل عکس، ویدیو و یک عنوان (Caption) ساده بسازند.
  • کاربران بتوانند کاربران دیگر را دنبال (Follow) کنند.
  • کاربران بتوانند فید زمانی (Timeline) پست‌های کاربرانی را که دنبال می‌کنند ببینند.

موارد خارج از محدوده:

  • لایک و نظر دادن روی پست‌ها
  • جست‌وجوی کاربران، هشتگ‌ها یا مکان‌ها
  • ساخت و مشاهده Story (محتوای زودگذر)
  • پخش زنده (Live Streaming) ویدیو

نیازمندی‌های غیرعملکردی

اگر اغلب در برآورده کردن نیازمندی‌های غیرعملکردی مشکل دارید، فهرست «نیازمندی‌های غیرعملکردی رایج» را مرور کنید. فقط یادتان باشد بیشتر سیستم‌ها همه این ویژگی‌ها را دارند (تحمل خطا، مقیاس‌پذیری و غیره)، اما هدف شما شناسایی ویژگی‌های منحصربه‌فردی است که این سیستم را چالش‌برانگیز یا متمایز می‌کند.

قبل از تعریف نیازمندی‌های غیرعملکردی در مصاحبه، عاقلانه است درباره مقیاس سیستم بپرسید، چون روی طراحی اثر معناداری می‌گذارد. در اینجا به سیستمی با 500 میلیون کاربر فعال روزانه (DAU) و 100 میلیون پست در روز نگاه می‌کنیم.

نیازمندی‌های اصلی:

  • سیستم باید بسیار در دسترس باشد و دسترس‌پذیری عکس‌ها و ویدیوها را بر Consistency (سازگاری) مقدم بدارد (Eventual Consistency یا سازگاری نهایی مشکلی ندارد، حداکثر تا 2 دقیقه).
  • سیستم باید محتوای فید را با Latency (تأخیر) کم ارائه دهد (زمان پاسخ انتها به انتها کمتر از 500 میلی‌ثانیه برای درخواست‌های فید).
  • سیستم باید عکس‌ها و ویدیوها را فوراً ارائه کند (تحویل رسانه با Latency کم).
  • سیستم باید مقیاس‌پذیر باشد و 500 میلیون DAU را پشتیبانی کند.

موارد خارج از محدوده:

  • سیستم امن باشد و از داده کاربر (احراز هویت، مجوزدهی، رمزگذاری) محافظت کند.
  • سیستم تحمل‌پذیر در برابر خطا و بسیار قابل اعتماد باشد (بدون از دست رفتن داده).
  • سیستم تحلیل رفتار و تعامل کاربر را فراهم کند.

افزودن ویژگی‌های خارج از محدوده «اضافه‌ای مفید» است. تفکر محصولی را نشان می‌دهد و به مصاحبه‌کننده فرصت می‌دهد بر اساس آنچه می‌خواهد ببیند، شما را در اولویت‌بندی کمک کند. اما اگر این ویژگی‌ها زود به ذهنتان نمی‌رسد، وقت را هدر ندهید و ادامه بدهید.

آماده‌سازی

تعریف موجودیت‌های اصلی

با شناسایی موجودیت‌های اصلی که به آن‌ها نیاز داریم شروع می‌کنیم. من ترجیح می‌دهم قبل از ورود به جزئیات با یک مرور کلی سطح بالا شروع کنم، چون به ساختن پایه‌ای که روی آن بنا می‌کنیم کمک می‌کند. اگر شما کسی هستید که ترسیم کامل طرح (Schema) را از ابتدا با همه ستون‌ها و روابط مفید می‌دانید، آن هم کاملاً خوب است! رویکرد «درست» واحدی وجود ندارد و باید همان کاری را بکنید که برای شما بهتر جواب می‌دهد. نکته کلیدی این است که درک روشنی از بلوک‌های سازنده اصلی که به آن‌ها نیاز داریم داشته باشیم.

برای برآورده کردن نیازمندی‌های عملکردی کلیدی به این موجودیت‌ها نیاز داریم:

  • User (کاربر): اطلاعات کاربر مثل نام کاربری، جزئیات پروفایل و غیره را ذخیره می‌کند (فعلاً آن را حداقلی نگه می‌داریم).
  • Post (پست): شامل ارجاع به فایل رسانه، یک عنوان و کاربری است که آن را ساخته. مهم‌تر از همه، یک موجودیت Post واحد هم عکس و هم ویدیو را مدیریت می‌کند.
  • Media (رسانه): بایت‌های واقعی فایل رسانه. برای این کار از S3 استفاده می‌کنیم.
  • Follow (دنبال‌کردن): رابطه بین دو کاربر که در آن یک کاربر (دنبال‌کننده یا Follower) کاربر دیگری (دنبال‌شده یا Followed) را دنبال می‌کند. این رابطه یک‌طرفه «دنبال‌کردن» را نشان می‌دهد.

در مصاحبه واقعی، یک فهرست کوتاه مثل همین کافی است. فقط حتماً موجودیت‌ها را با مصاحبه‌کننده مرور کنید تا در یک مسیر باشید.

API یا رابط سیستم

API مهم‌ترین راهی است که کاربران با Instagram کار می‌کنند. تعریف زودهنگام آن به ساختاردهی بقیه طراحی کمک می‌کند. ساده شروع می‌کنیم و مثل همیشه می‌توانیم در حین حرکت جزئیات بیشتری اضافه کنیم. برای هر نیازمندی اصلی فقط یک endpoint می‌سازم.

اول، کاربران باید پست بسازند. برای آن از یک درخواست POST استفاده می‌کنیم:

1
2
3
4
5
POST /posts -> postId
{
  "media": {photo or video bytes},
  "caption": "My cool photo!",
}

آپلود واقعی عکس یا ویدیو را جداگانه و با استفاده از Presigned URL (آدرس از پیش امضاشده) انجام می‌دهیم. بعداً که درباره فایل‌های حجیم صحبت می‌کنیم، جزئیات اینکه چرا این کار را می‌کنیم می‌گوییم. فعلاً فقط روی API اصلی ساخت Metadata پست تمرکز می‌کنم. می‌توانیم فرض کنیم کلاینت یک postId برمی‌گرداند که بعداً از آن استفاده می‌شود.

در مرحله بعد، کاربران باید کاربران دیگر را دنبال کنند. این را به‌عنوان یک منبع رابطه مدل می‌کنیم:

1
2
3
4
POST /follows
{
  "followedId": "123"
}

شناسه Follower از توکن احراز هویت (JWT یا Session) استخراج می‌شود، پس لازم نیست در بدنه درخواست مشخص شود. این هم امن‌تر است و هم از اصل حداقل امتیاز (Least Privilege) پیروی می‌کند. در عمل، آوردنش اینجا خوب است، فقط باید پیش از دنبال‌کردن آن را با نشست مقایسه کنید.

در نهایت، کاربران باید فیدشان را ببینند. این یک درخواست GET است:

1
GET /feed?cursor={cursor}&limit={page_size} -> Post[]

برای صفحه‌بندی از Cursor و برای کنترل اندازه صفحه از limit استفاده می‌کنیم. جزئیات آرایه پست‌ها را فعلاً مبهم نگه می‌دارم و بعداً می‌توانیم پرش کنیم. نکته مهم این است که مجموعه‌ای از پست‌ها و یک next_cursor برای گرفتن صفحه بعد برمی‌گردانیم.

طراحی سطح بالا

برای طراحی سطح بالا، نیازمندی‌های عملکردی را یکی‌یکی پیش می‌بریم.

1) ساخت پست با عکس، ویدیو و عنوان

وقتی کاربر در Instagram فعال است، باید دکمه بزرگی برای ساخت پست جایی ببیند. با کلیک روی آن به صفحه یا Modal جدیدی می‌رود که رسانه مورد نظر برای آپلود و یک عنوان می‌خواهد و endpoint ‏POST /posts ما را فعال می‌کند.

می‌توانیم اجزای اصلی لازم برای پاسخ به این درخواست را بچینیم، با این فرض اولیه که رسانه آن‌قدر کوچک است که مستقیم با یک درخواست آپلود می‌شود.

  • Client (مشتری): کاربران از طریق وب‌سایت یا اپلیکیشن با سیستم کار می‌کنند. همه درخواست‌های کلاینت از طریق API Gateway به باطن می‌روند.
  • API Gateway (دروازه API): نقطه ورود کلاینت‌ها به میکروسرویس‌های مختلف سیستم است. وظیفه اصلی آن مسیریابی درخواست به سرویس مناسب است، اما می‌تواند دغدغه‌های مشترک مثل احراز هویت، Rate Limiting (محدودسازی نرخ درخواست) و لاگ‌گیری را هم مدیریت کند.
  • Post Service (سرویس پست): اولین میکروسرویس ما، مسئول مدیریت درخواست‌های ساخت پست. رسانه و عنوان را می‌گیرد، Metadata پست را در پایگاه داده و بایت‌های رسانه را در Blob Store ذخیره می‌کند و یک postId به کلاینت برمی‌گرداند.
  • Posts DB (پایگاه داده پست‌ها): Metadata هر پست را ذخیره می‌کند، از جمله اینکه چه کسی آن را ساخته، کی ساخته شده، عنوان و لینک رسانه در Blob Store.
  • Blob Store (ذخیره‌سازی باینری حجیم): بایت‌های واقعی رسانه را ذخیره می‌کند. ما S3 را انتخاب می‌کنیم، چون محبوب‌ترین راه‌حل است.

سریع مرور می‌کنیم وقتی کاربر پست آپلود می‌کند چه می‌شود:

  1. کلاینت یک POST همراه رسانه و عنوان به API Gateway می‌فرستد.
  2. API Gateway درخواست را به Post Service می‌رساند.
  3. Post Service رسانه و عنوان را می‌گیرد، Metadata پست را در پایگاه داده و بایت‌های رسانه را در Blob Store ذخیره می‌کند.
  4. Post Service یک postId به کلاینت برمی‌گرداند.

به‌اندازه کافی آسان است!

2) دنبال‌کردن کاربران دیگر

Instagram اجتماعی است! برای دیدن پست‌های افرادی که دنبال می‌کنیم، باید در وهله اول بتوانیم کاربران دیگر را دنبال کنیم.

مهم است که این رابطه یک‌طرفه است. مثلاً من @leomessi را دنبال می‌کنم، اما او (متأسفانه) من را دنبال نمی‌کند. پس پست‌های او را در فیدم می‌بینم، ولی او پست‌های من را نمی‌بیند.

می‌توانیم این رابطه را فقط با جدول Followers در پایگاه داده مدل کنیم که followerId و followedId را ذخیره می‌کند. هر بار که درخواست POST /follows جدیدی می‌رسد، یک ردیف جدید در جدول درج می‌کنیم.

یک Follow Service اختصاصی اضافه کرده‌ایم تا عملیات دنبال‌کردن و لغو دنبال‌کردن جدا از Post Service انجام شود. چون دنبال‌کردن کاربران کمتر از ساخت پست و مشاهده محتوا رخ می‌دهد، این جداسازی اجازه می‌دهد هر سرویس را مطابق نیازهای خودش بهینه و مقیاس‌دهی کنیم.

نظرهای زیادی درباره افرادی که نگران نوشتن دو سرویس در یک پایگاه داده‌اند می‌گیریم. اگرچه بعضی ادعا می‌کنند هر سرویس باید داده‌اش را اختصاصی داشته باشد، این مسلماً بیشتر آکادمیک است تا عملی. بسیاری از سیستم‌های تولیدی در شرکت‌های بزرگ فناوری سرویس‌هایی دارند که در یک پایگاه داده می‌نویسند. مبادلات ارزش درک دارد:

پایگاه داده جدا برای هر سرویس:

  • ✅ کپسوله‌سازی و ایزوله‌سازی قوی‌تر
  • ✅ مقیاس‌دهی مستقل منابع پایگاه داده
  • ✅ آزادی انتخاب فناوری‌های مختلف پایگاه داده برای هر سرویس
  • ❌ برای عملیات بین‌سرویسی به تراکنش توزیع‌شده یا Eventual Consistency نیاز دارد
  • ❌ پیچیدگی عملیاتی را بالا می‌برد (پایگاه داده بیشتر برای نگهداری)
  • ❌ هزینه زیرساخت بالقوه بیشتر

پایگاه داده مشترک:

  • ✅ تراکنش‌های ساده‌تر در مرزهای دامنه
  • ✅ حفظ یکپارچگی ارجاعی (Referential Integrity) آسان‌تر است
  • ✅ اغلب از نظر عملیاتی و توسعه ساده‌تر است
  • ❌ اتصال (Coupling) محکم‌تر بین سرویس‌ها
  • ❌ خطر اینکه کوئری‌های یک سرویس روی عملکرد سرویس دیگر اثر بگذارد
  • ❌ تغییرات Schema هماهنگی بیشتری می‌خواهد

در عمل، بسیاری از سازمان‌ها با پایگاه داده مشترک شروع می‌کنند و با پیدا شدن نیازهای مشخص به سمت ذخیره‌سازی ایزوله‌تر داده تکامل می‌یابند. برای طراحی Instagram ما، دسترسی Post Service و Follow Service به یک پایگاه داده رویکرد کاملاً معتبری است، به‌ویژه چون با مفاهیم دامنه مرتبط سروکار دارند و Coupling بین آن‌ها طبیعی است.

3) فید زمانی پست‌های کاربران دنبال‌شده

آخرین نیازمندی عملکردی اصلی ما، دیدن فید زمانی پست‌های کاربرانی است که دنبال می‌کنیم. با ساده‌ترین رویکرد شروع می‌کنیم و بعد محدودیت‌هایش را شناسایی و در بررسی‌های عمیق بهترش می‌کنیم. در ابتدا می‌توانیم:

  1. گرفتن Followed ها: جدول Follow را کوئری کنیم تا فهرست user_id هایی را که کاربر فعلی دنبال می‌کند بگیریم.
  2. گرفتن پست‌ها: برای هر کدام از آن کاربران دنبال‌شده، جدول Post را کوئری کنیم تا پست‌های اخیرشان را بگیریم.
  3. ادغام و مرتب‌سازی: همه پست‌های بازیابی‌شده را ترکیب و به ترتیب زمانی مرتب کنیم (بر اساس مهر زمانی یا postId).
  4. بازگرداندن: پست‌های مرتب‌شده را به کلاینت برگردانیم.

تصمیم می‌گیرم فعلاً به‌جای ساختن سرویس جدید، Post Service این کار را انجام دهد، ولی پاسخ درست یا غلطی وجود ندارد. شما بین پیچیدگی سیستم و هزینه عملیات مبادله می‌کنید.

توجه کنید اگر لازم باشد هر ردیف Follower برای هر کاربر را بررسی کنیم و بعد در هر ردیف Post دنبال موارد مورد نظر بگردیم، این کوئری‌ها بسیار کند خواهند بود. برای جلوگیری از اسکن کامل جدول می‌توانیم چند ایندکس به پایگاه داده اضافه کنیم.

چون قرار است درباره ایندکس‌گذاری صحبت کنیم، زمان مناسبی برای انتخاب فناوری پایگاه داده است. با توجه به مقیاس، تعداد محدود روابط و اینکه Eventual Consistency قابل قبول است، با DynamoDB جلو می‌روم. با این حال PostgreSQL یا بیشتر پایگاه‌های داده دیگر هم به همان اندازه انتخاب‌های معتبرند. فقط باید بتوانید تصمیمتان را توجیه کنید.

نکته جالب: Instagram از PostgreSQL به‌عنوان پایگاه داده Metadata اصلی پست‌هایش استفاده می‌کند. این جالب است، چون اگر بحث‌های SQL در برابر NoSQL چند سال گذشته را دنبال کرده باشید، ممکن است متقاعد شوید یک SQL DB نمی‌تواند برای پشتیبانی از 500 میلیون DAU ما مقیاس بگیرد. معلوم است که می‌تواند!

برگردیم به ایندکس‌گذاری: چون DynamoDB را انتخاب کرده‌ایم، برای جلوگیری از اسکن کامل جدول باید چند ایندکس به جدول‌هایمان اضافه کنیم. برای جدول Follow، Partition Key را followerId و Sort Key را followedId می‌گذاریم. این اجازه می‌دهد کارآمد همه کاربرانی را که یک کاربر مشخص دنبال می‌کند کوئری کنیم.

برای جدول Post، Partition Key را userId می‌گذاریم، چون بیشتر کوئری‌های ما برای گرفتن پست‌های یک کاربر مشخص خواهد بود. می‌توانیم Sort Key را ترکیب createdAt و postId بگیریم تا ترتیب زمانی و یکتا بودن هر دو تضمین شوند.

اینجا با رویکرد ساده ما چه می‌شود:

  1. کلاینت یک GET همراه Cursor و limit به API Gateway می‌فرستد.
  2. API Gateway درخواست را به Post Service می‌رساند.
  3. Post Service جدول Follow را کوئری می‌کند تا کاربرانی را که کاربر فعلی دنبال می‌کند بگیرد.
  4. Post Service جدول Post را برای هر کاربر دنبال‌شده کوئری می‌کند تا پست‌های اخیرشان را بگیرد.
  5. Post Service پست‌ها را ادغام و مرتب می‌کند و محدود به Cursor و limit به کلاینت برمی‌گرداند.

احتمالاً دارید فکر می‌کنید: «یک لحظه، اگر هزاران نفر را دنبال کنم چه؟ این خیلی کند می‌شود، حتی با ایندکس، درست است؟» و حق با شماست! دقیقاً همین مشکلی است که در بررسی عمیق حل می‌کنیم.

بررسی‌های عمیق

در این مرحله سیستم پایه‌ای و کارایی داریم که نیازمندی‌های عملکردی اصلی Instagram را برآورده می‌کند: کاربران می‌توانند عکس و ویدیو آپلود کنند، کاربران دیگر را دنبال کنند و فید زمانی ببینند. با این حال، طراحی فعلی محدودیت‌های مهمی دارد، به‌ویژه در عملکرد تولید فید و تحویل رسانه در مقیاس بالا. نیازمندی‌های غیرعملکردی را مرور می‌کنیم و بررسی می‌کنیم چطور سیستم را برای 500 میلیون DAU با Latency کم و توزیع جهانی بهتر کنیم.

1) محتوای فید با Latency کم (کمتر از 500 میلی‌ثانیه)

بررسی‌های عمیق را از همان‌جا شروع می‌کنیم که در طراحی سطح بالا ایستادیم: تولید فید. رویکرد فعلی ما می‌تواند برای اپلیکیشن کوچک کار کند، اما به 500 میلیون DAU نمی‌رسد.

اول بفهمیم چرا با پایگاه داده‌ای که انتخاب کردیم مشکل داریم. اولین نگرانی ما این است که برای کاربرانی که بیش از 1000 حساب را دنبال می‌کنند، به بیش از 1000 کوئری روی جدول Post نیاز داریم تا از هر حساب دنبال‌شده پست بگیریم.

DynamoDB عملیات دسته‌ای (Batch) ارائه می‌کند، اما هر دسته به 100 آیتم و 16 مگابایت داده محدود است. برای کاربری که 1000 حساب را دنبال می‌کند، دست‌کم به 10 عملیات دسته‌ای جدا برای کوئری پست‌های اخیر نیاز داریم. اگرچه این‌ها می‌توانند موازی اجرا شوند، هنوز چند مشکل بزرگ می‌سازد:

  • Read Amplification (تقویت خواندن): هر بار که کاربر فیدش را رفرش می‌کند، تعداد زیادی خواندن پایگاه داده می‌سازیم. با 500 میلیون کاربر فعال روزانه که چند بار در روز فیدشان را رفرش می‌کنند، این خیلی زود غیرقابل تحمل می‌شود و گران هم هست.
  • کار تکراری: اگر دو کاربر بسیاری از حساب‌های یکسان را دنبال کنند (که رایج است)، مدام برای پست‌های یکسان کوئری می‌زنیم. در مقیاس Instagram، پست‌های محبوب ممکن است میلیون‌ها بار واکشی شوند.
  • عملکرد غیرقابل پیش‌بینی: Latency درخواست فید با توجه به تعداد حساب‌هایی که کاربر دنبال می‌کند و میزان فعال بودن آن حساب‌ها به‌طور چشمگیری فرق می‌کند. این عملکرد یکنواخت را تقریباً غیرممکن می‌کند.

با چند عدد این را روشن می‌کنیم:

  • هر رفرش فید ممکن است نیاز به پردازش 10 هزار پست داشته باشد (1000 حساب دنبال‌شده ضرب‌در 10 پست در روز).
  • با 500 میلیون DAU، اگر هر کاربر 5 بار در روز فیدش را رفرش کند، 2.5 میلیارد تولید فید در روز می‌شود.
  • در اوج مصرف (مثلاً عصرها یا رویدادهای مهم)، ممکن است بیش از 150 هزار درخواست فید در ثانیه ببینیم.

با این اعداد روشن است که حتی با پیاده‌سازی خوب DynamoDB، برای رسیدن به هدف Latency برابر 500 میلی‌ثانیه به مشکل می‌خوریم. یا باید:

  • ظرفیت پایگاه داده را بیش از حد تأمین کنیم، که سیستم را بسیار گران می‌کند، یا
  • Latency بالاتر را در اوج مصرف بپذیریم، که نیازمندی ما را نقض می‌کند.

این ناکارآمدی ذاتی مدل Fan-out در خواندن (Fan-out on Read) است. مسئله اصلی این است که کار محاسباتی را به زمان خواندن موکول می‌کنیم، که دقیقاً وقتی است که کاربران انتظار نتایج فوری دارند.

الگو: مقیاس‌دهی خواندن (Scaling Reads). Instagram سناریوی عالی مقیاس‌دهی خواندن است: کاربران روزانه صدها پست می‌بینند اما فقط گاهی پست می‌گذارند. این نسبت شدید خواندن به نوشتن ایجاب می‌کند پست‌ها بر اساس شناسه کاربر Shard شوند، پارتیشن‌بندی عمودی برای انواع مختلف داده (پروفایل، پست، تحلیل) انجام شود و ذخیره‌سازی لایه‌ای (Tiered) برای محتوای قدیمی داشته باشیم.

چند جایگزین را بررسی می‌کنیم.

رویکرد خوب: کش ساده فید

آشکارترین بهبود برای رویکرد Fan-out در خواندن، اضافه کردن کش جلوی جدول پست‌هاست تا پست‌های اخیر هر کاربر کش شوند. برای این کار می‌توانیم از Redis استفاده کنیم. ایده ساده است: قبل از کوئری پایگاه داده برای پست‌های کاربران دنبال‌شده، کش را بررسی می‌کنیم. اگر پست‌ها در کش بودند، همان‌ها را برمی‌گردانیم. در غیر این صورت پایگاه داده را کوئری می‌کنیم و نتایج را برای درخواست‌های بعدی کش می‌کنیم.

می‌توانیم کش را با ترکیب user_id و یک Cursor (یا مهر زمانی) کلید بزنیم. این اجازه می‌دهد «صفحه» مشخصی از فید را از کش بگیریم. نمونه کلید: feed:{user_id}:{cursor} و مقدار آن فهرستی از postId ها خواهد بود.

چالش‌ها: کش کمک می‌کند اما چالش‌های مقیاس‌پذیری ما را اساساً حل نمی‌کند. برای رسیدن به نرخ Hit معنادار در مقیاس Instagram، کش باید بسیار بزرگ باشد و هنوز برای هر درخواست فید، خواندن‌های گران برای جمع‌آوری پست‌های همه کاربران دنبال‌شده انجام می‌دهیم. کش کردن بهینه‌سازی مفیدی است، اما به‌جای پرداختن به مشکل ریشه‌ای معماری Fan-out در خواندن، علائم را درمان می‌کند.

رویکرد خوب: فیدهای از پیش‌محاسبه‌شده (Fan-out در نوشتن)

روش بسیار بهتر این است که فیدها را از پیش محاسبه کنیم. به‌جای تولید فید هنگامی که کاربر آن را درخواست می‌کند (Fan-out on Read)، آن را هنگامی تولید می‌کنیم که کاربر پست می‌گذارد (Fan-out on Write).

روش کار این است: وقتی کاربر پست جدیدی می‌سازد، جدول Follow را کوئری می‌کنیم تا همه کاربرانی را که کاربر پست‌گذار را دنبال می‌کنند بگیریم. برای کارآمد شدن این کار، به یک Global Secondary Index (GSI) روی جدول Follow نیاز داریم که followedId را Partition Key و followerId را Sort Key دارد. این اجازه می‌دهد سریع همه دنبال‌کنندگان کاربری را که تازه پست گذاشته پیدا کنیم. برای هر دنبال‌کننده، postId جدید را به فید از پیش‌محاسبه‌شده‌اش اضافه می‌کنیم. این فید از پیش‌محاسبه‌شده را می‌توان در جدول اختصاصی فیدها (مثلاً در DynamoDB) یا در یک کش مثل Redis ذخیره کرد.

از Redis استفاده می‌کنیم. دسترسی خواندن و نوشتن بسیار سریع می‌دهد که برای فید عالی است. می‌توانیم فید هر کاربر را به‌صورت Sorted Set (در Redis همان ZSET) ذخیره کنیم. اعضا همان postId ها هستند و امتیازها مهر زمانی (یا خود postId ها، اگر زمانی قابل مرتب‌سازی باشند).

مدل داده (Redis):

  • کلید: feed:{user_id}
  • نوع: Sorted Set (ZSET)
  • اعضا: postId
  • امتیازها: مهر زمانی (یا postId)

وقتی کاربر فیدش را می‌خواهد، N پست برتر را از Sorted Set او در Redis می‌خوانیم که یک عملیات واحد و سریع است. با این حال هنوز باید بر اساس این postId ها پست‌ها را «هیدراته» (Hydrate) کنیم، یعنی Metadata کامل را بگیریم. برای این کار 3 گزینه داریم:

  1. برای هر postId در کش، Metadata را از جدول Post در DynamoDB بگیریم. این ساده است، اما برای هر درخواست فید یک کوئری اضافه به پایگاه داده می‌خواهد.
  2. به‌جای کش کردن postId، کل Metadata پست را در Redis ذخیره کنیم. به این ترتیب برای گرفتن Metadata پست به کوئری دوم روی جدول Post نیاز نداریم. این سریع‌تر است، اما حافظه بیشتری مصرف می‌کند و چالش‌های سازگاری داده می‌آورد.
  3. رویکرد ترکیبی با دو ساختار داده Redis: یکی برای postId های فید (ZSET) و دیگری برای کش کوتاه‌مدت Metadata پست (HASH). هنگام هیدراته کردن پست‌ها:
    • اول سعی کنیم همه Metadata های پست را از کش پست Redis بگیریم.
    • برای هر Cache Miss، با BatchGetItem در DynamoDB به‌صورت دسته‌ای واکشی کنیم.
    • کش پست Redis را با Metadata واکشی‌شده به‌روز کنیم.
    • نتایج ترکیبی را برگردانیم.

رویکرد ترکیبی (گزینه 3) بهترین تعادل بین عملکرد و مصرف منابع را می‌دهد. می‌توانیم TTL کش Metadata پست را بر اساس محدودیت حافظه و نیازمندی‌های سازگاری تنظیم کنیم. مثلاً اگر پستی به‌روز شود، مانند تغییر عنوان، کش آن پست را باطل (Invalidate) می‌کنیم و در درخواست فید بعدی دوباره پر می‌شود.

برای جمع‌بندی، هنگام ساخت پست جدید این اتفاق‌ها می‌افتد:

  1. Metadata پست را در جدول Post و رسانه را مثل قبل در Blob Store ذخیره می‌کنیم.
  2. postId را در یک صف می‌گذاریم تا Feed Fan-out Service ناهمگام پردازش کند.
  3. Feed Fan-out Service جدول Follow را کوئری می‌کند تا همه کاربرانی را که کاربر پست‌گذار را دنبال می‌کنند بگیرد.
  4. برای هر دنبال‌کننده، postId جدید را به فید از پیش‌محاسبه‌شده‌اش اضافه می‌کند.
  5. Feed Fan-out Service فید جدید را در Redis ذخیره می‌کند.

سپس هنگامی که کاربر فیدش را می‌خواهد:

  1. N پست برتر را از Sorted Set او در Redis می‌خوانیم.
  2. پست‌ها را بر اساس این postId ها هیدراته می‌کنیم. اول بررسی می‌کنیم Metadata پست در کش پست Redis هست یا نه. اگر هست، از آن استفاده می‌کنیم. اگر نیست، با BatchGetItem از DynamoDB واکشی می‌کنیم.
  3. نتایج را ترکیب می‌کنیم و به کاربر برمی‌گردانیم.

چالش‌ها: این رویکرد Fan-out در نوشتن عملکرد خواندن را به‌طور چشمگیری بهتر می‌کند و بازیابی فید را به یک جست‌وجوی سریع تک‌Redis (به‌علاوه جست‌وجوی Metadata) تبدیل می‌کند. اما پیچیدگی زمان خواندن را با پیچیدگی زمان نوشتن و افزایش فضای ذخیره‌سازی معاوضه کرده‌ایم. چالش اصلی تقویت نوشتن (Write Amplification) است، به‌ویژه «مشکل سلبریتی‌ها». پستی که فقط یک کاربر با میلیون‌ها دنبال‌کننده می‌سازد، میلیون‌ها نوشتن در Redis فعال می‌کند که احتمالاً سیستم را تحت فشار می‌گذارد و Latency نوشتن را برای کاربران محبوب بالا می‌برد. دلیلش این است که باید کش فید میلیون‌ها نفر را که او را دنبال می‌کنند به‌روز کنیم.

رویکرد عالی: Fan-out ترکیبی

می‌توانیم با رویکرد ترکیبی، محدودیت اصلی رویکرد پیش‌محاسبه، یعنی تقویت عظیم نوشتن برای سلبریتی‌ها را رفع کنیم. Fan-out on Write را برای بیشتر کاربران با Fan-out on Read برای حساب‌های محبوب ترکیب می‌کنیم. این تعادل خوبی می‌دهد: خواندن سریع برای بیشتر کاربران و تقویت نوشتن قابل مدیریت.

روش کار این است که برای تعداد دنبال‌کنندگان آستانه‌ای تعریف می‌کنیم، مثلاً 100 هزار دنبال‌کننده. برای کاربرانی که کمتر از 100 هزار دنبال‌کننده دارند، فید دنبال‌کنندگانشان را دقیقاً مثل روش «خوب» بالا از پیش محاسبه می‌کنیم. برای کاربرانی که بیش از 100 هزار دنبال‌کننده دارند («سلبریتی‌ها»)، پست‌هایشان را از پیش در فید دنبال‌کنندگانشان محاسبه نمی‌کنیم. به‌جای آن:

  • وقتی «سلبریتی» پست می‌گذارد، آن را به جدول Post اضافه می‌کنیم و به‌روزرسانی ناهمگام فید دنبال‌کنندگانش را فعال نمی‌کنیم.
  • وقتی کاربر فیدش را می‌خواهد، بخش از پیش‌محاسبه‌شده فید او (پست‌های کاربران با کمتر از 100 هزار دنبال‌کننده) را از Redis می‌گیریم. بعد جدول Post را برای پست‌های اخیر «سلبریتی‌هایی» که او دنبال می‌کند هم کوئری می‌کنیم. سپس فید از پیش‌محاسبه‌شده را با پست‌های اخیر سلبریتی‌ها ادغام (Merge) می‌کنیم و فید ادغام‌شده را برمی‌گردانیم.

پس در نهایت ترکیب مؤثری از پیش‌محاسبه و ادغام بلادرنگ داریم:

  • Fan-out on Write برای بیشتر کاربران (تعداد دنبال‌کننده کمتر از 100 هزار)
  • Fan-out on Read برای تعداد کمی کاربر «سلبریتی» (تعداد دنبال‌کننده بیشتر از 100 هزار)

چالش‌ها: همان‌طور که همیشه هست، پیچیدگی بیشتر مبادلات خودش را دارد. آستانه 100 هزار دنبال‌کننده باید با دقت تنظیم شود. اگر خیلی پایین بگذارید مشکل تقویت نوشتن را حل نمی‌کنید و اگر خیلی بالا بگذارید روی عملکرد خواندن برای تعداد زیادی کاربر اثر می‌گذارید.

کاربرانی که سلبریتی‌های زیادی را دنبال می‌کنند، بارگذاری فید کندتری نسبت به آن‌هایی که این کار را نمی‌کنند تجربه می‌کنند و تجربه کاربری ناهمگون می‌سازند. باید برای بخش‌های مختلف کاربران SLA های روشن تعیین کنیم و بالقوه کش اضافه مخصوص پست‌های سلبریتی‌ها پیاده کنیم. همچنین با نگه داشتن دو سیستم جدا پیچیدگی ذخیره‌سازی می‌آوریم: فیدهای از پیش‌محاسبه‌شده در Redis برای کاربران عادی و سیستم جست‌وجوی بلادرنگ برای پست‌های سلبریتی‌ها. این پیچیدگی عملیاتی را بالا می‌برد و نظارت دقیق بر هر دو سیستم می‌خواهد.

با وجود این چالش‌ها، این رویکرد ترکیبی در عمل مؤثر بوده است. گفته می‌شود Instagram در واقع در تولید از رویکرد مشابهی استفاده می‌کند، که نشان می‌دهد مزایایش از پیچیدگی اضافه بیشتر است.

الگو: مدیریت وظایف طولانی‌مدت (Long-running Tasks). Fan-out در نوشتن نمونه خوبی از این الگوست. وقتی کاربر پست می‌گذارد، باید فید همه دنبال‌کنندگانش را به‌روز کنیم. این یک کار طولانی‌مدت است که ممکن است مدتی طول بکشد و باید ناهمگام انجام شود.

وقتی Redis را به‌عنوان راه‌حل ذخیره‌سازی فید در طراحی پیشنهاد می‌کنید، مطمئن شوید فعالانه نگرانی‌های دوام را مطرح می‌کنید. بعضی مصاحبه‌کننده‌ها به‌درستی ممکن است بپرسند اگر نمونه‌های Redis شما از کار بیفتند چه می‌شود. بدون پیکربندی مناسب ماندگاری، می‌توانید همه داده فید کش‌شده را از دست بدهید که به افت سرویس و تجربه کاربری بد می‌رسد. در مصاحبه می‌توانید توضیح بدهید که Redis Sentinel را برای دسترس‌پذیری بالا با Failover خودکار پیاده می‌کنید، احتمالاً از Redis Cluster برای Sharding (شاردینگ / افراز داده‌ها) بین چند گره استفاده می‌کنید و گزینه‌های ماندگاری مثل AOF (Append-Only File) یا RDB Snapshot را برای بازیابی داده بعد از خرابی پیکربندی می‌کنید. عبارتی سنجیده مثل «Redis را با ماندگاری AOF و Sentinel برای Failover پیاده می‌کنیم تا خرابی گره‌ها با حداقل از دست رفتن داده مدیریت شود» نشان می‌دهد حالت‌های خرابی احتمالی را در نظر گرفته‌اید و Redis را مثل یک جعبه سیاه جادویی نمی‌بینید.

2) رندر فوری عکس‌ها و ویدیوها (عکس تا 8 مگابایت، ویدیو تا 4 گیگابایت)

برای فایل‌های رسانه‌ای بزرگ دو چالش کلیدی داریم:

  • بازده آپلود
  • Latency دانلود و مشاهده

یکی‌یکی می‌گیریم.

بازده آپلود

اول، همان‌طور که در تحلیل‌های Dropbox و YouTube بحث می‌کنیم، آپلود فایل‌های رسانه‌ای بزرگ به Chunking (تکه‌تکه‌سازی) نیاز دارد. دلیلش این است که یک درخواست HTTP منفرد معمولاً به حداکثر Payload کمتر از 2 گیگابایت محدود است. پس برای آپلود ویدیوی 4 گیگابایتی باید دست‌کم 2 درخواست بفرستیم (در عمل بیشتر، به‌خاطر Metadata).

یک راه‌حل رایج استفاده از API آپلود چندقسمتی (Multipart Upload) در AWS S3 است. در سطح بالا روش کار این است:

  1. اول POST /posts را برای ساخت Metadata پست و گرفتن postId مثل قبل صدا می‌زنیم، اما حالا یک Presigned URL هم برمی‌گرداند که می‌توان با آن رسانه را آپلود کرد. این URL برای مدت محدودی معتبر است (مثلاً 1 ساعت) و به کاربر اجازه می‌دهد مستقیم در S3 آپلود کند، بدون اینکه از سرورهای ما عبور کند.
  2. در سمت کلاینت، از API آپلود چندقسمتی برای آپلود تکه‌تکه فایل روی Presigned URL استفاده می‌کنیم.
  3. S3 خودکار قطعات را دوباره سرهم می‌کند و فایل نهایی را در S3 ذخیره می‌کند.

الگو: مدیریت Blob های بزرگ (Handling Large Blobs). رویکرد Instagram برای مدیریت فایل‌های رسانه‌ای بزرگ نمونه الگوی Blob های بزرگ است که در بسیاری از مصاحبه‌ها استفاده می‌شود. ترکیب Presigned URL برای آپلود مستقیم، آپلود تکه‌ای برای قابلیت اطمینان و توزیع CDN برای دسترسی جهانی، در بسیاری از سیستم‌هایی که انتقال فایل‌های قابل توجه انجام می‌دهند دیده می‌شود، از پلتفرم‌های ویدیویی تا سرویس‌های ذخیره‌سازی فایل.

وقتی در ابتدا Metadata پست را ساختیم، یک فیلد وضعیت آپلود رسانه را ابتدا روی «در انتظار» (Pending) می‌گذاریم. وقتی رسانه آپلود شد، Metadata پست را با object_key در S3 به‌روز می‌کنیم و وضعیت آپلود رسانه را به «کامل» (Complete) تغییر می‌دهیم.

دو روش اصلی برای مدیریت به‌روزرسانی Metadata پست بعد از تکمیل آپلود وجود دارد:

  1. رویکرد مشتری‌محور: کلاینت بعد از تکمیل آپلود چندقسمتی، یک درخواست PATCH می‌فرستد تا Metadata پست را با object_key در S3 به‌روز کند و وضعیت آپلود رسانه را روی «کامل» بگذارد.
  2. رویکرد سرورمحور: اعلان‌های رویداد S3 (S3 Event Notifications) را طوری پیکربندی می‌کنیم که وقتی آپلود چندقسمتی کامل شد، یک تابع Lambda یا Background Job را فعال کنند. این کار سپس Metadata پست را خودکار به‌روز می‌کند.

رویکرد مشتری‌محور (گزینه 1) پیاده‌سازی ساده‌تری دارد اما کمتر قابل اعتماد است، چون باید به کلاینت‌ها اعتماد کنیم که تکمیل آپلود را درست گزارش کنند. رویکرد سرورمحور (گزینه 2) پیچیده‌تر است اما ضمانت سازگاری داده بهتری می‌دهد، چون باطن ما کنترل کامل به‌روزرسانی‌های Metadata را دارد. بیشتر سیستم‌های تولیدی با وجود پیچیدگی بیشتر، گزینه 2 را انتخاب می‌کنند.

Latency دانلود و مشاهده

حالا که همه رسانه‌ها را آپلود کرده‌ایم و در S3 داریم، درباره سرو سریع آن‌ها هنگام مشاهده توسط کاربر صحبت می‌کنیم.

رویکرد بد: سرو مستقیم از S3

ساده‌ترین روش سرو مستقیم رسانه از S3 است. وقتی کاربر پستی را می‌بیند، URL فایل رسانه در S3 را برمی‌گردانیم و کلاینت آن را مستقیم از S3 دانلود می‌کند. پیاده‌سازی ساده است و برای اپلیکیشن‌های کوچک خوب کار می‌کند، اما برای اپلیکیشن جهانی مثل Instagram خوب مقیاس نمی‌گیرد.

چالش‌ها: این رویکرد از چند جهت کم می‌آورد. اول، کاربرانی که از Region سطل S3 ما دورند، هنگام بارگذاری رسانه Latency بالایی تجربه می‌کنند. کاربری در سنگاپور که به رسانه ذخیره‌شده در us-east-1 دسترسی دارد، ممکن است چند ثانیه منتظر بماند تا تصاویر بارگذاری شوند. دوم، هر درخواست مستقیم به S3 می‌رسد که در مقیاس بالا گران است و از هیچ کشی استفاده نمی‌کند. در نهایت، همان فایل‌های با وضوح بالا را بدون توجه به شرایط دستگاه یا شبکه برای همه کاربران ارائه می‌کنیم که پهنای باند را هدر می‌دهد و تجربه کاربری را کند می‌کند.

رویکرد خوب: CDN جلوی S3

بهبود چشمگیر این است که یک CDN مثل CloudFront را جلوی سطل S3 بگذاریم. CDN شبکه‌ای جهانی از Edge Location ها نگه می‌دارد که فایل‌های رسانه ما را نزدیک‌تر به کاربران کش می‌کنند. وقتی کاربر رسانه می‌خواهد، خودکار به نزدیک‌ترین Edge Location هدایت می‌شود. اگر رسانه آنجا کش نشده باشد، CDN آن را از S3 می‌گیرد و برای درخواست‌های بعدی کش می‌کند.

به این ترتیب، به‌جای اینکه کاربر سنگاپوری ما چند ثانیه منتظر بماند تا تصویری که اصلاً در ساحل شرقی آمریکا ذخیره شده بارگذاری شود، می‌تواند آن را از سروری در نزدیک‌ترین منطقه به خودش دانلود کند.

می‌توانیم CDN را برای کش کردن فایل‌ها بر اساس نوع و محبوبیتشان پیکربندی کنیم. مثلاً ممکن است تصاویر را 24 ساعت در Edge Location ها کش کنیم، چون بعد از آپلود به‌ندرت تغییر می‌کنند. این Latency را برای کاربران به‌طور چشمگیری کم می‌کند و بار سطل S3 مبدأ را هم کم می‌کند. همچنین می‌توانیم از فشرده‌سازی داخلی CDN برای کم کردن اندازه فایل در حین انتقال استفاده کنیم.

چالش‌ها: اگرچه این مشکل توزیع جهانی را حل می‌کند، هنوز فایل‌های یکسانی را به همه کاربران ارائه می‌کنیم. کاربری روی دستگاه موبایل با اتصال کند همان تصویر با وضوح بالا را می‌گیرد که کسی روی دسکتاپ با اتصال سریع می‌گیرد. همچنین کنترل محدودی روی استراتژی کش داریم، یعنی ممکن است رسانه‌های پرطرفدار به‌خاطر محدودیت اندازه کش از Edge Location ها خارج شوند و به واکشی غیرضروری از مبدأ برسد.

رویکرد عالی: CDN همراه بهینه‌سازی پویای رسانه

مؤثرترین راه‌حل توزیع CDN را با بهینه‌سازی پویای رسانه ترکیب می‌کند. هنگام آپلود رسانه، از یک سرویس پردازش رسانه (مثل Cloudinary یا Imgix) برای تولید نسخه‌های بهینه برای دستگاه‌ها و شرایط شبکه مختلف استفاده می‌کنیم. برای تصاویر، این شامل وضوح‌ها و قالب‌های مختلف مثل WebP برای مرورگرهای پشتیبان است. برای ویدیوها، چند سطح کیفیت می‌سازیم و از پخش جریانی تطبیقی (Adaptive Streaming) استفاده می‌کنیم (جزئیات بیشتر در تحلیل YouTube).

سپس CDN این نسخه‌های بهینه را بر اساس دستگاه درخواست‌کننده و شرایط شبکه ارائه می‌کند. کاربران موبایل خودکار تصاویر با اندازه مناسب می‌گیرند، در حالی که کاربران دسکتاپ نسخه‌های با وضوح بالاتر می‌گیرند. کلاینت اطلاعات دستگاه و شرایط شبکه را در درخواست‌هایش می‌فرستد و به CDN اجازه می‌دهد مناسب‌ترین نسخه را ارائه کند.

همچنین استراتژی‌های کش هوشمند پیاده می‌کنیم. رسانه‌های پرطرفدار در Edge Location ها پرشدت‌تر کش می‌شوند، در حالی که محتوای کم‌استفاده‌تر ممکن است TTL کوتاه‌تری داشته باشد. برای محتوای وایرال که ناگهان محبوب می‌شود، می‌توانیم فعالانه کش را در مناطقی که انتظار بیننده زیاد داریم «گرم» (Warm) کنیم.

چالش‌ها: این رویکرد تجربه کاربری را به‌طور چشمگیری بهتر می‌کند اما پیچیدگی بیشتری به سیستم می‌آورد. باید چند نسخه از هر فایل رسانه را مدیریت کنیم که هزینه و پیچیدگی ذخیره‌سازی را بالا می‌برد. خط لوله پردازش رسانه به آپلود تأخیر اضافه می‌کند و نظارت دقیق می‌خواهد. با وجود این چالش‌ها، این رویکردی است که بیشتر پلتفرم‌های رسانه‌ای بزرگ‌مقیاس استفاده می‌کنند، چون بهترین تعادل عملکرد و مدیریت‌پذیری را می‌دهد.

3) مقیاس‌پذیری برای 500 میلیون DAU

مقیاس را در طول بررسی‌های عمیق در نظر گرفته‌ایم، اما انتخاب‌های کلیدی طراحی را که اجازه می‌دهند 500 میلیون کاربر فعال روزانه را مؤثر سرو کنیم و عملکرد و قابلیت اطمینان را حفظ کنیم، خلاصه می‌کنیم:

  • فیدهای از پیش‌محاسبه‌شده (رویکرد ترکیبی): سنگ بنای تولید فید مقیاس‌پذیر ما رویکرد ترکیبی است. با پیش‌محاسبه فیدها برای اکثریت قریب‌به‌اتفاق کاربران (آن‌هایی که حساب‌هایی را دنبال می‌کنند که تعداد دنبال‌کننده‌شان کمتر از آستانه تعریف‌شده است)، بار زمان خواندن را به‌شدت کم می‌کنیم. ادغام بلادرنگ برای پست‌های «سلبریتی‌ها» مبادله‌ای است که با دقت برای مدیریت تقویت نوشتن در نظر گرفته شده.
  • شبکه تحویل محتوا (CDN): استفاده از CDN مثل CloudFront برای تحویل رسانه برای مقیاس‌پذیری جهانی و دسترسی کم‌تأخیر ضروری است. بهینه‌سازی پویای رسانه عملکرد را برای دستگاه‌ها و شرایط شبکه مختلف باز هم بهتر می‌کند.
  • آپلود تکه‌ای (Chunked Upload): برای فایل‌های بزرگ (به‌ویژه ویدیوها)، آپلود تکه‌ای قابلیت اطمینان و تجربه کاربری را بهتر می‌کند و آپلود موازی را ممکن می‌سازد.
  • انتخاب پایگاه داده و ایندکس‌گذاری: انتخاب DynamoDB (یا پایگاه داده NoSQL مقیاس‌پذیر مشابه) مقیاس‌دهی افقی برای ذخیره Metadata فراهم می‌کند. استفاده دقیق از Partition Key ها و Sort Key ها کوئری‌های کارآمد را تضمین می‌کند.

بیایید کمی حساب کنیم و ببینیم استراتژی‌های مقیاس‌دهی فعلی کافی هستند یا نه.

برای خود فایل‌های رسانه، می‌توانیم میانگین اندازه رسانه را 2 مگابایت فرض کنیم. پس 100 میلیون پست ضرب‌در 2 مگابایت می‌شود 200 ترابایت داده باینری در هر روز. این در طول 10 سال حدود 750 پتابایت داده می‌شود. S3 می‌تواند این را تحمل کند، اما ارزان نیست. اگر نگران هزینه باشیم، همیشه می‌توانیم رسانه‌هایی را که مدت‌هاست به آن‌ها دسترسی نشده به ذخیره‌سازی ارزان‌تری مثل Glacier منتقل کنیم.

برای Metadata، به 100 میلیون ضرب‌در 1 کیلوبایت یعنی 100 گیگابایت داده جدید (غیرباینری) در هر روز نگاه می‌کنیم. به همین ترتیب، اگر نگران قیمت DynamoDB باشیم، می‌توانیم داده‌ای را که به‌ندرت به آن دسترسی می‌شود به S3 منتقل کنیم.

این یک الگوی رایج است. هرچه ذخیره‌ساز گرم‌تر باشد، گران‌تر است. با استفاده از CDN، سپس کش یا حافظه، سپس SSD، سپس HDD و سپس نوار (Tape)، همیشه می‌توانیم داده‌ای را که به‌ندرت به آن دسترسی می‌شود برداریم و برای صرفه‌جویی هزینه به لایه پایین‌تر ببریم.

درباره Throughput، مثل همیشه می‌توانیم میکروسرویس‌هایمان را پویا و افقی برای تحمل بار مقیاس بدهیم. این در بیشتر ارائه‌دهندگان ابری خودکار انجام می‌شود و با آستانه‌های مصرف CPU یا حافظه فعال می‌شود. پس هر مجموعه میکروسرویس مقیاس‌پذیر افقی، ضمناً یک Load Balancer جلویش دارد تا ترافیک را توزیع کند.

بعد از همه این‌ها، ممکن است طراحی‌ای شبیه این داشته باشید: کلاینت‌ها، API Gateway، Post Service و Follow Service، Posts DB (DynamoDB)، Blob Store (S3) با CDN، صف برای Feed Fan-out Service، Redis برای فیدهای از پیش‌محاسبه‌شده و کش Metadata پست.

انتظارات در هر سطح

بنابراین در هر سطح دنبال چه چیزی هستم؟

سطح Mid-level

در سطح Mid-level بیشتر دنبال توانایی داوطلب در ساخت طراحی سطح بالای کارآمدی هستم که نیازهای اصلی Instagram را برآورده کند. آن‌ها باید اجزای اساسی لازم برای آپلود عکس و ویدیو، دنبال‌کردن کاربران و تولید فید را بفهمند. انتظار دارم Schema ساده‌ای برای پایگاه داده پیشنهاد کنند و توضیح بدهند پست‌ها چطور ذخیره و بازیابی می‌شوند. اگرچه ممکن است در ابتدا رویکرد ساده Fan-out در خواندن را پیشنهاد کنند، با کمی راهنمایی باید بتوانند محدودیت‌هایش را تشخیص بدهند و به راه‌حل Fan-out در نوشتن برسند. می‌خواهم ببینم در صورت درخواست، گلوگاه‌های عملکردی را حل می‌کنند. همچنین باید مفاهیم پایه ذخیره‌سازی رسانه مثل استفاده از S3 را بفهمند و بفهمند چرا CDN ممکن است ضروری باشد، حتی اگر وارد جزئیات بهینه‌سازی رسانه نشوند.

سطح Senior

برای داوطلبان Senior انتظار دارم بیشتر بررسی‌های عمیق را پوشش بدهید، به‌ویژه درباره بهینه‌سازی تولید فید و تحویل رسانه. باید مبادلات بین رویکردهای Fan-out در خواندن، Fan-out در نوشتن و ترکیبی را بفهمید و بتوانید توضیح بدهید چرا Instagram مدل ترکیبی را برای حل «مشکل سلبریتی‌ها» انتخاب می‌کند. انتظار دارم مدیریت کارآمد آپلود رسانه برای فایل‌های بزرگ و توجیه قوی برای انتخاب‌های فناوری داشته باشید. باید بتوانید استراتژی‌های ایندکس‌گذاری پایگاه داده را به‌تفصیل بحث کنید و بفهمید چطور برای بارکاری خواندن‌محور بهینه شوید. احتمالاً برای پوشش همه بررسی‌های عمیقی که اینجا انجام دادیم وقت نخواهید داشت، اما اگر از شما خواسته شد، باید بتوانید مبادلات کلیدی را بیان کنید و به راه‌حل معقولی برسید.

سطح Staff به بالا

برای داوطلبان Staff توانایی شما را در شناسایی گلوگاه‌های واقعی مقیاس‌پذیری و پیشنهاد راه‌حل‌های ظریفی که پیچیدگی را در برابر نیازهای واقعی متعادل می‌کند می‌سنجم. باید سریع متوجه شوید که تولید فید و تحویل رسانه چالش‌های کلیدی‌اند و طراحی‌تان را بر همین اساس متمرکز کنید. انتظار دارم درباره تکامل سیستم در طول زمان بحث کنید: چطور رشد کاربران از 1 میلیون به 500 میلیون را مدیریت می‌کنیم؟ به‌جای پریدن فوری به سیستم‌های توزیع‌شده پیچیده، باید بتوانید توضیح بدهید کجا راه‌حل‌های ساده‌تر کافی است و دقیقاً چه زمانی باید به رویکردهای پیچیده‌تر تکامل پیدا کنیم. داوطلبان Staff درک دقیقی از دغدغه‌های عملیاتی نشان می‌دهند، حالت‌های خرابی را پیش‌بینی می‌کنند و راه‌حل‌های مقاومی پیشنهاد می‌دهند که تجربه کاربری را بالاتر از هر چیز در اولویت می‌گذارند.