تعرّف على إطار Moleculer microservice

مرحبا ٪ habrauser ٪!

اليوم ، أود أن أخبركم عن أحد أطر الخدمة الجزيئية الممتازة ، في رأيي.



في البداية ، تم كتابة هذا الإطار في Node.js ، لكنه ظهر لاحقًا على منافذ بلغات أخرى مثل Java و Go و Python و .NET ، وعلى الأرجح ، ستظهر تطبيقات أخرى في المستقبل القريب. لقد استخدمناها في الإنتاج في العديد من المنتجات لمدة عام تقريبًا ، ومن الصعب أن تصف بالكلمات كم كان يبارك لنا بعد استخدام Seneca ودراجاتنا. لقد حصلنا على كل ما نحتاجه من الصندوق: جمع المقاييس ، والتخزين المؤقت ، والموازنة ، والتسامح مع الأعطال ، وتحديد وسائل النقل ، والتحقق من المعلمة ، والتسجيل ، وإعلانات الطرق المختصرة ، وطرق عديدة للتفاعل بين الخدمات ، والخلطات ، وغير ذلك الكثير. والآن بالترتيب.

مقدمة


يتكون الإطار ، في الواقع ، من ثلاثة مكونات (في الواقع ، لا ، لكنك ستتعلم المزيد عن هذا أدناه).

الناقل


مسؤولة عن اكتشاف الخدمات والتواصل بينهما. هذه هي الواجهة التي ، مع رغبة كبيرة ، يمكنك تنفيذها بنفسك ، أو يمكنك استخدام تطبيقات جاهزة تشكل جزءًا من الإطار نفسه. تتوفر 7 وسائل نقل من الصندوق: TCP و Redis و AMQP و MQTT و NATS و NATS Streaming و Kafka. هنا يمكنك رؤية المزيد. نحن نستخدم النقل Redis ، لكننا نخطط للتبديل إلى TCP مع خروجه من الحالة التجريبية.

في الممارسة العملية ، عند كتابة التعليمات البرمجية ، نحن لا نتفاعل مع هذا المكون. تحتاج فقط إلى معرفة ما هو عليه. يتم تحديد النقل المستخدمة في التكوين. وبالتالي ، للتبديل من نقل إلى آخر ، ما عليك سوى تغيير التكوين. هذا كل شيء. شيء مثل هذا:

// ./moleculer.config.js module.exports = { transporter: 'redis://:pa$$w0rd@127.0.0.1:6379', // ...   } 

البيانات ، بشكل افتراضي ، تأتي في تنسيق JSON. ولكن يمكنك استخدام أي شيء: Avro و MsgPack و Notepack و ProtoBuf و Thrift ، إلخ.

الخدمة


الفئة التي نرثها عند كتابة خدماتنا الدقيقة.

هذه أبسط خدمة بدون طرق ، ومع ذلك ، سيتم اكتشافها بواسطة خدمات أخرى:

 // ./services/telemetry/telemetry.service.js const { Service } = require('moleculer'); module.exports = class TelemetryService extends Service { constructor(broker) { super(broker); this.parseServiceSchema({ name: 'telemetry', }); } }; 


وسيط الخدمة


جوهر الإطار.



المبالغة ، يمكننا القول أن هذه طبقة بين النقل والخدمة. عندما تريد إحدى الخدمات التفاعل مع خدمة أخرى بطريقة ما ، فإنها تفعل ذلك من خلال وسيط (سيتم تقديم أمثلة أدناه). يشارك الوسيط في موازنة التحميل (يدعم العديد من الاستراتيجيات ، بما في ذلك الاستراتيجيات المخصصة ، افتراضيًا - round-robin) ، مع مراعاة الخدمات المباشرة والطرق المتاحة في هذه الخدمات ، إلخ. لهذا ، يستخدم ServiceBroker مكونًا آخر تحت غطاء المحرك - السجل ، لكنني لن أتطرق إليه ، ولن نحتاج إليه للتعارف.

وجود وسيط يعطينا شيء مريح للغاية. الآن سأحاول شرح ذلك ، لكن سيتعين علي التنحي قليلاً. في سياق الإطار ، هناك شيء اسمه العقدة. بعبارات بسيطة ، العقدة هي عملية في نظام التشغيل (أي ، ما يحدث عندما ندخل "index index.js" في وحدة التحكم ، على سبيل المثال). كل عقدة هي ServiceBroker مع مجموعة من واحد أو أكثر من microservices. نعم سمعت صحيح. يمكننا أن نبني كومة خدمتنا حسب رغباتنا. لماذا هذا مريح؟ من أجل التطوير ، نبدأ عقدة واحدة يتم فيها تشغيل جميع الخدمات الميكروية في وقت واحد (1 لكل منهما) ، وهي عملية واحدة فقط في النظام مع القدرة على توصيل hotreload بسهولة ، على سبيل المثال. في الإنتاج - عقدة منفصلة لكل مثيل من الخدمة. حسنًا ، أو مزيج ، عندما يكون جزء من الخدمات في عقدة ما ، جزءًا في أخرى ، وما إلى ذلك (على الرغم من أنني لا أعرف لماذا أفعل ذلك ، فقط لفهم أنه يمكنك القيام بذلك أيضًا).

