وبسایت شخصی حسن هاشمی

برنامه نویس. ایران. قم :))

سیستم ارسال پوش نوتیفیکیشن

سلام دوستان

مدتی میشه روی یه پروژه اوپن سورس و فقط 4fun دارم کار می کنم، خیلی وقت هم ��ود که پستی نذاشته بودم گفتم مراحل انجام این پروژه رو توی وبلاگم با شما در میون بذارم تا از نظراتتون بهتره مند بشم.

2,3 سال قبل برای یه پروژه ای که روش کار می کردیم نیاز به یه سرور tcpبا performance و throughput بالا شد که البته با تشکر از سی شارپ 5 به راحتی انجام پذیر بود و در کمتر از 2 ماه نوشتمش.

پروژه رو هم تحویل دادیم رفتم :) مدتی قبل با بعضی از دوستان بحث ارسال پوش نوتیفیکشین برای اندروید و ios شد و همکاران بخش اندروید خیلی از GCM و زیرمجموعه های ایرانی اون می نالیدن :)) در نتیجه تصمیم گرفتم که سرور tcp که نوشته بودم رو بیارم برای ارسال پوش ویرایش کنم.

اما این طراحی خالص tcp یه سری مشکلاتی داشت که مجبور شدم کلاً این معماری رو بذارم کنار و از اول شروع کنم به نوشتمش.

اول می خواستم توی codeproject ارسال کنم ولی باز گفتم به فارسی هم امتحان کنیم یه بار :))

تقریباً 20% از پروژه پیاده کردم تا حالا و  فقط فرصت نشده روی github یا Codeplex بفرستمش بالا.


پس بذارید اول از معایب طراحی قبلی  tcp خالص بگیم.

اگرچه که TCP سرعت خیلی بالایی رو ارائه میده اما برای چنین پروژه ای سرعت TCP با بعضی پروتکل های جایگزین خیلی تأثیری روی نمای کلی کار نداره.

TCP بالانس کردنش خیلی سخت تر هست، البته نسبت به http.

برای بالانس کردن tcp یا باید از یه مکانیزم NAT استفاده کنیم یا از یه نرم افزار پروکسی. (که بهترینش HAProxy هست)

اگه بخوایم از پروکسی استفاده کنیم توی مقیاس بالا وقتی فرضا 100 ملیون دیوایس به سیستم متصل هستند خود پروکسی سرور هم باید بالانس باشه. و اگه بخوایم از tcp socket استفاده کنیم راحت ترین راه فورک کرنل لینوکس + سخت افزار خاص میخواد که دردسرش زیاده :)))

حالا از همه این ها هم که بگذریم دردسر اصلی TCP بسته بودن پورت هاش هست، خیلی از ISP ها فقط پورت های معروف رو باز میذارن و بقیه رو به صورت Select all می بندن :)) اینجوری پیام به دیوایس نمیرسه که هیچ برای سرورها هم بار اضافی خریدیم چون در صورت درخواست اتصال و refuse شدن یه exception رها میشه، هم ترافیک هدر میره و هم به cpu فشار میاد.


مورد بعدی هزینه های سرور tcp هست، برای اینکه بخواید برنامه رو روی نت بیارید بالا اگه از وب سوکت استفاده کنید به راحتی می تونید توی سایت asp یا php و یا ... که روی سرور شرکت های هاستینگ هست ازش استفاده کنید در حالیکه اگه بخواید از tcp socket استفاده کنید حتماً باید سرور مجزا داشته باشید (شرکت های هاستینگ نمی پذیرن این مورد رو)


در نتیجه تصمیم گرفتم بیارمش روی وب، و وب سوکت. 

خوبی استفاده از وب سوکت این هست که راحت می تونیم برای پلتفرم های مختلف پیاده سازیش کنیم (یعنی برای زامارین و ویندوز و لینوکس و یونیتی عملاً یه کتابخونه client side بیشتر نیازی نیست بنویسیم و فقط میمونه IOS و کتابخونه کاربرای Java که حتی اون رو هم می تونیم به صورت متحد توی ++C بنویسم البته قسمت ارتباطش با سرور رو). و حتی به راحتی می تونیم برای google glass هم پیاده سازیش کنیم.

