پیام‌رسان NATS با هسته‌ قوی و به شکلی باورنکردنی سریع!

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

در این شرایط بیشترین زمان صرف نحوه پیاده‌سازی Code Base می‌شود اما اغلب تیم‌ها ساختار و طراحی پیام‌رسانی(Messaging) را به بعد موکول می‌کنند در حالی که پیام‌رسانی، سیستم عصبی مرکزی نرم‌افزارهای بزرگی است که از ساختار توزیع شده و مدرن بهره می‌برند. صرف‌نظر از اینکه ما از الگوی Event Sourcing  یا از مدل Work Dispatch استفاده ‌کنیم، Messaging نخ تسبیح تمام میکروسرویس‌ها است که باعث عملکرد صحیح کل سیستم می‌شود و بدون آن، سیستم توزیع‌شده ما محکوم به شکست خواهد بود. بنابراین در راستای ایجاد ساختار Messaging آنچه تیم فنی آپارات پشت سر گذاشته را در ادامه می خوانیم.

چطور یک Message Broker یا Messaging Architecture را برای نرم‌افزارمان انتخاب کنیم؟

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

استفاده از Kafka

از Kafka اغلب به عنوان یک Log Store توزیع‌یافته یاد می‌شود. معمولا فرض می‌کنیم پیام‌هایی که به یک Topic خاص منتشر می‌شود در کافکا برای مدت مشخصی نگهداری شده و مفهوم Consumer Group به پیام‌ها اجازه می‌دهد تا به صورت متوازن بین Instance‌های آن سرویس پخش شوند. کافکا سرویس بسیار قدرتمندی است، اما قدرت با خودش مسئولیت هم به همراه می‌آورد! نگهداری از کافکا سخت است و معمولا یادگیری آن برای تیم‌هایی که بخواهند به این تکنولوژی مسلط باشند، پر زحمت و زمان‌بر است.

استفاده از RabbitMQ

از طرف دیگر RabbitMQ یا هر بروکر ‌دیگری که از پرتوکل AMQO تبعیت می‌کند یکی از انتخاب‌های پرطرفدار  برنامه‌نویس‌هاست. RabbitMQ به نسبت سبکتر بوده و به جای استفاده از کانسپت گروه مصرف‌کننده یونیک(unique consumer groups) راه‌حل ساده‌تری را دنبال می‌کند؛ به این صورت که وظیفه مصرف کردن پیام‌های صف را به کلاینت‌ها محول می‌کند. اگر یک کلاینت اعلام دریافت یک پیام را انجام ندهد، آن پیام در صف برگردانده شده تا توسط یک کلاینت دیگر پروسس شود. البته این روش ظرافت‌های استفاده خود را هم در‌پی‌دارد. مواردی مثل فاصله زمانی کوتاه که ممکن است در آن‌ها دو Worker یک پیام را دریافت کنند(دو بار یک درخواست بررسی شود) و موارد دیگری از این دست!

استفاده از Redis

نرم‌افزارهای مثل Redis که خود را به عنوان یک Message Broker معرفی نمی‌کنند هم از الگوی pub/sub پشتیبانی می‌کنند. همینطور که می‌بیند لیست محصولات و سرویس‌هایی که الگوی Meessage Brokering را ارایه می‌دهند بزرگ و متعدد است. هر کدام از این نرم‌افزارها نقاط و حوزه‌های قوت و ضعف خودشان را دارند.

کافکا قدرتش را در رقابت با سیستم‌هایی با مقیاس بزرگ و سناریوهای تجمیعی از طریق persistent message logs نشان می‌دهد.اما Rabbit در محیط‌هایی که به عملکرد ساده‌تر pub/sub نیاز داریم بهتر عمل می‌کند و Redis هم ممکن است برای شرایطی که تمامی کلاینت‌های شما برای Cache از قبل با Redis در تماس باشند و شما نیازی که به نگهداری پیام‌ها نداشته باشید، انتخاب خوبی باشد.

اگر واقعا بخواهیم ایده "سیستم عصبی مرکزی" را به معنی واقعی پیاده کنیم و نخواهیم overhead راهکارهای دیگر را تحمل کنیم، آن وقت راه بهینه چیست‌؟اگر بخواهیم به جز روش سنتی pub/sub روش‌هایی مثل request/reply و حتی رویه scatter-gather را خیلی سبک و ساده پیاده‌سازی کنیم، NATS ممکن است پیشنهاد بهتری باشه!

خوب NATS یک نرم‌افزار متن‌باز با یک هسته قوی و ساده، به شکلی باورنکردنی سریع! 

این دلال پیام از پروتوکل متن محور استفاده می‌کند، که شما به راحتی می‌توانید با `telnet` به یک سرور NATS متصل شده و پیام‌ها را ارسال یا دریافت کنید. NATS به گونه‌ای طراحی و پیاده‌سازی شده که دایم به کار، متصل و همیشه آماده دریافت دستورات باشد.

