وب‌سوکت لحظه‌نگار

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

وب‌سوکت قبلی لحظه‌نگار بر پایه Socket.IO بود و به علت مشکلات ساختاری با حدود ۳۰۰۰ سوکت باز با مشکل مواجه می‌شد که مشکلات زیادی رو درست کرده بود، مثلا اختلاف بسیار زیاد بین آمار بیننده. با توجه به قدیمی بودن کد وب‌سوکت و اشتباهات بسیار زیاد توی اون تصمیم گرفتیم که وب‌سوکت رو دوباره با Typescript و Node.js بنویسیم.

موارد مهمی که نیاز داشتیم

توانایی هندل کردن سوکت‌های باز بالا (بیش از صد هزار) با منابع کم

اسکیل کردن وب‌سوکت یک فرق مهم با اسکیل کردن چیزی مثل API داره. برای اسکیل کردن API کافیه که تعداد سرور‌هارو بیشتر کنی، اما روی وب‌سوکت باید سازوکاری باشه که سرور‌ها بتونن با هم صحبت کنن. مثلا اگر علی به سرور یک وصل شده و حسن به سرور دو و حسن می‌خواد به علی پیغام بفرسته، باید پیغام از سرور دو به سرور یک فرستاده شه. به این کار Messaging گفته می‌شه که توسط Message Brokerها انجام میشه. ما این کار رو با Redis Pub/Sub انجام دادیم به دو علت: اکثریت Eventها از نوع Broadcast هستند و باید به دست همه سرور‌ها برسند. از قبل Redis Sentinel Cluster رو توی زیرساختمون داشتیم.

برای کم کردن مصرف منابع و اسکیل راحت‌تر تصمیم گرفتیم که خودمون سرور وب‌سوکت رو بنویسیم و از Socket.IO استفاده نکنیم. (البته مهم‌ترین حسن Socket.IO که Failover کردن روی Long-Polling هست هم اهمیتی برای ما نداشت.)

توانایی انجام تعداد بالای Broadcast به تعداد زیادی سوکت در کمترین زمان ممکن

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

WSServer.sockets.forEach((socket) => {
    socket.send(data);
});

یک حلقه ساده که Event رو به تک‌تک سوکت‌ها ارسال می‌کنه. اما دو مشکل بزرگ در این حلقه وجود داره:

۱. اگر تعداد سوکت‌ها زیاد باشه، ارسال Event به همه اون‌ها طولانی میشه و بعضی از کاربران Eventها رو دیر دریافت خواهند کرد. برای حل این مشکل ایده ما زیاد کردن تعداد Workerها بود تا به صورت Parallel این عملیات انجام شه.

۲. حلقه‌های طولانی Node.js Event Loop رو بلاک می‌کنه. برای این مشکل ما هر Broadcast رو به تکه‌های ۱۰۰تایی تقسیم می‌کنیم و هرتکه رو بعد از اتمام فاز poll در Event Loop ارسال می‌کنیم.  

جلوگیری از ارسال Eventهای تکراری

بعضی از Eventها مثل تعداد بیننده به‌صورت دوره‌ای به همه سوکت‌ها Broadcast میشه. اگر تعداد Workerها و سرور‌ها بیشتر از یکی باشه، هر کدوم از اون‌ها این ایونت رو به همه خواهند فرستاد. برای رفع این مورد از روش Leader Election که با یک Lock ساده در Redis کار می‌کنه، استفاده کردیم.

معماری کلی جدید

  1. از دو کلاستر HAProxy به صورت Active/Passive برای LoadBalancing استفاده می‌کنیم.
  2. برای LoadBalancing بین دو کلاستر HAProxy از DNS Round-Robin استفاده می‌کنیم.
  3. Keepalived وظیفه انتقال Floating IP بین سرور‌های Active و Passive رو بر عهده داره.
  4. در هر پاد وب‌سوکت سرور، یک کانتینر Nginx و یک کانتینرjs داریم. Nginx درخواست هارو دریافت و به Node.js ارسال می‌کنه.
  5. از Redis برای Messaging بین ترد‌ها، پیاده سازی Leader Election و کش استفاده می‌کنیم. همچنین Redis وظیفه دیتابیس برای وب‌سوکت رو بر عهده داره.
  6. همه این قسمت‌ها در کلاستر Kubernetes هستند.

معماری داخلی جدید

  1. هر کانتینر ابتدا یک Master Process داره که اون Workerهارو Fork می‌کنه.
  2. مستر با Redis در ارتباط هست و یک Pub/Sub سرویس ارائه می‌کنه که Workerها می‌تونن با IPC از اون استفاده کنن. حسن این روش این هست که Workerها مستقیم با Redis ارتباط ندارند و بار روی Redis کمتر می‌شه.
  3. دوباره برای این‌که هر Worker جداگانه برای Leader Election به Redis وصل نشه، مستر Lock Service رو روی IPC ارائه می‌کنه.
  4. Metrics Service روی مستر وظیفه Aggregation متریک‌های Workerها رو برعهده داره تا متریک‌ها توسط Prometheus یک‌جا جمع‌آوری شه.
  5. هر Worker یک سرور وب‌سوکت برای خود داره.
  6. دوباره هر Worker یک API Server هم برای خود داره. کار API Server دریافت و ارسال Hookها هست. مثلا هوک دنبال کردن یا حمایت مالی.

پروتکل

برای ارتباط بین سرور و کلاینت یک پروتکل ساده مبتنی بر JSON طراحی کردیم. چهار مدل پیام رو الان پشتیبانی می‌کنیم:

  1. subscribe:برای subscribe کردن روی یک Event مثل تعداد بیننده
  2. unSubscribe:برای لغو اشتراک روی یک Event
  3. action:برای ارسال یک Action مثل ارسال پیام. همچنین امکان ارسال ACK از سمت سرور هم وجود داره.
  4. ping:برای ارسال ping و باز نگه‌داشن سوکت

کلاینت‌ها

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

متریک‌ها

برای این‌که بدونیم وب‌سوکت درست کار می‌کنه یا نه، متریک‌های مختلفی رو جمع‌آوری می‌کنیم:

  1. تعداد سوکت‌های باز
  2. تعداد Eventهای دریافت شده برحسب نوع
  3. تعداد Eventهای ارسال شده بر حسب نوع
  4. Error Rate برای هر عملیات
  5. Response Time برای هر عملیات
  6. Broadcast Time برای هر Event
  7. آمار تعداد Emoteهای استفاده شده برای هر نوع
  8. متریک‌های کلی مثل CPU و RAM

دوست دارم یکی از اون‌هارو اینجا به اشتراک بذارم:

Broadcast Time بین ۰ تا ۱۰ میلی‌ثانیه برای همه Eventها

ما توی لحظه‌نگار چندین موقعیت شغلی باز داریم، خوشحال میشم رزومه‌هاتون رو ببینم: lahzenegar.com/jobs

نویسنده محمدحسین عابدینپور

برچسب: