خشت اول: در باب معماری سیستم تبلیغات فروشندگان دیجیکالا

مقدمه

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

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

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

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

در ابتدای کار فشار زمانی زیادی بر تیم بود، نیازمندی‌های پیچیده و گوناگونی وجود داشت، همکاری با تیم و شرکتی بیرون از سازمان به دلیل اختلاف فرهنگ‌ها و فرآیندها چالش‌هایی مخصوص به خودش رو داشت و مثل هر پروژه دیگه، شروع کار شامل فعالیت‌هایی بسیار خسته کننده و متعدد (مثل تعیین اسم پروژه) بود.

با وجود تمام این مسائل، من و کامیاب تصمیم گرفتیم که تا جایی که توان داریم کدهای تمیزی بنویسیم، با تست نویسی پروژه رو جلو ببریم و قواعد رو رعایت کنیم. سعی کردیم هر جایی که تصمیمی باید گرفته بشه تلاش در راستای بهینگی و نگاه به آینده پروژه داشته باشیم و میانبر نزنیم.

تصمیم گرفته بودیم که از خشت اول همه چیز رو صاف بچینیم.

نیازمندی‌ها و چالش‌ها

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

نمونه‌ای از تبلیغات هنگام جستجوی کلمه «کفش»
نمونه‌ای از تبلیغات هنگام جستجوی کلمه «کفش»


در ادامه این قسمت نیازمندی‌هایی که برای این سیستم وجود داشت و چالش‌هایی که هر کدوم از اون‌ها ایجاد می‌کرد رو بررسی می‌کنیم.

اول از هر چیز ما باید پنلی ارائه می‌کردیم که سلرها بتونن در اون محصولات خودشون رو ببینن و به صلاح‌دیدی که دارند برای هر کدوم از اون محصولات تبلیغ بسازند. این موضوع چالش بزرگ همگام‌سازی داده‌ها بین ما و دیجی‌کالا رو تحمیل می‌کرد. برای مثال اگر کالایی تغییر نام پیدا می‌کرد، این تغییر باید سمت ما هم اتفاق می‌افتاد یا اگر موجودی یک کالا به پایان می‌رسید ما هم باید متوجه این موضوع می‌شدیم و اگر کالای جدیدی وارد می‌شد سمت ما هم باید افزوده می‌شد. در نتیجه دیجی‌کالا باید تغییرات موجودیت‌ها (entity) رو به سرعت به ما اطلاع می‌داد که سلرها احساس گسستگی بین دو سیستم نکنند.

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

سلرها باید بازخوردی از تبلیغاتشون داشته باشند. یک سلر برای تبلیغاتش هزینه می‌کنه و لازمه بدونه این هزینه به چه شکل مصرف می‌شه و چه تاثیرات مثبتی روی سودآوری و فروش خودش داشته. این بازخورد در قالب گزارش‌هایی داده می‌شه که باید از تبلیغات و هزینه‌ها به دست سلرها برسه؛ پس نیاز بود که تمام آمارهایی که از تبلیغ‌ها در میاد به درستی ذخیره بشن تا زمانی که سلر نیاز داشت به راحتی و به سرعت بتونه از اونها گزارشاتش رو استخراج کنه.

مساله بعدی کارآمد بودن سیستم یا همون پرفورمنس بود. یک نیازمندی که از سمت تیم دیجی‌کالا به ما در ابتدای کار اعلام شده بود زمان پاسخ‌دهی (Response Time) کمتر از ۱۰۰ میلی‌ثانیه برای فچ هر تبلیغ بود. از طرف دیگه سایت دیجی‌کالا به صورت میانگین درخواست بر ثانیه (Request/Second - RpS) بالایی داره و ممکنه در شب‌هایی خاص مثل شب یلدا یا روز مادر میزان درخواست‌ها به ۷۰۰ تا ۱۰۰۰ عدد در ثانیه برسه. این دو موضوع ایجاد یک سیستم قوی و بهینه رو ضروری می‌کرد.

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

عکسی سبک و کلیشه‌ای که برای به فکر بردن خواننده مورد استفاده قرار گرفته است.
عکسی سبک و کلیشه‌ای که برای به فکر بردن خواننده مورد استفاده قرار گرفته است.


زیرسیستم‌ها

برای ارضای تمام نیازمندی‌ها گفته شده باید زیر سیستم‌های مختلفی شکل می‌گرفت. در ادامه به معرفی هر یک از این زیر سیستم‌ها می‌پردازیم.

پنل

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