- روی emulator هم خیلی راحت جواب میده و عملاً هیچ زحمتی برای ما نداره اجراش.

- پروتکل ساده ای هست و به راحتی می تونیم توی سی شارپ پروتکلش رو پیاده کنیم.

- Load balancing راحتی داره چون Handeshake ش روی http هست و load balancer های معمولی http همگی به راحتی بالانسش می کنن. (حتی Arr)

- وقتی به صورت دستی پروتکل وب سوکت رو پیاده سازی کنیم، هر موقع خواستیم به راحتی می تونیم به سوکت Tcp اصلی دسترسی داشته باشیم. (پیاده سازی مایکروسافت سوکت اصلی رو مخفی می کنه و قبلاً ازش استفاده کرده بودم و با تشخیص قطع ارتباط همیشه مشکل داشتم. البته دات نت 4.5 یه امکان جدید به  TPL  اضافه کرده که می تونید برای Cancellation Token که به Task میدین یه تایم اوتی رو ست کنید و با استفاده از اون بتونید سوکت هایی که disconnect شدن رو تشخیص بدین به این صورت که شما با استفاده از یه task یه keep alive به کانکشن ارسال می کنید و وقتی که Task تایم اوت شد نتیجه می گیرید که اتصال به مشکل خورده. اطلاعات بیشتر در اینجا)


ترافیک و پهنای باند

مشکل بعدی سایز دیتای در حال انتقال روی سیم هست، فرض کنید  یه کلاسی داریم به این شکل که می خوام serialize ش کنم و بفرستم به دیوایس کاربر:

class NotificationDTO
{
public string Title { get; set; }
public string Content { get; set; }
public string Subtext { get; set; }
// the command that is going to be executed when notification tapped
public Command Command { get; set; }
// action buttons and commands for the notification item
public List<ButtonDTO> Buttons { get; set; }
}
وقتی به صورت معمولی Binary Serialize ش می کنیم، میشه 2 کیلوبایت تقریباً. حالا فرض کنیم اگه بخوایم به کلاینت های یه app که 10 ملیون کاربر داره این رو ارسال کنیم، جمعاً میشه 20 گیگ.
در حالیکه همین object به صورت Json حداکثر 300 کیلوبایت هست. بخاطر هیمن برای فرمت انتقال هم Json در نظر گرفتم. که برای مثال بالا از20 گیگ به 3 گیگ کاهش پیدا می کنه.


حالا بذارید بقیه مطلب رو با یه دیاگرام ادامه بدیم:


(میدونم نقاشیم افتضاحه :)) )

این دیاگرام کلی و ساده یه  component diagram خیلی ژیگول و پیگول هست (استاندارد مهندسی نرم افزار توش رعایت نشده اما فعلاً به همین بسنده می کنیم.)


لود بالانسر که توضیح نمی خواد و خودش تابلو هست :)

وب سرور یا (سرور وب)

با اینکه خیلی از سوکت و بچه هاش صحبت کردیم اما فراموش نکنید که بالاخره اون api که می خوایم به کاربر بدیم web api هست.

در نتیجه این سرور وب بای�� رابط کاربری توسعه دهنده (یعنی وب اپلیکیشن داشبرد برنامه نویس) رو فراهم کنه حالا این رابط کاربری خودش چند بخش هست:

    1- اطلاعات لایو در مورد دیوایس ها

    2- اطلاعات افلاین در مورد آمارهای مربوط به برنامه ها (مثل تعداد نصب ها، مدل دیوایس ها، جزئیات سخت افزاری و ..)

     3- ارتباط با سرورهای سوکت:

          بالاخره دیوایس ها از طریق سوکت به سرور متصل هستند و خودتون میدونید که سوکت Tcp، با خودش Server affinity میاره.

        به همین منظور یه Message bus در نظر گرفتم که همه سوکت سرورها به اون متصل بشن و از اون طرف هم سرور وب درخواست ارسال نوتیفیکیشن بذاره روی bus و به صورت خودکار سوکت سرورها دریافتش کنن.

