Cloud-Native Applications in Java
上QQ阅读APP看书,第一时间看更新

Microservice design guidelines

The whole notion of microservices is about the separation of concerns. This requires a logical and architectural separation between the services with different responsibilities. Here are a few guidelines to design the microservices.

These guidelines are in line with the 12-factor applications guidelines given by Heroku engineers:

  • Lightweight: Microservices have to be lightweight in order to facilitate smaller memory footprints and faster startup times. This facilitates faster MTTR, and allows for services to be deployed on smaller runtime instances, hence horizontally scaling better. Compared to heavy runtime times, such as application servers, smaller runtimes such as Tomcat, Netty, Node.js, and Undertow are more suited. Also, the services should exchange data in lightweight text formats, such as JSON, or binary formats, such as Avro, Thrift, or Protocol Buffers.
  • Reactive: This is applicable to services with highly concurrent loads or slightly longer response times. Typical server implementations block threads to execute imperative programming styles. As microservices could depend on other microservices or I/O resources such as a database, blocking threads could increase operating system overheads. The Reactive style operates on non-blocking I/O, uses call back handlers, and reacts to events. This does not block threads and as a result, increases the scalability and load handling characteristics of the microservices much better. Database drivers have started supporting reactive paradigms, for example, MongoDB Reactive Streams Java Driver.
  • Stateless: Stateless services scale better and start faster as there is no state to be stored on disk on shutdown or activated on start-up. They are also more resilient, as termination of a service will not result in a loss of data. Being stateless is also a step towards being lightweight. If a state is required, a service can delegate state storage to a high speed persistent (key value) store, or hold it in distributed caches.
  • Atomic: This is the core design principle of microservices. They should be easy to change, test, and deploy. All these can be achieved if the services are reasonably small and do the smallest business unit of work that can be done independently. If there is low coupling, the services will be easier to modify and independently deploy. Composite microservices may be required on a need basis but should be limited in design.
  • Externalized configuration: Typical application properties and configurations were traditionally managed as configuration files. Given the multiple and large deployments of microservices, this practice will start getting cumbersome, as the scale of the services increase. Hence, it is better to externalize the configurations in the configuration server, so that it can be maintained in a hierarchical structure per environment. Features such as hot changes can also be easier to reflect many services at once.
  • Consistent: Services should be written in a consistent style as per the coding standards and naming convention guidelines. Common concerns such as serialization, REST, exception handling, logging, configuration, property access, metering, monitoring, provisioning, validations, and data access should be consistently done through reusable assets, annotations, and so on. It should be easier for another developer from the same team to understand the intent and operation of the service.
  • Resilient: Services should handle exceptions arising from technical reasons (connectivity, runtime), and business reasons (invalid inputs) and not crash. They should use patterns such as timeouts and circuit breakers to ensure that the failures are handled carefully.
  • Good citizens: Report their usage statistics, number of times accessed, average response times, and so on through JMX API, and/or publish it through libraries to central monitoring infrastructures, log audit, error, and business events in the standards prescribed. Expose their condition through health check interfaces, for example, as done by Spring Actuator.
  • Versioned: Microservices may need to support multiple versions for different clients, till all clients migrate to higher versions. Hence the deployments and URL should support semantic versioning, that is, X.X.X.

In addition, microservices will need to leverage additional capabilities that are typically built at an enterprise level such as:

  • Dynamic service registry: Microservice registers itself with a service registry when up.
  • Log aggregation: The logs generated by a microservice can be aggregated for central analysis and troubleshooting. The log aggregation is a separate infrastructure and typically built as an async model. Products such as Splunk and ELK Stack in conjunction with event streams such as Kafka are used to build/deploy the log aggregation systems.
  • External configuration: The microservice can get the parameters and properties from an external configuration such as Consul and Zookeeper to initialize and run.
  • Provisioning and auto-scaling: The service is automatically started by a PaaS environment if it detects a need to start an additional instance based on incoming load, some services failing, or not responding in time.
  • API gateway: A microservice interface can be exposed to the clients or other divisions through an API gateway that provides abstraction, security, throttling, and service aggregation.

We cover all the service design guidelines in subsequent chapters as we start building and deploying the services.