Ideally, each service should have only a small set of responsibilities. Robert 'Uncle Bob' Martin talks about designing classes using the Single Responsibility Principle (SRP). The SRP defines a responsibility of a class as a reason to change, and states that a class should only have one reason to change. It make sense to apply the SRP to service design as well.
Another analogy that helps with service design is the design of Unix utilities. Unix provides a large number of utilities such as grep, cat and find. Each utility does exactly one thing, often exceptionally well, and can be combined with other utilities using a shell script to perform complex tasks.
Adrian Cockcroft (former Netflix Cloud Architect, current Amazon VP of Cloud Architecture) says that a microservice is something that one developer should be able to write/re-write from scratch in two to three weeks.
Helpful patterns for designing microservices:
- The number of service instances and their locations (host+port) changes dynamically
- Partitioning into services can change over time and should be hidden from clients
- Services might use a diverse set of protocols, some of which might not be web friendly
- client-side-discovery pattern (via Eureka)
- self-registration (via Eureka)
- 3rd party registration (via Sidecar)
- single service per host pattern (via Sagoku)
- externalized configuration pattern (via Zookeeper / Persistent Variables)
- microservice chassis pattern (via AMI)
- api gateway pattern ( via Apps Gateway)
Service location is service discovery and service registration.
- Service discovery is finding other services
- Service registration is making your service discoverable by others.
Sequoia uses Eureka for service location, and non-java apps use Eureka through a Sidecar Pattern to intermediate service location, among other things.
You should read this: service discovery in a microservices architecture
The Apps Gateway is an implementation of the API gateway pattern that is the single entry point for all clients. The API gateway handles requests in one of two ways. Some requests are simply proxied/routed to the appropriate service. It handles other requests by fanning out to multiple services.
Our Apps Gateway is implemented via Netflix Zuul.
Further Reference:
At the center of Zuul is a series of filters that are capable of performing a range of actions during the routing of HTTP requests and responses. The following are the key characteristics of a Zuul filter:
- Type: most often defines the stage during the routing flow when the filter will be applied (although it can be any custom string)
- Execution Order: applied within the Type, defines the order of execution across multiple filters
- Criteria: the conditions required in order for the filter to be executed
- Action: the action to be executed if the Criteria are met
- PRE filters execute before routing to the origin. Examples include request authentication, choosing origin servers, and logging debug info.
- ROUTE filters handle routing the request to an origin. This is where the origin HTTP request is built and sent using Apache HttpClient or Netflix Ribbon.
- POST filters execute after the request has been routed to the origin. Examples include adding standard HTTP headers to the response, gathering statistics and metrics, and streaming the response from the origin to the client.
- ERROR filters execute when an error occurs during one of the other phases.
You are probably always going to write only PRE filters. Our only ROUTE filter is the HttpForwarder.
class SuchABadFilter extends ZuulFilter {
def domains = [
"www.jstor.org",
"firefly.jstor.org"
]
@Override
String filterType() {
return 'pre'
}
@Override
int filterOrder() {
return 430
}
@Override
boolean shouldFilter() {
RequestContext ctxt = RequestContext.currentContext
HttpServletRequest req = ctxt.request
return !ctxt.appsServiceName && domains.contains(req.serverName) &&
(req.requestURI.startsWith("/clockss-manifest"))
}
@Override
Object run() {
RequestContext ctxt = RequestContext.currentContext
HttpServletRequest req = ctxt.request
// Do some stuff
ctxt.appsServiceName = "MY_EUREKA_SERVICE_NAME"
ctxt.routeRequestURI = req.requestURI
}
}
- Filters communicate with one another through the request context.
- Setting the request context appServiceName and routeRequestURI lets other filters do the obscure parts
- Don't filter if
ctxt.appsServiceName
is already set, since some other filter already ran - Pick a filter order larger than 200 and smaller than 1000 or you need to do more yourself
- {SERVICE_NAME}.apps.test.cirrostratus.org already works for any eureka registered service because of AppsServiceDetection.groovy
Best practice filters:
- StaticTrafficCop.groovy - Static application, single page application
- FrontEndAppBaseFilter.groovy - Example of nicely extensible pattern for complicated routing
ORDER | NAME |
---|---|
100 | CORS.groovy |
200 | PreserveRequestedHost.groovy |
500 | AppsServiceDetection.groovy |
600 | StaticBanana.groovy |
601 | StaticTrafficCop.groovy |
730 | FrontEndAppBaseFilter.groovy |
1000 | AppsRouting.groovy |
1010 | RunStatic.groovy |