(منبع: bravenewgeek.com/dissecting-message-queues)
(منبع: bravenewgeek.com/dissecting-message-queues)


برای اینکه بهتر موضوع را درک کنید، بیاید چند مورد استفاده معمول را باهم بررسی کنیم:

الگوهای پیام رسانی :

سه الگوی پیام‌رسانی توسط NATS پشتیبانی می‌شود، در حالی که در اصل به عنوان یک موتور publish-subscribe عمل می‌کند.

  1. Publish-Subscribe
  2. Request-Reply
  3. Queue Grouping

اجزای معماری پیام رسانی :

اینها لیستی از کامپوننت‌های اصلی زیر ساخت NATS هستند:‌

  1. پیام (Message):  پیام‌ها واحدهای انتقال داده هستند، پیام‌های Payload هایی هستند که برای انتقال داده بین اپلیکشن‌ها استفاده می‌شوند.
  2. موضوع (Subject): موضوع مقصد پیام‌ها را مشخص می‌کند.
  3. تولیدکننده (Producer): تولیدکننده‌ها پیام‌ها را به NATS سرور ارسال می‌کنند.
  4. مصرف‌کننده (Consumer): مصرف‌کننده‌ها پیام ها را از NATS سرور دریافت می‌کنند.
  5. سرور (Messaging Server): سرور پیام‌ها را از تولیدکننده‌ها به مصرف کننده‌ها می‌رساند.

الگو Publish-Subscribe :

در ساده‌ترین مدل pub-sub شما یک تولید‌کننده دارید که  پیامی را به یک موضوع (subject یا topic) مشخص ارسال می‌کند و هر کلاینتی که به پیام‌های آن موضوع مشخص علاقه داشته باشد را به آن موضوع خاص subscribe می‌کند. در اینجا NATS تضمین می‌کند که هر پیام به هر مشترک فقط یکبار ارسال شود به عبارت دیگر NATS تضمین می‌کند پیام‎ها به ترتیب ارسال، برای کلاینت‌ها ارسال شود ولی ترتیب دریافت توسط کلاینت‌ها مختلط را تضمین نمی‌کند به عبارت دیگر اگر در یک ارسال، کلاینت‌ها به ترتیب ۱-۳-۴، پیام‌ها را دریافت کنند، در ارسال بعدی این ترتیب تضمین نمی‌شود.

در مقایسه NATS در این حوزه با دیگر رقبا باید به این موضوع اشاره کرد که در Kafka و یا Rabbit شما باید topic را قبل از اینکه سرویس‌ها شروع به کار کنند، بسازید در حالی که در Redis و NATS به ترتیب Channel و Subject به صورت on-fly ساخته می‌شوند و نیازی به پیش تعریف آنها نیست. همین قابلیت ساخت موضوعات به صورت on-fly سنگ بنای الگوی request-reply در NATS است.

برای تست کردن این الگو به آدرس زیر مراجعه کنید :‌
https://github.com/nats-io/go-nats-examples/tree/master/patterns/publish-subscribe

الگو Request-Reply :

یک سرویس RESTful را در نظر بگیرید، وقتی که ما یک درخواست HTTP را برای یک سرویس ارسال می‌کنیم از سرویس هم یک پاسخ دریافت می‌کنیم در این حالت ما از الگوی سنتی request-reply همگام استفاده می‌کنیم.در بسیاری از سیستم‌های Messaging پیاده‌سازی الگوی request-reply بسیار مشکل بوده و یا پیاده‌سازی آنها نیازمند توافقات عجیب و غریبی است. در حالی که در NATS پیاده‌سازی این الگو به سادگی اعلام یک subject به عنوان reply-to در زمان ارسال یک پیام است.مراحلی که به ترتیب در یک سناریو نمایش لیست ویدیوهای یک شخص خاص بر مبنای الگوی Request-Reply اتفاق می‌افتندُ به شرح ذیل است:

  1. منتشرکننده (Publisher) برای یک موضوع مشخص به طور مثال myvideos.reply ثبت نام می‌کند.
  2. منتشر‌کننده یک پیام برای موضوع myvideos.inquiry ارسال می‌کند و در این پیام نام موضوع reply-to را myvideos.reply قرار می‌دهد.
  3. منتشرکننده برای مدت زمانی مشخص response timeout منتظر دریافت یک پیام برروی موضوع myvideos.reply باقی می‌ماند.
  4. منتشرکننده از موضوع myvideos.reply ثبت نام خود را لغو می‌کند یا (unsubscribe‌) می‌کند.
  5. منتشر کننده پاسخ ارسالی را بررسی و پردازش می‌کند.

