Salut% habrauser%!
Aujourd'hui, je veux vous parler d'un excellent, à mon avis, le cadre de microservice
Moleculer .

Initialement, ce cadre a été écrit dans Node.js, mais plus tard, il est apparu sur des ports dans d'autres langages tels que Java, Go, Python et .NET et, très probablement, d'autres implémentations
apparaîtront dans un avenir proche. Nous l'utilisons en production dans plusieurs produits depuis environ un an maintenant et il est difficile de décrire avec des mots à quel point il nous a semblé béni après avoir utilisé Seneca et nos vélos. Nous avons obtenu tout ce dont nous avons besoin: collecte des métriques, mise en cache, équilibrage, tolérance aux pannes, transports sélectionnés, validation des paramètres, journalisation, déclarations de méthode concises, plusieurs façons d'interaction interservices, mixins, et bien plus encore. Et maintenant en ordre.
Présentation
Le cadre se compose en fait de trois composants (en fait, non, mais vous en apprendrez plus à ce sujet ci-dessous).
Transporteur
Responsable de la découverte des services et de la communication entre eux. Il s'agit d'une interface que vous pouvez implémenter vous-même si vous le souhaitez, ou vous pouvez utiliser des implémentations prêtes à l'emploi qui font partie du cadre lui-même. 7 transports sont disponibles dans la boîte: TCP, Redis, AMQP, MQTT, NATS, NATS Streaming, Kafka.
Ici vous pouvez en voir plus. Nous utilisons le transport Redis, mais nous prévoyons de passer à TCP avec sa sortie de l'état expérimental.
En pratique, lors de l'écriture de code, nous n'interagissons pas avec ce composant. Vous avez juste besoin de savoir ce qu'il est. Le transport utilisé est spécifié dans la configuration. Ainsi, pour passer d'un transport à un autre, il suffit de changer la configuration. C’est tout. Quelque chose comme ça:
Par défaut, les données sont au format JSON. Mais vous pouvez utiliser n'importe quoi: Avro, MsgPack, Notepack, ProtoBuf, Thrift, etc.
Le service
La classe dont nous héritons lors de l'écriture de nos microservices.
Voici le service le plus simple sans méthodes, qui sera cependant détecté par d'autres services:
Courtier de service
Le cœur du cadre.

Exagérant, on peut dire qu'il s'agit d'une couche entre transport et service. Lorsqu'un service souhaite interagir avec un autre service d'une manière ou d'une autre, il le fait via un courtier (des exemples seront donnés ci-dessous). Le courtier est engagé dans l'équilibrage de charge (prend en charge plusieurs stratégies, y compris celles personnalisées, par défaut - round-robin), en tenant compte des services en direct, des méthodes disponibles dans ces services, etc. Pour cela, ServiceBroker utilise un autre composant sous le capot - le Registre, mais je ne m'y attarderai pas, nous n'en aurons pas besoin pour faire connaissance.
Avoir un courtier nous donne une chose extrêmement pratique. Maintenant, je vais essayer d'expliquer, mais je vais devoir m'écarter un peu. Dans le contexte du cadre, il existe une chose telle que le nœud. Dans un langage simple, un nœud est un processus dans le système d'exploitation (c'est-à-dire ce qui se passe lorsque nous saisissons «node index.js» dans la console, par exemple). Chaque nœud est un ServiceBroker avec un ensemble d'un ou plusieurs microservices. Oui, vous avez bien entendu. Nous pouvons construire notre pile de services selon nos désirs. Pourquoi est-ce pratique? Pour le développement, nous démarrons un nœud dans lequel tous les microservices sont lancés en même temps (1 chacun), un seul processus dans le système avec la possibilité de connecter très facilement hotreload, par exemple. En production - un nœud distinct pour chaque instance du service. Eh bien, ou un mélange, quand une partie des services dans un nœud, une partie dans un autre, et ainsi de suite (même si je ne sais pas pourquoi le faire, juste pour comprendre que vous pouvez le faire aussi).
Voici à quoi ressemble notre 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(); } });
En l'absence de variable d'environnement, tous les services du répertoire sont chargés, sinon par masque. Soit dit en passant, broker.repl () est une autre fonctionnalité pratique du framework. Lors du démarrage en mode développement, nous avons juste là, dans la console, une interface pour appeler les méthodes (ce que vous feriez, par exemple, via postman dans votre microservice qui communique via http), seulement ici c'est beaucoup plus pratique: l'interface est dans la même console où ils ont commencé npm.
Interaction interservices
Elle s'effectue de trois manières:
appeler
Le plus couramment utilisé. A fait une demande, a reçu une réponse (ou une erreur).
Comme mentionné ci-dessus, les appels sont automatiquement équilibrés. Nous augmentons simplement le nombre requis d'instances de service, et le cadre lui-même fera l'équilibrage.