کور

سیستم کور (Core) قسمت Back-End پنل و لاجیک‌های بیزنسی برای تبلیغات، موجودیت‌ها، آمارها و همگام‌سازی‌های بین دیتابیس‌ها رو هندل می‌کنه. این زیر سیستم در واقع قلب تپنده‌ی کل سیستم رو تشکیل می‌ده.

وب‌هوک‌ها

وب‌هوک‌ اسمیه که به زیر مجموعه‌ای از سیستم کور دادیم که وظیفه همگام‌سازی داده‌های دیجی‌کالا و داده‌های ما رو داره. هر تغییری که در موجودیت‌هایی خاص مثل محصولات در سمت دیجی‌کالا اتفاق میافته، وب‌هوک متناظر با اون موجودیت صدا زده می‌شه تا این تغییرات در سیستم ما هم لحاظ بشه.

تخلیص

تخلیص نام انبار داده‌های یکتانته و ما از همین سیستم برای ثبت تمام آمار تبلیغات و نمایش گزارشات استفاده می‌کنیم. این آمارها موارد زیر رو تشکیل میدن:

  1. نمایش: میزان مشاهده شدن یک تبلیغ به وسیله کاربران دیجی‌کالا
  2. کلیک: میزان کلیک بر یک تبلیغ که هزینه یک تبلیغ بر اساس آن مشخص می‌گردد
  3. هزینه: مجموع هزینه‌ای که سلر برای یک تبلیغ پرداخت کرده است
  4. افزودن به سبد خرید: آماری که نشان می‌دهد یک کالا بعد از کلیک چند بار به سبد خرید کاربر افزوده شده است
  5. سفارش: آماری که نشان می‌دهد یک کالا بعد از کلیک چند بار به وسیله کاربر سفارش داده شده است

فچ

این سیستم وظیفه ارائه تبلیغات برای صفحه‌ای که کاربر مشاهده می‌کند را دارد. نیازمندی‌های اصلی این سیستم قابلیت تحمل لود بالا و ریسپانس تایم پایین است. فچ با استفاده از پارامترهای ورودی یک لینک از سایت دیجی‌کالا، در دیتابیس الستیک تبلیغات مناسب رو جستجو می‌کنه.

اوراکل

فرض کنید کاربر دیجی‌کالا کلمه‌ای مثل جاسوییچی رو جستجو می‌کنه و سیستم ما فاقد تبلیغ کالایی با این کلمه است؛ با این وجود تبلیغی برای کلمه «جاکلیدی» وجود داره. نیازه که برای جستجوی کاربر تبلیغاتی که معنای معادلی برای این جستجو دارند هم ارائه بشه. اوراکل وظیفه درک و استنباط این معادل‌ها هنگام جستجو رو داره.

معماری کلی

معماری کلی سیستم  (رسم شده در نرم‌افزار XMind)
معماری کلی سیستم (رسم شده در نرم‌افزار XMind)

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

همگام‌سازی تبلیغات بین دیتابیس کور و الستیک

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

نیازه که فچ جستجو رو بین تبلیغات فعال انجام بده. اولین راه حلی که به نظر می‌رسه اینه که منطق فعال بودن یا نبودن یک تبلیغ رو خود فچ پیش ببره. این راه حل بهینه نیست چون منطق فعال بودن یک تبلیغ نیاز به یک کوئری پیچیده داره که برای سیستمی مثل فچ که در ثانیه بیش از ۳۰۰ درخواست رو پاسخ میده عملی نیست و زمان هر کوئری رو بالا می‌بره.

بهتره از این جا به بعد دو حالت برای یک تبلیغ در نظر بگیریم. تبلیغ فعال (اکتیو) و تبلیغ کثیف (dirty). تبلیغ اکتیو تبلیغیه که می‌تونه نمایش بگیره. تبلیغ کثیف هم تبلیغیه که اطلاعاتش در دیتابیس کور تغییر کرده اما این تغییرات هنوز در الستیک لحاظ نشده. سوال اصلی اینه که ما چطوری تبلیغات فعال و تبلیغات کثیف رو به الستیک اعلام کنیم که هم در حجم بالا مشکلی رخ نده و هم سرعت بالایی داشته باشه؟

