Recently, at E-Bee, I developed a framework for enabling Micro Services using Apache Camel. I have always been a big fan, nay, a fanatic, of modularisation. Combine the modularisation concept with the notion of Micro services and you would stipulate that all modules need to expose Micro services. All the services that are available from one module must be exposable as Micro Services to another dependent module. One module can only consume another module via its exposed services. No back door entry permitted! This means that one module cannot sneak in and take a peek at the other module's database. Or shared memory. Or cache. If module1 depends on module2, this means that it can only depend on module2's APIs. Nothing else! We have always had a great modular architecture in E-Bee. Each module has use two coding units a.k.a. two Eclipse projects - viz. module1-api and module1-service. module1-api contains all the API interfaces whilst module1-service will contain all the implementations of these interfaces. module1-service will depend on module1-api for obvious reasons. It has to implement the APIs. However, module1-api must not obviously depend on module1-services. Same goes for module2-api and module2-service. Now, module1, since it depends on module2, will have a dependency to module2-api. However, module1-service will not depend on module2-service directly. It is dependent only on the API contracts. (shown below) Typically we had always used the Spring container for Dependency Injection. Spring injects module2-service classes into module1 service classes.
But as we started to move towards Micro services, my desire was to allow module1-service to interact with module2-service either directly (as in a POJO invocation) or over the wires using a remote invocation. How we do that must be transparent. So how are we going to accomplish this? The key is to expose all the services using messages and leverage messaging to communicate from module1-service to module2-service. Then there must be a layer that allows this to happen. I decided to use Apache ActiveMQ for messaging and Apache Camel for allowing seamless communication between clusters of services.
For those unfamiliar to Camel, it is a simple framework that uses an awesome DSL (Domain Specific Language) to set up routes. Routes allow the passage of "messages" between the producers and consumers. Producers request for various services and send a message along a route. This message is processed by a consumer which composes a reply and sends it back to the producer.
So, in our new framework my idea was to use Camel for communicating between module1 and module2. For this to happen, I set up a simple route in Camel. I called this route somewhat bombastically as the "Main Highway". The Main Highway takes a message and routes it to the consumer and returns a reply. So, if module1-service wants to communicate with module2-service, all it has to do is to send a message along the Main highway. Now, I hooked up this Main Highway to JMS and HTTP as well so that any message destined for module2-service can also come via JMS or a HTTP request. The JMS supports both Serializable Java Objects and JSON. As it can be seen, the main highway has an entry point. This entry point is reachable only within the same JVM. (Camel calls such an entry point as a direct entry point).
The JMS and the HTTP entry points are also wired to the main highway. They have transformers that convert JSON into a "canonical" model that is understood within the framework. Main Highway also contains several components that can route the request to the end consumer which then consumes the request and composes a reply. The consumer can also be surrounded by multiple interceptors that enforce horizontal policies such as Security, Caching etc.
This abstraction allows the producer to avail the services exposed by the consumer in multiple ways:
- The producer can directly send a message within the same JVM to the entry point of the Main Highway.
- The producer can use the HTTP end point to communicate with the consumer.
- The producer can also communicate via the JMS end point. These options make the camel architecture extremely rich.
The Service Registry
How do services know about each other? They need to publish the information about their existence somewhere. This becomes complicated when there are multiple services residing in multiple deployable units (WARS or EARS in the Java world). How does module1-service residing in a war called module1.war know about the module2 services offered presumably in another war called module2.war? The service registry provides this feature. It is a service that can be called to find out about other services. This service of course needs to be well known so that it does not become "chicken and egg" to discover the registry service itself. We achieved this using REDIS as a Global repository for all services. The picture below illustrates the interactions. The different clusters host different kinds of services. There can be multiple nodes in a cluster. These nodes publish their service information into REDIS. Details include service name, operations and the parameters required to invoke the operation. The Queue name at which these services are listening is also published. The global registry is read on start up by each of the nodes and cached locally for the life of the application. Any updates to the global registry is also published via REDIS channels so that all the nodes across all the clusters can update themselves with this information. This allows the services to invoke each other across the ecosystem.
A class called Camel Proxy is also created using JDK proxies. This class connects to the global registry to discover where each service is hosted. If the service is local, it directly calls the service by invoking the "Main Highway" end point. Else, it uses JMS (with Java Serializable objects) to invoke a service in a remote server. This allows seamless integration between micro services without the clients being aware of whether a service is hosted locally or remotely. Hawtio is able to seamlessly integrate with Camel JMX beans via the Jolokia library. Since the REST library in Camel supports Swagger, we are able to automatically publish all our services using Swagger.