بهار ۹۴ در تیم فنی دیوار تصمیم به پیادهسازی چت دیوار گرفتیم. برای تست اولیه و ارائه سرویس چت به کاربران در زمان کوتاه Tigase، که یک Message Broker متنباز مبتنی بر پروتکل XMPP است، انتخاب کردیم و امکان ارسال پیام متنی و نگهداری تاریخچه پیامها را برای اندروید پیادهسازی و منتشر کردیم.
در ادامه به دلیل کمبود نیروی انسانی و بالا بودن اولویتهای محصولی دیگر این سرویس توسعه بیشتری نیافت و در خرداد ۹۵ با هدف تمرکز روی توسعهی چت دیوار یک تیم مجزا برای این سرویس شکل گرفت.
در تیم جدید چت دیوار را با زبان الیکسیر (Elixir) و فریم ورک فینکس (Phoenix) و دیتابیس کسندرا (Cassandra) از پایه طراحی و پیادهسازی کردیم. در ادامه این یادداشت تجربه خودمان را به اشتراک خواهیم گذاشت.
روزانه در دیوار هزاران داد و ستد با گرفتن چند میلیون اطلاعات تماس (تماس تلفنی، پیامک و ایمیل) شکل میگیرد. به جز ایمیل بقیه راهها رایگان نیستند، از سوی دیگر ایمیل یک راه تماس آنی نیست و همه ایرانیها از آن بهره نمیگیرند. از طرفی ماندن کاربر در دیوار، تسهیل ارتباط و انجام خرید و فروش با سرویس چت بهبود پیدا میکرد.
همچنین بسیاری از کاربران به دلیل مسائلی مثل حریم خصوصی (نمایش آواتار، مزاحمت و …) تمایلی به استفاده از پیامرسانهای عمومی خود برای خرید و فروش نداشتند و وجود یک سرویس چت داخلی در دیوار این مشکل را برطرف میکرد.
بنابراین به یک راه ارتباطی ساده، رایگان و آشنا برای جامعه نیاز بود که با توجه به رشد و محبوبیت سرویسهای پیامرسان ارائه سرویس چت دیوار گزینه مناسبی بود.
ما حدود دو ماه و نیم در حال تحقیق، توسعه و پیادهسازیهای کوچک بودیم و در پایان این زمان به دید مناسبی دربارهی نیازمندیها، ابزارها و طراحی فنی مورد نظرمان رسیدیم.
نیاز فنی ما در واقع ویژگیهایی بود که یک Message Broker خوب و سرویس آنی (Real Time) باید داشته باشد.
قابل اطمینان بودن (Reliability) و دسترس پذیری (Availability) بالا
در واقع خوب است که سرویسهای آنی مثل چت در قضیه CAP به صورت AP طراحی شوند که قابلیت آپتایم بالایی داشته باشند و همچنین درصورت قطعی شبکه و ارتباط بین Node ها (Network Partition Tolerance) مشکلی در ادامه فعالیت سرویس وجود نداشته باشد. البته در مورد Consistency نیز باید مکانیزمهایی در نظر گرفته شود که داده سرویس شما در حد قابل قبولی معتبر باشد.
مقیاسپذیری
با توجه به رشد دیوار نیاز بود که سرویس چت ما قابلیت مقیاس پذیری افقی (Horizontal) داشته باشد و به صورت توزیع شده (Distributed) طراحی و پیاده سازی شود.
ذخیره پیامها و امکان بازیابی تاریخچه پیامهای کاربر از سرور
این مورد با اینکه شاید در نگاه اول با داشتن یک دیتابیس مطمئن قابل انجام باشد، ولی در مقیاس و سرعت رساندن بالای پیامها و همچنین با داشتن قابلیت استفاده همزمان در چند دستگاه طراحی مدل و انتخاب دیتابیس سخت خواهد شد.
قابلیت استفاده همزمان در چند دستگاه
این که کاربر همزمان در وب، اندروید و یا آی او اس آنلاین باشد و یک پیام ارسال یا دریافت کند و در هر سه بدون تاخیر به روز باشند قابلیت جذابی است ولی از نظر فنی طراحی پروتکل و مدل ذخیره سازی داده را پیچیده میکند تا حدی که پیامرسان جدید گوگل Allo و یا Whatsapp از خیر این قابلیت گذشتهاند.
سرعت و تاخیر در رسیدن پیام
یکی از ویژگیهای مهم در سرویس چت سرعت رسیدن پیام به سرور و سپس به مخاطب است که اگر دارای تاخیر زیاد باشد و یا ترتیب پیامهای ارسالی و دریافتی یکی نباشند برای کاربر خوشایند نیست. معماری سرویس، تاخیر در بخشهای مختلف، پروتکل انتخاب شده و حجم هر کدام از درخواستهای جابجا شده بین سرور و کاربر در این مورد خیلی موثر هستند.
چابک بودن و اختراع نکردن دوبارهی چرخ
یکی از موارد مهم این بود که تا حد امکان از فریمورکها، ابزارهای متنباز و معماریهای استاندارد استفاده کنیم که سرعت توسعه بالا و همچنین ضریب خطا و باگ کمتری داشته باشد.
توسعهی تست محور و لود تست
در طراحی و انتخاب معماری و زبان امکان نوشتن سریع و تا حد امکان سادهی تست موثر است. بعلاوه با داشتن لود تست قبل از انتشار سرویس میتوان آن را در شرایط واقعی تست کرد و اکثر مشکلات را شناسایی و رفع نمود.
یکی از چالشهای ما نداشتن تجربه در توسعه سرویسهای آنی مثل چت بود. در دنیا نیز تعداد شرکتهایی که سرویسهای مشابه را توسعه دادهاند کم است و تجربیات و ابزارهایی که به اشتراک گذاشتهاند محدود است. برای مثال شرکتهای بزرگی مثل گوگل و فیسبوک و مایکروسافت که توسعه دهنده پیامرسانهای Hangouts, Allo WhatsApp و Skype هستند که تجربیات خود را خیلی به اشتراک نگذاشتهاند. ما در این مسیر مجبور بودیم که خودمان با مطالعه در مورد معماریهای مختلف، پروتکلها، سیستمهای توزیعشده و کدهای محدود متنباز موجود، سرویس چت دیوار را طراحی کنیم.
در ادامه به دلیل اینکه باید تعداد زیادی ابزار و طراحیهای مختلف را تست میکردیم و همزمان نیاز بود که چابک باشیم و سریعتر به مرحله پیادهسازی برسیم، تصمیم گرفتیم طراحی و معماری را بصورت تیمی در جلسات مشترک انجام دهیم و تست ابزارها و پیادهسازیهای تستی را به صورت فردی انجام داده و بین آنها مقایسه و انتخاب کنیم که این مکانیزم به کوتاه و مفید شدن دوره تحقیق و توسعه کمک زیادی کرد.
برای رسیدن به ویژگیهای فنی ذکر شده تصمیم گرفتیم که زبان برنامهنویسی و فریمورک انتخابی قابلیت برنامهنویسی Reactive و پیادهسازی Actor Model را داشته باشد و یا پیادهسازی این موارد در آن به سادگی انجام شود.
دربارهی پروتکل ارتباطی برای ما دو موضوع مهم بود، حجم پیام و قالب آن سبک و تا حد امکان در پیادهسازی ایپیآیها خوانا، سریع و ساده باشد. ما پروتکلهای MQTT, XMPP و WAMP را مطالعه و خودمان نیز یک پروتکل مبتنی بر وبسوکت و فرمت Protocol Buffers به صورت آزمایشی توسعه دادیم. در نهایت پروتکل ارتباطی Phoenix Channel را شخصی سازی و استفاده کردیم.
در این بخش بطور خلاصه دلیل عدم استفاده از زبانهایی که تست کردیم و انتخاب الیکسیر را به اشتراک میگذاریم. در مهندسی نرمافزار تقریبا هر سرویسی را میتوان با هر ابزار و زبانی توسعه داد و مباحثی مثل زمان یادگیری، تجربه و دانش فعلی تیم، سرعت و سادگی توسعه و آیندهنگری دربارهی محصول عوامل مهم در انتخاب هستند و در نهایت ممکن است سلیقه افراد تیم در این انتخاب تاثیرگذار باشد.
با توجه به اینکه تیم ما تجربه خوبی در زبانهای پایتون و گو داشت، ابتدا امکان پیادهسازی را در این دو زبان بررسی کردیم. در پایتون، مشکل کندی زبان بخاطر مفسر بودن آن مطرح بود و دو سال و نیم پیش بستههای AsyncIO خیلی محدود و پر از باگ بودند. برای مثال ما یک پروژه متنباز به اسم Autobahn را بررسی کردیم که با پروتکل WAMP بود و امکان پیادهسازی سرویس آنی را به سادگی داشت ولی توسعهدهندگان آن برنامهای برای مقایسپذیری و توزیعپذیری آن نداشتند.
در ادامه با توجه به معماری خام مایکروسرویسیای که داشتیم، به صورت آزمایشی به مدت دو هفته یک سرویس ساده با قابلیت انتقال پیام بین دو کاربر روی وبسوکت و یک پروتکل ساده روی Protocol Buffers با زبان گو توسعه دادیم و از کافکا (Kafka) به عنوان Message Broker استفاده کردیم ولی هیچ فریمورک استاندارد و بستهی آمادهای وجود نداشت و باید حجم زیادی کد برای رسیدن به محصول توسعه میدادیم. برای مثال پروتکل اختصاصی، مایکروسرویسهای مورد نیاز و حتی برای رفع بعضی از نیازهای ما مجبور بودیم بستههای موجود زبان گو مثل درایور کافکا توسعه دهیم.
همزمان نمونه مشابه سرور تلگرام با زبان اسکالا (Scala) توسط یک تیم خارجی به صورت متنباز منتشر شده بود و بخشی از تیم زمان خود را صرف بررسی و امکانسنجی استفاده از آن و یا توسعه با زبان اسکالا بههمراه ابزار آکا (Akka) کردند که به دلیل کد پیچیده، ناخوانا و مستندات ناقص از آن استفاده نکردیم و از زبان اسکالا و چارچوب آکا به دلیل نیاز به زمان یادگیری بالا و پیچیدگی زبان نیز استفاده نکردیم.
پس از تست این ابزارها و زبانها و با توجه به تجربهی یکی از اعضای تیم در ارلنگ (Erlang OTP) متوجه شدیم که ارلنگ بیشتر نیازهای ما را به صورت Built-in پشتیبانی میکند. در ارلنگ قابلیتهایی مثل Immutability, Lightweight Processes و ارتباط بین آنها به صورت Message Passing در نتیجه ایزوله شدن وضعیت (State) در هر Process، نوشتن یک سرویس همروند (Concurrent) مطمئن و پیادهسازی Actor Model را بسیار ساده میکند. ارلنگ با داشتن امکانات و ابزارهایی مثل کلاستر شدن ماشینهای مجازی و در نتیجه امکان ارتباط پردازشها در سطح کلاستر و داشتن ابزارهای Process Group, Supervision, GenServer و … مقیاسپذیری و توسعه یک سرویس توزیعشده با قابلیت اطمینان بالا را بسیار ساده میکند.
در نتیجه ما با انتخاب زبان مدرن و جدید الیکسیر(ساخته شده روی ماشینمجازی ارلنگ) علاوه بر تمامی امکانات ارلنگ از ویژگیهایی مثل سینتکس خواناتر و مدرن، پیادهسازی بهتر String و Unicode و Polymorphism و Meta-Programming، وجود فریمورک چابک فینکس و ابزارهای مدرنی مثل Mix, ExUnit, ExDoc که امکان توسعهسریع، تستنویسی و مستندسازی پروژه را در اختیار ما قرار میداد، بهرهمند بودیم.
بعد گذر از مرحله تحقیق و توسعه وارد مرحله پیادهسازی چت دیوار شدیم. ابتدا در یک بازه دو ماهه بخاطر اینکه توسعه سمت کاربر هم بتواند شروع به کار کند API احراز هویت کاربر، برقراری ارتباط بین سرویس چت و پست دیوار، ارسال و دریافت Event هایی مثل Text, Seen, Conversation و Typing را توسعه دادیم و برای دیتابیس به دلیل اینکه Adapter پیشفرض الیکسیر از Postgresql پشتیبانی میکرد و همچنین از آنجایی که در کسندرا نیاز است از قبل کوئریهای مورد استفاده کاملا مشخص شوند به صورت موقت بجای کسندرا از Postgresql استفاده کردیم.
علت مهاجرت ما از Postgresql به کسندرا سرعت نبود، بلکه بر اساس نیاز فنی که داشتیم مهم بود که دیتابیس ما نیز مثل سرویسمان، کاملا مقیاسپذیر و توزیع شده باشد. برای Postgresql بستهها و راهکارهای متنباز موجود برای مثال Postgres-XL و … را بررسی کردیم، ولی در آن زمان تمام امکانات موجود ناقص و یا در کانفیگ و نگهداری پیچیده بودند و کسندرا در مقیاسپذیری و توزیع پیشرفتهتر و آزمونهای لازم را پس داده بود.
کسندرا یک دیتابیس NoSQL توزیع شده است که قابلیت مقیاسپذیری و دسترسی بالایی را ارائه میدهد. همچنین با اینکه به صورت پیشفرض AP (در قضیهی CAP) است ولی در صورت نیاز میتواند در جدول یا کوئریهای مورد نیاز نیز CP باشد. بنابراین بر اساس نیازمندیهای ما و برای یک پیامرسان گزینهی مناسبی است. البته حتما قبل از انتخاب و استفاده از کسندرا مطالعه مستندات آن مهم است. کسندرا با اینکه امکانات بالایی را ارائه میدهد ولی دارای محدودیت، پیچیدگیها و همچنین طراحی خاص خودش است. برای مثال Denormalization و Duplication برعکس دیتابیسهای دیگر در کسندرا زیاد استفاده میشود. همچنین قبل از شروع کدنویسی و ایجاد جداول باید کوئریهای سرویس را ابتدا طراحی کنید و همیشه به بحث تعداد Partition Key ها در نوشتن و خواندن برای کارایی بالا و توزیع درست در کلاستر توجه شود. اگر خلاصه کنیم میتوان یک پست بلاگ مفصل در مورد کسندرا نوشت.
در آن زمان به مشکل عدم وجود یک درایور ساده و سریع برای کسندرا در الیکسیر که از تمام امکانات آن پشتیبانی کند و همچنین Adapter کسندرا که امکان کدنویسی سریع، مایگریشن و نوشتن تست را ساده کند برخوردیم. بنابراین در تیم تصمیم به نوشتن درایور و Adapter و انتشار آن به صورت متنباز گرفتیم. ابتدا تخمین زمانی ما به دلیل نداشتن تجربهی نوشتن درایور کم بود. اما پیادهسازی امکانات زیاد پروتکل کسندرا (برای مثال تشخیص خودکار بالا و پایین بودن سرورها، Policy های مختلف مثل RoundRobin و TokenAware در ارتباط و اجرای کوئری روی هر سرور) تست و دیباگ مورد نیاز آن را پیچیدهتر میکرد. نکتهی بعدی اینکه ما روی مشارکت جامعه الیکسیر با متنباز کردن درایور حساب کرده بودیم که در عمل به دلیل ضعف ما در مستندسازی و کوچک بودن جامعهی الیکسیر میسر نشد و بیشتر آن توسط تیم پیادهسازی شد و در نتیجه درایور زمان بیشتری گرفت. در نهایت با پیادهسازی امکانات مورد نیاز ما در پروتکل کسندرا، درایور و Adapter را با سرعت بهینه، Test Coverage متوسط و پایدار منتشر و در سرویس چت خودمان استفاده کردیم. ولی متاسفانه در ادامه با بالاتر بودن اولویت کارهای دیگر هنوز موفق به بروزرسانی امکانات و مستندسازی بهتر نشدهایم.
پیشبینی ما برای چت دیوار یک میلیون کاربر آنلاین و ده هزار کاربر فعال (کاربری که پیام ارسال و دریافت میکند) بود. اگر معماری سرویسی استاندارد و زیرساخت سختافزاری موجود باشد لود تست قابل انجام است. ما در لود تست شبیهسازی ثبتنام، احراز هویت، اتصال به سوکت چت و شروع گفتگو با دیگر کاربران را انجام دادیم که علاوه بر پیشبینی لازم بسیاری از باگهای سرویس را قبل از انتشار متوجه شدیم و رفع کردیم. البته به دلیل محدودیت سخت افزار برای لود تست با ظرفیت یک دهم آن را انجام دادیم.
در خرداد ۹۶ موفق شدیم با حداقل باگ، آپتایم ٪ ۹۹.۹۹ و متوسط زمان ۳۰ میلیثانیه برای جابجایی هر پیام سرویس جدید چت دیوار را منتشر کنیم. بعد از انتشار سرویس به دلیل داشتن Test Coverage بالا و انجام لود تست با حداقل باگ در زمان اجرا مواجه شدیم.
در ادامه موفق شدیم که امکانات تکمیلی چت مثل ارسال تصویر، اشتراک گذاری اطلاعات تماس و مکان، حذف مکاتبه، گزارش تخلف، تعلیق و مسدود کردن کاربر متخلف را به سرعت به سرویس اضافه کنیم و با اینکه بعضی از این امکانات با نسخهی قبلی سازگار نبودند با داشتن Adapter های ساده آنها را نیز تا به حال سازگار نگه داشتهایم.
با رشد استفاده چت بین کاربران دیوار تعداد اسپمرها و پیامهای مزاحم رشد کردند. به صورت موازی اعضای متخصص دادهی تیم یک Agent خودکار برای تحلیل رفتار کاربر بر اساس گزارش اسپمها، مسدود کردنها و محتوا برای شناسایی آنها توسعه دادهاند. در نتیجه برای حفظ کیفیت و مقابله با اسپمرها مایکروسرویس مجزایی را طراحی و پیادهسازی کردیم که با گذاشتن RabbitMQ بین سرویس چت، این مایکروسرویس و سرویس دیتا با فاصله زمانی مناسب اسپمرها را شناسایی و از برخی امکانات چت تعلیق یا مسدود میکند.
با رشد خطی کاربران نیاز داریم به جای نصب سرورهای جدید و اضافه کردن آنها به کلاستر با توجه به داشتن تجربه و امکانات لازم مثل CI/CD و Kubernetes در شرکت سرویس را به Kubernetes منتقل کنیم.
با توجه به عدم پیشبینی تعداد زیاد مکاتبه در طراحی اولیه در بعضی از قسمتها مثل لیست مکاتبات یا مسدود شدهها Stateless هستیم و کاربر بعد از اتصال کل لیست مکاتبات را از سرور گرفته و به روز میشود که باید این قسمت را مانند پیامها به صورت رویدادگرا در آینده پیادهسازی کنیم و کاربرها فقط تغییرات لیست مکاتبات را دریافت کنند.
مستندات درایور و Adapter کسندرا را کاملتر و با نسخه و امکانات Adapter نسخه جدید سازگار کنیم.