نرخ فعال و غیرفعال شدن تبلیغات بالاست. ما برای هندل کردن همگام‌سازی تبلیغات فعال با الستیک از تکنیکی ساده استفاده کردیم که اسمش رو Active Ads Probing یا سرکشی به تبلیغات فعال گذاشتیم. روند به این صورته که تبلیغات فعال با همون لاجیک پیچیده‌ای که دارند، سمت کور کوئری خواهند شد. در مرحله بعدی با استفاده از شناسه این تبلیغات اون‌ها رو در الستیک probe می‌کنیم. probe کردن یک تبلیغ می‌تونه به معنای ورژن زدن یا افزودن یک timestamp به اون باشه. حالا فچ کافیه بین تبلیغاتی که آخرین probe رو دارند جستجو کنه.

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

وب‌هوک‌ها

از ابتدای کار ضرورت همگام‌سازی داده بین ما و دیجی‌کالا مشخص بود. برای این کار مجموعه APIهایی که نام اونا رو WebHooks گذاشتیم تعبیه شد. وب‌هوک‌ها علاوه بر این که با نرخ بالایی دیتا می‌گیرند احتمال بروز مشکل Race Condition در اون‌ها خیلی بالاست. فرض کنید که موجودی یک کالا در کمتر از چند ثانیه تغییر کرده و این تغییرات به وب‌هوک‌ها داده می‌شوند. حال با دو Update سروکار داریم که هر دو سعی در تغییر یک فیلد در یک موجودیت دارند. همچنین اطلاعاتی که به وب‌هوک داده می‌شود می‌تواند اعلام تغییراتی به یک موجودیت از پیش داده شده یا اعلام موجودیتی جدید باشد.

برای حل این مشکل‌ها اندپوینت‌های وب‌هوک تمام تغییرات گفته شده رو در یک صف می‌ریزن. این صف اسکن می‌شه و فقط جدیدترین موجودیت بین موجودیت‌های تکراری برای تغییر در نظر گرفته خواهد شد.

آمارها

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

اوراکل

اوراکل یکی از جذاب‌ترین بخش‌های این پروژه بود. هدف اصلی این زیرپروژه اینه که بتونیم دامنه بیشتری از تبلیغات رو ارائه بدیم و خودمون رو محدود به پارامترهای لینک نکنیم. ما می‌خواستیم کوئری‌هایی که فچ انجام می‌ده هوشمندانه‌تر باشه تا تبلیغاتی که به واقع کاملا مرتبط هستند اما به طور کامل با فیلترها همخوانی ندارند هم ارائه بشن.

به صورت کلی اوراکل دو کار اصلی انجام میده. «نرم‌کردن» (relaxation) و «بسط‌دادن» (augmentation) کوئری. نرم‌کردن کوئری به این معناست که اگر کاربر دنبال کالایی با بازه قیمتی بین ۱ تا ۲ میلیون تومان می‌گشت؛ اگر تبلیغی برای این بازه وجود نداشت ما با نرم‌کردن کوئری تبلیغاتی که در بازه قیمتی (مثلا) ۵۰۰هزار تومان تا ۳ میلیون تومان می‌گنجند هم نمایش بدیم.

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

گام‌های آینده

تعامل بین دیجی‌کالا و یکتانت در ابتدای کار خودشه. با بلوغ این پروژه سلرهای بیشتری به سیستم وارد خواهند شد تا بتونن به بهترین شکل کالاهای خودشون رو در معرض دید قرار بدن. تلاش ما در یکتانت اینه که بستری مناسب و بدون مشکل برای این کار ارائه کنیم. همچنین تبلیغات رو به مرتبط و کاربردی‌ترین شکل ممکن ارائه بدیم که هم از تجربه کاربری مصرف‌کننده‌های دیجی‌کالا کم نکنه و هم تبلیغ‌کننده‌ها تاثیر خوبی از هزینه‌هایی که می‌کنند بگیرن.

کلام آخر

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

به تازگی سه نیروی جدید به پروژه اضافه شدند و طراحی و معماری اولیه پروژه کمک شایانی به سرعت توسعه فعلی برای کل تیم کرده. اگر دوست داشته باشی شما هم می‌تونید نفر بعدی باشید که به تیم ما اضافه می‌شه (اینجا رو ببین). اگر سوالی یا مساله‌ای در مورد یکتانت یا سیستم سلرهای دیجی‌کالا داشتید هم می‌تونید با من در اینجا یا اینجا در ارتباط باشید و همچنین از بقیه مقاله‌های یکتانت هم استفاده کنید.

جا داره در نهایت یک تشکر ویژه از کامیاب برای تحمل گیرهایی که می‌دادم و از سینا بابت بحث‌هایی طولانی که در مورد مسائل معماری باهم داشتیم بکنم.