هذا ما يبدو عليه index.js
 const { resolve } = require('path'); const { ServiceBroker } = require('moleculer'); const config = require('./moleculer.config.js'); const { SERVICES, NODE_ENV, } = process.env; const broker = new ServiceBroker(config); broker.loadServices( resolve(__dirname, 'services'), SERVICES ? `*/@(${SERVICES.split(',').map(i => i.trim()).join('|')}).service.js` : '*/*.service.js', ); broker.start().then(() => { if (NODE_ENV === 'development') { broker.repl(); } }); 


في حالة عدم وجود متغير بيئة ، يتم تحميل جميع الخدمات من الدليل ، وإلا عن طريق القناع. بالمناسبة ، broker.repl () هو ميزة أخرى ملائمة للإطار. عند البدء في وضع التطوير ، هناك في وحدة التحكم ، هناك واجهة للاتصال بأساليب الاتصال (ما ستفعله ، على سبيل المثال ، من خلال ساعي البريد في الخدمة الصغيرة التي تتصل عبر http) ، هنا فقط أكثر ملاءمة: الواجهة في وحدة التحكم نفسها حيث فعلوا بداية npm.

التفاعل بين الخدمات


يتم تنفيذها بثلاث طرق:

اتصل


الأكثر استخداما. قدم طلبًا ، وتلقى ردًا (أو خطأ).

 //   "report",     "csv". async getCsvReport({ jobId }) { const rows = []; // ... return this.broker.call('csv.stringify', { rows }); } 

كما ذكر أعلاه ، يتم إجراء مكالمات متوازنة تلقائيًا. نحن فقط نرفع العدد المطلوب من مثيلات الخدمة ، وسيعمل الإطار نفسه على تحقيق التوازن.



ينبعث منها


يتم استخدامها عندما نريد فقط إخطار خدمات أخرى حول حدث ما ، لكننا لسنا بحاجة إلى النتيجة.

 //   "user"    . async registerUser({ email, password }) { // ... this.broker.emit('user_registered', { email }); return true; } 

قد تشترك خدمات أخرى في هذا الحدث ، وتستجيب وفقًا لذلك. اختياريًا ، الوسيطة الثالثة ، يمكنك تعيين الخدمات المتاحة لتلقي هذا الحدث بشكل صريح.

النقطة المهمة هي أن الحدث سيتلقى مثيلًا واحدًا فقط من كل نوع من أنواع الخدمة ، أي إذا كان لدينا 10 خدمات "بريد" و 5 "اشتراك" مشتركة في هذا الحدث ، فحينئذٍ فقط ستحصل عليه نسختان - "بريد" واحد و "اشتراك" واحد. بشكل تخطيطي يبدو مثل هذا:



البث


نفس ينبعث منها ، ولكن دون قيود. جميع خدمات البريد 10 و 5 الاشتراكات ستجذب هذا الحدث.

التحقق من صحة المعلمات


افتراضيًا ، يتم استخدام أداة التحقق من الصحة الأسرع للتحقق من صحة المعلمات ، ويبدو أنها سريعة جدًا. لكن لا شيء يمنع استخدام أي شيء آخر ، على سبيل المثال ، نفس joi ، إذا كنت بحاجة إلى تحقق أكثر تقدماً.

عندما نكتب خدمة ، فإننا نرث من خدمة الفئة الأساسية ، ونعلن عن أساليب بها منطق أعمال ، لكن هذه الطرق "خاصة" ، لا يمكن استدعاؤها من الخارج (من خدمة أخرى) حتى نرغب في ذلك صراحةً ، مع الإعلان عنها في قسم الإجراءات الخاصة أثناء تهيئة الخدمة (تسمى الأساليب العامة للخدمات في سياق الإطار إجراءات).