émettre
Utilisé lorsque nous voulons simplement informer d'autres services d'un événement, mais nous n'avons pas besoin du résultat.
D'autres services peuvent s'abonner à cet événement et répondre en conséquence. Facultativement, le troisième argument, vous pouvez définir explicitement les services disponibles pour recevoir cet événement.
Le point important est que l'événement ne recevra qu'une seule instance de chaque type de service, c'est-à-dire si nous avons 10 services «courrier» et 5 services «abonnement» qui sont abonnés à cet événement, alors en fait seulement 2 exemplaires le recevront - un «courrier» et un «abonnement». Ressemble schématiquement à ceci:

diffuser
Identique à émettre, mais sans restrictions. Les 10 services de courrier et 5 abonnements participeront à cet événement.
Validation des paramètres
Par défaut,
le validateur le plus rapide est utilisé pour valider les paramètres, il semble être très rapide. Mais rien n'empêche d'utiliser un autre, par exemple, le même joi, si vous avez besoin d'une validation plus avancée.
Lorsque nous écrivons un service, nous héritons de la classe de base Service, déclarons des méthodes avec une logique métier, mais ces méthodes sont «privées», elles ne peuvent pas être appelées de l'extérieur (d'un autre service) jusqu'à ce que nous le voulions explicitement, en les déclarant dans section d'actions spéciales lors de l'initialisation du service (les méthodes publiques de services dans le contexte du cadre sont appelées actions).
Exemple d'une déclaration de méthode avec validation 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 }) {
Mixins
Utilisé, par exemple, pour initialiser une connexion à une base de données. Évitez la duplication de code d'un service à l'autre.
Exemple de mixin pour initialiser une connexion à 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(); }, });
Utiliser mixin dans le service 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 })], }); } }
Mise en cache
Les appels de méthode (actions) peuvent être mis en cache de plusieurs manières: LRU, Memory, Redis. Facultativement, vous pouvez spécifier par quels appels de clés seront mis en cache (par défaut, le hachage d'objet est utilisé comme clé de mise en cache) et avec quel TTL.
Exemple de déclaration de méthode mise en cache 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; }, }, }, }); }
La méthode de mise en cache est définie via la configuration ServiceBroker.
Journalisation
Ici, cependant, tout est également assez simple. Il y a un assez bon enregistreur intégré qui écrit sur la console, il est possible de spécifier un formatage personnalisé. Rien n'empêche de voler tout autre enregistreur populaire, que ce soit Winston ou Bunyan. Un manuel détaillé se trouve dans la
documentation . Personnellement, nous utilisons l'enregistreur intégré, le formateur personnalisé pour quelques lignes de code qui spamment dans la console JSON est simplement coupé dans le prod, après quoi ils entrent dans graylog en utilisant le pilote de journal docker.
Mesures
Si vous le souhaitez, vous pouvez collecter des métriques pour chaque méthode et tout suivre dans un zipkin. Voici
la liste complète des exportateurs disponibles. Actuellement, il y en a cinq: Zipkin, Jaeger, Prometheus, Elastic, Console. Il est configuré, comme la mise en cache, lors de la déclaration d'une méthode (action).
Des exemples de visualisation pour le paquet elasticsearch + kibana utilisant le module
élastique-apm-node peuvent être consultés sur
ce lien dans Github.
La manière la plus simple, bien sûr, est d'utiliser l'option console. Cela ressemble à ceci:

Tolérance aux pannes
Le cadre possède un disjoncteur intégré, qui est contrôlé via les paramètres ServiceBroker. Si un service échoue et que le nombre de ces échecs dépasse un certain seuil, il sera marqué comme malsain, ses demandes seront sévèrement limitées jusqu'à ce qu'il cesse de faire des erreurs.
En prime, il existe également une solution de secours réglable individuellement pour chaque méthode (action), au cas où nous supposerions que la méthode pourrait échouer et, par exemple, envoyer des données en cache ou un talon.
Conclusion
L'introduction de ce cadre pour moi est devenue une bouffée d'air frais, ce qui a permis d'économiser une énorme quantité de chutes (à l'exception du fait que l'architecture de microservice est un gros problème) et le cyclisme, a rendu l'écriture du prochain microservice simple et transparente. Il n'y a rien de superflu, il est simple et très flexible, et vous pouvez écrire le premier service dans une heure ou deux après avoir lu la documentation. Je serai heureux si ce matériel vous est utile et dans votre prochain projet vous voudrez essayer ce miracle, comme nous l'avons fait (et nous ne l'avons pas encore regretté). Bon à tous!
De plus, si vous êtes intéressé par ce cadre, rejoignez le chat dans Telegram -
@moleculerchat