Les logiciels ont rarement, voire jamais, un vide informationnel. C'est du moins l'hypothèse que les ingénieurs logiciels peuvent faire pour la plupart des applications que nous développons.
A n'importe quelle échelle, chaque logiciel - d'une manière ou d'une autre - communique avec d'autres logiciels pour diverses raisons: pour obtenir des données de référence de quelque part, pour envoyer des signaux de surveillance, pour rester en contact avec d'autres services, tout en faisant partie d'un système distribué et plus.
Dans ce didacticiel, vous découvrirez certains des plus grands défis liés à l'intégration de gros logiciels et comment Apache Camel les résout facilement.
Vous avez peut-être effectué les opérations suivantes au moins une fois dans votre vie d'ingénieur logiciel:
Pourquoi est-ce un mauvais plan d'action?
Bien que vous n'ayez que quelques connexions de ce type, cela reste gérable. Avec un nombre croissant de relations entre systèmes, la logique métier de l'application se mêle à la logique d'intégration qui tente d'adapter les données, de compenser les différences technologiques entre deux systèmes et de transférer des données vers le système externe avec des requêtes SOAP, REST ou plus exotiques.
Si vous intégriez plusieurs applications, il serait extrêmement difficile de redessiner l'image complète des dépendances dans un tel code: où les données sont-elles produites et quels services les consomment? Vous aurez de nombreux endroits où la logique d'intégration est dupliquée pour démarrer.
Avec cette approche, même si la tâche est techniquement terminée, nous nous retrouvons avec de gros problèmes de maintenabilité et d'évolutivité de l'intégration. Une réorganisation rapide des flux de données dans ce système est presque impossible, sans parler de problèmes plus profonds tels que le manque de surveillance, l'interruption de circuit, la recherche laborieuse de données, etc.
Ceci est particulièrement important lors de l'intégration de logiciels dans le cadre d'une entreprise de taille considérable. S'attaquer à l'intégration commerciale signifie travailler avec un ensemble d'applications qui fonctionnent sur un large éventail de plates-formes et sont situées à différents endroits. L'échange de données dans un tel environnement logiciel est assez exigeant. Il doit répondre aux normes de sécurité élevées de l'industrie et fournir un moyen fiable de transférer des données. Dans un environnement d'entreprise, l'intégration de systèmes nécessite une conception d'architecture distincte et entièrement élaborée.
Cet article vous présentera les difficultés uniques rencontrées par l'intégration de logiciels et fournira des solutions basées sur l'expérience pour les tâches d'intégration. Nous ferons connaissance avec Chameau Apache , un cadre utile qui peut atténuer les pires aspects des maux de tête d'un développeur d'intégration. Nous continuerons avec un exemple de la façon dont Camel peut aider à établir la communication dans un cluster de microservices alimenté par Kubernetes.
Une approche largement utilisée pour résoudre le problème consiste à découpler une couche d'intégration dans votre application. Il peut exister dans l'application elle-même ou en tant que logiciel dédié qui s'exécute indépendamment - dans ce dernier cas, il est appelé middleware .
Quels problèmes rencontrez-vous normalement lors du développement et du soutien middleware ? En général, il comporte les éléments clés suivants:
Tous les canaux de données ne sont pas fiables dans une certaine mesure. Les problèmes découlant de ce manque de fiabilité peuvent ne pas se produire tant que l'intensité des données est faible ou modérée. Chaque niveau de stockage de la mémoire d'application aux caches inférieurs et l'équipement ci-dessous est sujet à une panne potentielle. Certaines erreurs rares surviennent uniquement avec de gros volumes de données. Même les produits matures de fournisseurs prêts pour la production ont des problèmes de suivi des bogues non résolus liés à la perte de données. Un système de middleware Il devrait être en mesure de vous informer sur ces données victimes et de transmettre le message de provisionnement en temps opportun.
Les applications utilisent différents protocoles et formats de données. Cela signifie qu'un système d'intégration est un rideau pour les transformations de données et les adaptateurs pour les autres participants et utilise une variété de technologies. Celles-ci peuvent inclure de simples appels d'API REST, mais peuvent également accéder à un courtier de files d'attente, envoyer des commandes CSV via FTP ou extraire des données par lots vers une table de base de données. C'est une longue liste et elle ne sera jamais raccourcie.
Les modifications des formats de données et des règles de routage sont inévitables. Chaque étape du processus de développement d'une application, qui modifie la structure des données, entraîne généralement des changements dans les formats de données d'intégration et des transformations. Parfois, des changements d'infrastructure avec des flux de données d'entreprise réorganisés sont nécessaires. Par exemple, ces modifications peuvent se produire lorsqu'un seul point de validation des données de référence est entré et doit traiter toutes les entrées de données de base dans toute l'entreprise. Avec les systèmes N
, on peut finir par avoir un maximum de presque connexions N^2
entre eux, de sorte que le nombre d'endroits où des changements doivent être appliqués augmente assez rapidement. Ce sera comme une avalanche. Pour maintenir la maintenabilité, une couche de middleware vous devez fournir une image claire des dépendances avec un routage polyvalent et une transformation des données.
Ces idées doivent être prises en compte lors de la conception de l'intégration et du choix de la solution pour middleware plus approprié. L'un des moyens possibles de le gérer est de profiter d'un bus de service d'affaires ( ESB ). Mais le ESB fournis par les principaux fournisseurs sont généralement trop lourds et souvent très gênants - il est presque impossible de démarrer rapidement avec un ESB , il a une courbe d'apprentissage assez raide et sa flexibilité est sacrifiée à une longue liste de fonctionnalités et d'outils intégrés. À mon avis, les solutions d'intégration open source légères sont bien supérieures: elles sont plus élastiques, faciles à déployer dans le cloud et à évoluer.
L'intégration logicielle n'est pas facile à faire. Aujourd'hui, alors que nous construisons des architectures de microservices et traitons des nuées de petits services, nous attendons également beaucoup de leur efficacité à communiquer.
Comme vous vous en doutez, comme le développement logiciel en général, la transformation des données et le développement du routage impliquent des opérations répétitives. L'expérience dans ce domaine a été résumée et systématisée par des professionnels confrontés à des problèmes d'intégration depuis un certain temps. Dans la sortie, il y a un ensemble de modèles extraits appelés modèles d'intégration commerciale utilisé pour concevoir des flux de données. Ces méthodes d'intégration ont été décrites dans le livre du même nom de Gregor Hophe et Bobby Wolfe, qui est très similaire à l'important livre Gang of Four, mais dans le domaine des logiciels de collage.
Pour donner un exemple, le modèle de normalisation introduit un composant qui mappe sémantiquement les mêmes messages qui ont des formats de données différents à un seul modèle canonique, ou l'agrégateur est un EIP qui combine une séquence de messages en un seul.
Puisqu'il s'agit d'abstractions technologiquement indépendantes utilisées pour résoudre des problèmes architecturaux, les EIP aident à écrire une conception d'architecture qui ne va pas en profondeur au niveau du code, mais décrit plutôt les flux de données avec suffisamment de détails. Une telle notation pour décrire les chemins d'intégration rend non seulement la conception concise, mais établit également une nomenclature commune et un langage commun, qui sont très importants dans le contexte de la résolution d'une tâche d'intégration avec des membres de l'équipe de divers domaines commerciaux
Il y a plusieurs années, je construisais une intégration d'entreprise dans un grand réseau de vente au détail d'épicerie avec des magasins dans des endroits largement distribués. J'ai commencé avec une solution propriétaire de ESB qui s'est avéré trop lourd à entretenir. Notre équipe est donc tombée sur Apache Camel et après avoir effectué un travail de «preuve de concept», nous avons rapidement réécrit tous nos flux de données sur les routes Camel.
Chameau Apache peut être décrit comme un 'routeur de médiation', un cadre de middleware orienté message qui implémente la liste EIP , avec lequel je suis devenu familier. Il utilise ces modèles, prend en charge tous les protocoles de transport courants et comprend un ensemble complet d'adaptateurs utiles. Camel permet de gérer une série de routines d'intégration sans avoir besoin d'écrire votre propre code.
En dehors de cela, je voudrais souligner les fonctionnalités suivantes d'Apache Camel:
Les routes Apache Camel peuvent être écrites en Java ou en DSL Scala. (Une configuration XML est également disponible mais elle devient trop verbeuse et a de pires capacités de débogage.) Il n'impose pas de restrictions sur la pile technologique des services de communication, mais si vous écrivez en Java ou Scala, vous pouvez intégrer Camel dans une application au lieu de l'exécuter indépendamment.
La notation de routage utilisée par Camel peut être décrite avec le pseudo-code simple suivant:
from(Source) .transform(Transformer) .to(Destination)
Les Fuente
, Transformador
et Destino
sont les points de terminaison qui font référence aux composants d'implémentation par leurs URI.
Qu'est-ce qui permet à Camel de résoudre les problèmes d'intégration que j'ai décrits ci-dessus? Nous allons jeter un coup d'oeil. Tout d'abord, la logique de routage et de transformation ne se trouve désormais que dans une configuration Apache Camel dédiée. Deuxièmement, grâce au DSL concis et naturel associé à l'utilisation d'EIP, une image des dépendances entre les systèmes apparaît. Il est composé d'abstractions compréhensibles et la logique de routage est facilement réglable. Et enfin, nous n'avons pas besoin d'écrire beaucoup de code de transformation car les adaptateurs appropriés sont probablement déjà inclus.
Je dois ajouter qu'Apache Camel est un framework mature et reçoit des mises à jour régulières. Il a une grande communauté et une base de connaissances accumulée considérable.
Il a ses propres inconvénients. Camel ne doit pas être considéré comme une suite d'intégration complexe. Il s'agit d'une boîte à outils sans fonctionnalités de haut niveau telles que des outils de gestion des processus métier ou des moniteurs d'activité, mais elle peut être utilisée pour créer de tels logiciels.
Les systèmes alternatifs peuvent être, par exemple, Spring Integration ou Mule ESB . Pour Spring Integration, bien qu'il soit considéré comme léger, d'après mon expérience, l'assemblage et l'écriture de nombreux fichiers de configuration XML peuvent être étonnamment compliqués et pas une solution facile. Mule ESB Il s'agit d'un ensemble d'outils robustes et hautement fonctionnels, mais comme son nom l'indique, il s'agit d'un bus de services d'entreprise, il appartient donc à une classe de poids différente. Mule peut être comparé à Fuse ESB , un produit similaire basé sur Apache Camel avec un riche ensemble de fonctionnalités. Pour moi, utiliser Apache Camel pour coller des services de nos jours est une tâche évidente. Il est facile à utiliser et produit une description claire de ce qui se passe, en même temps, il est suffisamment fonctionnel pour construire des intégrations complexes.
Commençons à écrire le code. Nous allons commencer par un flux de données synchrone qui achemine les messages d'une source unique vers une liste de destinataires. Les règles de routage seront écrites dans Java DSL .
Nous utiliserons Maven pour construire le projet. Ajoutez d'abord la dépendance suivante au pom.xml
:
... org.apache.camel camel-core 2.20.0
Alternativement, l'application peut être construite au-dessus de l'archétype camel-archetype-java
.
Les définitions de route de chameau sont déclarées dans la méthode RouteBuilder.configure
.
public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); from('file:orders?noop=true').routeId('main') .log('Incoming File: ${file:onlyname}') .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List .split().simple('body.items') // split list to process one by one .to('log:inputOrderItem') .choice() .when().simple('${body.type} == 'Drink'') .to('direct:bar') .when().simple('${body.type} == 'Dessert'') .to('direct:dessertStation') .when().simple('${body.type} == 'Hot Meal'') .to('direct:hotMealStation') .when().simple('${body.type} == 'Cold Meal'') .to('direct:coldMealStation') .otherwise() .to('direct:others'); from('direct:bar').routeId('bar').log('Handling Drink'); from('direct:dessertStation').routeId('dessertStation').log('Handling Dessert'); from('direct:hotMealStation').routeId('hotMealStation').log('Handling Hot Meal'); from('direct:coldMealStation').routeId('coldMealStation').log('Handling Cold Meal'); from('direct:others').routeId('others').log('Handling Something Other'); }
Dans cette définition, nous créons une route qui récupère les enregistrements du fichier JSON, les divise en éléments et les achemine vers un ensemble de contrôleurs en fonction du contenu du message.
Exécutons-le sur les données de test préparées. Nous obtiendrons la sortie:
INFO | Total 6 routes, of which 6 are started INFO | Apache Camel 2.20.0 (CamelContext: camel-1) started in 10.716 seconds INFO | Incoming File: order1.json INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{id='1', type='Drink', name='Americano', qty='1'}] INFO | Handling Drink INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{id='2', type='Hot Meal', name='French Omelette', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{id='3', type='Hot Meal', name='Lasagna', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{id='4', type='Hot Meal', name='Rice Balls', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{id='5', type='Dessert', name='Blueberry Pie', qty='1'}] INFO | Handling Dessert
Comme prévu, Camel a envoyé des messages aux destinations.
Dans l'exemple précédent, l'interaction entre les composants est synchrone et s'effectue via la mémoire de l'application. Cependant, il existe de nombreuses autres façons de communiquer lorsque nous gérons des applications distinctes qui ne partagent pas de mémoire:
Il existe d'autres façons d'interagir, mais nous devons garder à l'esprit que, de manière générale, il existe deux types d'interaction: synchrone et asynchrone. Le premier est comme appeler une fonction dans votre code - le flux d'exécution attendra jusqu'à ce qu'il s'exécute et renvoie une valeur. Avec une approche asynchrone, les mêmes données sont envoyées via une file d'attente de messages intermédiaire ou une rubrique d'abonnement. Vous pouvez implémenter un appel de fonction à distance asynchrone comme Demande-réponse EIP .
Cependant, la messagerie asynchrone n'est pas un remède; implique certaines restrictions. Vous voyez rarement des API de messagerie sur le Web; Les services REST synchrones sont beaucoup plus populaires. Mais le middleware La messagerie est largement utilisée dans l'intranet d'entreprise ou l'infrastructure backend de système distribué.
Faisons notre exemple asynchrone. Un système logiciel qui gère les files d'attente et les sujets d'abonnement est appelé agent de messagerie . C'est comme un Système de gestion de base de données associé ( SGBDR ) pour les tableaux et les colonnes. Les files d'attente fonctionnent comme une intégration peer-to-peer, tandis que les rubriques sont destinées à la communication par publication-abonnement avec de nombreux destinataires. Nous utiliserons Apache ActiveMQ en tant que courtier de messages JMS car il est robuste et intégrable.
Ajoutez la dépendance suivante. Parfois, il est excessif d'ajouter activemq-all
, qui contient tous les fichiers jar ActiveMQ, au projet, mais nous garderons nos dépendances d'application sans complications.
org.apache.activemq activemq-all 5.15.2
Ensuite, démarrez le courtier par programme. Dans Spring Boot, nous avons une configuration automatique pour cela lors de la connexion de la dépendance Maven. spring-boot-starter-activemq
.
Exécutez un nouveau courtier de messages avec les commandes suivantes, en spécifiant uniquement le point de terminaison du connecteur:
BrokerService broker = new BrokerService(); broker.addConnector('tcp://localhost:61616'); broker.start();
Et ajoutez l'extrait de configuration suivant au corps de la méthode configure
:
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory('tcp://localhost:61616'); this.getContext().addComponent('activemq', ActiveMQComponent.jmsComponent(connectionFactory));
Nous pouvons maintenant mettre à jour l'exemple précédent en utilisant des files d'attente de messages. Les files d'attente seront créées automatiquement lors de la remise du message.
public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); ConnectionFactory connectionFactory = new ActiveMQConnectionFactory('tcp://localhost:61616'); this.getContext().addComponent('activemq', ActiveMQComponent.jmsComponent(connectionFactory)); from('file:orders?noop=true').routeId('main') .log('Incoming File: ${file:onlyname}') .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List .split().simple('body.items') // split list to process one by one .to('log:inputOrderItem') .choice() .when().simple('${body.type} == 'Drink'') .to('activemq:queue:bar') .when().simple('${body.type} == 'Dessert'') .to('activemq:queue:dessertStation') .when().simple('${body.type} == 'Hot Meal'') .to('activemq:queue:hotMealStation') .when().simple('${body.type} == 'Cold Meal'') .to('activemq:queue:coldMealStation') .otherwise() .to('activemq:queue:others'); from('activemq:queue:bar').routeId('barAsync').log('Drinks'); from('activemq:queue:dessertStation').routeId('dessertAsync').log('Dessert'); from('activemq:queue:hotMealStation').routeId('hotMealAsync').log('Hot Meals'); from('activemq:queue:coldMealStation').routeId('coldMealAsync').log('Cold Meals'); from('activemq:queue:others').routeId('othersAsync').log('Others'); }
D'accord, maintenant l'interaction est devenue asynchrone. Les consommateurs potentiels de ces informations peuvent y accéder lorsqu'ils sont prêts à le faire. Ceci est un exemple de couplage lâche que nous essayons de réaliser dans une architecture réactive. L'indisponibilité de l'un des services ne bloquera pas les autres. En outre, un consommateur peut mettre à l'échelle et lire à partir de la file d'attente en parallèle. La file d'attente elle-même peut évoluer et être partitionnée. Les files d'attente persistantes peuvent stocker des données sur le disque, en attente de traitement, même lorsque tous les participants se sont écrasés. Par conséquent, ce système est plus tolérant aux pannes.
Un fait surprenant est que Le CERN utilise Apache Camel et ActiveMQ pour surveiller les systèmes du Grand collisionneur de hadrons ( LHC ) . Il y a aussi une thèse de une expertise intéressante qui explique le choix d'une solution de middleware approprié pour cette tâche . Donc, comme on dit dans la partie clé, 'Sans JMS - pas de physique des particules!'
Dans l'exemple précédent, nous créons le canal de données entre deux services. C'est un point de défaillance potentiel supplémentaire dans une architecture, nous devons donc en prendre soin. Jetons un coup d'œil aux fonctionnalités de surveillance qu'offre Apache Camel. Fondamentalement, il expose des informations statistiques sur vos itinéraires à travers les MBeans auxquels JMX peut accéder. ActiveMQ expose les statistiques de la file d'attente de la même manière.
Nous allons activer le serveur JMX dans l'application pour lui permettre de fonctionner avec les options de ligne de commande:
-Dorg.apache.camel.jmx.createRmiConnector=true -Dorg.apache.camel.jmx.mbeanObjectDomainName=org.apache.camel -Dorg.apache.camel.jmx.rmiConnector.registryPort=1099 -Dorg.apache.camel.jmx.serviceUrlPath=camel
Maintenant, exécutez l'application pour que le chemin ait fait son travail. Ouvre l'outil standard jconsole
et connectez-vous au processus de candidature. Connectez-vous à l'URL service:jmx:rmi:///jndi/rmi://localhost:1099/camel
. Accédez au domaine org.apache.camel dans l'arborescence MBeans.
Nous pouvons voir que tout ce qui concerne le routage est sous contrôle. Nous avons le nombre de messages en vol, le nombre d'erreurs et le nombre de messages dans les files d'attente. Ces informations peuvent être canalisées vers un ensemble d'outils de surveillance hautement fonctionnels tels que Graphana ou Kibana. Vous pouvez le faire en implémentant la célèbre pile ELK.
Il existe également une console Web enfichable et extensible qui fournit une interface utilisateur pour la gestion de Camel, ActiveMQ et bien d'autres, appelée hawt.io .
Apache Camel a des fonctionnalités assez étendues pour écrire des chemins de test avec des composants simulés . C'est un outil puissant, mais l'écriture de chemins séparés juste pour les tests est un processus qui prend du temps. Il serait plus efficace d'exécuter des tests sur les routes de production sans modifier votre pipeline. Camel a cette fonctionnalité et peut être implémentée à l'aide du composant ConseilsAvec .
Nous allons activer la logique de test dans notre exemple et exécuter un exemple de test.
junit junit 4.11 test org.apache.camel camel-test 2.20.0 test
La classe de test est:
public class AsyncRouteTest extends CamelTestSupport { @Override protected RouteBuilder createRouteBuilder() throws Exception { return new AsyncRouteBuilder(); } @Before public void mockEndpoints() throws Exception { context.getRouteDefinition('main').adviceWith(context, new AdviceWithRouteBuilder() { @Override public void configure() throws Exception { // we substitute all actual queues with mock endpoints mockEndpointsAndSkip('activemq:queue:bar'); mockEndpointsAndSkip('activemq:queue:dessertStation'); mockEndpointsAndSkip('activemq:queue:hotMealStation'); mockEndpointsAndSkip('activemq:queue:coldMealStation'); mockEndpointsAndSkip('activemq:queue:others'); // and replace the route's source with test endpoint replaceFromWith('file://testInbox'); } }); } @Test public void testSyncInteraction() throws InterruptedException { String testJson = '{'id': 1, 'order': [{'id': 1, 'name': 'Americano', 'type': 'Drink', 'qty': '1'}, {'id': 2, 'name': 'French Omelette', 'type': 'Hot Meal', 'qty': '1'}, {'id': 3, 'name': 'Lasagna', 'type': 'Hot Meal', 'qty': '1'}, {'id': 4, 'name': 'Rice Balls', 'type': 'Hot Meal', 'qty': '1'}, {'id': 5, 'name': 'Blueberry Pie', 'type': 'Dessert', 'qty': '1'}]}'; // get mocked endpoint and set an expectation MockEndpoint mockEndpoint = getMockEndpoint('mock:activemq:queue:hotMealStation'); mockEndpoint.expectedMessageCount(3); // simulate putting file in the inbox folder template.sendBodyAndHeader('file://testInbox', testJson, Exchange.FILE_NAME, 'test.json'); //checks that expectations were met assertMockEndpointsSatisfied(); } }
Exécutez maintenant des tests pour l'application avec mvn test
. Nous pouvons voir que notre route a été exécutée avec succès avec les conseils de test. Aucun message n'est passé dans les files d'attente réelles et les tests ont réussi.
INFO | Route: main started and consuming from: file://testInbox INFO | Incoming File: test.json INFO | Asserting: mock://activemq:queue:hotMealStation is satisfied
L'un des problèmes d'intégration aujourd'hui est que les applications ne sont plus statiques. Dans une infrastructure cloud, nous travaillons avec des services virtuels qui s'exécutent sur plusieurs nœuds en même temps. Il permet l'architecture de microservices avec un réseau de petits services légers qui interagissent les uns avec les autres. Ces services ont une vie peu fiable et nous devons les découvrir de manière dynamique.
Coller des services cloud ensemble est une tâche qui peut être résolue avec Apache Camel. C'est particulièrement intéressant pour la saveur EIP et le fait que Camel dispose de nombreux adaptateurs et prend en charge une large gamme de protocoles. La version récente 2.18 ajoute le composant Appel de service , qui introduit une fonctionnalité d'appel d'une API et de résolution de son adresse via des mécanismes de découverte de cluster. Actuellement, il prend en charge Consul, Kubernetes, Ribbon, etc. Certains exemples de code, où ServiceCall est configuré avec Consul, peuvent être trouvés facilement. Nous utiliserons Kubernetes ici car c'est ma solution de cluster préférée.
Le schéma d'intégration sera le suivant:
Le service Order
et le service Inventory
sera quelques applications triviales Botte de printemps renvoyer des données statiques. Nous ne sommes pas liés à une pile technologique particulière ici. Ces services produisent les données que nous voulons traiter.
Contrôleur de service de commande:
@RestController public class OrderController { private final OrderStorage orderStorage; @Autowired public OrderController(OrderStorage orderStorage) { this.orderStorage = orderStorage; } @RequestMapping('/info') public String info() { return 'Order Service UUID = ' + OrderApplication.serviceID; } @RequestMapping('/orders') public List getAll() { return orderStorage.getAll(); } @RequestMapping('/orders/{id}') public Order getOne(@PathVariable Integer id) { return orderStorage.getOne(id); } }
Produit des données au format:
[{'id':1,'items':[2,3,4]},{'id':2,'items':[5,3]}]
Le contrôleur de service Inventory
est absolument similaire au service Order
:
@RestController public class InventoryController { private final InventoryStorage inventoryStorage; @Autowired public InventoryController(InventoryStorage inventoryStorage) { this.inventoryStorage = inventoryStorage; } @RequestMapping('/info') public String info() { return 'Inventory Service UUID = ' + InventoryApplication.serviceID; } @RequestMapping('/items') public List getAll() { return inventoryStorage.getAll(); } @RequestMapping('/items/{id}') public InventoryItem getOne(@PathVariable Integer id) { return inventoryStorage.getOne(id); } }
InventoryStorage
c'est un référentiel générique qui contient des données. Dans cet exemple, il renvoie des objets prédéfinis statiques, qui sont classés selon le format suivant.
[{'id':1,'name':'Laptop','description':'Up to 12-hours battery life','price':499.9},{'id':2,'name':'Monitor','description':'27-inch, response time: 7ms','price':200.0},{'id':3,'name':'Headphones','description':'Soft leather ear-cups','price':29.9},{'id':4,'name':'Mouse','description':'Designed for comfort and portability','price':19.0},{'id':5,'name':'Keyboard','description':'Layout: US','price':10.5}]
Nous allons écrire un chemin de passerelle qui les connecte, mais sans ServiceCall dans cette étape:
rest('/orders') .get('/').description('Get all orders with details').outType(TestResponse.class) .route() .setHeader('Content-Type', constant('application/json')) .setHeader('Accept', constant('application/json')) .setHeader(Exchange.HTTP_METHOD, constant('GET')) .removeHeaders('CamelHttp*') .to('http4://localhost:8082/orders?bridgeEndpoint=true') .unmarshal(formatOrder) .enrich('direct:enrichFromInventory', new OrderAggregationStrategy()) .to('log:result') .endRest(); from('direct:enrichFromInventory') .transform().simple('${null}') .setHeader('Content-Type', constant('application/json')) .setHeader('Accept', constant('application/json')) .setHeader(Exchange.HTTP_METHOD, constant('GET')) .removeHeaders('CamelHttp*') .to('http4://localhost:8081/items?bridgeEndpoint=true') .unmarshal(formatInventory);
Imaginez maintenant que chaque service n'est plus une instance spécifique mais un nuage d'instances qui fonctionnent comme une seule. Nous utiliserons Minikube pour tester le cluster Kubernetes localement.
Configurez les chemins réseau pour voir les nœuds Kubernetes localement (l'exemple donné est pour un environnement Mac / Linux):
# remove existing routes sudo route -n delete 10/24 > /dev/null 2>&1 # add routes sudo route -n add 10.0.0.0/24 $(minikube ip) # 172.17.0.0/16 ip range is used by docker in minikube sudo route -n add 172.17.0.0/16 $(minikube ip) ifconfig 'bridge100' | grep member | awk '{print }’ # use interface name from the output of the previous command # needed for xhyve driver, which I'm using for testing sudo ifconfig bridge100 -hostfilter en5
Emballez les services dans Conteneurs Docker avec une configuration Dockerfile comme celle-ci:
FROM openjdk:8-jdk-alpine VOLUME /tmp ADD target/order-srv-1.0-SNAPSHOT.jar app.jar ADD target/lib lib ENV JAVA_OPTS='' ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
Créez et soumettez les images de service au registre Docker. Exécutez maintenant les nœuds sur le cluster Kubernetes local.
Configuration du déploiement de Kubernetes.yaml:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: inventory spec: replicas: 3 selector: matchLabels: app: inventory template: metadata: labels: app: inventory spec: containers: - name: inventory image: inventory-srv:latest imagePullPolicy: Never ports: - containerPort: 8081
Exposez ces déploiements en tant que services en cluster:
kubectl expose deployment order-srv --type=NodePort kubectl expose deployment inventory-srv --type=NodePort
Nous pouvons maintenant vérifier si les demandes sont servies par des nœuds choisis au hasard dans le cluster. Exécuter curl -X http://192.168.99.100:30517/info
plusieurs fois de façon séquentielle pour accéder au minikube NodePort, pour le service exposé (en utilisant votre hôte et votre port). Dans le résultat, nous voyons que nous avons atteint l'équilibre des demandes. ~~~ UUID du service d'inventaire = 22f8ca6b-f56b-4984-927b-cbf9fcf81da5 UUID du service d'inventaire = b7a4d326-1e76-4051-a0a6-1016394fafda Service d'inventaire UUID = b7a4d326-1e76-4051-aUafUID Service d'inventaire 224d326-1e76-4051-a UafUID6- Service d'inventaire 224d326-1e76-4051-aUafUID6-104 4984-927b-cbf9fcf81da5 UUID du service d'inventaire = 50323ddb-3ace-4424-820a-6b4e85775af4 ~~~
Ajoutez les dépendances camel-kubernetes
et camel-netty4-http
al pom.xml
du projet. Ensuite, configurez le composant ServiceCall pour utiliser la découverte de nœud maître Kubernetes partagé pour tous les appels de service entre les définitions de route:
KubernetesConfiguration kubernetesConfiguration = new KubernetesConfiguration(); kubernetesConfiguration.setMasterUrl('https://192.168.64.2:8443'); kubernetesConfiguration.setClientCertFile('/Users/antongoncharov/.minikube/client.crt'); kubernetesConfiguration.setClientKeyFile('/Users/antongoncharov/.minikube/client.key'); kubernetesConfiguration.setNamespace('default”); ServiceCallConfigurationDefinition config = new ServiceCallConfigurationDefinition(); config.setServiceDiscovery(new KubernetesClientServiceDiscovery(kubernetesConfiguration)); context.setServiceCallConfiguration(config);
Le ServiceCall EIP complète bien le Spring Boot. La plupart des options peuvent être définies directement dans le fichier application.properties
.
Améliorez la route Camel avec le composant ServiceCall:
rest('/orders') .get('/').description('Get all orders with details').outType(TestResponse.class) .route() .hystrix() .setHeader('Content-Type', constant('application/json')) .setHeader('Accept', constant('application/json')) .setHeader(Exchange.HTTP_METHOD, constant('GET')) .removeHeaders('CamelHttp*') .serviceCall('customer-srv','http4:customer-deployment?bridgeEndpoint=true') .unmarshal(formatOrder) .enrich('direct:enrichFromInventory', new OrderAggregationStrategy()) .to('log:result') .endRest(); from('direct:enrichFromInventory') .transform().simple('${null}') .setHeader('Content-Type', constant('application/json')) .setHeader('Accept', constant('application/json')) .setHeader(Exchange.HTTP_METHOD, constant('GET')) .removeHeaders('CamelHttp*') .serviceCall('order-srv','http4:order-srv?bridgeEndpoint=true') .unmarshal(formatInventory);
Nous avons également activé le disjoncteur sur l'itinéraire. Il s'agit d'un hook d'intégration qui permet d'arrêter les appels distants du système, en cas d'erreurs de livraison ou de manque de disponibilité du destinataire. Ceci est conçu pour éviter les pannes système en cascade. Le composant Hystrix aide à atteindre cet objectif en mettant en œuvre le modèle de disjoncteur.
Exécutons-le et envoyons une demande de test; nous obtiendrons la réponse globale des deux services.
[{'id':1,'items':[{'id':2,'name':'Monitor','description':'27-inch, response time: 7ms','price':200.0},{'id':3,'name':'Headphones','description':'Soft leather ear-cups','price':29.9},{'id':4,'name':'Mouse','description':'Designed for comfort and portability','price':19.0}]},{'id':2,'items':[{'id':5,'name':'Keyboard','description':'Layout: US','price':10.5},{'id':3,'name':'Headphones','description':'Soft leather ear-cups','price':29.9}]}]
Le résultat est comme prévu.
J'ai montré comment Apache Camel peut intégrer des microservices dans un cluster. Quelles sont les autres utilisations de ce cadre? En général, il est utile partout où le routage basé sur des règles est une solution. Par exemple, Apache Camel peut être un middleware pour l'Internet des objets avec l'adaptateur Eclipse Kura. Il peut gérer la surveillance en transportant des signaux de journal à partir de divers composants et services, comme dans le système du CERN. Il peut également s'agir d'un cadre d'intégration pour la SOA d'entreprise ou d'un portefeuille pour le traitement de données par lots, bien qu'il ne soit pas en concurrence avec Apache Spark dans cette zone.
Vous pouvez voir que l'intégration des systèmes n'est pas un processus facile. Nous avons de la chance car beaucoup d'expérience a été accumulée. Il est important de l'appliquer correctement pour créer des solutions flexibles et tolérantes aux pannes.
Pour garantir une application correcte, je recommande d'avoir une liste de contrôle des aspects d'intégration importants. Les articles indispensables comprennent:
Dans cet article, nous avons testé Apache Camel, un cadre système d'intégration léger, qui permet d'économiser du temps et des efforts lors de la résolution des problèmes d'intégration. Comme nous l'avons montré, il peut servir d'outil prenant en charge l'architecture de microservices pertinente en prenant l'entière responsabilité de l'échange de données entre les microservices.
Si vous souhaitez en savoir plus sur Apache Camel, je vous recommande vivement le livre 'Camel in Action' du créateur du cadre , Claus Ibsen. La documentation officielle est disponible sur camel.apache.org .