Message bus ما واقعاً چیزی بیشتر از یه سرور سوکت داخلی دیگه نیست :). و مکانیزمش هم pub/sub ساده هست، یعنی تمام سرورهای سوکت و سرور وب بهش متصل میشن و از اونطرف وقتی درخواستی از لایه وب بهش رسید، درخواست رو به سوکت سرورهایی که دیوایس به شون متصل هستن broad cast کنه.

در مورد آیتم اول، که می تونیم به راحتی اون رو از back plane سرورهای سوکت دریافت کنیم (در این مورد توی بخش back plane توضیح بیشتری میدم)

و مورد دوم هم رو هم به راحتی می تونیم با استفاده از یه دیتابیس خیلی معمولی sql server و ... فراهم کنیم چون اطلاعات مربوط به توسعه دهنده حجم زیادی ندارن و حداکثر قرار تاریخچه نوتیفیکیشن و اطلاعات اکانت خود توسعه دهنده رو نگه دارن.

  

سوکت سرورها و Back plane

(توی این پست نمی خوام خیلی زیاد وارد جزئیات پیاده سازی سوکت سرورها بشم)

وقتی که دیوایس به یکی از سوکت سرورها متصل میشه، اون سرور اطلاعات مربوط به اون کانکشن رو توی یه کش داخلی نگه داری می کنه تا بتونه بعداً بهشون دسترسی داشته باشه و از طرفی اطلاعاتی که توی کش خودش نگه داری می کنه رو به صورت مدام به Back plane که در اصل یه دیتابیس No sql هست Replicate می کنه.

اینجوری می تونیم اطلاعات لایو در مورد دیوایس های متصل شده داشته به منظور Analytics داشته باشیم، بدون خسارت چشم گیری به عملکرد سرور.

برای تکنولوژی Back plane تصمیم گرفتم از Redis استفاده کنم.  (وب سرور برای دسترسی به اطلاعات سوکت ها و دیوایس ها به راحتی می تونه به back plane کوئری بزنه و اطلاعات رو دریافت کنه و Redis هم که واقعاً فوق العاده س (تقریبا تا 200 برابر سریع تر از Sql server هست) از طرفی اینجوری هر موقع خواستیم می تونیم بار ترافیک رو به لایه وب منتقل کنیم).


توی پست بعدی میخوام وارد جزئیات پیاده سازی سوکت سرور و Message Bus بشم.

البته یکی دو بخش ساده دیگه هم هستن که یادم رفت توی دیاگرام ذکر کنم که انشالله اوناروهم توی پست بعدی انجام میدم.                  


نظرات (5) -

  • محمد

    06/07/1395 09:24:14 ب.ظ | پاسخ به این نظر

    خیلی وقت که مطالبی با این سطح فنی رو توی وبلاگ های فارسی ندیدم.
    ممنون.

  • شایان

    06/10/1395 12:21:11 ب.ظ | پاسخ به این نظر

    در کل به نظرم خوبه و Scale خطی راحتی داره ولی برای فیلترینگش چی کار می کنی مهندس؟
    الان وقتی پیام رو روی باس میذاری اون سرورهایی که به دیتای اون پیام نیازی هم ندارن باز دریافتش می کنن و بعد از دریافت تازه باید بفهمن که بدردش میخوره یا نه.

  • شایان

    06/10/1395 12:27:22 ب.ظ | پاسخ به این نظر

    تازه اگه یه یوزر با تعداد کلاینت کم بخواد پوش بفرسته که فاجعه میشه (همه سرورهات باید روی همه سوکت هاشون الکی چک کنن؟)

    • حسن

      06/10/1395 11:19:18 ب.ظ | پاسخ به این نظر

      سلام مهندس، چطوری تو ازینورا ؟ Smile)
      یادم رفت توی پست بنویسم:

      این MessageBus به صورت pub/sub هست، یعنی وقتی یه سرور به MessageBus متصل میشه، موقعی که میخوایم پیام رو بذاریم روی باس فقط به اون سرورهایی pub می کنیم که به یه تاپیک خاص (که توی این کیس appid هست) sub شدن.

      • حسن

        06/11/1395 12:40:48 ق.ظ | پاسخ به این نظر

        البته الان که فکر می کنم این راه حلی که برای فیلترینگش تو ذهنم هست هم خوب جواب نمیده.
        Filtered subscription

Loading