طراحی سرویس ذخیرهسازی فایل مانند Dropbox
بررسی گامبهگام طراحی سیستم Dropbox شامل آپلود و دانلود، اشتراکگذاری، همگامسازی، فایلهای حجیم و امنیت
درک مسئله
Dropbox یک سرویس ذخیرهسازی فایل مبتنی بر ابر است. کاربران با آن فایلهایشان را ذخیره و به اشتراک میگذارند و از هر جا و روی هر دستگاهی، امن و قابل اعتماد به آنها دسترسی دارند.
نیازمندیهای عملکردی
نیازمندیهای اصلی:
- کاربر بتواند از هر دستگاهی فایل آپلود کند.
- کاربر بتواند از هر دستگاهی فایل دانلود کند.
- کاربر بتواند فایل را با دیگران به اشتراک بگذارد و فایلهایی را که با او به اشتراک گذاشته شده ببیند.
- فایلها بهصورت خودکار بین دستگاهها همگام شوند.
موارد خارج از محدوده:
- ویرایش فایل
- مشاهده فایل بدون دانلود آن
طراحی خود Blob Storage (ذخیرهسازی باینری حجیم) یک مسئله جداگانه در طراحی سیستم است و در این مبحث نمیگنجد. اگر دوست داشتید، میتوانید جداگانه بررسی کنید که Blob Storage چطور کار میکند و چگونه طراحی میشود.
نیازمندیهای غیرعملکردی
نیازمندیهای اصلی:
- دسترسپذیری بالا؛ در این سیستم، Availability (دسترسپذیری) بر Consistency (سازگاری) اولویت دارد.
- پشتیبانی از فایلهایی تا حجم 50 گیگابایت.
- امنیت و قابلیت اطمینان؛ اگر فایلی گم یا خراب شد، باید بتوانیم آن را بازیابی کنیم.
- آپلود، دانلود و همگامسازی تا حد ممکن سریع باشد (Latency یا تأخیر کم).
موارد خارج از محدوده:
- محدودیت فضای ذخیرهسازی برای هر کاربر
- نسخهبندی فایل
- اسکن فایلها برای ویروس و بدافزار
بسیاری از داوطلبان در این سؤال با مبادلهای که قضیه CAP مطرح میکند درگیر میشوند. یادتان باشد فقط وقتی سازگاری را بر دسترسپذیری مقدم میدارید که هر خواندن باید آخرین نوشتن را برگرداند. در غیر این صورت سیستم دچار مشکل میشود. مثلاً در یک اپلیکیشن معاملات سهام، اگر کاربری در آلمان سهم APPL بخرد و کاربر دیگری بلافاصله در آمریکا همین کار را بکند، باید مطمئن شوید معامله اول در آمریکا هم اعمال شده است. اما در سیستمی مثل Dropbox مشکلی ندارد که فایلی که کاربر آلمانی آپلود کرده، تا چند ثانیه برای کاربر آمریکایی دیده نشود.
آمادهسازی
برنامهریزی رویکرد
پیش از ورود به طراحی، کمی وقت بگذارید و استراتژیتان را مشخص کنید. خوشبختانه برای سؤالهایی از نوع طراحی محصول، این برنامه ساده است. طراحی را مرحلهبهمرحله بسازید و نیازمندیهای عملکردی را یکییکی جلو ببرید. این کار تمرکزتان را حفظ میکند و نمیگذارد در جزئیات گم شوید. بعد از پوشش دادن نیازمندیهای عملکردی، سراغ نیازمندیهای غیرعملکردی میروید و آنها مسیر بررسیهای عمیق را مشخص میکنند.
تعریف موجودیتهای اصلی
بهتر است با یک نمای کلی از موجودیتها (Entities) شروع کنید. در این مرحله لازم نیست همه ستونها و جزئیات را بدانید. وقتی در طراحی سطح بالا درک روشنتری از سیستم پیدا کردیم، به آنها میپردازیم. فعلاً همین موجودیتها فکر ما را جهت میدهند و پایهای محکم برای تعریف API میسازند.
موجودیتهای Dropbox سادهاند:
- فایل داده خامی است که کاربران آپلود، دانلود و به اشتراکگذاری میکنند.
- Metadata فایل اطلاعات مربوط به فایل است، مثل نام، اندازه، نوع MIME و کاربری که آن را آپلود کرده.
- کاربر همان کاربر سیستم ماست.
در مصاحبه واقعی، همین فهرست کوتاه کافی است. فقط حتماً موجودیتها را با مصاحبهکننده مرور کنید تا مطمئن شوید هر دو در یک مسیر هستید.
API یا رابط سیستم
API مهمترین رابطی است که کاربران با آن کار میکنند. بهتر است زود تعریف شود، چون طراحی سطح بالا را هدایت میکند. کافی است برای هر نیازمندی عملکردی یک endpoint داشته باشیم.
برای آپلود فایل:
1
2
3
4
5
6
POST /files
Request:
{
File,
FileMetadata
}
برای دانلود فایل:
1
GET /files/{fileId} -> File & FileMetadata
API ها با پیشرفت طراحی تغییر میکنند. در همین مسئله، API های آپلود و دانلود وقتی مبادلات رویکردهای مختلف را در طراحی سطح بالا بسنجیم، بهطور محسوسی تکامل پیدا میکنند. میتوانید این را از ابتدا به مصاحبهکننده بگویید: «چند API ساده را بیان میکنم، ولی ممکن است با عمیقتر شدن در طراحی برگردم و آنها را بهبود بدهم.»
برای اشتراکگذاری فایل:
1
2
3
4
5
POST /files/{fileId}/share
Request:
{
User[] // The users to share the file with
}
در نهایت، کلاینتها باید بتوانند تغییرات فایلهای روی سرور راه دور را بپرسند تا بفهمند کدام فایلها باید روی دستگاه محلی همگام شوند:
1
GET /files/{fileId}/changes -> FileMetadata[]
در همه این درخواستها اطلاعات کاربر از طریق هدرها (با Session Token یا JWT) ارسال میشود. این الگوی رایجی است و راه خوبی برای احراز هویت و بررسی مجوز کاربر است. اطلاعات کاربر را در بدنه درخواست نفرستید، چون کلاینت بهراحتی میتواند آن را دستکاری کند.
طراحی سطح بالا
1) آپلود فایل از هر دستگاه
برای ذخیره یک فایل باید دو سؤال را جواب بدهیم: محتوای خام فایل کجا ذخیره شود و Metadata آن کجا.
برای Metadata میتوانیم از یک پایگاه داده NoSQL مثل DynamoDB استفاده کنیم. DynamoDB یک پایگاه داده NoSQL کاملاً مدیریتشده روی AWS است. Metadata ما ساختار منعطفی دارد، ارتباطش با دادههای دیگر کم است و الگوی اصلی کوئری، واکشی فایلهای یک کاربر است. این ویژگیها DynamoDB را انتخاب مناسبی میکند. با این حال در مصاحبه خیلی روی انتخاب پایگاه داده گیر نکنید. در واقع یک پایگاه داده SQL مثل PostgreSQL هم برای این مورد خوب کار میکند.
طرح اولیه Metadata یک سند ساده است:
1
2
3
4
5
6
7
{
"id": "123",
"name": "file.txt",
"size": 1000,
"mimeType": "text/plain",
"uploadedBy": "user1"
}
برای ذخیره خود فایل چند گزینه داریم. مبادلات هر کدام را بررسی میکنیم.
رویکرد بد: ذخیره فایل روی سرور باطن
سادهترین راه این است که فایل را مستقیم به سرور باطن (مثلاً File Service) آپلود کنیم و همانجا روی دیسک ذخیره کنیم. endpoint ما یعنی POST /files فایل و Metadata را میگیرد، فایل را در سیستم فایل محلی سرور مینویسد و Metadata را در پایگاه داده ذخیره میکند. این روش برای یک اپلیکیشن کوچک منطقی است، ولی مقیاسپذیر و قابل اعتماد نیست.
چالشها:
- با زیاد شدن فایلها باید دائماً به سرور فضای بیشتری اضافه کنیم یا با اضافه کردن سرور، مقیاس افقی بدهیم.
- اگر سرور از کار بیفتد، دسترسی به همه فایلها را از دست میدهیم.
به راهی نیاز داریم که هم خرابی سرور را تحمل کند و هم بهراحتی مقیاس بگیرد. خوشبختانه این مسئله قبلاً حل شده و Blob Storage هر دو مشکل را برطرف میکند.
رویکرد خوب: ذخیره فایل در Blob Storage
فایل را در یک سرویس Blob Storage مثل Amazon S3 یا Google Cloud Storage نگه میداریم. وقتی کاربر فایلی را به باطن ما آپلود میکند، آن را به Blob Storage میفرستیم و Metadata را در پایگاه داده ذخیره میکنیم. Blob Storage مقیاسدهی را خودش انجام میدهد و عملاً میتوانیم تعداد نامحدودی فایل در آن نگه داریم. قابل اعتمادتر هم هست، چون اگر سرور ما خراب شود، فایلها از دست نمیروند. امکاناتی مثل Lifecycle Policy برای حذف خودکار فایلهای قدیمی و Versioning برای ردیابی تغییرات هم دارد، هرچند این موارد خارج از محدوده این مسئله است.
چالشها:
- پیچیدگی بیشتر است. باید با سرویس Blob Storage یکپارچه شویم و حالتهایی را مدیریت کنیم که فایل آپلود شده ولی Metadata ذخیره نشده، یا برعکس. میتوان با یک رویکرد تراکنشی این مشکل را حل کرد، به این صورت که Metadata فقط وقتی ذخیره شود که آپلود فایل موفق بوده باشد و بالعکس.
- از نظر فنی فایل دو بار آپلود میشود، یک بار به باطن و یک بار به Blob Storage. این کار زائد است. راه حل این است که کاربر مستقیم به Blob Storage آپلود کند.
رویکرد عالی: آپلود مستقیم از کلاینت
بهترین راه این است که کلاینت فایل را مستقیم در Blob Storage آپلود کند. این کار از آپلود از طریق باطن سریعتر و ارزانتر است. برای این منظور از Presigned URL (آدرس از پیش امضاشده) استفاده میکنیم. این آدرسها به کاربر اجازه میدهند فایل را در یک مکان مشخص از Blob Storage آپلود کند. وقتی آپلود تمام شد، Blob Storage یک اعلان به باطن ما میفرستد تا Metadata را ذخیره کنیم.
بنابراین API آپلود که قبلاً یک POST ساده به /files بود، اکنون یک فرایند سهمرحلهای میشود:
- از باطن یک Presigned URL درخواست میکنیم. باطن خودش آن را از سرویس Blob Storage مثل S3 میگیرد و Metadata فایل را با وضعیت «در حال آپلود» (uploading) در پایگاه داده ذخیره میکند.
1
2
3
4
5
POST /files/presigned-url -> PresignedUrl
Request:
{
FileMetadata
}
- کلاینت با یک درخواست
PUTمستقیم به همان Presigned URL، فایل را در Blob Storage آپلود میکند. بدنه درخواست، خود فایل است. - بعد از آپلود، سرویس Blob Storage با S3 Notifications یک اعلان به باطن میفرستد. باطن هم وضعیت Metadata را به «آپلودشده» (uploaded) تغییر میدهد.
الگو: مدیریت Blob های بزرگ. آپلود مستقیم با Presigned URL نمونه کلاسیک انتقال کارآمد فایلهای بزرگ است. این الگو سه ایده دارد: دور زدن سرورهای اپلیکیشن برای انتقال داده، استفاده از URL های امضاشده برای امنیت و آپلود تکهتکه (Chunked) برای قابلیت اطمینان. این الگو در بسیاری از سیستمهای توزیعشدهای که فایلهای حجیم آپلود و دانلود میکنند دیده میشود.
2) دانلود فایل از هر دستگاه
مثل آپلود، برای دانلود هم چند راه وجود دارد.
رویکرد بد: دانلود از طریق باطن
رایجترین پاسخ داوطلبان این است که باطن فایل را یک بار از Blob Storage بگیرد و بعد دوباره آن را برای کلاینت بفرستد. این راه کند و گران است، چون فایل دو بار منتقل میشود. میتوانیم مثل آپلود، اجازه بدهیم کلاینت مستقیم از Blob Storage دانلود کند.
رویکرد خوب: دانلود مستقیم با Presigned URL
کلاینت از باطن یک Presigned URL برای دانلود میگیرد و با آن مستقیم از Blob Storage فایل را میگیرد. مثل آپلود، این آدرس فقط برای مدت محدودی اجازه دانلود از یک مکان مشخص را میدهد.
1
GET /files/{fileId}/presigned-url -> PresignedUrl
چالش: این روش تقریباً بهینه است، اما برای کاربرانی که در سراسر جهان پخشاند هنوز میتواند کند باشد. Blob Storage در یک منطقه (Region) قرار دارد و کاربرانی که از آن دورند، دانلود کندتری تجربه میکنند. با CDN (شبکه تحویل محتوا) میتوانیم فایل را به کاربر نزدیکتر کنیم.
رویکرد عالی: استفاده از CDN
CDN شبکهای از سرورهای پخششده در سراسر جهان است که فایلها را کش میکند و از نزدیکترین سرور به کاربر تحویل میدهد. این کار Latency را کم و سرعت دانلود را زیاد میکند و بسیار سریعتر از تحویل فایل از باطن یا از خود Blob Storage است.
برای امنیت، مثل Presigned URL های S3، یک URL امضاشده میسازیم که کاربر با آن فقط برای مدتی محدود و از یک مکان مشخص CDN فایل را دانلود کند. توضیح بیشتر این موضوع در بخش بررسی امنیت همین مطلب آمده است.
چالش: CDN ها نسبتاً گراناند. برای کنترل هزینه، باید هوشمندانه تعیین کنیم چه فایلهایی و تا چه مدتی کش شوند. با هدر Cache-Control مشخص میکنیم فایل چقدر در CDN بماند. همچنین با مکانیزم Cache Invalidation (ابطال کش) میتوانیم فایلها را هنگام بهروزرسانی یا حذف از CDN خارج کنیم. اینطور فقط فایلهایی که زیاد استفاده میشوند در کش میمانند و برای فایلهایی که بهندرت خوانده میشوند هزینه نمیدهیم.
3) اشتراکگذاری فایل با کاربران دیگر
برای تکمیل نیازمندیهای عملکردی باید از اشتراکگذاری فایل هم پشتیبانی کنیم. آن را شبیه Google Drive پیاده میکنیم، یعنی کافی است ایمیل کاربر مقصد را وارد کنید. فرض میکنیم کاربران قبلاً احراز هویت شدهاند.
نکته اصلی در مصاحبه این است که این فرایند چطور سریع و کارآمد انجام شود.
رویکرد بد: فهرست اشتراک داخل Metadata
شروع خوب این است که فهرست کاربرانی که به فایل دسترسی دارند را به Metadata خود فایل اضافه کنیم. با اشتراکگذاری، کاربر به این فهرست اضافه میشود و هنگام دانلود بررسی میکنیم که کاربر در فهرست باشد.
1
2
3
4
5
6
7
8
{
"id": "123",
"name": "file.txt",
"size": 1000,
"mimeType": "text/plain",
"uploadedBy": "user1",
"sharelist": ["user2", "user3"]
}
چالش: وقتی کاربر سایت را باز میکند، انتظار دارد هم فایلهای خودش و هم فایلهایی را که با او به اشتراک گذاشتهاند ببیند. گرفتن فایلهای خود کاربر آسان است، چون uploadedBy را کلید اصلی داریم. اما گرفتن فایلهای بهاشتراکگذاشتهشده با او کند است، چون باید فهرست اشتراک تکتک فایلها را اسکن کنیم تا ببینیم کاربر در آن هست یا نه.
رویکرد خوب: نگهداری نگاشت معکوس
علاوه بر فهرست اشتراک در Metadata، یک فهرست معکوس هم نگه میداریم. این فهرست هر کاربر را به فایلهای بهاشتراکگذاشتهشده با او نگاشت میکند. با این کار، وقتی کاربر سایت را باز میکند، فقط شناسه او را در این کش اشتراکگذاری جستوجو میکنیم. هر ورودی کش یک جفت کلید-مقدار ساده است:
1
user1:["fileId1", "fileId2"]
چالش: باید فهرست فایلهای مشترک را با فهرست اشتراک داخل Metadata همگام نگه داریم. بهترین راه این است که هر دو را در یک پایگاه داده نگه داریم و در یک تراکنش بهروزرسانی کنیم.
رویکرد خوب: نرمالسازی کامل
راه دیگر نرمالسازی کامل دادههاست. یک جدول جدید میسازیم که userId را به fileId نگاشت میکند. هر ردیف یعنی فایلی با کاربر مشخص به اشتراک گذاشته شده است. وقتی کاربر سایت را باز میکند، با جستوجوی همه ردیفهای userId او در جدول SharedFiles فهرست فایلهای مشترک را سریع میگیریم.
| userId (PK) | fileId (SK) |
|---|---|
| user1 | fileId1 |
| user1 | fileId2 |
| user2 | fileId3 |
در این طراحی دیگر به فهرست اشتراک در Metadata نیازی نیست. فقط جدول SharedFiles را برای کاربر درخواستکننده کوئری میکنیم و دیگر لازم نیست دو فهرست را همگام نگه داریم.
چالش: این کوئری کمی از رویکرد قبلی کارآمدتر است، چون بهجای جستوجوی ساده کلید-مقدار از Index استفاده میکند. ولی این مبادله احتمالاً میارزد، چون دیگر نیازی به همگامسازی فهرست اشتراک با فهرست فایلهای مشترک نداریم.
4) همگامسازی خودکار فایلها بین دستگاهها
در نهایت باید مطمئن شویم فایلها بهصورت خودکار بین دستگاهها همگام میشوند. در سطح بالا، یک کپی از هر فایل روی هر دستگاه کلاینت (محلی) و یک کپی هم در ذخیرهسازی راه دور (ابر) نگه داشته میشود. بنابراین دو جهت همگامسازی داریم:
- محلی به راه دور
- راه دور به محلی
محلی به راه دور
وقتی کاربر فایلی را روی دستگاهش بهروزرسانی میکند، باید تغییر را با سرور راه دور همگام کنیم. سرور راه دور را منبع حقیقت (Source of Truth) میدانیم، پس مهم است که این تغییر سریع به آن برسد تا دستگاههای دیگر بدانند چه تغییری رخ داده است.
برای این کار به یک Sync Agent سمت کلاینت نیاز داریم که:
- پوشه محلی Dropbox را با رویدادهای سیستم فایل زیر نظر بگیرد (مثلاً FileSystemWatcher در ویندوز یا FSEvents در macOS).
- هنگام تشخیص تغییر، فایل تغییرکرده را برای آپلود در صف قرار دهد.
- با API آپلود، تغییرات را همراه Metadata بهروزشده به سرور بفرستد.
تعارضها (Conflicts) با استراتژی «آخرین نوشتن برنده است» (Last Write Wins) حل میشوند. یعنی اگر دو کاربر یک فایل را ویرایش کنند، ویرایشی که آخر انجام شده ذخیره میشود.
نسخهبندی از محدوده این مبحث خارج است. فقط بدانید که معمولاً فایل را بازنویسی نمیکنند. بهجای آن یک فایل جدید (یا دستکم تکههای جدید) اضافه میشود و شماره نسخه و اشارهگر در Metadata بهروز میشود.
راه دور به محلی
در جهت دیگر، هر کلاینت باید بداند چه زمانی روی سرور تغییری رخ داده تا آن را دریافت کند. دو رویکرد اصلی داریم:
- Polling (نظرسنجی دورهای): کلاینت هر چند وقت از سرور میپرسد «از آخرین همگامسازی من تا حالا چیزی تغییر کرده؟» سرور از پایگاه داده بررسی میکند که آیا فایلهای این کاربر مهر زمانی
updatedAtجدیدتر از آخرین همگامسازی دارند یا نه. این روش ساده است، اما تشخیص تغییر کند است و اگر چیزی عوض نشده باشد، پهنای باند هدر میرود. - WebSocket یا SSE: سرور یک اتصال باز با هر کلاینت نگه میدارد و هر تغییری که رخ بدهد، اعلان میفرستد. پیچیدهتر است، ولی بهروزرسانی بلادرنگ (Real-time) میدهد.
برای Dropbox میتوان از یک رویکرد ترکیبی استفاده کرد و فایلها را دو دسته کرد:
- فایلهای تازه: فایلهایی که اخیراً (مثلاً در چند ساعت گذشته) ویرایش شدهاند. برای اینها اتصال WebSocket نگه میداریم تا همگامسازی بلادرنگ باشد.
- فایلهای قدیمی: فایلهایی که مدتی است تغییر نکردهاند. برای آنها به Polling دورهای برمیگردیم، چون بهروزرسانی فوری اهمیت کمتری دارد.
این رویکرد ترکیبی مزیت هر دو روش را دارد: برای فایلهای فعال بهروزرسانی بلادرنگ و برای فایلهای غیرفعال صرفهجویی در منابع.
جمعبندی اجزای سیستم
حالا یک قدم عقب میرویم و کل سیستم را میبینیم. تا اینجا طراحی سادهای داریم که همه نیازمندیهای عملکردی را پوشش میدهد. اجزای آن اینها هستند:
- Uploader (آپلودکننده): کلاینتی که فایل را آپلود میکند و میتواند مرورگر وب، اپلیکیشن موبایل یا اپلیکیشن دسکتاپ باشد. این کلاینت مسئول شناسایی فعال تغییرات محلی و ارسال آنها به ذخیرهسازی راه دور هم هست.
- Downloader (دانلودکننده): کلاینتی که فایل را دانلود میکند. میتواند همان Uploader باشد، اما لازم نیست. برای وضوح، آنها را در طراحی جدا کردهایم. وظیفه این کلاینت تشخیص تغییر فایلها روی سرور راه دور و دانلود آنهاست.
- Load Balancer و API Gateway (دروازه API): این بخش جلوی سرورهای اپلیکیشن قرار دارد و درخواستها را به سرور مناسب میرساند. مدیریت SSL Termination، Rate Limiting (محدودسازی نرخ درخواست) و اعتبارسنجی درخواست هم با آن است.
- File Service (سرویس فایل): فقط Metadata فایل را در پایگاه داده مینویسد و از S3 درخواست Presigned URL میکند. خودش آپلود یا دانلود فایل را انجام نمیدهد و فقط واسطه بین کلاینت و S3 است.
- پایگاه داده Metadata فایل: اینجا Metadata فایلها (نام، اندازه، نوع MIME و کاربر آپلودکننده) را نگه میداریم. جدول فایلهای مشترک هم اینجاست که فایلها را به کاربران دارای دسترسی نگاشت میکند. هنگام دانلود، از همین جدول برای اعمال مجوزها استفاده میکنیم.
- S3: جایی که خود فایلها ذخیره میشوند. فایلها با Presigned URL هایی که از سرویس فایل میگیریم، مستقیم به S3 آپلود و از آن دانلود میشوند.
- CDN: فایلها را نزدیک کاربر کش میکند تا Latency کم شود و فایل را به Downloader تحویل میدهد.
بررسیهای عمیق
1) پشتیبانی از فایلهای حجیم
اولین چیزی که درباره فایلهای حجیم باید در نظر بگیرید، تجربه کاربری است. دو نکته کلیدی طراحی را جهت میدهند:
- نمایش پیشرفت: کاربر باید پیشرفت آپلود را ببیند تا بداند کار میکند و چقدر زمان لازم است.
- آپلود قابل ازسرگیری (Resumable): کاربر باید بتواند آپلود را متوقف و دوباره شروع کند. اگر اینترنتش قطع شد یا مرورگر را بست، باید از همانجا ادامه بدهد و مجبور نباشد 49 گیگابایتی را که شاید قبل از قطع شدن آپلود شده، از اول بفرستد.
این بخش در واقع هسته مسئله است و در مصاحبه واقعی معمولاً بیشترین وقت را به آن اختصاص میدهم.
قبل از رفتن سراغ راهحلها، محدودیتهای آپلود یک فایل بزرگ با یک درخواست POST را ببینیم:
- مهلت زمانی (Timeout): وبسرورها و کلاینتها معمولاً Timeout دارند تا بینهایت منتظر پاسخ نمانند. یک
POSTبرای فایل 50 گیگابایتی بهراحتی از این مهلتها رد میشود. این فرصت خوبی برای یک محاسبه سریع در مصاحبه است. با اینترنت 100 مگابیت بر ثانیه، آپلود 50 گیگابایت چقدر طول میکشد؟ 50 گیگابایت ضرب در 8 بیت بر بایت، تقسیم بر 100 مگابیت بر ثانیه، میشود 4000 ثانیه. یعنی حدود 1.11 ساعت. این زمان زیادی است برای اینکه هیچ پاسخی از سرور نگیریم. - محدودیت مرورگر و سرور: معمولاً بهخاطر محدودیتهای پیکربندی در مرورگر یا سرور، اصلاً نمیتوان فایل 50 گیگابایتی را با یک
POSTفرستاد. مرورگرها و وبسرورها معمولاً برای اندازه بدنه درخواست سقف میگذارند. مثلاً Apache و NGINX سقفهای قابل تنظیم دارند، اما مقدار پیشفرض معمولاً زیر 2 گیگابایت است. بیشتر سرویسهای مدرن مثل Amazon API Gateway سقف پیشفرض بسیار پایینتری دارند که قابل افزایش هم نیست. این مقدار در API Gateway آمازون که در طراحی ما هست، فقط 10 مگابایت است. - قطع شدن شبکه: فایلهای بزرگ بیشتر در معرض قطع شبکهاند. اگر کاربر در حال آپلود فایل 50 گیگابایتی باشد و اینترنتش قطع شود، باید از ابتدا شروع کند.
- تجربه کاربری: کاربر از پیشرفت آپلود بیخبر است. نمیداند چقدر طول میکشد و حتی نمیداند کار میکند یا نه.
برای رفع این محدودیتها از تکنیک Chunking (تکهتکهسازی) استفاده میکنیم. فایل را به قطعات کوچکتر تقسیم میکنیم و آنها را یکییکی یا موازی (بسته به پهنای باند) آپلود میکنیم. این کار باید روی کلاینت انجام شود، قبل از ارسال به سرور (در طراحی ما S3). اشتباه بسیار رایج داوطلبان، تکهتکه کردن فایل روی سرور است. این کار هدف را از بین میبرد، چون برای رساندن فایل به سرور باز هم باید کل آن را یکجا آپلود کنیم. معمولاً اندازه هر تکه 5 تا 10 مگابایت است، ولی میتوان آن را بر اساس شرایط شبکه و اندازه فایل تنظیم کرد.
با تکهها، نمایش پیشرفت ساده است. پیشرفت هر تکه را دنبال میکنیم و با آپلود موفق هر تکه، نوار پیشرفت را بهروز میکنیم. این تجربه بسیار بهتر از این است که کاربر یک ساعت به یک نشانگر در حال چرخش نگاه کند.
سؤال بعدی این است که آپلود قابل ازسرگیری را چطور مدیریت کنیم. باید بدانیم کدام تکهها آپلود شدهاند و کدامها نه. این وضعیت را در پایگاه داده و بهطور مشخص در جدول FileMetadata ذخیره میکنیم. طرح Metadata را با فیلد chunks بهروز میکنیم:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"id": "123",
"name": "file.txt",
"size": 1000,
"mimeType": "text/plain",
"uploadedBy": "user1",
"status": "uploading",
"chunks": [
{
"id": "chunk1",
"status": "uploaded"
},
{
"id": "chunk2",
"status": "uploading"
},
{
"id": "chunk3",
"status": "not-uploaded"
}
]
}
وقتی کاربر آپلود را از سر میگیرد، فیلد chunks را بررسی میکنیم تا ببینیم کدام تکهها آپلود شدهاند و فقط بقیه را میفرستیم. پس اگر اینترنت قطع شود یا مرورگر بسته شود، لازم نیست از اول شروع کند.
اما چطور مطمئن شویم فیلد chunks با تکههای واقعاً آپلودشده هماهنگ است؟ دو رویکرد داریم.
رویکرد خوب: هماهنگسازی توسط کلاینت
واضحترین راه این است که کلاینت وضعیت تکهها را هماهنگ کند. جریان کار این است:
- کلاینت فایل را میگیرد، تکهتکه میکند و تکهها را مستقیم در S3 آپلود میکند.
- S3 برای هر تکه آپلودشده پیام موفقیت برمیگرداند.
- بعد از موفقیت، کلاینت یک درخواست
PATCHبه باطن میفرستد تا فیلدchunksدر جدولFileMetadataبهروز شود.
1
2
3
4
5
6
7
8
9
10
PATCH /files/{fileId}/chunks
Request:
{
"chunks": [
{
"id": "chunk1",
"status": "uploaded"
}
]
}
چالش: اینجا به کلاینت اعتماد کردهایم که فیلد chunks را با تکههای واقعی هماهنگ نگه دارد و این یک ریسک امنیتی است. کاربر مخرب میتواند یک PATCH بفرستد و همه تکهها را بدون آپلود واقعی «آپلودشده» علامت بزند. اگرچه فقط فایل خودش خراب میشود و نه فایل دیگران، اما باز هم ممکن است به وضعیت ناسازگاری برسد که اشکالزداییاش سخت است. با رویکرد سمت سرور میتوانیم مطمئن شویم فیلد chunks با آپلودهای واقعی همگام است.
رویکرد عالی: هماهنگسازی با S3 Event Notifications
راه بهتر این است که با اعلانهای رویداد S3 فیلد chunks را با تکههای واقعاً آپلودشده همگام نگه داریم. S3 Event Notifications قابلیتی است که با آن میتوانیم هنگام آپلود فایل در S3 یک تابع Lambda را اجرا کنیم یا به یک SNS Topic پیام بفرستیم. با آن میتوانیم وقتی تکهای با موفقیت آپلود شد، پیامی به باطن بفرستیم و فیلد chunks را بدون اتکا به کلاینت بهروز کنیم.
شناسایی یکتای فایل و تکه
وقتی میخواهیم آپلودی را ادامه بدهیم، اولین سؤالها اینهاست: (1) آیا قبلاً سعی کردهام این فایل را آپلود کنم؟ (2) اگر بله، کدام تکهها را قبلاً آپلود کردهام؟
برای سؤال اول نمیتوانیم سادهلوحانه به نام فایل تکیه کنیم، چون دو کاربر مختلف (یا حتی یک کاربر) ممکن است فایلهایی با نام یکسان آپلود کنند. باید به یک شناسه یکتا تکیه کنیم که از محتوای فایل ساخته میشود. به آن Fingerprint (اثر انگشت) میگویند.
Fingerprint یک محاسبه ریاضی است که از محتوای فایل یک مقدار هش یکتا میسازد. این هش را معمولاً با توابع هش رمزنگاری مثل SHA-256 تولید میکنند و فارغ از نام و منبع آپلود، شناسهای قوی و یکتا برای فایل است. با آن میتوانیم بهطور مؤثر تشخیص بدهیم که فایل یا بخشی از آن قبلاً آپلود شده یا نه.
برای آپلودهای قابل ازسرگیری، علاوه بر Fingerprint کل فایل، برای هر تکه هم Fingerprint جداگانه میسازیم. این کار به سیستم اجازه میدهد دقیقاً بفهمد کدام بخشهای فایل قبلاً فرستاده شدهاند.
حالا همه چیز را کنار هم بگذاریم. وقتی کاربر یک فایل بزرگ را آپلود میکند، این اتفاقها میافتد:
- کلاینت فایل را به تکههای 5 تا 10 مگابایتی تقسیم میکند و برای هر تکه Fingerprint میسازد. برای کل فایل هم یک Fingerprint میسازد که همان
fileIdمیشود. - کلاینت یک درخواست
GETبرای گرفتنFileMetadataفایلی با همان شناسه (Fingerprint) میفرستد تا ببیند از قبل وجود دارد یا نه. اگر وجود داشته باشد، آپلود را ادامه میدهیم. - اگر فایل وجود نداشته باشد، کلاینت به
/files/presigned-urlدرخواست میدهد و Presigned URL میگیرد. باطن، Metadata فایل را با وضعیت «در حال آپلود» در جدولFileMetadataذخیره میکند و آرایهchunksفهرستی از Fingerprint تکهها با وضعیت «آپلودنشده» خواهد بود. - کلاینت هر تکه را با Presigned URL در S3 آپلود میکند. بعد از آپلود هر تکه، S3 با Event Notifications پیامی به باطن میفرستد و باطن آن تکه را در فیلد
chunks«آپلودشده» علامت میزند. - وقتی همه تکههای آرایه
chunks«آپلودشده» شدند، باطن وضعیت کل فایل را درFileMetadataبه «آپلودشده» تغییر میدهد.
در تمام این فرایند، کلاینت مسئول دنبال کردن پیشرفت آپلود و بهروز کردن رابط کاربری است تا کاربر بداند در چه مرحلهای است و چقدر زمان لازم دارد.
آپلود چندقسمتی در AWS
رویکردی که توضیح دادیم ابداع تازهای نیست. ارائهدهندگان ذخیرهسازی ابری مثل Amazon S3 این مسئله را حل کردهاند. S3 قابلیتی به نام Multipart Upload دارد که اجازه میدهد اشیای بزرگ را در چند قسمت آپلود کنید. دقیقاً همان چیزی است که توضیح دادیم. کلاینت فایل را تکهتکه میکند، هر قسمت را در S3 آپلود میکند و S3 قسمتها را به یک شیء واحد تبدیل میکند. AWS حتی یک SDK جاوااسکریپت هم دارد که تکهتکه کردن و آپلود را برای شما انجام میدهد.
در عمل هم برای سیستمی مثل Dropbox به همین API تکیه میکنید. اما در مصاحبه نمیتوانید فقط بگویید «از S3 Multipart Upload API استفاده میکنم» و رد شوید. باید بتوانید توضیح بدهید چطور کار میکند و اگر مجبور بودید خودتان پیادهسازی کنید، چطور این کار را میکردید. با این حال خوب است به مصاحبهکننده نشان بدهید با Multipart Upload آشنایید، چون نشانه تجربه است.
2) آپلود، دانلود و همگامسازی با بیشترین سرعت
چند راه برای سریعتر کردن آپلود و دانلود را قبلاً دیدیم، ولی هنوز کارهای بیشتری میشود کرد. برای دانلود، از CDN استفاده کردیم تا فایل به کاربر نزدیکتر شود. این کار مسافتی را که فایل طی میکند کم میکند، Latency را پایین میآورد و سرعت دانلود را بالا میبرد.
برای آپلود، Chunking علاوه بر امکان ازسرگیری، نقش مهمی در سرعت دارد. پهنای باند ثابت است (یعنی لوله خیلی بزرگ است)، اما با Chunking میتوانیم از آن حداکثر استفاده را ببریم. با ارسال موازی چند تکه و استفاده از اندازه تکه تطبیقی بر اساس شرایط شبکه، بهرهبرداری از پهنای باند را بیشینه میکنیم.
همین روش را برای همگامسازی هم میشود به کار برد. وقتی فایلی تغییر میکند، تشخیص میدهیم کدام تکهها عوض شدهاند و فقط همانها را همگام میکنیم، نه کل فایل را. این کار همگامسازی را خیلی سریعتر میکند.
علاوه بر اینها، فشردهسازی (Compression) هم میتواند سرعت آپلود و دانلود را بالا ببرد. فشردهسازی حجم فایل را کم میکند و بایت کمتری باید منتقل شود. میتوانیم فایل را قبل از آپلود روی کلاینت فشرده کنیم و بعد از آپلود روی سرور از حالت فشرده خارج کنیم. در جهت دانلود هم میتوانیم فایل را روی سرور فشرده کنیم و کلاینت آن را باز کند.
با این حال باید هوشمندانه عمل کنیم. فشردهسازی فقط وقتی مفید است که زمان صرفهجوییشده در انتقال، از زمان فشرده کردن و باز کردن بیشتر باشد. برای بعضی فایلها، بهخصوص فایلهای رسانهای مثل تصویر و ویدیو، نسبت فشردهسازی آنقدر پایین است که ارزش وقت گذاشتن ندارد. اگر همین حالا یک فایل png را فشرده کنید، خوششانس باشید که حجمش چند درصد کم شود. پس صرف نمیکند. در مقابل، برای فایلهای متنی نسبت فشردهسازی خیلی بالاتر است و بسته به شرایط شبکه ممکن است بیارزد. یک فایل متنی 5 گیگابایتی بسته به محتوا میتواند به 1 گیگابایت یا حتی کمتر برسد.
در نهایت باید روی کلاینت منطقی پیاده کنید که بر اساس نوع فایل، اندازه و شرایط شبکه تصمیم بگیرد قبل از آپلود فشردهسازی انجام شود یا نه.
الگوریتمهای فشردهسازی
الگوریتمهای متعددی برای فشردهسازی وجود دارد. رایجترینها Gzip، Brotli و Zstandard هستند و هر کدام در نسبت فشردهسازی و سرعت مبادلههای خودش را دارد.
- Gzip: پرکاربردترین است و همه مرورگرهای مدرن از آن پشتیبانی میکنند.
- Brotli: جدیدتر است و نسبت فشردهسازی بهتری از Gzip دارد، اما پشتیبانی همه مرورگرها را ندارد.
- Zstandard: جدیدترین است و نسبت فشردهسازی و سرعت بالایی دارد، اما آن هم پشتیبانی همه مرورگرها را ندارد.
الگوریتم را باید بر اساس نیاز خاص خودتان انتخاب کنید.
نکته مهم دیگر این است که اگر رمزگذاری لازم است، همیشه ابتدا فشردهسازی کنید و بعد رمزگذاری. چون رمزگذاری ذاتاً تصادفیبودن را وارد فایل میکند و فشردهسازی را سخت میکند. فشردهسازی پیش از رمزگذاری نسبت فشردهسازی بسیار بهتری میدهد.
3) تضمین امنیت فایل
امنیت در هر سیستم ذخیرهسازی فایل حیاتی است. باید مطمئن شویم فایلها امناند و فقط کاربران مجاز به آنها دسترسی دارند.
- رمزگذاری در حین انتقال (Encryption in Transit): برای بیشتر داوطلبان این مورد بدیهی است. برای رمزگذاری دادهها هنگام انتقال بین کلاینت و سرور از HTTPS استفاده میکنیم. این روش استاندارد است و همه مرورگرهای مدرن از آن پشتیبانی میکنند.
- رمزگذاری در حالت ذخیره (Encryption at Rest): فایلها را هنگام ذخیره در S3 هم باید رمزگذاری کنیم. این قابلیت S3 است و فعال کردنش آسان است. هنگام آپلود فایل در S3 مشخص میکنیم که باید رمزگذاری شود. S3 فایل را با یک کلید یکتا رمزگذاری میکند و کلید را جدا از فایل نگه میدارد. به این ترتیب حتی اگر کسی به فایل دسترسی پیدا کند، بدون کلید نمیتواند آن را رمزگشایی کند.
- کنترل دسترسی (Access Control): فهرست اشتراک یا جدول و کش جداگانه اشتراکگذاری، ACL اصلی ماست. همانطور که گفتیم، لینکهای دانلود را فقط به کاربران مجاز میدهیم.
اما اگر کاربر مجاز لینک دانلود را به یک کاربر غیرمجاز بدهد چه؟ مثلاً عمداً یا سهواً لینک را در یک انجمن عمومی یا شبکه اجتماعی منتشر کند. باید مطمئن شویم کاربران غیرمجاز نمیتوانند فایل را دانلود کنند.
اینجا همان URL های امضاشدهای که قبلاً گفتیم به کار میآیند. وقتی کاربر لینک دانلود میخواهد، یک URL امضاشده میسازیم که فقط برای مدت کوتاهی (مثلاً 5 دقیقه) معتبر است. کاربر با آن فایل را دانلود میکند. اگر فرد غیرمجازی این URL را به دست بیاورد، چون منقضی شده، نمیتواند از آن استفاده کند.
این URL ها با CDN های مدرن مثل CloudFront هم کار میکنند و از قابلیتهای S3 هستند. روند کار اینطور است:
- تولید: URL امضاشده روی سرور ساخته میشود. امضا معمولاً شامل مسیر URL، مهر زمانی انقضا و احتمالاً محدودیتهای دیگر (مثل آدرس IP) است. این امضا با یک کلید مخفی ساخته میشود که فقط ارائهدهنده محتوا و CDN آن را میدانند.
- توزیع: URL امضاشده به کاربر مجاز داده میشود و او میتواند با آن مستقیم به منبع مشخصشده در CDN دسترسی پیدا کند.
- اعتبارسنجی: وقتی CDN درخواستی با URL امضاشده دریافت میکند، امضا را با قالب و پارامترهای مورد انتظار (از جمله مهر زمانی انقضا) مقایسه میکند. اگر امضا معتبر و URL منقضینشده باشد، محتوا را تحویل میدهد. در غیر این صورت دسترسی را رد میکند.
انتظارات در هر سطح
شاید فکر کنید «در مصاحبه واقعاً چقدر از اینها را باید بدانم؟» بیایید بر اساس سطح تفکیک کنیم.
سطح Mid-level
- عرض در برابر عمق: تمرکز اصلی روی عرض است (حدود 80 درصد عرض و 20 درصد عمق). باید بتوانید یک طراحی سطح بالا بسازید که نیازمندیهای عملکردی تعریفشده را پوشش بدهد، حتی اگر با خیلی از اجزا فقط در حد آشنایی سطحی باشید.
- مرور مبانی: مصاحبهکننده مدتی را صرف بررسی مبانی میکند تا مطمئن شود میدانید هر جزء چه کاری انجام میدهد. مثلاً اگر API Gateway اضافه کنید، انتظار داشته باشید بپرسد چه کاری میکند و در سطح بالا چطور کار میکند. یعنی هیچ چیزی را بدیهی فرض نمیکند.
- ترکیب هدایت و همراهی: در مراحل اولیه مصاحبه باید خودتان جلو بروید، اما مصاحبهکننده انتظار ندارد مشکلات طراحی را دقیق و فعالانه تشخیص بدهید. بنابراین طبیعی است که در مراحل بعد، با بررسی طراحی شما، خودش هدایت را به دست بگیرد.
- معیار برای Dropbox: داوطلب E4 باید endpoint های API و مدل داده را روشن تعریف کند و یک طراحی سطح بالا بسازد که آپلود، دانلود و اشتراکگذاری را پوشش بدهد. انتظار ندارم از ابتدا درباره Presigned URL یا آپلود مستقیم به S3 یا Chunking بداند. اما وقتی سؤالهایی میپرسم مثل «الان فایل را دو بار آپلود میکنی، چطور میشود از آن جلوگیری کرد؟» یا «چطور پیشرفت را نشان میدهی و در عین حال امکان ازسرگیری میدهی؟»، انتظار دارم بتواند مسئله را تحلیل کند و به راهحل برسد.
سطح Senior
- عمق تخصص: انتظارات به سمت دانش عمیقتر میرود، حدود 60 درصد عرض و 40 درصد عمق. باید در حوزههایی که تجربه عملی دارید وارد جزئیات فنی شوید و درک عمیقی از مفاهیم و فناوریهای کلیدی مسئله نشان بدهید.
- طراحی سیستم پیشرفته: باید با اصول پیشرفته طراحی سیستم آشنا باشید. مثلاً بدانید برای فایلهای بزرگ چطور از Blob Storage استفاده کنید یا CDN را چطور برای دانلود سریعتر به کار ببرید. باید بتوانید مبادلات انتخابهای مختلف را بحث کنید و تصمیمهایتان را بر اساس تجربه توجیه کنید.
- تشریح تصمیمهای معماری: باید مزایا و معایب انتخابهای معماری را روشن بیان کنید، بهویژه تأثیر آنها روی مقیاسپذیری، عملکرد و قابلیت نگهداری.
- حل مسئله و پیشقدمی: باید مهارت حل مسئله قوی و رویکرد فعالانه نشان بدهید. یعنی چالشهای احتمالی طراحی را پیشبینی کنید و بهبود پیشنهاد بدهید، گلوگاهها را پیدا و رفع کنید، عملکرد را بهینه کنید و از قابلیت اطمینان سیستم مطمئن شوید.
- معیار برای Dropbox: از داوطلب E5 انتظار دارم طراحی سطح بالای اولیه را سریع انجام بدهد تا وقت بیشتری برای بحث درباره آپلود فایلهای بزرگ بماند. او باید از داوطلب Mid-level فعالتر باشد، چند گزینه را بسنجد و به یک راهحل منطقی برسد. اگرچه الزامی نیست، بسیاری از داوطلبان تجربه آپلود فایل دارند و میتوانند مستقیم درباره API های خاص (مثل Multipart Upload) و نحوه کارشان صحبت کنند.
سطح Staff به بالا
- تأکید بر عمق: از داوطلب Staff به بالا انتظار میرود وارد ظرافتهای طراحی شود. تعادل حدود 40 درصد عرض و 60 درصد عمق است. حتی اگر این مسئله خاص را قبلاً حل نکرده باشید، باید در دنیای واقعی آنقدر مسئله حل کرده باشید که با پشتوانه تجربهتان با اطمینان راهحل طراحی کنید.
- دانش عملی فناوریها: باید بدانید از چه فناوریهایی استفاده کنید، نه فقط در تئوری، بلکه در عمل. باید بتوانید با تجربههای گذشته توضیح بدهید که چطور برای حل مسائل خاص به کار میروند. مصاحبهکننده میداند که مباحث سادهتر (REST API، نرمالسازی داده و غیره) را میدانید، پس میتوانید سریع از آنها عبور کنید تا برای بخشهای جذابتر وقت بماند.
- پیشقدمی بالا: انتظار میرود مسائل را مستقل شناسایی و حل کنید و چالشهای اصلی طراحی را بشناسید. این فقط پاسخ دادن به مشکلات پس از بروز نیست، بلکه پیشبینی آنها و اجرای راهحلهای پیشگیرانه است. مصاحبهکننده فقط باید برای تمرکز دخالت کند، نه برای هدایت.
- حل مسئله پیچیده و تصمیمگیری: نهتنها باید چالشهای فنی پیچیده را حل کنید، بلکه تصمیمهای آگاهانهای بگیرید که مقیاسپذیری، عملکرد، قابلیت اطمینان و نگهداری را در نظر میگیرد.
- طراحی پیشرفته و مقیاسپذیری: رویکرد شما باید روی مقیاسپذیری و قابلیت اطمینان در شرایط بار بالا متمرکز باشد. این یعنی درک کامل سیستمهای توزیعشده، Load Balancing، استراتژیهای ذخیرهسازی و سایر مفاهیم پیشرفته لازم برای ساخت سیستمهای مقاوم و مقیاسپذیر.
- معیار برای Dropbox: از داوطلب سطح Staff انتظار عمق و کیفیت بالا در راهحلها دارم، بهویژه برای سناریوهای پیچیدهای که بحث شد. داوطلبان استثنایی در هر یک از موضوعات بالا عمیق میشوند و حتی ممکن است گفتوگو را به سمتی ببرند که به نظرشان جالبتر یا مرتبطتر است. همچنین انتظار میرود مبادلات بین راهحلها را کامل درک کنند، آنها را روشن بیان کنند و با مصاحبهکننده مثل یک همتا رفتار کنند.