طراحی سرویس ردیابی قیمت مانند CamelCamelCamel
بررسی گامبهگام طراحی سرویس ردیابی قیمت محصولات آمازون شامل جمعآوری داده با خزنده و افزونه کروم، اعتبارسنجی داده جمعسپاریشده، اعلان کاهش قیمت رویدادمحور و ذخیرهسازی سری زمانی
درک مسئله
CamelCamelCamel یک سرویس ردیابی قیمت است که قیمت محصولات آمازون را در طول زمان زیر نظر میگیرد و وقتی قیمتها از آستانههای تعیینشده پایینتر بیایند، به کاربران هشدار میدهد. این سرویس یک افزونه محبوب Chrome با 1 میلیون کاربر فعال هم دارد که تاریخچه قیمت را مستقیم داخل صفحه محصولات آمازون نشان میدهد و امکان اشتراک تککلیکی برای اعلان کاهش قیمت را بدون ترک صفحه محصول آمازون فراهم میکند.
نیازمندیهای عملکردی
نیازمندیهای اصلی:
- کاربران بتوانند تاریخچه قیمت محصولات آمازون را (از طریق وبسایت یا افزونه Chrome) ببینند.
- کاربران بتوانند در اعلانهای کاهش قیمت بر اساس آستانه (از طریق وبسایت یا افزونه Chrome) مشترک شوند.
افزونه Chrome با 1 میلیون کاربر فعال تاریخچه قیمت را مستقیم در صفحات محصول آمازون نشان میدهد و اشتراک تککلیکی را ممکن میکند. بنابراین هر دو نیازمندی از هر دو مسیر (وبسایت و افزونه) باید در دسترس باشند.
موارد خارج از محدوده:
- جستوجو و کشف محصولات روی پلتفرم
- مقایسه قیمت بین چند فروشگاه
- یکپارچگی با نقد و بررسی و امتیاز محصول
نیازمندیهای غیرعملکردی
مقیاس و نیازمندیهای عملکرد CamelCamelCamel بهخاطر کاتالوگ عظیم محصولات آمازون و نیاز به اعلان بهنگام قیمت است.
نیازمندیهای اصلی:
- سیستم باید Availability (دسترسپذیری) را بر Consistency (سازگاری) مقدم بدارد و Eventual Consistency (سازگاری نهایی) قابل قبول است.
- سیستم باید 500 میلیون محصول آمازون را در مقیاس پشتیبانی کند.
- سیستم باید کوئریهای تاریخچه قیمت را با تأخیر کمتر از 500 میلیثانیه سرو کند.
- سیستم باید اعلان کاهش قیمت را ظرف 1 ساعت پس از تغییر قیمت ارائه دهد.
موارد خارج از محدوده:
- Strong Consistency (سازگاری قوی) برای دادههای قیمت
- بهروزرسانی قیمت بلادرنگ (زیر یک دقیقه)
ما سیستمی میسازیم که باید با آمازون «مؤدب» باشد و در عین حال ردیابی قیمت ارزشمندی برای میلیونها کاربر فراهم کند. این چالشهای فنی جالبی درباره جمعآوری داده، کارایی ذخیرهسازی و تحویل اعلان میسازد که در بررسیهای عمیق به آنها میپردازیم.
آمادهسازی
برنامهریزی رویکرد
چارچوب Hello Interview را دنبال میکنیم: طراحی را با پوشش نیازمندیهای عملکردی به ترتیب میسازیم و بعد نیازمندیهای غیرعملکردی را برای هدایت بررسیهای عمیق درباره چالشهای مقیاسدهی استفاده میکنیم.
تعریف موجودیتهای اصلی
بهتر است با یک مرور کلی از موجودیتهای اولیه شروع کنیم، بدون اینکه هنوز در همه جزئیات گیر کنیم. همیشه میتوانیم با تکرار طراحی، پیچیدگی بیشتر اضافه کنیم. فقط حتماً برنامهتان را به مصاحبهکننده بگویید تا در یک مسیر باشید. من اغلب توضیح میدهم که فقط با یک فهرست ساده شروع میکنم، اما وقتی به طراحی سطح بالا رسیدم، مدل داده را کامل مستند میکنم.
برای برآورده کردن نیازمندیهای عملکردی کلیدی به این موجودیتها نیاز داریم:
- Product (محصول): یک محصول آمازون که ردیابی میکنیم، شامل قیمت فعلی و Metadata لازم برای نمایش تاریخچه قیمت و اعلانها.
- User (کاربر): فردی که از سرویس ما استفاده میکند، شامل اطلاعات تماس و ترجیحات او برای دریافت اعلان کاهش قیمت.
- Subscription (اشتراک): کاربران را به محصولاتی که میخواهند زیر نظر بگیرند وصل میکند و آستانه قیمتی را که اعلان را فعال میکند مشخص میکند.
- Price (قیمت): داده سری زمانی که تغییرات قیمت هر محصول را در طول زمان ثبت میکند و به ما اجازه میدهد نمودار قیمت تاریخی بسازیم.
در مصاحبه واقعی، یک فهرست کوتاه مثل همین کافی است. فقط حتماً موجودیتها را با مصاحبهکننده مرور کنید تا در یک مسیر باشید.
API
API مهمترین راهی است که کاربران از طریق وبسایت و افزونه Chrome با سرویس ردیابی قیمت ما کار میکنند. endpoint هایی طراحی میکنیم که مستقیم از دو نیازمندی عملکردی اصلی پشتیبانی کند.
1
2
// Retrieve historical price data for charts and displays
GET /products/{product_id}/price?period=30d&granularity=daily -> PriceHistory[]
product_id را برای کش بهتر و پایبندی به REST در مسیر URL میگذاریم، نه در بدنه درخواست. پارامتر granularity اجازه میدهد برای دورههای بلند میانگین روزانه و برای بازههای اخیر داده ساعتی برگردانیم.
1
2
3
4
5
6
7
8
// Subscribe to price drop notifications with threshold
POST /subscriptions
{
product_id,
price_threshold,
notification_type,
}
-> 200
جریان داده
قبل از ورود به طراحی فنی، بیایید بفهمیم داده در سیستم ما چطور جریان دارد. جریان داده نشان میدهد اطلاعات از جمعآوری اولیه تا خروجی کاربر نهایی چطور حرکت میکند.
درک این جریان در مراحل اولیه چند هدف دارد. اول، کمک میکند قبل از رفتن سراغ جزئیات پیادهسازی، با مصاحبهکننده درباره عملکرد اصلی همراستا شویم. دوم، نقشه راه روشنی میدهد که تصمیمهای طراحی سطح بالای ما را هدایت میکند. و در نهایت اجازه میدهد قبل از تعهد به انتخابهای معماری مشخص، گلوگاهها یا مشکلات بالقوه را شناسایی کنیم.
در این مورد، نیازمندی «پنهان» این است که در وهله اول باید بتوانیم داده را به دست بیاوریم!
- خزندههای وب (Web Crawler) و افزونه Chrome قیمتهای فعلی را از صفحات محصول آمازون جمع میکنند.
- داده قیمت اعتبارسنجی میشود و در پایگاه داده قیمت ذخیره میشود.
- تغییرات قیمت رویدادهایی برای پردازش اعلان فعال میکنند.
- وقتی قیمت از آستانه پایینتر بیاید، کاربر اعلان ایمیلی دریافت میکند.
توجه کنید این ساده است و هرچه جلو میرویم بهترش میکنیم، اما مهم است ساده شروع کنیم و از آنجا گسترش بدهیم.
طراحی سطح بالا
طراحی را بهتدریج میسازیم، از اساسیترین نیازها شروع میکنیم و با هر نیازمندی پیاپی پیچیدگی اضافه میکنیم. این تضمین میکند اول سیستم کاری داریم و بعد قابلیتهای اضافه را لایهلایه روی آن میگذاریم.
آمازون با سرویسهای ردیابی قیمت دوستانه رفتار نمیکند. API ارائه نمیدهد، فعالانه جلوی Scraping را میگیرد و Rate Limiting اعمال میکند (معمولاً 1 درخواست در ثانیه برای هر آدرس IP). باید سیستم را طوری طراحی کنیم که با این محدودیتها کار کند و در عین حال به شرایط خدمات آمازون احترام بگذارد.
1) مشاهده تاریخچه قیمت محصولات آمازون
کاربران میخواهند ببینند قیمت محصولات مشخص آمازون در طول زمان چطور تغییر کرده است. این پایه کل سیستم ماست و به دو چالش جدا تقسیم میشود: بهدست آوردن داده قیمت از آمازون و سرو کارآمد کوئریهای قیمت تاریخی.
اول زیرساخت پایه جمعآوری و ذخیره داده را میسازیم. معماری ما به سیستم Web Scraping برای جمعآوری داده قیمت، لایه ذخیرهسازی برای قیمتهای تاریخی و یک سرویس API برای ارائه تاریخچه قیمت به کاربران نیاز دارد. اجزای اینجا:
- Client (مشتری): کلاینت ما یا وبسایت است یا افزونه Chrome. در هر صورت راهی است که کاربر نمودارهای تاریخچه قیمت را ببیند.
- API Gateway (دروازه API): احراز هویت، Rate Limiting (محدودسازی نرخ درخواست) و مسیریابی درخواستها به سرویسهای مناسب را انجام میدهد.
- Price History Service (سرویس تاریخچه قیمت): بازیابی اطلاعات قیمت را مدیریت میکند و با پایگاه داده هماهنگ میشود.
- Web Crawler Service (سرویس خزنده وب): صفحات محصول آمازون را Scrape میکند تا قیمتهای فعلی را جمع کند.
- Price DB (پایگاه داده قیمت): اطلاعات فعلی محصول و داده قیمت تاریخی را ذخیره میکند.
- Primary DB (پایگاه داده اصلی): همه جدولهای دیگر بهجز قیمت، مثل کاربران، محصولات و اشتراکها را ذخیره میکند.
Price History Service را از خزنده جدا میکنیم، چون ویژگیهای مقیاسدهی متفاوتی دارند. سرویس تاریخچه باید درخواستهای خواندن همزمان زیادی را از کاربرانی که نمودار میبینند تحمل کند، در حالی که خزنده بر اساس زمانبندی و با الگوی نوشتن متغیر کار میکند. این جداسازی اجازه میدهد هر سرویس را مستقل بهینه کنیم.
درباره پایگاههای داده، جداسازی راهبردی بر اساس مقیاس و الگوی دسترسی انجام میدهیم. Primary DB کاربران، محصولات و اشتراکها را با هم نگه میدارد، چون ویژگیهای عملیاتی مشابهی دارند: نسبتاً کوچکاند و الگوی دسترسی سنتی CRUD دارند. اما Price DB را کاملاً جدا میگذاریم، چون داده تاریخچه قیمت اساساً نیازمندیهای متفاوتی دارد: به میلیاردها ردیف میرسد، فقط Append میشود، به بهینهسازی سری زمانی نیاز دارد و Eventual Consistency را تحمل میکند. این جداسازی اجازه میدهد هر پایگاه داده را مستقل بهینه و مطابق نیازهای خاص خودش مقیاس بدهیم.
فعلاً Web Crawler Service را «جعبه سیاه» میگذاریم. میتوانیم فرض کنیم دورهای صفحات محصول آمازون را Scrape میکند و جدول قیمت را بهروز میکند. البته در بررسی عمیق جزئیات بیشتری درباره نحوه انجام این کار میگوییم.
وقتی کاربری تاریخچه قیمت را از طریق وبسایت یا افزونه Chrome میخواهد:
- کلاینت یک
GETبه/products/{product_id}/price?period=30dمیفرستد. - API Gateway درخواست را اعتبارسنجی و به Price History Service مسیریابی میکند.
- Price History Service جدول قیمت را فیلترشده بر اساس
product_idو بازه زمانی کوئری میکند. - قیمتهای تاریخی بهصورت JSON برمیگردند و برای رندر نمودار آمادهاند.
- افزونه Chrome یا وبسایت نمودار قیمت را به کاربر نشان میدهد.
2) اشتراک در اعلان کاهش قیمت بر اساس آستانه
بر پایه تاریخچه قیمت، کاربران حالا میخواهند وقتی قیمت محصول به زیر آستانه تعیینشدهشان رسید اعلان بگیرند. این نیازمندیهای جدیدی برای مدیریت کاربر، ذخیره اشتراک و تحویل اعلان میآورد.
ضمن اینکه از زیرساخت موجود استفاده میکنیم، اجزای جدید اضافه میکنیم. برای پیادهسازی اولیه میتوانیم با رویکرد Polling سادهای شروع کنیم که کار را انجام میدهد، با این دانسته که بعداً در بررسی عمیق بهینهاش میکنیم.
معماری گسترشیافته ما شامل این اجزاست:
- Subscription Service (سرویس اشتراک): اشتراکهای کاربر و تنظیمات آستانه قیمت را مدیریت میکند.
- Notification Cron Job: دورهای تغییرات قیمت را بررسی میکند و ایمیل اعلان میفرستد.
Subscription Service را از Price History Service جدا میکنیم، چون اقدامهای کاربری متفاوتی را مدیریت میکنند و الگوی دسترسی به دادهشان فرق دارد. Subscription Service عملیات حساب کاربری (ساخت، بهروزرسانی و حذف اشتراک) را مدیریت میکند، در حالی که Price History Service روی کوئریهای قیمت سنگین متمرکز است.
برای سیستم اعلان اولیه یک رویکرد پردازش دستهای ساده داریم:
- Cron Job هر 2 ساعت یکبار برای بررسی اعلانها اجرا میشود.
- جدول قیمت را برای هر تغییر قیمت در 2 ساعت گذشته کوئری میکند.
- برای هر تغییر قیمت، جدول اشتراک را در Primary DB کوئری میکند تا کاربرانی را که آستانهشان بزرگتر یا مساوی
new_priceاست پیدا کند. - برای هر اشتراک فعالشده، ایمیل اعلان میفرستد.
- اعلانها را بهعنوان ارسالشده علامت میزند تا از تکراری شدن جلوگیری شود.
این رویکرد Polling کار میکند اما محدودیتهای آشکار دارد. کاربران ممکن است تا 2 ساعت منتظر اعلان کاهش قیمت بمانند و هر چند ساعت یکبار اسکن گران پایگاه داده انجام میدهیم. در بررسی عمیق رویکردهای بلادرنگ بهتری را که اعلان زیر یک دقیقه میدهند بررسی میکنیم.
وقتی کاربر در هشدار قیمت مشترک میشود:
- کاربر اشتراک را از طریق وبسایت یا افزونه Chrome ارسال میکند.
- API Gateway درخواست
POSTرا به Subscription Service مسیریابی میکند. - Subscription Service یک رکورد در جدول اشتراکها میسازد (
user_idوproduct_idوprice_threshold). - سرویس تأیید را به کاربر برمیگرداند.
این رویکرد ساده مطمئن میشود سیستم اشتراک کارایی داریم که نیازمندیهای عملکردی را برآورده میکند. کاربران هشدار کاهش قیمت میگیرند (فقط نه فوراً) و سیستم ما درک و عیبیابی آسانی دارد. ماهیت دورهای آن همچنین جلوی تداخل پردازش اعلانها با عملکرد اصلی ردیابی قیمت را میگیرد.
بررسیهای عمیق
وقت بخش سرگرمکننده است. طراحی ساده موجود را میگیریم و لایههای پیچیدگی را با بررسیهای عمیق روی آن میگذاریم. وقتی کارمان تمام شود، سیستمی داریم که همه نیازمندیهای عملکردی و غیرعملکردی را برآورده میکند.
1) کشف و ردیابی کارآمد 500 میلیون محصول آمازون
تا اینجا خزنده وب را «جعبه سیاه» گرفته بودیم و ساده فرض کرده بودیم که یکجوری داده قیمت را از آمازون میگیرد و پایگاه داده را بهروز میکند. اما وقتی با واقعیت ردیابی 500 میلیون محصول روبهرو میشویم و در عین حال محدودیتهای Rate Limit آمازون را رعایت میکنیم، میفهمیم این در واقع یکی از بزرگترین چالشهای فنی ماست.
دو مسئله متمایز ولی مرتبط را حل میکنیم. چالش اول کشف محصول است: باید هنگام راهاندازی همه 500 میلیون محصول موجود را پیدا کنیم و علاوه بر آن حدود 3000 محصول جدیدی را که آمازون روزانه اضافه میکند کشف کنیم. چالش دوم نظارت بر قیمت است: باید قیمت میلیونها محصول شناختهشده را کارآمد بهروز کنیم و در عین حال اولویتبندی کنیم کدامها بیشتر بررسی شوند.
هر دو چالش با همان محدودیت اساسی Rate Limit آمازون روبهرو هستند، اما جداگانه فهمیدن آنها به طراحی راهحلهای مؤثرتر کمک میکند.
رویکرد بد: خزنده وب ساده
سادهترین رویکرد استقرار خزندههای وب سنتی است که سیستماتیک وبسایت آمازون را پیمایش میکنند. از URL های اولیه مثل صفحه اصلی و صفحات دستهبندی آمازون شروع میکنیم، لینکهای محصول را از هر صفحه استخراج میکنیم و این لینکها را بازگشتی دنبال میکنیم تا کل کاتالوگ محصول را کشف کنیم. هر خزنده دستهها را روشمند پیمایش میکند و قبل از رفتن به صفحه بعدی، شناسه محصول، قیمت و Metadata پایه را استخراج میکند.
از نظر پیادهسازی، این عمدتاً همان چیزی است که در مسئله «طراحی خزنده وب» (Web Crawler) طراحی کردیم. با آمازون مثل هر وبسایت دیگری که ایندکس میشود رفتار میکند. یک صف Frontier شامل URL هایی که باید بازدید شوند، یک ردیاب URL بازدیدشده برای جلوگیری از چرخه و خزندههایی که URL ها را پیاپی پردازش میکنند پیاده میکنیم. وقتی خزنده به لینکهای توصیه محصول یا بخشهای «مشتریان اینها را هم دیدند» میرسد، این URL های جدید را برای پردازش آینده به صف Frontier اضافه میکند. سیستم پایگاه داده سادهای از محصولات کشفشده نگه میدارد و هر وقت صفحه محصول دوباره بازدید شود قیمتها را بهروز میکند.
چالشها: مشکل اساسی حسابوکتاب مقیاس است. با ردیابی 500 میلیون محصول و Rate Limit آمازون که تقریباً 1 درخواست در ثانیه برای هر IP است، یک خزنده تکی بیش از 15 سال طول میکشد تا فقط یکبار همه صفحات محصول را بازدید کند. حتی اگر 1000 سرور با IP های مختلف مستقر کنیم، همچنان 5 روز یا بیشتر برای بهروزرسانی کامل کاتالوگ لازم است که در این مدت میلیونها تغییر قیمت شناسایی نمیشود.
این همچنین مشکل تأخیر کشف میسازد. محصولات جدید آمازون ممکن است بسته به جایگاهشان در توالی خزش ما، هفتهها یا ماهها پیدا نشوند. محصولات پرطرفدار که قیمتشان چند بار در روز تغییر میکند، برای دورههای طولانی داده کهنه خواهند داشت و سرویس ما را برای معاملات حساس به زمان غیرقابل اعتماد میکنند. هزینه عظیم زیرساخت اجرای هزاران سرور خزنده این را برای بیشتر شرکتها از نظر اقتصادی غیرممکن میکند.
رویکرد خوب: خزش اولویتدار
بینش کلیدی اینجا این است که ما اساساً منابع خزش محدودی داریم، پس بهتر است عاقلانه از آنها استفاده کنیم. محبوبیت محصول از توزیع پارتو (Pareto) پیروی میکند: درصد کمی از محصولات آمازون توجه بیشتر کاربران را جلب میکند، در حالی که دم بلندی از محصولات هست که مردم بهندرت به آنها اهمیت میدهند. بهجای اینکه با همه 500 میلیون محصول یکسان رفتار کنیم، میتوانیم خزش را بر اساس علاقه واقعی کاربر لایهبندی کنیم و کیفیت سرویس را در جاهایی که بیشترین اهمیت را دارد چشمگیر بهتر کنیم.
میتوانیم سیستم امتیازدهی اولویت پیاده کنیم که در آن محصولات بر اساس تعامل فعال کاربر امتیاز بالاتری میگیرند. محصولاتی که اشتراک کاهش قیمت زیاد دارند بالاترین اولویت را دارند، چون کاربران فعالانه منتظر بهروزرسانیهای آنها هستند. محصولاتی که کاربران مرتب در وبسایت ما جستوجو میکنند هم اولویت بالاتری میگیرند، چون علاقه فعلی را نشان میدهند، حتی اگر هنوز اشتراکی ساخته نشده باشد.
سیستم خزنده صفهای اولویتی نگه میدارد که در آن محصولات با علاقه بالا ممکن است هر چند ساعت یکبار بررسی شوند، محصولات با علاقه متوسط روزانه بهروز شوند و محصولات کمعلاقه هفتگی یا حتی کمتر. همچنین حلقههای بازخورد پیاده میکنیم که در آن اعلانهای کاهش قیمت موفق (آنهایی که به کلیک یا خرید کاربر میرسند) امتیاز اولویت محصول را بالا میبرند و سیستم خودتقویتکنندهای میسازند که روی باارزشترین بهروزرسانیها تمرکز میکند.
این به ما اجازه میدهد با کسری از زیرساخت لازم برای خزش جامع، پوشش عالی برای محصولاتی که برای کاربران ما بیشترین اهمیت را دارند داشته باشیم. محصولات پرطرفدار بهروز میمانند و محصولات جدید هم بهروزرسانیهای دورهای میگیرند.
چالشها: محدودیت اساسی، شکافهای پوششی برای محصولات جدید یا پرطرفداری است که هنوز سیگنال علاقه کاربر نساختهاند. یک محصول جدید داغ ممکن است دیر کشف شود اگر هنوز هیچ کاربری آن را جستوجو نکرده باشد، و فرصتهای پذیرش اولیه را وقتی قیمتها پرنوساناند از دست بدهیم.
رویکرد عالی: جمعسپاری داده از افزونه Chrome
افزونه Chrome با 1 میلیون کاربر که قبلاً گفتیم یادتان هست؟ معلوم میشود این چیزی بیش از یک ویژگی راحتی است. در واقع سلاح پنهان ما برای جمعآوری داده است.
ظریفترین راهحل از 1 میلیون کاربر افزونه Chrome بهعنوان شبکه جمعآوری داده توزیعشده استفاده میکند. وقتی کاربرانی که افزونه ما را نصب کردهاند در آمازون مرور میکنند، افزونه خودکار شناسه محصول، قیمت فعلی و Metadata صفحه را ثبت میکند و بعد این اطلاعات را به سرویسهای باطن ما گزارش میدهد. این رویکرد جمعسپاری (Crowdsourcing)، رفتار مرور کاربر را به مکانیزم اصلی جمعآوری داده ما تبدیل میکند.
افزونه شفاف عمل میکند و با DOM Parsing داده ساختاریافته را از صفحات محصول آمازون استخراج میکند و بهروزرسانیها را به Price Reporting API ما میفرستد. داده قیمت بلادرنگ را برای محصولاتی که کاربران فعالانه مرور میکنند میگیریم، که بهطور طبیعی اقلام محبوب و پرطرفدار را در اولویت میگذارد. این داده تولیدشده توسط کاربر، محصولاتی را که مردم واقعاً به آنها اهمیت میدهند پوشش میدهد، بدون نیاز به زیرساخت خزش گسترده.
خزندههای سنتی ما حالا فقط باید محصولاتی را مدیریت کنند که اخیراً توسط کاربران افزونه دیده نشدهاند. سیستم همچنین از داده افزونه برای کشف محصولات جدید استفاده میکند. وقتی کاربران از صفحات محصول ناشناخته قبلی بازدید میکنند، آنها را به جدول محصول اضافه میکنیم. درخشان!
چالشها: این وابستگی به پذیرش کاربر و الگوهای مرور میآورد. محصولات در دستههای خاص با علاقه کم کاربران ممکن است بهروزرسانیهای نادر بگیرند و شکاف پوششی بسازند. اما مهمتر، باید چالش اعتبارسنجی داده را با دقت مدیریت کنیم، چون گزارشهای قیمت تولیدشده توسط کاربر ممکن است دستکاریشده یا نادرست باشند و سیستمهای تأیید لازم است، که در بررسی عمیق بعدی بررسی میکنیم.
این رویکرد ترکیبی چالش مقیاس ما را با تبدیل بزرگترین محدودیتمان (نیاز به نظارت بر میلیونها محصول) به مزیت رقابتی از طریق جمعآوری داده جمعسپاری حل میکند. بهجای مبارزه با Rate Limit آمازون با زیرساخت عظیم خزنده، از رفتار کاربران برای اولویتبندی و جمعآوری طبیعی داده برای محصولاتی که بیشترین اهمیت را دارند استفاده میکنیم.
اگر با خودتان فکر میکنید «لعنتی، من از کجا باید این را بدانم؟ از افزونه Chrome چیزی نمیدانم!»، نگران نباشید. وقتی این را در مصاحبه میپرسم، معمولاً با این جمله هدایت میکنم: «افزونه شما روی میلیونها مرورگر نصب شده که فعالانه صفحات محصول را مرور میکنند. میتوانید یکجوری از آن استفاده کنید؟»
2) مدیریت بهروزرسانیهای قیمت بالقوه مخرب از کاربران افزونه
رویکرد افزونه Chrome از بررسی عمیق قبلی مزیت قدرتمند جمعآوری داده میسازد، اما چالش مهم قابل اعتماد بودن را معرفی میکند. با 1 میلیون کاربری که بالقوه داده قیمت میفرستند، باید فرض کنیم درصدی از آنها عوامل مخرب، کاربران اشتباهکننده یا سیستمهایی با مشکل فنی هستند.
این سناریو را در نظر بگیرید: کاربر مخربی گزارش میدهد قیمت آخرین iPhone برابر 0.01 دلار است و این باعث میشود اعلان کاهش قیمت برای هزاران مشترک برود. کاربران به آمازون هجوم میآورند تا ببینند قیمت معمولی است، و این به اعتبار و اعتماد کاربران به ما آسیب میزند. اثربخشی سیستم اعلان ما کاملاً به دقت داده بستگی دارد، پس درست حل کردن این چالش ضروری است. پس چه کار کنیم؟
رویکرد خوب: تأیید با اجماع (Consensus)
یکی از کارهایی که میتوانیم بکنیم، تأیید توسط اجماع است. اگرچه ممکن است چند بازیگر بد داشته باشیم، احتمالاً فراگیر نیستند. بهجای پذیرفتن فوری هر گزارش قیمت، منتظر میمانیم تا N کاربر مستقل همان تغییر قیمت را گزارش کنند و بعد آن را «تأییدشده» علامت میزنیم و اعلانها را فعال میکنیم.
این سیستم با نگه داشتن بهروزرسانیهای قیمت در حالت «در انتظار تأیید» (Pending Validation) وقتی برای اولین بار از کاربران افزونه میرسند، کار میکند. جدول اجماعی داریم که تعداد کاربران یکتا را که هر قیمت را برای هر محصول در یک بازه زمانی (مثلاً 1 ساعت) گزارش کردهاند ردیابی میکند. فقط وقتی به آستانه توافق خود برسیم (مثلاً 3 کاربر مستقل برای محصولات محبوب یا 2 کاربر برای محصولات کمتر محبوب)، تغییر قیمت را بهعنوان قانونی میپذیریم و اعلانها را پردازش میکنیم.
برای محصولاتی با تعداد اشتراک بالا یا افت قیمت قابل توجه، میتوانیم اجماع بیشتری بخواهیم. افت قیمت 500 دلاری در جدیدترین iPhone ممکن است 5 تأیید مستقل بخواهد، در حالی که تغییر 2 دلاری برای یک کتاب شاید فقط 2 تأیید. سیستم همچنین میتواند شهرت کاربر را در طول زمان ردیابی کند، پس گزارشهای کاربرانی که دائماً دقیقاند ممکن است در نیاز اجماع وزن بیشتری بگیرند.
این یک دفاع طبیعی در برابر بازیگران مخرب و خطاهای فنی میسازد. بازیگر بد نمیتواند اعلان جعلی فعال کند، چون به چند همدست نیاز دارد. مشکلات فنی مثل مشکل کش مرورگر یا باگهای شبکه بعید است چند کاربر را همزمان تحت تأثیر قرار دهند، پس خودکار فیلتر میشوند.
چالشها: محدودیت اساسی تأخیر برای تغییرات قیمت قانونی است، بهویژه محصولات تخصصی. اگر فقط تعداد کمی کاربر یک دسته محصول تخصصی را زیر نظر داشته باشند، ممکن است ساعتها یا حتی روزها طول بکشد تا به آستانه توافق برسیم، یعنی مشترکان معاملات حساس به زمان را از دست میدهند. فروشهای ویژه (Flash Sale) یا پیشنهادهای محدود به زمان ممکن است قبل از اینکه کاربران کافی محصول را مرور کنند تا افت قیمت تأیید شود، تمام شوند.
محصولات محبوب هم در رویدادهای فروش بزرگ مشکلات خودشان را دارند. وقتی آمازون قیمت هزاران کالا را همزمان کم میکند، سیستم ما ممکن است با اعتبارسنجیهای در انتظار غرق شود و اعلانها حتی برای محصولاتی که سریع به اجماع میرسند به تأخیر بیفتد.
در نهایت، هنوز امکان دارد بازیگران بد با هماهنگی، سیستم اجماع شما را «بازی» کنند. اگرچه بعید است، ولی هنوز امکانپذیر است.
رویکرد عالی: اعتماد فوری و تأیید با خزنده
بهترین روش ساده است: فوراً به داده افزونه اعتماد کنید، اما وقتی چیزی مشکوک به نظر میرسد، سریع با خزندههای خودتان تأییدش کنید.
روش کار اینطور است. وقتی افزونه ما تغییر قیمت را گزارش میدهد، فوراً میپذیریم و میتوانیم فوراً اعلانها را بفرستیم. اما اگر این تغییر مشکوک به نظر برسد (مثل افت شدید قیمت، یا از کاربری باشد که قبلاً اشتباه کرده، یا محصول محبوبی با تعداد زیاد مشترک)، خودکار یک کار خزش برای تأیید در صف میگذاریم تا مستقیم ظرف چند دقیقه آمازون را بررسی کند.
از زیرساخت خزنده موجودمان استفاده میکنیم، ولی به این کارهای تأیید اولویت بالا میدهیم. بهجای اینکه ساعتها یا روزها منتظر برنامه خزش عادی بمانیم، بهروزرسانیهای قیمت مشکوک ظرف 1 تا 5 دقیقه بررسی میشوند. اگر خزنده بفهمد داده افزونه اشتباه بوده، میتوانیم اعلان اصلاحیه بفرستیم و آن کاربر را «کماعتماد» علامت بزنیم.
برای محصولات واقعاً مهم، میتوانیم صبر کنیم تا چند کاربر یک تغییر قیمت را گزارش کنند و بعد کاملاً به آن اعتماد کنیم. اگر از کاربران مختلف گزارشهای متناقض ببینیم، فوراً خزنده میفرستیم تا بفهمیم حق با کیست.
نکته خوب این است که بیشتر داده افزونه فوراً پردازش میشود (اعلانهای سریع)، اما موارد بد را آنقدر سریع تشخیص میدهیم که آسیب واقعی نمیزنند. کاربران برای معاملات قانونی هشدار سریع میگیرند و ما با اصلاح اشتباههای گاهبهگاه، اعتمادمان را حفظ میکنیم.
چالشها: این پیچیدگی قابل توجهی به زیرساخت خزنده اضافه میکند و مدیریت صف اولویت و قابلیتهای پاسخ سریع میخواهد که منابع سرور اضافه مصرف میکند. خزش تأییدی بار بیشتری روی سرورهای آمازون میگذارد و خطر Rate Limiting یا مسدود شدن IP را بالا میبرد.
این رویکرد «اعتماد کن اما تأیید کن» تعادل بسیار خوبی میسازد و در عین حال یکپارچگی داده را برای اعتماد بلندمدت حفظ میکند و ارزش فوری کاربر را میدهد.
3) پردازش کارآمد تغییرات قیمت و اطلاعرسانی به مشترکان
فعلاً از یک Cron Job ساده استفاده میکنیم که هر 2 ساعت برای اسکن تغییرات قیمت و ارسال اعلان اجرا میشود. اگرچه ممکن است برای عملکرد پایه کار کند، نیازمندی غیرعملکردی ما یعنی ارسال اعلان ظرف 1 ساعت پس از تغییر قیمت را برآورده نمیکند. مهمتر، با میلیونها بهروزرسانی قیمت و اشتراک، رویکرد Polling بار عظیمی روی پایگاه داده میگذارد.
اثربخشی سیستم اعلان مستقیم روی رضایت و حفظ کاربر اثر میگذارد. کاربران انتظار هشدار بهنگام کاهش قیمت دارند، بهویژه برای معاملات رقابتی که ممکن است سریع تمام شوند. تأخیر 2 ساعته اغلب یعنی از دست دادن فرصتهای حساس به زمان و ارزش سرویس ما را نسبت به رقبایی با سیستم اعلان سریعتر کم میکند.
مشکل اساسی رویکرد Polling فعلی این است که در زمان اشتباه سؤال اشتباه میپرسیم. بهجای اینکه هر 2 ساعت بپرسیم «در 2 ساعت گذشته چه چیزی تغییر کرده؟»، باید در لحظه وقوع تغییر بپرسیم «چه کسی به این تغییر اهمیت میدهد؟».
حرکت به اعلان رویدادمحور ساده است. وقتی قیمت تغییر میکند، فوراً میدانیم کدام کاربران بدون اسکن پایگاه داده گران نیاز به هشدار دارند. دو راه جدی برای پیادهسازی داریم که هرکدام مبادلات متفاوتی دارد.
گزینه اول: Change Data Capture (CDC). پایگاه داده را طوری پیکربندی میکنیم که هر وقت داده قیمت تغییر کند، خودکار رویداد منتشر کند. وقتی خزندهها یا پردازشگرهای داده افزونه رکورد قیمت جدید درج میکنند، پایگاه داده فعال میشود و رویدادهایی شامل شناسه محصول، قیمت قدیم و قیمت جدید به Kafka (یا مشابه آن) میفرستد. Notification Service ما در این رویدادها مشترک میشود و فوراً جدول اشتراکها را برای پیدا کردن کاربران تحتتأثیر کوئری میکند.
این روش تمیز است، چون تغییرات قیمت خودکار اعلان را بدون هماهنگی در سطح اپلیکیشن فعال میکنند. لازم نیست رویدادها را در کد اپلیکیشن منتشر کنیم، چون پایگاه داده این را برایمان مدیریت میکند. جریان داده کاملاً خودکار میشود.
گزینه دوم: نوشتن دوگانه (Dual Write). سرویسهای جمعآوری قیمت ما همزمان در پایگاه داده مینویسند و رویداد را در Kafka منتشر میکنند. وقتی داده خزنده یا بهروزرسانی افزونه میرسد، همان سرویسی که قیمت را در پایگاه داده مینویسد، رویدادهای ساختاریافته را در Stream اعلان ما هم منتشر میکند.
رویکرد نوشتن دوگانه به ما اجازه میدهد درباره اینکه کدام تغییرات اعلان فعال کنند هوشمندتر باشیم. میتوانیم نوسانهای قیمتی را که برای کاربران جالب نیستند فیلتر کنیم یا چند تغییر سریع پیاپی را قبل از انتشار رویداد دستهای کنیم. همچنین از سربار Trigger های پایگاه داده روی هر بهروزرسانی قیمت دوری میکنیم.
فارغ از اینکه کدام گزینه را انتخاب کنیم، Consumer های اعلان ما یکسان کار میکنند. رویدادهای تغییر قیمت را از Kafka میخوانند و فوراً جدول اشتراکها را کوئری میکنند تا کاربرانی را که آستانهشان فعال شده پیدا کنند. برای هر اشتراک منطبق، ایمیل اعلان را از طریق Notification Service موجودمان میفرستیم.
زیبایی کار این است که فقط تغییرات قیمت واقعی را پردازش میکنیم، نه اسکن گران جدول میلیونها رکورد. قیمت بیشتر محصولات مکرر تغییر نمیکند، پس حتی در مقیاس بالا درباره تعداد قابل مدیریتی رویداد صحبت میکنیم. وقتی قیمت یک محصول محبوب کم میشود، یک کوئری انجام میدهیم تا همه مشترکانی را که آستانهشان بالاتر از قیمت جدید است پیدا کنیم و آن اعلانها را فوراً بفرستیم.
4) سرو سریع کوئریهای تاریخچه قیمت برای تولید نمودار
با نگاه به رویکرد فعلی ذخیرهسازی داده، تاریخچه قیمت را در یک جدول ساده PostgreSQL با ایندکس پایه (product_id و timestamp) نگه میداریم. این برای عملکرد اولیه جواب میدهد، اما وقتی برای محصولات محبوب با داده تاریخچه گسترده نمودار قیمت سرو میکنیم، نیازمندی Latency کمتر از 500 میلیثانیه را برآورده نمیکند.
چالش مقیاس ناشی از جمع شدن داده قیمت برای محصولات محبوب را در نظر بگیرید. محصولات پرطرفدار ممکن است چند سال هر چند ساعت یکبار نقطه داده قیمت داشته باشند که به هزاران رکورد برای هر محصول میرسد. وقتی کاربران نمودار قیمت 2 ساله میخواهند، باید این داده را در سطلهای زمانی مناسب (میانگین روزانه یا هفتگی) تجمیع کنیم و آن را آنقدر سریع برگردانیم که تجربه کاربری روان باشد. کوئریهای خام پایگاه داده با این حجم کاری تجمیعی مشکل دارند.
رویکرد خوب: پیشتجمیع زمانبندیشده
راهحل ساده، پیشمحاسبه تجمیع قیمتها در دانهبندیهای زمانی مختلف با Batch Job های زمانبندیشده است. هر شب کاری اجرا میکنیم که خلاصه قیمت روزانه، هفتگی و ماهانه را برای همه محصولات محاسبه میکند و این تجمیعها را در جدول سادهای بهینه برای جستوجوی سریع نمودار ذخیره میکند.
Batch Job ما داده قیمت روز قبل را پردازش میکند و آمار مربوط را محاسبه میکند: میانگین قیمت، کمترین قیمت، بیشترین قیمت، قیمت آغازین و قیمت پایانی برای هر دوره زمانی. همه اینها را در جدول price_aggregations با یک ستون دانهبندی (Granularity) ذخیره میکنیم که مشخص میکند هر ردیف داده روزانه، هفتگی یا ماهانه است.
وقتی کاربران نمودار قیمت میخواهند، API ما بهجای داده قیمت خام، از این جدول تجمیع کوئری میکند. نمودار 30 روزه تجمیع روزانه میخواهد، در حالی که نمودار 2 ساله از تجمیع ماهانه استفاده میکند. با ایندکس مناسب روی (product_id، دانهبندی، تاریخ)، این کوئریها بهجای تجمیع هزاران نقطه قیمت خام، دهها رکورد از پیش محاسبهشده را در میلیثانیه برمیگردانند.
چالشها: چون نمودارها دادهای را نشان میدهند که تا 24 ساعت قدمت دارد، پیشتجمیع مشکل تازگی داده میآورد. برای یک سرویس ردیابی قیمت این کهنگی کلاً قابل قبول است، چون کاربران به روندهای تاریخی نگاه میکنند، نه قیمتهای بلادرنگ. رویکرد پردازش دستهای همچنین به ذخیرهسازی اضافه برای همه خلاصههای از پیش محاسبهشده نیاز دارد، اگرچه این بهصورت خطی با تعداد محصولات و دورههای زمانی رشد میکند و قابل مدیریت است.
رویکرد عالی: TimescaleDB
بهجای اجرای این Cron Job ها میتوانیم از TimescaleDB استفاده کنیم، افزونه سری زمانی برای PostgreSQL که عمداً برای این نوع بارکاری ساخته شده است. چون از قبل PostgreSQL را برای داده عملیاتی (کاربران، محصولات، اشتراکها) به کار میبریم، TimescaleDB اجازه میدهد تحلیل تاریخچه قیمت را در همان اکوسیستم پایگاه داده انجام بدهیم و عملکرد سری زمانی تخصصی هم بگیریم.
با TimescaleDB میتوانیم تجمیعهای بلادرنگ را مستقیم و بدون هیچ پیشمحاسبهای انجام بدهیم. وقتی کاربر نمودار قیمت 6 ماهه میخواهد، کوئریای مثل زیر مینویسیم و نتایج را حتی با میلیاردها رکورد قیمت در میلیثانیه میگیریم:
1
2
3
4
5
SELECT time_bucket('1 day', timestamp) AS day, AVG(price) AS avg_price
FROM prices
WHERE product_id = ?
GROUP BY 1
ORDER BY 1;
پارتیشنبندی و فشردهسازی خودکار TimescaleDB این تجمیعها را بسیار سریع میکند و رابط آشنای PostgreSQL را حفظ میکند.
این معماری ما را ساده نگه میدارد. به Job های پیچیده پیشتجمیع یا چند سیستم پایگاه داده نیاز نداریم. کاربران میتوانند هر بازه یا دانهبندی زمانی بخواهند و TimescaleDB نتایج را بر حسب تقاضا محاسبه میکند. این بهطور طبیعی نیازهای مقیاس ما را تحمل میکند، چون TimescaleDB دقیقاً برای این نوع بارکاری سری زمانی طراحی شده است، و همه مزایای عملیاتی ماندن در اکوسیستم PostgreSQL را حفظ میکنیم.
جایگزینهایی مثل ClickHouse میتوانند عملکرد تحلیلی حتی بالاتری بدهند، اما TimescaleDB بهترین توازن بین عملکرد و سادگی عملیاتی را میدهد، چون کل پشته داده ما یکپارچه میماند.
رویکرد TimescaleDB عملکرد و انعطاف لازم برای سرو نمودار قیمت در مقیاس تولید را میدهد و پاسخگویی بلادرنگی را که کاربران انتظار دارند حفظ میکند. با انتخاب ابزار مناسب برای بارکاری تحلیلی، به سادگی و کارایی بدون استراتژیهای ذخیرهسازی پیچیده یا پیشتجمیع میرسیم.
طراحی نهایی
با یک قدم به عقب، سیستمی در مقیاس بالا طراحی کردهایم که میتواند داده قیمت را هوشمندانه از آمازون جمع کند، اعتبارسنجی کند، هنگام کاهش قیمت به کاربران اطلاع بدهد و نمودار تاریخچه قیمت را بلادرنگ به کاربران سرو کند. بد نیست!
درباره نمودار نهایی باید اعتراف کنم که کشیدن آن با همه فلشهای متقاطع کمی از دستم در رفت. اما در نهایت به طراحی نهایی میرسیم که شامل این است: وبسایت و افزونه Chrome، API Gateway، Price History Service، Subscription Service، Web Crawler Service، Price Reporting API، Kafka برای رویدادهای تغییر قیمت، Notification Service، Primary DB (PostgreSQL) و Price DB (TimescaleDB).
انتظارات در هر سطح
وقتی از داوطلبان میخواهم سیستم ردیابی قیمت مثل CamelCamelCamel را طراحی کنند، در هر سطح دنبال چه چیزی هستم؟
سطح Mid-level
در سطح Mid-level بیشتر دنبال داوطلبانی هستم که بتوانند طراحی سطح بالایی بسازند که نیازمندیهای عملکردی اصلی، یعنی نمایش تاریخچه قیمت و اشتراک اعلانها را پوشش بدهد. آنها باید چالش اساسی جمعآوری داده قیمت از آمازون را بفهمند و رویکرد ساده Web Scraping پیشنهاد کنند، حتی اگر مقیاسپذیر نباشد. انتظار دارم طرح سادهای برای پایگاه داده با محصولات، کاربران، اشتراکها و تاریخچه قیمت طراحی کنند. اگرچه ممکن است در ابتدا Polling مکرر هر محصول را پیشنهاد کنند، با کمی راهنمایی باید محدودیتهای مقیاس را بشناسند و بفهمند چرا اولویتبندی ممکن است ضروری باشد.
سطح Senior
برای داوطلبان Senior انتظار دارم سریع تشخیص بدهند جمعآوری داده چالش فنی اصلی است و گفتوگو را به سمت راهحلهایی مثل رویکرد افزونه Chrome ببرند. باید مبادلات بین استراتژیهای مختلف خزش را بفهمند و بدانند خزش 500 میلیون محصول منابع عظیم یا زمان بیپایان میخواهد. دنبال درک کامل معماری سیستم اعلان هستم: باید بتوانید توضیح بدهید چرا Polling مقیاس نمیگیرد و جایگزینهای رویدادمحور با صف پیام پیشنهاد بدهید. در حالت ایدهآل دستکم در دو بررسی عمیق وارد میشوید، حتی اگر همانهایی که من در بالا آوردم نباشند.
سطح Staff به بالا
برای داوطلبان Staff توانایی شما را در دیدن تصویر بزرگتر و متعادل کردن ظرافت فنی با واقعیت عملیاتی میسنجم. باید فعالانه تشخیص بدهید که افزونه Chrome فقط یک ویژگی کاربر نیست، بلکه میتوان از آن برای حل مسئله مقیاس جمعآوری داده پایه استفاده کرد. انتظار دارم تکامل سیستم را سنجیده بحث کنید: چطور ساده شروع کنیم اما برای مقیاسی که در نهایت لازم داریم طراحی کنیم؟ باید پیامدهای کسبوکاری انتخابهای فنی را بفهمید. مثلاً چرا اعتبارسنجی داده روی اعتماد و حفظ کاربر اثر میگذارد، نه فقط درستی فنی.
داوطلبان قوی اغلب نگرانیهایی را مطرح میکنند که من دربارهشان نپرسیدهام، مثل مدیریت اقدامات ضد Scraping آمازون، مدیریت حریم خصوصی کاربران افزونه، یا برنامهریزی برای وقتی که آمازون ساختار صفحهاش را تغییر میدهد. تفکر سیستمی نشان میدهید، یعنی در نظر میگیرید اجزای مختلف چطور روی هم اثر میگذارند و راهحلهایی پیشنهاد میکنید که نگهداری بلندمدت را کنار عملکرد فوری اولویت میدهند.