در اینجا NATS برای اینکه بتواند چندین درخواست همزمان را بررسی کند و مطمئن شود که کد نوشته شده پاسخ درخواست را تنها به همان درخواست کننده مشخص ارسال نماید، اغلب به انتهای موضوع reply-to یک رشته متن تصادفی GUID اضافه می‌نماید. به طور مثال reply-to: myvideos.reply.78707692-c45b-4711-a721-49e13f90308d خواهد بود. البته معمولا این پیچیدگی‌ها توسط client های هریک از زبان‌های برنامه‌نویسی انجام می‌شود و به طور مثال در زبان GO کد شما به این شکل خواهد بود.

import nats "github.com/nats-io/nats.go"

// Connect to a server
nc, _ := nats.Connect(nats.DefaultURL)
// Requests
msg, err := nc.Request("myvideos.inquiry", []byte("help me"), 100*time.Millisecond)
// Replies
nc.Subscribe("myvideos.inquiry", func(m *Msg) {
    nc.Publish(m.Reply, []byte("I can help!"))
})

در این نمونه کد یک درخواست مطابق رویه بالا ارسال شده و برای مدت ۱۰۰ میلی‌ثانیه منتظر پاسخ باقی می‌ماند. در این کتابخانه زبان GO اطلاعات reply-to را از دید برنامه‌نویس مخفی کرده است. در این کد همچنان بخش subscribe و unsubscribe کردن از reply-to نیز مخفی شده است و خود کلاینت این عملیات را در پشت صحنه انجام می‌دهد.

برای تست کردن این الگو به آدرس زیر مراجعه کنید :‌
https://github.com/nats-io/go-nats-examples/tree/master/patterns/request-reply

الگو Queue grouping :

نتس (NATS) به صورت built-in از یک توزیع‌کننده بار داخلی به نام distributed queues بهره می‌برد. با استفاده از این نوع از subscriber ها به صورت خودکار توزیع پیام‌ها را بین subscriber های عضو یک گروه مشخص انجام می‌دهد.

برای ساخت یک queue group هریک از کلاینت‌ها باید یک نام‌گروه مشخص را ثبت‌نام کرده و روی یک موضوع عملیات انجام دهند. همه کلاینت‌هایی که نام گروه مشترکی دارندُ با هم تشکیل یک گروه می‌دهند. این کار به جز نام‌گروه همسانُ به هیچ تنظیماتی نیاز ندارد. حال هنگامی که یک پیام برای آن موضوع منتشر می‌شودُ یکی از اعضای گروه به صورت تصادفی انتخاب شده و پیام را دریافت می‌کند. به عبارت دیگر با اینکه همه اعضای گروه در حال گوش کردن به آن موضوع خاص هستند، فقط و فقط یکی از اعضا آن پیام را دریافت یا به عبارتی پردازش می‌کند. این الگو ایده‌آل‌ترین الگو برای scale کردن میکروسرویس‌ها می‌باشد. در این روش scale-up کردن سیستم به سادگی اجرا کردن یک application دیگر و scale-down کردن به سادگی terminate کردن یکی از application ها است.

این انعطاف‌پذیری و حداقل تنظیمات لازم NATS را به یکی از بهترین تکنولوژی‌ها برای ارتباط بین میکروسرویس‌ها تبدیل کرده است.

import nats "github.com/nats-io/nats.go"

// Connect to a server
nc, _ := nats.Connect(nats.DefaultURL)

// Simple Publisher
nc.Publish("foo", []byte("Hello World"))

// All subscriptions with the same queue name will form a queue group.
// Each message will be delivered to only one subscriber per queue group,
// using queuing semantics. You can have as many queue groups as you wish.
// Normal subscribers will continue to work as expected.

nc.QueueSubscribe("foo", "job_workers", func(_ *Msg) {
  received += 1;
})

// Drain connection (Preferred for responders)
// Close() not needed if this is called.
nc.Drain()

// Close connection
nc.Close()
برای تست کردن این الگو به آدرس زیر مراجعه کنید :‌
https://github.com/nats-io/go-nats-examples/tree/master/patterns/competing-consumer

کلام آخر

چیزی که NATS را قدرتمند می‌کند پیچیدگی نیست،‌ بلکه سادگی است.

در واقع استفاده از پروتوکل ساده در زیرساخت، توجه به سادگی و کارایی بالا و قابل اتکا بودن در cloud-native در NATS ما را قادر می‌سازد انواع الگوهای پیام‌رسانی قدرتمند را بدون از دست‌دادن امکانات مهم در کنار بسیاری از امکاناتی بدون استفاده در نرم افزارهای بزرگتر در برنامه ها پیاده‌سازی کنیم.

برای آشنایی بیشتر با نحوه استفاده از NATS در زبان Go می توانید به آدرس زیر مراجعه کنید در این منبع Github که متعلق به NATS میباشد انواع الگوهای استفاده از این Message Broker توضیح داده شده است.

https://github.com/nats-io/go-nats-examples/tree/master/tools

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