فناوریهای کلیدی در مصاحبههای طراحی سیستم
معرفی فناوریهای اصلی طراحی سیستم شامل پایگاه داده، Blob Storage، موتور جستجو، API Gateway، Load Balancer، صف، استریم، قفل توزیعشده، کش و CDN
فناوریهای کلیدی
این فصل فناوریهای کلیدی را معرفی میکند که باید برای مصاحبههای طراحی سیستم بشناسید. مدیران و مهندسان ارشد شرکتهای FAANG این مطالب را گردآوری کردهاند.
طراحی سیستم یعنی کنار هم گذاشتن مؤثرترین بلوکهای سازنده برای حل یک مسئله. برای همین شناخت خوب رایجترین این بلوکها اهمیت زیادی دارد. بیشتر مصاحبهگران اهمیتی نمیدهند که شما درباره یک راهحل خاص، مثلاً یک فناوری صف، چقدر میدانید. فقط باید بتوانید از آن درست استفاده کنید. اما اگر درباره هیچ راهحل صفی اطلاعاتی نداشته باشید، طراحی سیستمی که به صف نیاز دارد برایتان سخت میشود.
ساختار کلی
در این بخش دستهبندیهای کلیدی فناوریهایی را بررسی میکنیم که برای حل حدود ۹۰ درصد مسائل طراحی سیستم لازماند. برای هر دسته چند گزینه رایج هم معرفی میکنیم. انتخاب فناوری در هر دسته با خودتان است، اما توصیه میکنیم دستکم یکی از آنها را خوب بلد باشید.
از بخش Leveling به خاطر داشته باشید که عمق سوالهای مصاحبهگر به سطحی که برایش مصاحبه میدهید بستگی دارد. اگر کاندیدای سطح متوسط بتواند ElasticSearch را تقریباً بهعنوان یک ایندکس جستجو توصیف کند، احتمالاً مشکلی ندارد. اما کاندیدای ارشدی که نتواند inverted index یا دلیل مقیاسپذیری آن را توضیح بدهد، احتمالاً پرچم زرد میگیرد. در هر حال، اول به پهنا و بعد به عمق فکر کنید.
پایگاه داده اصلی
تقریباً در همه مسائل طراحی سیستم باید مقداری داده ذخیره کنید. این دادهها به احتمال زیاد در یک پایگاه داده یا در Blob Storage قرار میگیرند. پایگاه داده انواع مختلفی دارد، اما رایجترین آنها پایگاههای داده رابطهای (مثل Postgres) و NoSQL (مثل DynamoDB) هستند. توصیه میکنیم یکی از این دو را برای مصاحبه انتخاب کنید. اگر بیشتر در مصاحبههای طراحی محصول شرکت میکنید، پایگاه داده رابطهای را انتخاب کنید. اگر بیشتر مصاحبههای طراحی زیرساخت دارید، NoSQL گزینه بهتری است.
بسیاری از کاندیداها سعی میکنند در پاسخشان مقایسهای بین پایگاه داده رابطهای و NoSQL بگنجانند و خودشان را به دردسر میاندازند. واقعیت این است که این دو فناوری همپوشانی زیادی دارند. جملههای کلی مثل «چون دادههایم رابطه دارند، باید از پایگاه داده رابطهای استفاده کنم» درست نیستند، چون پایگاههای NoSQL هم میتوانند این کار را بهخوبی انجام دهند. جمله «چون به مقیاس و کارایی نیاز دارم، باید NoSQL بگیرم» هم نشاندهنده بیتجربگی است، چون پایگاههای رابطهای هم معمولاً در مقیاس بالا بهخوبی کار میکنند.
حقیقت این است که بیشتر مصاحبهگران لازم نمیدانند مقایسه صریحی بین SQL و NoSQL انجام دهید. این یک تله است و بهتر است کاملاً از آن دوری کنید. بهجای آن درباره ویژگیهای پایگاه دادهای که انتخاب کردهاید حرف بزنید و توضیح دهید چطور به حل مسئله کمک میکند. اگر هم از شما خواستند مقایسه کنید، روی تفاوتهای پایگاههایی که میشناسید و اثر آنها بر طراحیتان تمرکز کنید. برای مثال، جمله «اینجا از Postgres استفاده میکنم چون ویژگیهای ACID آن اجازه میدهد یکپارچگی دادهها حفظ شود» شروع بسیار خوبی است.
پایگاههای داده رابطهای
پایگاه داده رابطهای (RDBMS) رایجترین نوع پایگاه داده است. معمولاً برای دادههای تراکنشی مثل اطلاعات کاربر و سفارش به کار میرود و انتخاب پیشفرض در مصاحبههای طراحی محصول است. این پایگاهها داده را در جدولهایی از سطر و ستون نگه میدارند. هر سطر یک رکورد و هر ستون یک فیلد از آن رکورد است. مثلاً جدول کاربران میتواند ستون نام و ستون ایمیل داشته باشد. پرسوجو در این پایگاهها معمولاً با SQL انجام میشود، زبانی اعلانی (declarative) برای کار با داده.
بررسی نحوه کار داخلی RDBMS خارج از محدوده این راهنماست. اگر علاقهمندید، پیشنهاد میکنیم یک منبع آموزشی عمیق درباره طرز کار پایگاههای داده بخوانید. وقتی بدانید دادهها در داخل پایگاه داده چطور جریان پیدا میکنند، ترفندهای بیشتری برای بهینهسازی کارایی و مقیاس در مصاحبه خواهید داشت.
چیزهایی که باید درباره پایگاههای رابطهای بدانید
پایگاههای رابطهای علاوه بر ذخیرهسازی ساده، چند ویژگی دارند که در مصاحبههای طراحی سیستم به کار میآیند. مهمترین آنها اینهاست.
- Join در SQL: Join راهی برای ترکیب دادههای چند جدول است. مثلاً اگر جدول کاربران و جدول پستها دارید، میتوانید همه پستهای یک کاربر مشخص را بگیرید. پایگاههای SQL از Join دلخواه بین جدولها پشتیبانی میکنند. اما Join میتواند گلوگاه بزرگی برای کارایی باشد، پس تا جای ممکن آن را کم کنید.
- ایندکسها: ایندکس راهی برای ذخیره داده است که پرسوجو را سریعتر میکند. مثلاً اگر جدول کاربران ستون نام دارد، میتوانید روی آن ایندکس بسازید تا جستجوی کاربر بر اساس نام سریعتر شود. ایندکسها معمولاً با B-Tree یا Hash Table پیادهسازی میشوند. دو نکته درباره پایگاههای رابطهای مهم است. اول اینکه از تعداد زیادی ایندکس پشتیبانی میکنند و میتوانید برای جستجوهای مختلف بهینهسازی کنید. دوم اینکه ایندکس چندستونی و تخصصی (مثل ایندکس مکانی و متن کامل) دارند.
- تراکنشها: تراکنش چند عملیات را در یک عملیات اتمی گروهبندی میکند. مثلاً میخواهید همزمان یک کاربر جدید و یک پست برای او بسازید. اگر این کار را در یک تراکنش انجام دهید، یا هر دو عملیات موفق میشوند یا هر دو شکست میخورند. به این ترتیب داده نامعتبر، مثل پست متعلق به کاربری که وجود ندارد، نخواهید داشت.
رایجترین پایگاههای رابطهای
رایجترین آنها Postgres و MySQL هستند. اگر انتخاب خاصی ندارید، Postgres را پیشنهاد میکنیم، هرچند هر دو گزینه خوباند.
پایگاههای داده NoSQL
NoSQL دسته وسیعی از پایگاههای داده است که مدلهای داده متنوعی را پوشش میدهد، از جمله کلید-مقدار، سند، ستونخانواده و گراف. بر خلاف پایگاههای رابطهای، ساختار سنتی جدولی ندارند و اغلب بدون schema هستند. این انعطاف باعث میشود بتوانند حجم زیادی از دادههای بدون ساختار، نیمهساختیافته یا ساختیافته را مدیریت کنند و بهراحتی افقی مقیاس پیدا کنند.
پایگاههای NoSQL در این شرایط گزینههای قویای هستند.
- مدل داده انعطافپذیر: مدل داده شما در حال تغییر است یا باید ساختارهای مختلف را بدون schema ثابت ذخیره کنید.
- مقیاسپذیری: برنامه شما برای تحمل حجم زیاد داده یا بار کاربری بالا باید روی چند سرور افقی مقیاس پیدا کند.
- دادههای بزرگ و برنامههای وب بلادرنگ: با حجم زیادی داده، بهخصوص بدون ساختار، سروکار دارید یا به پردازش و تحلیل همزمان داده نیاز دارید.
جاهایی که NoSQL عملکرد خوبی دارد لزوماً جاهایی نیست که پایگاه رابطهای ناتوان باشد (و برعکس). مثلاً NoSQL برای داده بدون ساختار عالی است، اما پایگاههای رابطهای هم میتوانند ستون JSON با schema انعطافپذیر داشته باشند. NoSQL برای مقیاس افقی عالی است، ولی پایگاه رابطهای هم با معماری مناسب افقی مقیاس مییابد. پس وقتی در مصاحبه درباره NoSQL صحبت میکنید، حرفهای کلی نزنید. درباره ویژگیهای مشخص همان پایگاه داده و کمکی که به حل مسئله میکنند توضیح بدهید.
چیزهایی که باید درباره NoSQL بدانید
- مدلهای داده: NoSQL انواع مختلفی دارد و هر نوع مدل داده خودش را دارد. رایجترین انواع آن Key-Value Store، Document Store، Column-Family Store و پایگاه داده گراف هستند.
- مدلهای سازگاری: پایگاههای NoSQL مدلهای سازگاری مختلفی ارائه میدهند، از سازگاری قوی (Strong Consistency) تا سازگاری نهایی (Eventual Consistency). در سازگاری قوی، همه گرهها همزمان داده یکسانی دارند. در سازگاری نهایی، همه گرهها در نهایت به داده یکسان میرسند.
- ایندکسگذاری: مثل پایگاههای رابطهای، NoSQL هم برای سریعتر شدن پرسوجو از ایندکس پشتیبانی میکند. رایجترین انواع ایندکس، B-Tree و Hash Table هستند.
- مقیاسپذیری: پایگاههای NoSQL با Consistent Hashing یا Sharding (افراز دادهها)، داده را بین سرورهای زیادی پخش میکنند و افقی مقیاس مییابند.
رایجترین پایگاههای NoSQL
رایجترین آنها DynamoDB، Cassandra و MongoDB هستند. DynamoDB بهخاطر تنوع ویژگیها و استقبال گسترده، یکی از گزینههای مورد علاقه ماست. Cassandra بهدلیل مدل ذخیرهسازی append-only برای بارهای نوشتن سنگین انتخاب خوبی است، هرچند با بعضی مصالحهها در کارایی همراه است.
Blob Storage
گاهی باید Blobهای بزرگ و بدون ساختار داده را ذخیره کنید، مثل تصویر، ویدیو یا فایلهای دیگر. نگهداری این Blobهای بزرگ در پایگاه داده سنتی هم گران است و هم ناکارآمد، پس در صورت امکان باید از آن پرهیز کرد. بهجایش از یک سرویس Blob Storage مثل Amazon S3 یا Google Cloud Storage استفاده کنید. این پلتفرمها دقیقاً برای مدیریت Blobهای بزرگ ساخته شدهاند و از پایگاههای داده سنتی خیلی ارزانترند.
Blob Storage ساده است. یک Blob را آپلود میکنید، ذخیره میشود و یک URL میگیرید. بعداً با همین URL آن Blob را دانلود میکنید. این سرویسها اغلب همراه با CDN کار میکنند تا دانلود در هر نقطه از دنیا سریع باشد. فایل را در Blob Storage آپلود میکنید که منبع اصلی (origin) است و بعد با CDN آن را در لبههای شبکه در سراسر جهان کش میکنید.
از Blob Storage مثل S3 بهعنوان پایگاه داده اصلی استفاده نکنید، مگر دلیل بسیار خوبی داشته باشید. در یک ساختار معمول، پایگاه داده اصلی مثل Postgres یا DynamoDB رکوردهایی دارد که فقط یک URL به Blob ذخیرهشده در S3 اشاره میکنند. به این ترتیب برای پرسوجو و ایندکسگذاری از پایگاه داده با تأخیر بسیار کم استفاده میکنید و همزمان از Blob Storage ارزان بهره میبرید.
چند نمونه رایج استفاده از Blob Storage:
- طراحی YouTube: ویدیوها در Blob Storage و metadata در پایگاه داده ذخیره میشوند.
- طراحی Instagram: تصویرها و ویدیوها در Blob Storage و metadata در پایگاه داده ذخیره میشوند.
- طراحی Dropbox: فایلها در Blob Storage و metadata در پایگاه داده ذخیره میشوند.
ساختار بسیار رایج برای کار با فایلهای باینری بزرگ به این صورت است.
آپلود:
- کلاینت برای آپلود فایل، از سرور یک presigned URL درخواست میکند.
- سرور یک presigned URL به کلاینت برمیگرداند و آن را در پایگاه داده ثبت میکند.
- کلاینت فایل را روی همان presigned URL آپلود میکند.
- Blob Storage به سرور اعلان میدهد که آپلود کامل شده و وضعیت بهروز میشود.
دانلود:
- کلاینت یک فایل مشخص را از سرور درخواست میکند و یک presigned URL دریافت میکند.
- کلاینت با این URL فایل را از طریق CDN دانلود میکند. CDN درخواست را به Blob Storage زیرین پروکسی میکند.
چیزهایی که باید درباره Blob Storage بدانید
- دوام (Durability): سرویسهای Blob Storage برای دوام بسیار بالا طراحی شدهاند. آنها با تکنیکهایی مثل replication و erasure coding مطمئن میشوند دادهتان حتی با خرابی دیسک یا سرور هم سالم میماند.
- مقیاسپذیری: سرویسهای میزبانیشده مثل AWS S3 را میتوان تقریباً بینهایت مقیاسپذیر فرض کرد. حجم داده و تعداد درخواست آنها عملاً نامحدود است (البته در چارچوب محدودیتهای حساب شما). بنابراین در مصاحبه لازم نیست مقیاسپذیری Blob Storage را جداگانه بررسی کنید و آن را یک پیشفرض بدانید.
- هزینه: این سرویسها برای صرفه اقتصادی طراحی شدهاند و خیلی ارزانتر از نگهداشتن Blobهای بزرگ در پایگاه داده سنتیاند. مثلاً AWS S3 برای ۵۰ ترابایت اول، ماهانه ۰٫۰۲۳ دلار به ازای هر گیگابایت میگیرد. این مبلغ خیلی کمتر از ذخیره همان داده در DynamoDB است که برای ۱۰ ترابایت اول، ماهانه ۱٫۲۵ دلار به ازای هر گیگابایت میگیرد.
- امنیت: این سرویسها ویژگیهای امنیتی داخلی دارند، مثل رمزگذاری دادههای ذخیرهشده و در حال انتقال. کنترل دسترسی هم دارند و میتوانید تعیین کنید چه کسی به دادهها دسترسی داشته باشد.
- آپلود و دانلود مستقیم از کلاینت: Blob Storage اجازه میدهد Blob را مستقیم از کلاینت آپلود و دانلود کنید. این قابلیت برای برنامههایی که تصویر یا ویدیوی بزرگ ذخیره و بازیابی میکنند مفید است. با presigned URL و نحوه استفاده از آن برای دادن دسترسی موقت به یک Blob، چه برای آپلود و چه دانلود، آشنا شوید.
- Chunking: برای آپلود فایلهای بزرگ معمولاً فایل را به قطعههای کوچکتر تقسیم میکنند. اگر آپلود شکست بخورد، میتوانید از همانجا ادامه دهید و قطعهها را میتوان موازی آپلود کرد. این کار برای فایلهای بزرگ که آپلود یکجای آنها زمان زیادی میبرد خیلی مفید است. سرویسهای مدرن مثل S3 از طریق Multipart Upload API بهصورت پیشفرض از Chunking پشتیبانی میکنند.
نمونه سرویسهای Blob Storage
محبوبترین سرویسها Amazon S3، Google Cloud Storage و Azure Blob هستند. همه آنها سریع، بادوام و مقرونبهصرفهاند و ویژگیهایی مثل versioning، lifecycle policy و کنترل دسترسی دارند. شرکتهایی مثل Netflix، Airbnb و Spotify از آنها برای ذخیره دادههای حجیمی مثل تصویر، ویدیو و فایل استفاده میکنند.
اگر با هیچکدام تجربه ندارید، S3 را انتخاب کنید. چون محبوبترین است و بیشتر مصاحبهگران آن را میشناسند. حتی پلتفرمهای غیر از S3 هم معمولاً API سازگار با S3 دارند.
پایگاه داده بهینه برای جستجو
گاهی یکی از ویژگیهای طراحی شما جستجوی متن کامل (full-text search) است. یعنی بتوانید در حجم زیادی از داده متنی جستجو کنید و نتایج مرتبط را پیدا کنید. این با پرسوجوی سنتی پایگاه داده فرق دارد که معمولاً بر اساس تطابق دقیق یا بازه است. اگر پایگاه داده بهینه برای جستجو نداشته باشید، باید پرسوجویی شبیه این بنویسید:
1
SELECT * FROM documents WHERE document_text LIKE '%search_term%'
این پرسوجو کند و ناکارآمد است و مقیاسپذیر نیست، چون به اسکن کامل جدول (full table scan) نیاز دارد. یعنی پایگاه داده باید تکتک رکوردها را بخواند و شرط را روی آنها بسنجد، و نمیتواند از ایندکس استفاده کند. کند است!
در مقابل، پایگاههای داده بهینه برای جستجو دقیقاً برای جستجوی متن کامل ساخته شدهاند. آنها با تکنیکهایی مثل ایندکسگذاری، Tokenization و Stemming جستجو را سریع و کارآمد میکنند. بهطور خلاصه، این پایگاهها inverted index میسازند. inverted index ساختار دادهای است که هر کلمه را به اسنادی که شامل آن کلمه هستند نگاشت میکند. با آن میتوانید سریع اسنادی را پیدا کنید که کلمه مشخصی دارند. یک نمونه ساده از inverted index شبیه این است:
1
2
3
4
5
{
"word1": ["doc1", "doc2", "doc3"],
"word2": ["doc2", "doc3", "doc4"],
"word3": ["doc1", "doc3", "doc4"]
}
حالا بهجای اسکن کل جدول، پایگاه داده میتواند کلمه موجود در پرسوجو را سریع پیدا کند و همه اسناد منطبق را برگرداند. سریع است!
نمونههای استفاده از این پایگاهها سادهاند. برنامهای مثل Ticketmaster را در نظر بگیرید که برای پیدا کردن نتایج مرتبط باید در تعداد زیادی رویداد جستجو کند. یا پلتفرمی مثل Twitter که برای پیدا کردن نتایج مرتبط باید در تعداد زیادی توییت جستجو کند. در هر دو حالت، پایگاه داده بهینه برای جستجو بهترین انتخاب است.
چیزهایی که باید درباره آنها بدانید
- Inverted Index: همانطور که گفتیم، این پایگاهها برای سریع و کارآمد کردن جستجو از inverted index استفاده میکنند. این ساختار هر کلمه را به اسنادی که آن را دارند نگاشت میکند.
- Tokenization: فرایندی که متن را به کلمههای جداگانه تقسیم میکند. به این ترتیب میتوان از کلمهها به اسناد در inverted index رسید.
- Stemming: فرایند تبدیل کلمهها به شکل ریشهایشان. به این ترتیب شکلهای مختلف یک کلمه با هم تطبیق پیدا میکنند. مثلاً running و runs هر دو به run تبدیل میشوند.
- جستجوی فازی (Fuzzy Search): توانایی پیدا کردن نتایج مشابه عبارت جستجو. بیشتر پایگاههای بهینه برای جستجو این قابلیت را بهصورت یک گزینه پیکربندی دارند. این کار با الگوریتمهایی انجام میشود که غلط املایی جزئی یا تغییرات کوچک در عبارت جستجو را تحمل میکنند. تکنیکی مثل فاصله ویرایشی (edit distance) تعداد حرفهایی را میسنجد که باید عوض، اضافه یا حذف شوند تا یک کلمه به کلمه دیگر تبدیل شود.
- مقیاسپذیری: مثل پایگاههای سنتی، این پایگاهها هم با اضافه شدن گره به خوشه و Sharding داده بین گرهها مقیاس پیدا میکنند.
نمونهها
بدون شک پیشرو این حوزه Elasticsearch است. بهطور خلاصه، Elasticsearch یک موتور جستجو و تحلیل توزیعشده و RESTful است که روی Apache Lucene ساخته شده. سریع، مقیاسپذیر و آسان است، محبوبترین پایگاه داده بهینه برای جستجو به شمار میآید و شرکتهایی مثل Netflix، Uber و Yelp از آن استفاده میکنند.
گزینههای دیگر، استفاده از قابلیتهای جستجوی متن کامل خود پایگاه داده شماست. Postgres ایندکسهای GIN دارد که از جستجوی متن کامل پشتیبانی میکنند. Redis هم قابلیت جستجوی متن کامل دارد که به نظر من کاملاً نابالغ و ضعیف است. استفاده از پایگاه داده موجود میتواند برای کوچکتر نگه داشتن طراحی ایده خوبی باشد، اما دانستن این امکانات ضروری است.
API Gateway
بهخصوص در معماری میکروسرویس، API Gateway جلوی سیستم قرار میگیرد و درخواستهای ورودی را به سرویس backend مناسب هدایت میکند. مثلاً اگر سیستم درخواست GET /users/123 را دریافت کند، gateway آن را به سرویس کاربران میفرستد و پاسخ را به کلاینت برمیگرداند. gateway معمولاً مسئول نگرانیهای مشترک بین همه سرویسها (cross-cutting concerns) مثل احراز هویت، Rate Limiting (محدودسازی نرخ درخواست) و logging هم هست.
تقریباً در همه مصاحبههای طراحی سیستم از نوع طراحی محصول، خوب است که API Gateway را اولین نقطه تماس کلاینتها در طراحیتان بگذارید. با این حال، مصاحبهگران بهندرت وارد جزئیات API Gateway میشوند و معمولاً میخواهند سوالهایی بپرسند که به خود مسئله مربوط است.
رایجترین API Gatewayها
رایجترین آنها AWS API Gateway، Kong و Apigee هستند. استفاده از nginx یا وبسرور Apache بهعنوان API Gateway هم غیرمعمول نیست. در روزهای اولیه آمازون، ناوگان بزرگی از وبسرورهای Apache همین نقش را داشتند.
Load Balancer
بیشتر مسائل طراحی سیستم از شما میخواهند سیستمی بسازید که حجم زیادی ترافیک را تحمل کند. وقتی ترافیک زیاد است، باید آن را بین چند ماشین پخش کنید (همان مقیاس افقی) تا هیچ ماشینی بیش از حد بار نگیرد و hot spot ایجاد نشود. اینجا Load Balancer وارد میشود. برای مصاحبه میتوانید Load Balancer را یک جعبه سیاه فرض کنید که کار را در سیستم توزیع میکند.
در واقع هر جا چند ماشین دارید که میتوانند یک درخواست را پردازش کنند، به Load Balancer نیاز دارید. با این حال در مصاحبه، کشیدن یک Load Balancer جلوی هر سرویس زیادی است. یا Load Balancer را کلاً از طرح حذف کنید و فقط بگویید سرویسها افقی مقیاس مییابند، یا فقط یکی را بهصورت یک انتزاع جلوی طرح بگذارید.
گاهی باید ویژگیهای خاصی از Load Balancer مثل sticky session یا اتصالهای پایدار (persistent connection) را در نظر بگیرید. رایجترین تصمیم این است که از Load Balancer لایه ۴ (L4) استفاده کنید یا لایه ۷ (L7).
با یک قاعده ساده میتوانید این تصمیم را تا حدی کوتاه کنید. اگر اتصالهای دائمی مثل WebSocket دارید، احتمالاً L4 را میخواهید. در غیر این صورت L7 انعطاف زیادی در مسیریابی ترافیک به سرویسهای مختلف میدهد و بار اتصال روی سرویسهای پاییندست را هم کم میکند. درباره نحوه مدیریت اتصالهای WebSocket در مسائلی که به بهروزرسانی بلادرنگ نیاز دارند، بیشتر مطالعه کنید.
رایجترین Load Balancerها
رایجترین آنها AWS Elastic Load Balancer (سرویس میزبانیشده AWS)، NGINX (وبسرور متنباز که اغلب بهعنوان Load Balancer استفاده میشود) و HAProxy هستند. توجه کنید که برای مسائل با ترافیک بسیار بالا، Load Balancerهای سختافزاری تخصصی بهتر از Load Balancerهای نرمافزاری خودمیزبان عمل میکنند. در این حالت خیلی زود وارد دنیای پیچیده مهندسی شبکه میشوید.
صف (Queue)
صفها بهعنوان بافر برای ترافیک ناگهانی یا ابزاری برای توزیع کار در سیستم عمل میکنند. یک منبع محاسباتی پیامها را به صف میفرستد و دیگر کاری به آنها ندارد. از طرف دیگر، مجموعهای از workerها (آنها هم منابع محاسباتیاند) پیامها را با سرعت خودشان پردازش میکنند. پیام میتواند هر چیزی باشد، از یک رشته ساده تا یک شیء پیچیده.
کار صف این است که بار روی سیستم را هموار کند. اگر ۱۰۰۰ درخواست بگیرم اما فقط بتوانم ۲۰۰ درخواست در ثانیه پردازش کنم، ۸۰۰ درخواست قبل از پردازش در صف منتظر میمانند، اما رد نمیشوند. صفها همچنین تولیدکننده و مصرفکننده را از هم جدا میکنند و اجازه میدهند هرکدام را مستقل مقیاس بدهید. میتوانم سرویسهای پشت صف را با اثر ناچیز بالا و پایین ببرم.
درباره قرار دادن صف در مسیر کارهای همزمان (synchronous) محتاط باشید. اگر الزام تأخیر سختگیرانهای دارید (مثلاً کمتر از ۵۰۰ میلیثانیه)، با اضافه کردن صف تقریباً مطمئناً آن محدودیت را نقض میکنید.
چند کاربرد رایج صف:
- بافر برای ترافیک ناگهانی: در اپلیکیشنی مثل Uber، صفها میتوانند افزایش ناگهانی درخواست سفر را مدیریت کنند. در ساعات اوج یا رویدادهای خاص، تعداد درخواستها بهشدت بالا میرود. صف این درخواستها را بافر میکند و سیستم میتواند آنها را با نرخ قابلمدیریت پردازش کند، بدون اینکه سرور بیش از حد بار بگیرد یا تجربه کاربر افت کند.
- توزیع کار در سیستم: در سرویس پردازش عکس مبتنی بر ابر، صفها میتوانند وظایف پرهزینه پردازش تصویر را توزیع کنند. وقتی کاربر عکسهایی برای ویرایش یا اعمال فیلتر آپلود میکند، این وظایف در صف قرار میگیرند. بعد گرههای worker مختلف آنها را از صف برمیدارند. به این ترتیب بار کاری یکنواخت پخش میشود و منابع محاسباتی کارآمد استفاده میشوند.
نکاتی که درباره صفها باید بدانید
- ترتیب پیام: بیشتر صفها FIFO هستند (اولین ورودی، اولین خروجی)، یعنی پیامها به ترتیب دریافت پردازش میشوند. اما بعضی صفها (مثل Kafka) تضمینهای ترتیب پیچیدهتری مثل ترتیب بر اساس اولویت یا زمان را هم ممکن میکنند.
- مکانیزم تلاش مجدد (Retry): بسیاری از صفها مکانیزم داخلی دارند که قبل از شکستخورده حساب کردن یک پیام، چند بار دوباره آن را ارسال میکنند. میتوانید تعداد دفعات، فاصله بین تلاشها و حداکثر تعداد تلاش را تنظیم کنید.
- Dead Letter Queue: این صفها پیامهایی را نگه میدارند که قابل پردازش نیستند. برای دیباگ و ممیزی مفیدند، چون میتوانید پیامهای پردازشنشده را بررسی کنید و بفهمید چرا شکست خوردهاند.
- مقیاسپذیری با پارتیشن: صفها را میتوان روی چند سرور پارتیشنبندی کرد تا پیامهای بیشتری را تحمل کنند. هر پارتیشن را میتوان مجموعه متفاوتی از workerها پردازش کنند. مثل پایگاههای داده، برای اینکه پیامهای مرتبط در یک پارتیشن بمانند، باید یک کلید پارتیشن مشخص کنید.
- Backpressure: بزرگترین مشکل صفها این است که بهراحتی سیستم را زیر بار میبرند. اگر سیستم من ۲۰۰ درخواست در ثانیه را پشتیبانی کند اما ۳۰۰ درخواست در ثانیه بگیرم، هرگز صف را تمام نمیکنم! صف فقط این مشکل را پنهان میکند که ظرفیت کافی ندارم. راهحل Backpressure است، یعنی کند کردن تولید پیام وقتی صف پر میشود. این کار جلوی تبدیل شدن صف به گلوگاه را میگیرد. مثلاً اگر صف پر است، میتوانید پیامهای جدید را رد کنید یا نرخ پذیرش را کم کنید، که احتمالاً به خطا برای کاربر یا تولیدکننده منجر میشود.
رایجترین فناوریهای صف
رایجترین آنها Kafka و SQS هستند. Kafka یک پلتفرم استریم توزیعشده است که میتوان از آن بهعنوان صف هم استفاده کرد. SQS هم یک سرویس صف کاملاً مدیریتشده از AWS است.
استریم و Event Sourcing
گاهی سوالی مطرح میشود که نیاز به پردازش بلادرنگ حجم زیادی از داده دارد، یا سناریوهای پردازشی پیچیدهای مثل Event Sourcing را میخواهد.
Event Sourcing تکنیکی است که در آن تغییرات وضعیت برنامه بهصورت دنبالهای از رویدادها ذخیره میشود. با بازپخش (replay) این رویدادها میتوان وضعیت برنامه را در هر نقطه از زمان بازسازی کرد. به همین دلیل برای سیستمهایی که به ردپای ممیزی دقیق یا امکان برگشت و بازپخش تراکنشها نیاز دارند، استراتژی مؤثری است.
در هر دو حالت احتمالاً باید از یک استریم استفاده کنید. برخلاف Message Queue، استریمها میتوانند داده را برای مدت قابلتنظیمی نگه دارند و به مصرفکنندهها اجازه بدهند پیامها را از یک موقعیت مشخص یا از یک زمان معین در گذشته بخوانند و دوباره بخوانند. استریمها در این موقعیتها انتخاب خوبی هستند.
- پردازش بلادرنگ حجم بالای داده: فرض کنید سیستمی برای یک شبکه اجتماعی طراحی میکنید که باید تحلیلهای بلادرنگ تعامل کاربران (لایک، کامنت، اشتراکگذاری) را روی پستها نشان دهد. میتوانید از یک استریم برای دریافت حجم بالای رویدادهای تعاملی کاربران سراسر دنیا استفاده کنید. سپس یک سیستم پردازش استریم (مثل Apache Flink یا Spark Streaming) این رویدادها را بلادرنگ پردازش میکند و داشبورد تحلیلی را بهروز میکند.
- سناریوهای پیچیده مثل Event Sourcing: یک سیستم بانکی را در نظر بگیرید که هر تراکنش (واریز، برداشت، انتقال) باید ثبت شود و ممکن است روی چند حساب اثر بگذارد. با Event Sourcing و یک استریم مثل Kafka، هر تراکنش یک رویداد است که میتوان ذخیره، پردازش و بازپخش کرد. این ساختار علاوه بر پردازش بلادرنگ، به بانک اجازه میدهد تراکنشها را ممیزی کند، تغییرات را برگرداند، یا وضعیت هر حساب را با بازپخش رویدادها در هر لحظه بازسازی کند.
- چند مصرفکننده از یک استریم: در یک اپلیکیشن چت بلادرنگ، وقتی کاربر پیامی میفرستد، پیام در استریمی مربوط به همان اتاق چت منتشر میشود. این استریم مثل یک کانال مرکزی عمل میکند و همه شرکتکنندگان چت مشترک آن هستند. با توزیع پیام از طریق استریم، هر شرکتکننده (مصرفکننده) پیام را همزمان دریافت میکند. این نمونه عالی الگوی publish-subscribe است که کاربرد رایج استریم است.
چیزهایی که باید درباره استریمها بدانید
- مقیاسپذیری با پارتیشنبندی: برای مقیاس دادن استریمها میتوان آنها را روی چند سرور پارتیشنبندی کرد. هر پارتیشن را یک مصرفکننده متفاوت پردازش میکند و مقیاس افقی ممکن میشود. مثل پایگاههای داده، برای اینکه رویدادهای مرتبط در یک پارتیشن بمانند، باید کلید پارتیشن مشخص کنید.
- چند Consumer Group: استریمها از چند گروه مصرفکننده پشتیبانی میکنند و به مصرفکنندههای مختلف اجازه میدهند مستقل از هم از یک استریم بخوانند. این برای وقتی مفید است که میخواهید یک داده را به چند روش مختلف پردازش کنید. مثلاً در یک سیستم تحلیل بلادرنگ، یک گروه رویدادها را برای بهروزرسانی داشبورد پردازش میکند و گروه دیگر همان رویدادها را برای تحلیل تاریخی در پایگاه داده ذخیره میکند.
- Replication: برای تحمل خطا (Fault Tolerance)، استریمها مثل پایگاههای داده میتوانند داده را روی چند سرور تکثیر کنند. اگر سروری از کار بیفتد، داده هنوز از سرور دیگر قابل خواندن است.
- Windowing: استریمها از پنجرهبندی پشتیبانی میکنند، یعنی گروهبندی رویدادها بر اساس زمان یا تعداد. این برای وقتی مفید است که میخواهید رویدادها را دستهای پردازش کنید، مثل محاسبه مجموع ساعتی یا روزانه. داشبوردی بلادرنگ را در نظر بگیرید که میانگین زمان تحویل در هر منطقه را در ۲۴ ساعت گذشته نشان میدهد.
رایجترین فناوریهای استریم
رایجترین آنها Kafka، Flink و Kinesis هستند.
قفل توزیعشده
در سیستمهای آنلاینی مثل Ticketmaster ممکن است لازم باشد یک منبع، مثل بلیت کنسرت، را برای مدت کوتاهی قفل کنید (در این مثال ۱۰ دقیقه). دلیلش این است که تا وقتی کاربری وسط خرید بلیت است، هیچکس دیگری نتواند آن را بردارد. پایگاههای داده سنتی با ویژگی ACID از قفلهای تراکنشی برای حفظ سازگاری داده استفاده میکنند. این قفلها برای اطمینان از اینکه هنگام بهروزرسانی یک رکورد توسط یک کاربر، هیچکس دیگری نتواند آن را تغییر دهد عالیاند، اما برای قفلهای طولانی طراحی نشدهاند. اینجاست که قفل توزیعشده به کار میآید.
قفلهای توزیعشده برای وقتی مناسباند که باید چیزی را بین چند سیستم یا فرایند برای مدت معقولی قفل کنید. این قفلها اغلب با یک ذخیرهگاه کلید-مقدار توزیعشده مثل Redis یا ZooKeeper پیادهسازی میشوند. ایده اصلی این است که قفل را در یک ذخیرهگاه کلید-مقدار نگه دارید و از اتمیک بودن عملیات آن استفاده کنید تا در هر لحظه فقط یک فرایند بتواند قفل را بگیرد.
برای مثال، فرض کنید یک نمونه Redis با کلید ticket-123 دارید و میخواهید آن را قفل کنید. میتوانید مقدار ticket-123 را روی locked بگذارید. اگر فرایند دیگری بخواهد همین کار را بکند، شکست میخورد، چون مقدار از قبل locked است. وقتی فرایند اول کارش تمام شد، مقدار ticket-123 را روی unlocked میگذارد و فرایند دیگری میتواند قفل را بگیرد.
ویژگی مفید دیگر قفلهای توزیعشده این است که میتوان برایشان زمان انقضا تعیین کرد. این برای جلوگیری از گیر کردن قفل در حالت locked هنگام crash یا از بین رفتن فرایند عالی است. مثلاً اگر مقدار ticket-123 را روی locked بگذارید و فرایند crash کند، قفل بعد از مدت مشخصی (مثلاً ۱۰ دقیقه) منقضی میشود و فرایند دیگری میتواند آن را بگیرد.
چند نمونه رایج از استفاده قفل توزیعشده در مصاحبههای طراحی سیستم:
- سیستم پرداخت فروشگاه آنلاین: با قفل توزیعشده، کالای پرتقاضا مثل کفش ورزشی نسخه محدود را هنگام پرداخت برای مدت کوتاهی (مثلاً ۱۰ دقیقه) در سبد خرید کاربر نگه دارید. این کار تضمین میکند وقتی کاربر پرداخت را تکمیل میکند، کالا به شخص دیگری فروخته نشده باشد.
- تطبیق راننده و مسافر در سرویس سواری: قفل توزیعشده میتواند تخصیص راننده به مسافر را مدیریت کند. وقتی مسافری درخواست سفر میدهد، سیستم میتواند راننده نزدیک را قفل کند تا همزمان به چند مسافر اختصاص داده نشود. این قفل تا وقتی راننده سفر را بپذیرد یا رد کند، یا تا گذشتن مدت مشخصی، نگه داشته میشود.
- Cron Job توزیعشده: در سیستمهایی که کارهای زمانبندیشده (cron job) را روی چند سرور اجرا میکنند، قفل توزیعشده تضمین میکند که هر کار در هر زمان فقط روی یک سرور اجرا شود. مثلاً در یک پلتفرم تحلیل داده، کاری روزانه دادههای کاربران را برای گزارشها تجمیع میکند. قفل توزیعشده جلوی اجرای تکراری این کار روی چند سرور را میگیرد و منابع محاسباتی را ذخیره میکند.
- سیستم مزایده آنلاین: در یک حراج آنلاین میتوان در لحظات پایانی مزایده از قفل توزیعشده استفاده کرد. وقتی پیشنهادی در ثانیههای آخر ثبت میشود، سیستم کالا را برای مدت کوتاهی قفل میکند تا پیشنهادها یکییکی پردازش شوند و بالاترین قیمت بهدرستی تعیین شود، نه اینکه چند پیشنهاد همزمان پردازش شوند.
چیزهایی که باید درباره قفلهای توزیعشده بدانید
- مکانیزم قفل: روشهای مختلفی برای پیادهسازی قفل توزیعشده وجود دارد. یکی از رایجترین آنها با Redis است و Redlock نام دارد. Redlock از چند نمونه Redis استفاده میکند تا قفل بهصورت ایمن و سازگار گرفته و آزاد شود.
- انقضای قفل: قفلهای توزیعشده را میتوان طوری تنظیم کرد که بعد از مدت مشخصی منقضی شوند. این برای جلوگیری از گیر کردن قفل در حالت locked هنگام crash یا از بین رفتن یک فرایند مهم است.
- Granularity قفل: قفل توزیعشده میتواند یک منبع یا گروهی از منابع را قفل کند. مثلاً ممکن است بخواهید در سیستم فروش بلیت یک بلیت را قفل کنید یا گروهی از بلیتهای یک بخش استادیوم را.
- Deadlock: وقتی دو یا چند فرایند منتظر یکدیگر برای آزاد کردن قفل باشند، deadlock پیش میآید. مثلاً فرایند اول قفل A را میگیرد و بعد سعی میکند قفل B را بگیرد، در حالی که فرایند دوم قفل B را گرفته و سعی میکند قفل A را بگیرد. نتیجه این میشود که هر دو منتظر هم میمانند. باید آماده باشید درباره نحوه جلوگیری از آن صحبت کنید. یک اشتباه رایج این است که قفلها را از بخشهای دور از هم در زیرساخت یا کد بگیرید. این کار تشخیص و جلوگیری از deadlock را سخت میکند.
کش توزیعشده
در بیشتر مصاحبههای طراحی سیستم باید هم مقیاس سیستم را بالا ببرید و هم تأخیر (latency) را کم کنید. یکی از راههای رایج، کش توزیعشده است. کش فقط یک سرور یا خوشهای از سرورهاست که داده را در حافظه نگه میدارد. برای ذخیره دادههایی که محاسبه یا خواندنشان از پایگاه داده گران است عالی است.
از کش برای این موارد استفاده میکنید.
- ذخیره معیارهای تجمیعی: پلتفرم تحلیلی را در نظر بگیرید که داده را از منابع مختلف جمع میکند تا داشبوردی از معیارها نشان دهد. محاسبه این معیارها گران است، پس پلتفرم آنها را ناهمزمان (مثلاً ساعتی با یک کار پسزمینه) محاسبه میکند و نتیجه را در کش توزیعشده میگذارد. وقتی کاربر داشبورد را باز میکند، پلتفرم بهجای محاسبه دوباره، داده را از کش میخواند و تأخیر کم میشود.
- کاهش تعداد پرسوجو به DB: در یک برنامه وب، session کاربر اغلب در کش توزیعشده نگه داشته میشود تا بار پایگاه داده کم شود. این برای سیستمهایی که باید کاربران همزمان زیادی را پشتیبانی کنند بسیار مهم است. وقتی کاربر وارد میشود، سیستم داده session او را در کش میگذارد و در درخواستهای بعدی سریع آن را بازیابی میکند.
- تسریع درخواستهای گران: اجرای بعضی پرسوجوهای پیچیده در پایگاه داده سنتی دیسکی زمان زیادی میبرد. مثلاً در شبکه اجتماعیای مثل Twitter ممکن است بخواهید فهرست پستهای افرادی را که کاربر دنبال میکند نشان دهید. این پرسوجوی پیچیدهای است که چند جدول را Join میکند و روی چند ستون فیلتر میگذارد. اجرای آن در Postgres ممکن است خیلی طول بکشد. بهجایش میتوانید پرسوجو را یک بار اجرا کنید، نتیجه را در کش توزیعشده بگذارید و هر بار کاربر درخواست کرد، از کش بخوانید.
چیزهایی که باید درباره کش توزیعشده بدانید
- سیاست Eviction: کشهای توزیعشده سیاستهای مختلفی دارند که تعیین میکنند وقتی کش پر شد، کدام موارد حذف شوند. رایجترین سیاستها اینها هستند.
- LRU (Least Recently Used): مواردی را حذف میکند که مدت بیشتری است به آنها دسترسی نشده.
- FIFO (First In, First Out): موارد را به همان ترتیبی که اضافه شدهاند حذف میکند.
- LFU (Least Frequently Used): مواردی را حذف میکند که کمتر به آنها دسترسی شده.
- استراتژی Cache Invalidation: این استراتژی تضمین میکند دادههای کش بهروز بمانند. مثلاً اگر Ticketmaster را طراحی میکنید و رویدادهای محبوب را کش کردهاید، وقتی رویدادی در پایگاه داده تغییر میکند (مثلاً مکانش عوض میشود)، باید آن رویداد را در کش باطل کنید.
- استراتژی نوشتن در کش: این استراتژی تعیین میکند دادهها چگونه و با چه سازگاریای در کش نوشته شوند. چند استراتژی رایج اینهاست.
- Write-Through: داده را همزمان در کش و در ذخیرهگاه اصلی مینویسد. سازگاری را تضمین میکند، اما ممکن است نوشتن را کند کند.
- Write-Around: داده را مستقیم در ذخیرهگاه اصلی مینویسد و کش را دور میزند. این روش آلودگی کش را کم میکند، اما ممکن است در خواندنهای بعدی زمان واکشی داده را زیاد کند.
- Write-Back: داده را در کش مینویسد و بعد بهصورت ناهمزمان در ذخیرهگاه اصلی مینویسد. برای نوشتن سریعتر است، اما اگر کش قبل از نوشتن داده در ذخیرهگاه از کار بیفتد، ممکن است داده از بین برود.
یادتان نباشد درباره دادهای که کش میکنید و ساختار دادهای که به کار میبرید دقیق صحبت کنید. کشهای مدرن فقط ذخیرهگاه ساده کلید-مقدار نیستند و ساختارهای داده متنوعی دارند. مثلاً اگر فهرستی از رویدادها را کش میکنید، ممکن است بخواهید از Sorted Set استفاده کنید تا بتوانید بهراحتی محبوبترین رویدادها را بگیرید. بسیاری از کاندیداها فقط میگویند «رویدادها را در کش میگذارم» و همانجا تمام میکنند. این یک فرصت از دست رفته است و ممکن است سوالهای پیگیری بیشتری برایتان ایجاد کند.
رایجترین فناوریهای کش توزیعشده
دو کش درونحافظهای رایج Redis و Memcached هستند. Redis یک ذخیرهگاه کلید-مقدار است که از ساختارهای داده متنوعی مثل رشته، Hash، List، Set، Sorted Set، Bitmap و HyperLogLog پشتیبانی میکند. Memcached ذخیرهگاه کلید-مقدار سادهای است که رشته و شیء باینری را پشتیبانی میکند.
CDN
سیستمهای مدرن اغلب به کاربران سراسر دنیا سرویس میدهند و رساندن سریع محتوا به آنها چالشبرانگیز است. کاربران (و مصاحبهگران) زمان بارگذاری سریع انتظار دارند و تأخیر زیاد میتواند تجربه کاربری را خراب کند و باعث از دست رفتن ترافیک شود.
شبکه توزیع محتوا (CDN) نوعی کش است که از سرورهای توزیعشده استفاده میکند و محتوا را بر اساس موقعیت جغرافیایی کاربر ارائه میدهد. CDNها اغلب برای محتوای ایستا مثل تصویر، ویدیو و فایلهای HTML به کار میروند، اما برای محتوای پویا مثل پاسخ API هم قابل استفادهاند.
طرز کارشان این است که محتوا را روی سرورهای نزدیک به کاربر ذخیره میکنند. وقتی کاربر محتوایی را میخواهد، CDN درخواست را به نزدیکترین سرور میفرستد. اگر محتوا روی آن سرور کش شده باشد، CDN همان نسخه را برمیگرداند. اگر کش نشده باشد، CDN محتوا را از سرور اصلی (origin) میگیرد، روی آن سرور کش میکند و بعد به کاربر برمیگرداند.
رایجترین کاربرد CDN در مصاحبه، کش کردن داراییهای رسانهای ایستا مثل تصویر و ویدیوست. مثلاً در شبکه اجتماعیای مثل Instagram میتوانید از CDN برای کش کردن تصویر پروفایل کاربران استفاده کنید. به این ترتیب تصویر پروفایل برای کاربران سراسر دنیا سریع ارائه میشود.
چیزهایی که باید درباره CDN بدانید
- CDN فقط برای داراییهای ایستا نیست: CDNها اغلب برای کش کردن داراییهای ایستا مثل تصویر، ویدیو و فایلهای جاوااسکریپت استفاده میشوند، اما میتوانند محتوای پویا را هم کش کنند. این برای محتوایی که زیاد خوانده میشود اما کم تغییر میکند خیلی مفید است. مثلاً پست وبلاگی که روزی یک بار بهروز میشود را میتوان در CDN کش کرد.
- کش پاسخ API: میتوان از CDN برای کش کردن پاسخ API هم استفاده کرد. اگر APIای دارید که زیاد به آن دسترسی میشود، میتوانید پاسخهایش را در CDN کش کنید. این کار بار سرورها را کم میکند و کارایی API را بهتر میکند.
- سیاست Eviction: مثل کشهای دیگر، CDNها سیاستهای eviction دارند که تعیین میکنند محتوای کششده کی حذف شود. مثلاً میتوانید برای محتوای کششده TTL (زمان زنده ماندن) تعیین کنید، یا از Cache Invalidation استفاده کنید تا هنگام تغییر محتوا، نسخه کششده حذف شود.
نمونههای CDN
محبوبترین CDNها Cloudflare، Akamai و Amazon CloudFront هستند. این سرویسها ویژگیهای متنوعی دارند، از جمله کش کردن، محافظت در برابر DDoS و فایروال برنامه وب. همچنین شبکه جهانی از نقاط لبه (edge location) دارند، یعنی میتوانند محتوا را با تأخیر کم به کاربران سراسر دنیا برسانند.