مثال لإعلان الطريقة مع التحقق من الصحة
 module.exports = class JobService extends Service { constructor(broker) { super(broker); this.parseServiceSchema({ name: 'job', actions: { update: { params: { id: { type: 'number', convert: true }, name: { type: 'string', empty: false, optional: true }, data: { type: 'object', optional: true }, }, async handler(ctx) { return this.update(ctx.params); }, }, }, }); } async update({ id, name, data }) { // ... } } 


يمزج


يستخدم ، على سبيل المثال ، لتهيئة اتصال قاعدة البيانات. تجنب الازدواجية في الكود من الخدمة إلى الخدمة.

مثال mixin لتهيئة اتصال إلى Redis
 const Redis = require('ioredis'); module.exports = ({ key = 'redis', options } = {}) => ({ settings: { [key]: options, }, created() { this[key] = new Redis(this.settings[key]); }, async started() { await this[key].connect(); }, stopped() { this[key].disconnect(); }, }); 


باستخدام mixin في الخدمة
 const { Service, Errors } = require('moleculer'); const redis = require('../../mixins/redis'); const server = require('../../mixins/server'); const router = require('./router'); const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, } = process.env; const redisOpts = { host: REDIS_HOST, port: REDIS_PORT, password: REDIS_PASSWORD, lazyConnect: true, }; module.exports = class AuthService extends Service { constructor(broker) { super(broker); this.parseServiceSchema({ name: 'auth', mixins: [redis({ options: redisOpts }), server({ router })], }); } } 


التخزين المؤقت


يمكن أن يتم مؤقتا استدعاءات الطريقة (الإجراءات) في عدة طرق: LRU ، الذاكرة ، Redis. اختياريًا ، يمكنك تحديد أي مكالمات تخزين مؤقت للمكالمات (بشكل افتراضي ، يتم استخدام تجزئة الكائن كمفتاح تخزين مؤقت) وأي TTL.

مثال لإعلان الطريقة المخبأة
 module.exports = class InventoryService extends Service { constructor(broker) { super(broker); this.parseServiceSchema({ name: 'inventory', actions: { getInventory: { params: { steamId: { type: 'string', pattern: /^76\d{15}$/ }, appId: { type: 'number', integer: true }, contextId: { type: 'number', integer: true }, }, cache: { keys: ['steamId', 'appId', 'contextId'], ttl: 15, }, async handler(ctx) { return true; }, }, }, }); } // ... } 


يتم تعيين طريقة التخزين المؤقت من خلال تكوين ServiceBroker.

تسجيل


هنا ، ومع ذلك ، كل شيء بسيط جدا أيضا. هناك مسجل مدمج جيد جدًا يكتب إلى وحدة التحكم ، من الممكن تحديد التنسيق المخصص. لا شيء يمنع سرقة أي مسجل شعبي آخر ، سواء كان winston أو bunyan. دليل مفصل في الوثائق . شخصياً ، نستخدم المسجل المدمج ، والمنسق المخصص لبضعة أسطر من التعليمات البرمجية التي ترسل بريدًا عشوائيًا إلى وحدة تحكم JSON يتم تقطيعه ببساطة في prod ، وبعد ذلك يندمجون في graylog باستخدام برنامج تشغيل سجل docker.

المقاييس


إذا كنت ترغب في ذلك ، يمكنك جمع المقاييس لكل طريقة وتتبعها في بعض zipkin. هنا قائمة كاملة من المصدرين المتاحة. يوجد حاليًا خمسة منهم: Zipkin و Jaeger و Prometheus و Elastic و Console. يتم تكوينه ، مثل التخزين المؤقت ، عند إعلان الطريقة (الإجراء).

يمكن الاطلاع على أمثلة التصور لحزمة elasticsearch + kibana باستخدام وحدة العقدة المرنة apm على هذا الرابط في جيثب.

أسهل طريقة ، بالطبع ، هي استخدام خيار وحدة التحكم. يبدو مثل هذا:



خطأ التسامح


يحتوي الإطار على قاطع دائرة مدمج ، يتم التحكم فيه من خلال إعدادات ServiceBroker. إذا فشلت أي خدمة وكان عدد هذه الإخفاقات يتجاوز عتبة معينة ، فسيتم تعليمها على أنها غير صحية ، وستكون طلباتها محدودة للغاية حتى تتوقف عن ارتكاب الأخطاء.

على سبيل المكافأة ، هناك أيضًا احتياطي قابل للتعديل بشكل فردي لكل طريقة (إجراء) ، في حال افترضنا أن الطريقة قد تفشل ، على سبيل المثال ، إرسال بيانات مخزنة مؤقتًا أو كعب روتين.

الخاتمة


أصبح إدخال هذا الإطار بالنسبة لي بمثابة هواء منعش ، الأمر الذي وفر كمية هائلة من الهراوات (باستثناء حقيقة أن بنية الخدمة المجهرية هي لعبة كبيرة) وركوب الدراجات ، مما جعل كتابة الخدمة المجهرية التالية بسيطة وشفافة. لا يوجد شيء لا لزوم له فيه ، فهو بسيط ومرن للغاية ، ويمكنك كتابة الخدمة الأولى في غضون ساعة أو ساعتين بعد قراءة الوثائق. سأكون سعيدًا إذا كانت هذه المادة مفيدة لك وفي مشروعك التالي سوف ترغب في تجربة هذه المعجزة ، كما فعلنا (ولم نندم عليها بعد). جيد للجميع!

أيضًا ، إذا كنت مهتمًا بهذا الإطار ، فعليك الانضمام إلى الدردشة في Telegram -moleculerchat

Source: https://habr.com/ru/post/ar439810/


All Articles