We'll be taking a look at Spring Cloud, the open source microservices framework from Pivotal.
Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer's own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
# src/main/resources/config/boostrap.yml
spring:
application:
name: example
cloud:
config:
uri: ${SPRING_CONFIG_URI:http://localhost:2020}
# it is possible to use service discovery instead of a well-known URI
# application.yml in the Git repository
# shared configuration for all applications
applications: example
endpoints:
health:
time-to-live: 1000
sensitive: false
logging:
config: classpath:logback.xml
management:
contextPath: /operations
security:
enabled: false
role: admin
sessions: stateless
security:
user:
name: developer
password: developer
basic:
enabled: false
realm: example
server:
contextPath: /
port: 8080
useForwardHeaders: true
tomcat:
portHeader: X-Forwarded-Port
protocolHeader: X-Forwarded-Protocol-Header
remoteIpHeader: X-Forwarded-Remote-IP-Header
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
healthCheckInterval: 15s
healthCheckPath: ${management.contextPath}/health
instanceId: ${spring.application.name}:${random.value}
preferAgentAddress: true
preferIpAddress: true
groovy:
template:
check-template-location: false
inetutils:
timeoutSeconds: 1
defaultHostname: localhost
localhost: 127.0.0.1
ignoredInterfaces:
- docker0
jackson:
serialization:
indent_output: true
serialization-inclusion: non_empty
main:
banner-mode: console
rabbitmq:
host: localhost
password: guest
port: 5672
virtualHost: /
username: guest
turbine:
aggregator:
clusterConfig: ${applications}
appConfig: ${applications}
# example-default.yml in the Git repository
# configuration for the example application when using the default profile
example:
foo: default
exchangeName: some-exchange
queueName: some-queue
deadLetterExchangeName: dead-letter
deadLetterQueueName: dead-letter
messageRetryAttempts: 3
# example-bamboo.yml in the Git repository
# configuration for the example application when using the bamboo profile
example:
foo: bamboo
// The only code needed to spin up a configuration server
@SpringBootApplication
@EnableConfigServer <----- magic incantation
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
# src/main/resources/config/application.yml
spring:
cloud:
config:
server:
git:
uri: https://github.com/kurron/spring-configuration-files.git
# application.yml
nginx:
server:
name: example.com
# nginx.conf
server {
listen 80;
server_name ${nginx.server.name};
}
http localhost:8080/CONFIGSERVER/example/default/master/nginx.conf
HTTP/1.1 200 OK
Content-Disposition: inline;filename=f.txt
Content-Type: text/plain;charset=UTF-8
Date: Wed, 30 Mar 2016 01:39:02 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
server {
listen 80;
server_name example.com; <---- two files are combined
}
// src/main/groovy/org/kurron/example/Application.groovy
@SpringBootApplication
@EnableCircuitBreaker
@EnableHystrixDashboard
@EnableTurbine
@EnableDiscoveryClient <---- magic incantation
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
# pulled down as part of the shared configuration
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
healthCheckInterval: 15s
healthCheckPath: ${management.contextPath}/health
instanceId: ${spring.application.name}:${random.value}
preferAgentAddress: true
preferIpAddress: true
// src/main/groovy/org/kurron/example/Application.groovy
@SpringBootApplication
@EnableCircuitBreaker
@EnableHystrixDashboard
@EnableTurbine
@EnableDiscoveryClient <---- magic incantation
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
def 'call service by hand'() {
given: 'a proper testing environment'
assert serviceName <----- injected by Spring
assert discoveryClient <----- injected by Spring
when: 'we call checkTheTime'
def template = new TestRestTemplate()
//WARNING: this call can come back empty/null!
def instances = discoveryClient.getInstances( serviceName )
def chosen = randomElement( instances ) as ServiceInstance
def uri = constructURI( chosen, '/descriptor/application' )
ResponseEntity<String> response = template.getForEntity( uri, String )
then: 'we get a proper response'
response.statusCode == HttpStatus.OK
}
URI constructURI( ServiceInstance service, String path ) {
UriComponentsBuilder.newInstance()
.scheme( 'http' )
.host( service.host )
.port( service.port )
.path( path )
.build().toUri()
}
def 'call service using load balancer'() {
given: 'a proper testing environment'
assert serviceName <--- we use a logical name, not a host name
assert loadBalancer <-- watches Consul for health changes
when: 'we call checkTheTime'
def template = new TestRestTemplate()
def chosen = Optional.ofNullable( loadBalancer.choose( serviceName ) )
def uri = constructURI( chosen.orElseThrow( unavailableLogic ), '/descriptor/application' )
ResponseEntity<String> response = template.getForEntity( uri, String )
then: 'we get a proper response'
response.statusCode == HttpStatus.OK
}
def unavailableLogic = { new RuntimeException( 'No service instances!' ) }
URI constructURI( ServiceInstance service, String path ) {
UriComponentsBuilder.newInstance()
.scheme( 'http' )
.host( service.host )
.port( service.port )
.path( path )
.build().toUri()
}
@Category( OutboundIntegrationTest )
@IntegrationTest
@ContextConfiguration( classes = FeignIntegrationTestConfiguration,
loader = SpringApplicationContextLoader )
class FeignIntegrationTest extends Specification implements GenerationAbility {
@Autowired
private RestGatewayClient client <---- resource-specific client
def 'call happy path'() {
given: 'a proper testing environment'
assert client
when: 'we call happyPath'
def results = client.happyPath() <---- convenience method
then: 'we get a proper response'
results
println results
}
}
@FeignClient( name = 'example', <---- logical service name
configuration = RestGatewayClientConfiguration.
fallback = RestGatewayClientFallback ) <---- fallback logic
interface RestGatewayClient {
@RequestMapping( method = RequestMethod.GET,
path = '/descriptor/application',
consumes = 'application/json' )
String happyPath()
@RequestMapping( method = RequestMethod.GET,
path = '/descriptor/fail',
consumes = 'application/json' )
String systemFailure()
@RequestMapping( method = RequestMethod.GET,
path = '/descriptor/fail/application',
consumes = 'application/json' )
String applicationFailure()
}
class RestGatewayClientFallback implements RestGatewayClient {
@Override
String happyPath() {
'happyPath fallback returned'
}
@Override
String systemFailure() {
'systemFailure fallback returned'
}
@Override
String applicationFailure() {
'applicationFailure fallback returned'
}
}
@Configuration
class RestGatewayClientConfiguration {
@Bean
RestGatewayClientFallback restGatewayClientFallback() {
new RestGatewayClientFallback()
}
@Bean
Logger.Level feignLoggerLevel() {
Logger.Level.FULL
}
@Bean
CustomErrorDecoder customErrorDecoder() {
new CustomErrorDecoder()
}
@Bean
Request.Options requestOptions() {
int connectionTimeout = 1000
int readTimeout = 1000
new Request.Options( connectionTimeout, readTimeout )
}
@Bean
RequestInterceptor customRequestInterceptor() {
{ RequestTemplate template ->
template.header( 'X-Custom-Header', Instant.now().toString() )
} as RequestInterceptor
}
}
Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. As an edge service application, Zuul is built to enable dynamic routing, monitoring, resiliency and security. It also has the ability to route requests to multiple Amazon Auto Scaling Groups as appropriate.
@SpringBootApplication
@EnableZuulProxy <---- embeds the proxy
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
zuul:
routes:
users:
path: /myusers/** <---- the path getting proxied
serviceId: users_service <---- service name in Consul
sensitiveHeaders: Cookie,Set-Cookie,Authorization
# more elaborate configuration is required if circuit-breakers are desired
# Example of API strangulation
zuul:
routes:
first: <---- send traffic to external URL
path: /first/**
url: http://first.example.com
second: <---- send traffic to internal /second service
path: /second/**
url: forward:/second
third: <---- send traffic to internal /3rd service
path: /third/**
url: forward:/3rd
legacy: <---- default to the old system
path: /**
url: http://legacy.example.com
Spring Cloud Bus links nodes of a distributed system with a lightweight message broker. This can then be used to broadcast state changes (e.g. configuration changes) or other management instructions. A key idea is that the Bus is like a distributed Actuator for a Spring Boot application that is scaled out, but it can also be used as a communication channel between apps. The only implementation currently is with an AMQP broker as the transport, but the same basic feature set (and some more depending on the transport) is on the roadmap for other transports.
Spring Cloud Stream is a framework for building message-driven microservices. Spring Cloud Stream builds upon Spring Boot to create DevOps friendly microservice applications and Spring Integration to provide connectivity to message brokers. Spring Cloud Stream provides an opinionated configuration of message brokers, introducing the concepts of persistent pub/sub semantics, consumer groups and partitions across several middleware vendors. This opinionated configuration provides the basis to create stream processing applications.
@Slf4j
@OutboundGateway
@EnableBinding( Source ) <---- magic incantation
class StreamProducer implements GenerationAbility{
@InboundChannelAdapter( Source.OUTPUT )
public String send() {
def message = "Hello, World. It is ${Instant.now().toString()}"
log.info( 'Sending {}', message )
message
}
}
// we could have sent an object and had Spring auto-transform it into JSON
// we can also assemble messages by hand if we need more control, e.g. headers
Slf4j
@InboundGateway
@EnableBinding( Sink )
class StreamGateway {
@StreamListener( Sink.INPUT )
void processMessage( String request ) {
log.info( 'Hearing {}', request )
}
}
// we could have accepted an object and had Spring auto-transform it from JSON
# example-default.yml on the configuration server
spring:
cloud:
stream:
bindings:
input: <---- this is the channel name in the code
destination: rendezous <---- RabbitMQ exchange to use
group: example
contentType: text/plain
consumer:
concurrency: 1
partitioned: false
maxAttempts: 3
backOffInitialInterval: 1000
backOffMaxInterval: 10000
backOffMultiplier: 2.0
acknowledgeMode: AUTO
autoBindDlq: true
durableSubscription: true
maxConcurrency: 1
prefetch: 1
prefix: ${spring.application.name}.
requeueRejected: true
republishToDlq: true
transacted: false
txSize: 1
output: <---- this is the channel name in the code
destination: rendezous <---- RabbitMQ exchange to use
contentType: text/plain
producer:
autoBindDlq: true
batchingEnabled: false
batchSize: 100
batchBufferLimit: 1000
batchTimeout: 5000
compress: false
deliveryMode: PERSISTENT
prefix: ${spring.application.name}.
http localhost:8080/operations/health
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Mon, 28 Mar 2016 23:54:03 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: example:8080
X-B3-Sampled: 0
X-B3-SpanId: 64740575bcecba80
X-B3-TraceId: 64740575bcecba80
{
"binders": { <---- all the brokers Spring Cloud Stream is bound to
"rabbit": {
"binderHealthIndicator": {
"status": "UP",
"version": "3.6.1"
},
"status": "UP"
},
"status": "UP"
},
"configServer": {
"propertySources": [
"https://github.com/kurron/spring-configuration-files.git/application.yml"
],
"status": "UP"
},
"description": "Spring Cloud Consul Discovery Client",
"discoveryComposite": {
"description": "Spring Cloud Consul Discovery Client",
"discoveryClient": {
"description": "Spring Cloud Consul Discovery Client",
"services": [
"consul",
"example"
],
"status": "UP"
},
"status": "UP"
},
"diskSpace": {
"free": 25699893248,
"status": "UP",
"threshold": 10485760,
"total": 39233855488
},
"hystrix": {
"status": "UP"
},
"rabbit": {
"status": "UP",
"version": "3.6.1"
},
"refreshScope": {
"status": "UP"
},
"status": "UP"
}
While Spring Cloud Stream makes it easy for individual boot apps to connect to messaging systems, the typical scenario for Spring Cloud Stream is the creation of multi-app pipelines, where microservice apps are sending data to each other. This can be achieved by correlating the input and output destinations of adjacent apps.
Uses Netflix's Hystrix library
Bulkheading provides isolation
@Slf4j
@SpringBootApplication
@EnableCircuitBreaker <---- magic incantation
@EnableHystrixDashboard
@EnableTurbine
@EnableDiscoveryClient
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
@OutboundGateway
class RemoteTimeGateway implements TimeService {
@HystrixCommand( fallbackMethod = 'defaultTime' ) <--- magic
Instant checkTheTime() {
// force an error to trigger the circuit-breaker
throw = new UnsupportedOperationException( 'checkTime' )
}
// You don't have to provide a fallback
private Instant defaultTime() {
// can't reach the time server, use local time instead
Instant.now()
}
}
We're jumping threads so some thought has to be given if thread-specific context needs to be propogated
{
"hystrix": {
"openCircuitBreakers": [
"RemoteTimeGateway::checkTheTime"
],
"status": "CIRCUIT_OPEN" <---- not getting to the service
},
"rabbit": {
"status": "UP",
"version": "3.6.1"
},
"status": "UP" <---- up because we have fallbacks in place
}
Each service can host its own dashboard
Turbine aggregates breaker data into one panel
Spring Cloud Security offers a set of primitives for building secure applications and services with minimum fuss. A declarative model which can be heavily configured externally (or centrally) lends itself to the implementation of large systems of co-operating, remote components, usually with a central indentity management service. It is also extremely easy to use in a service platform like Cloud Foundry. Building on Spring Boot and Spring Security OAuth2 we can quickly create systems that implement common patterns like single sign on, token relay and token exchange.
@SpringBootApplication
@EnableOAuth2Sso <---- Turns on OAuth2 single sign on
@EnableResourceServer <---- Makes this application a resource server
@EnableAuthorizationServer <---- Make this application an authorization server
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
http http://10.0.2.15:8080/operations/metrics --pretty=format
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Sun, 27 Mar 2016 22:41:26 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: example:8080
{
"classes": 12101,
"classes.loaded": 12101,
"classes.unloaded": 0,
"currentTimeCounter(type=NORMALIZED)": 0.016666666666666666,
"currentTimeDistribution(statistic=count)": 13.0,
"currentTimeDistribution(statistic=totalAmount)": 4.2259077013282903e+18,
"currentTimeTimer(statistic=count)": 0.016666666666666666,
"currentTimeTimer(statistic=max)": 0.365126288,
"currentTimeTimer(statistic=totalOfSquares)": 0.13331720618865894,
"currentTimeTimer(statistic=totalTime)": 0.006085438133333334,
"gc.ps_marksweep.count": 3,
"gc.ps_marksweep.time": 336,
"gc.ps_scavenge.count": 17,
"gc.ps_scavenge.time": 225,
"heap": 1359872,
"heap.committed": 550400,
"heap.init": 96256,
"heap.used": 350533,
"httpsessions.active": 0,
"httpsessions.max": -1,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.0()": 2.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.100()": 12.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.25()": 2.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.50()": 4.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.75()": 7.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.90()": 11.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.95()": 11.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.99()": 12.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.99.5()": 12.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.currentConcurrentExecutionCount()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.errorCount()": 8.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.errorPercentage()": 100.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.latencyExecute_mean()": 5.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.latencyTotal_mean()": 5.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_circuitBreakerErrorThresholdPercentage()": 50.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_circuitBreakerRequestVolumeThreshold()": 20.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_circuitBreakerSleepWindowInMilliseconds()": 5000.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_executionIsolationSemaphoreMaxConcurrentRequests()": 10.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_executionIsolationThreadTimeoutInMilliseconds()": 1000.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_executionTimeoutInMilliseconds()": 1000.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests()": 10.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.propertyValue_metricsRollingStatisticalWindowInMilliseconds()": 10000.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.reportingHosts()": 1.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.requestCount()": 8.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountBadRequests()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountCollapsedRequests()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountEmit()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountExceptionsThrown()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFailure()": 8.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFallbackEmit()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFallbackFailure()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFallbackMissing()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFallbackRejection()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountFallbackSuccess()": 8.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountResponsesFromCache()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountSemaphoreRejected()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountShortCircuited()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountSuccess()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountThreadPoolRejected()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingCountTimeout()": 0.0,
"hystrix.HystrixCommand.RemoteTimeGateway.checkTheTime.rollingMaxConcurrentExecutionCount()": 1.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentActiveCount()": 0.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentCompletedTaskCount()": 13.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentCorePoolSize()": 10.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentLargestPoolSize()": 10.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentMaximumPoolSize()": 10.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentPoolSize()": 10.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentQueueSize()": 0.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.currentTaskCount()": 13.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.propertyValue_metricsRollingStatisticalWindowInMilliseconds()": 10000.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.propertyValue_queueSizeRejectionThreshold()": 5.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.reportingHosts()": 1.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.rollingCountCommandRejections()": 0.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.rollingCountThreadsExecuted()": 8.0,
"hystrix.HystrixThreadPool.RemoteTimeGateway.rollingMaxActiveThreads()": 1.0,
"instance.uptime": 480861,
"mem": 644575,
"mem.free": 199866,
"nonheap": 0,
"nonheap.committed": 96384,
"nonheap.init": 2496,
"nonheap.used": 94175,
"processors": 2,
"response.descriptor.application()": 8.0,
"response.operations.health()": 248.0,
"response.operations.metrics()": 30.0,
"systemload.average": 0.4,
"threads": 48,
"threads.daemon": 36,
"threads.peak": 48,
"threads.totalStarted": 79,
"uptime": 491444
}
An Atlas standalone node running on an r3.2xlarge (61GB RAM) can handle roughly 2 million metrics per minute for a given 6 hour window.
@SpringBootApplication
@EnableCircuitBreaker
@EnableHystrixDashboard
@EnableTurbine
@EnableDiscoveryClient
@EnableFeignClients
@EnableAtlas <---- magic incantation
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
@Bean
AtlasTagProvider atlasCommonTags( @Value( '${spring.application.name}' ) String appName ) {
[ 'defaultTags': { ['app': appName] } ] as AtlasTagProvider
}
}
/**
* Metrics collector.
*/
private final Registry registry
@Override
Instant currentTime() {
// the timer also counts so this is redundant
def counter = registry.counter( 'currentTimeCounter' )
counter.increment()
// contrived example: normally this is used to record some incoming value
def distribution = registry.distributionSummary( 'currentTimeDistribution' )
distribution.record( randomLong() )
// gauges, which track the current number of something, like queue size, are also available
def timer = registry.timer( 'currentTimeTimer' )
// in a real implementation we would interact with multiple services and take
// the best result but this is only an example
// The timer simultaneously records 4 statistics: count, max, totalOfSquares, and totalTime.
timer.record( { gateway.checkTheTime() } as Callable<Instant> )
}
# shared application.yml
netflix:
atlas:
uri: http://localhost:7101 <---- location of the Atlas instance
/api/v1/graph?
e=2012-01-01T00:00
&no_legend=1
&q=
name,sps,:eq,
(,nf.cluster,),:by,
:pct,
:stack
&s=e-1w
Span: The basic unit of work. For example, sending an RPC is a new span, as is sending a response to an RPC. Span’s are identified by a unique 64-bit ID for the span and another 64-bit ID for the trace the span is a part of. Spans also have other data, such as descriptions, timestamped events, key-value annotations (tags), the ID of the span that caused them, and process ID’s (normally IP address).
Spans are started and stopped, and they keep track of their timing information. Once you create a span, you must stop it at some point in the future.
Trace: A set of spans forming a tree-like structure. For example, if you are running a distributed big-data store, a trace might be formed by a put request.
Annotation: is used to record existence of an event in time. Some of the core annotations used to define the start and stop of a request are:
cs - Client Sent - The client has made a request. This annotation depicts the start of the span.
sr - Server Received - The server side got the request and will start processing it. If one subtracts the cs timestamp from this timestamp one will receive the network latency.
All this is under a single trace id
HTTP/1.1 200 OK
Content-Type: application/json; type=FIXME; version=1.0.0;charset=UTF-8
Date: Sun, 27 Mar 2016 23:15:57 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: example:8080
X-B3-Sampled: 1
X-B3-SpanId: 6fca525591c416be
X-B3-TraceId: 6fca525591c416be
{
"path": "/descriptor/application",
"status": 200,
"time": "2016-03-27T23:15:57.917Z",
"timestamp": "2016-03-27T23:15:57.562Z"
}
HTTP/1.1 200 OK
Content-Type: application/json; type=FIXME; version=1.0.0;charset=UTF-8
Date: Sun, 27 Mar 2016 23:16:00 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: example:8080
X-B3-Sampled: 1
X-B3-SpanId: 29882e8f3c43d283
X-B3-TraceId: 29882e8f3c43d283
{
"path": "/descriptor/application",
"status": 200,
"time": "2016-03-27T23:16:00.066Z",
"timestamp": "2016-03-27T23:16:00.048Z"
}
HTTP/1.1 200 OK
Content-Type: application/json; type=FIXME; version=1.0.0;charset=UTF-8
Date: Sun, 27 Mar 2016 23:22:18 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: example:8080
X-B3-Sampled: 1
X-B3-SpanId: 6db06cf2872894e
X-B3-TraceId: 6db06cf2872894e
{
"path": "/descriptor/application",
"status": 200,
"time": "2016-03-27T23:22:18.887Z",
"timestamp": "2016-03-27T23:22:18.881Z"
}
No code changes required
Never found documentation on how to do global locks.
Spring Cloud Cluster offers a set of primitives for building "cluster" features into a distributed system. Example are leadership election, consistent storage of cluster state, global locks and one-time tokens.
@InboundGateway
class ClusterEventListener implements ApplicationListener<AbstractLeaderEvent> {
@Override
void onApplicationEvent( AbstractLeaderEvent event ) {
switch ( event ) {
case OnGrantedEvent:
log.info( 'A new leader has been elected for role {}', event.role )
break
case OnRevokedEvent:
break
log.info( 'An old leader has been removed for role {}', event.role )
break
default:
log.info( 'Heard an unclassified event of type {}', event.class.name )
break
}
}
}
// Never got it to work with Redis
Never found any documentation on how to manage cluster state.
@SpringBootApplication
@EnableSidecar <---- magic incantation
@EnableConfigurationProperties( ApplicationProperties )
class Application {
static void main( String[] args ) {
SpringApplication.run( Application, args )
}
}
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
compile('org.springframework.cloud:spring-cloud-starter-ribbon')
compile('org.springframework.cloud:spring-cloud-starter-zuul')
compile('org.springframework.cloud:spring-cloud-netflix-sidecar') {
exclude module: 'spring-cloud-netflix-eureka-client'
exclude module: 'eureka-client'
exclude module: 'ribbon-eureka'
}
}
# src/main/resources/config/application.yml
info:
app:
name: ${name}
description: ${description}
version: ${version}
spring:
application:
name: configuration-service <---- non-JVM service name
sidecar:
port: 2020
health-uri: http://localhost:2020/operations/health <---- Consul checks this
http localhost:8080/hosts/configuration-service
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Tue, 29 Mar 2016 21:30:18 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
[
{
"host": "127.0.0.1",
"port": 8080,
"secure": false,
"serviceId": "configuration-service",
"uri": "http://127.0.0.1:8080" <---- points back to sidecar process
}
]
http localhost:8080/example/descriptor/application <---- Zuul proxy
HTTP/1.1 200 OK
Content-Type: application/json; type=FIXME; version=1.0.0;charset=UTF-8
Date: Tue, 29 Mar 2016 21:44:05 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
X-B3-Sampled: 1
X-B3-SpanId: 5dffc13065fde452
X-B3-TraceId: 5dffc13065fde452
{
"path": "/descriptor/application",
"status": 200,
"time": "2016-03-29T21:44:05.716Z",
"timestamp": "2016-03-29T21:44:05.370Z"
}
http localhost:8080/hosts/example/
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Wed, 30 Mar 2016 02:14:15 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
[
{
"host": "127.0.0.1",
"port": 7070,
"secure": false,
"serviceId": "example",
"uri": "http://127.0.0.1:7070" <---- no proxy
},
{
"host": "127.0.0.1",
"port": 9090,
"secure": false,
"serviceId": "example",
"uri": "http://127.0.0.1:9090" <---- no proxy
}
]
http localhost:8080/CONFIGSERVER/example/default
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Tue, 29 Mar 2016 22:36:51 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
{
"name": "example",
"profiles": [
"default"
],
"propertySources": [
{
"name": "https://github.com/kurron/spring-configuration-files.git/example-default.yml",
"source": {
"example.deadLetterExchangeName": "dead-letter",
"example.deadLetterQueueName": "dead-letter",
"example.exchangeName": "some-exchange",
"example.foo": "default",
"example.inputChannelName": "my-input-channel",
"example.messageRetryAttempts": 3,
"example.queueName": "some-queue",
"sidecar.health-uri": "http://localhost:2020/operations/health",
"sidecar.port": 2020,
"spring.cloud.stream.bindings.input.consumer.acknowledgeMode": "AUTO",
"spring.cloud.stream.bindings.input.consumer.autoBindDlq": true,
"spring.cloud.stream.bindings.input.consumer.backOffInitialInterval": 1000,
"spring.cloud.stream.bindings.input.consumer.backOffMaxInterval": 10000,
"spring.cloud.stream.bindings.input.consumer.backOffMultiplier": 2.0,
"spring.cloud.stream.bindings.input.consumer.concurrency": 1,
"spring.cloud.stream.bindings.input.consumer.durableSubscription": true,
"spring.cloud.stream.bindings.input.consumer.maxAttempts": 3,
"spring.cloud.stream.bindings.input.consumer.maxConcurrency": 1,
"spring.cloud.stream.bindings.input.consumer.partitioned": false,
"spring.cloud.stream.bindings.input.consumer.prefetch": 1,
"spring.cloud.stream.bindings.input.consumer.prefix": "${spring.application.name}.",
"spring.cloud.stream.bindings.input.consumer.republishToDlq": true,
"spring.cloud.stream.bindings.input.consumer.requeueRejected": true,
"spring.cloud.stream.bindings.input.consumer.transacted": false,
"spring.cloud.stream.bindings.input.consumer.txSize": 1,
"spring.cloud.stream.bindings.input.contentType": "text/plain",
"spring.cloud.stream.bindings.input.destination": "rendezous",
"spring.cloud.stream.bindings.input.group": "example",
"spring.cloud.stream.bindings.output.contentType": "text/plain",
"spring.cloud.stream.bindings.output.destination": "rendezous",
"spring.cloud.stream.bindings.output.producer.autoBindDlq": true,
"spring.cloud.stream.bindings.output.producer.batchBufferLimit": 1000,
"spring.cloud.stream.bindings.output.producer.batchSize": 100,
"spring.cloud.stream.bindings.output.producer.batchTimeout": 5000,
"spring.cloud.stream.bindings.output.producer.batchingEnabled": false,
"spring.cloud.stream.bindings.output.producer.compress": false,
"spring.cloud.stream.bindings.output.producer.deliveryMode": "PERSISTENT",
"spring.cloud.stream.bindings.output.producer.prefix": "${spring.application.name}.",
"zuul.ignoredServices": "*",
"zuul.routes.first.path": "/first/**",
"zuul.routes.first.url": "http://first.example.com",
"zuul.routes.second.path": "/second/**",
"zuul.routes.second.url": "forward:/second",
"zuul.routes.third.path": "/third/**",
"zuul.routes.third.url": "forward:/3rd",
"zuul.routes.users.path": "/myusers/**",
"zuul.routes.users.sensitiveHeaders": "Cookie,Set-Cookie,Authorization",
"zuul.routes.users.serviceId": "users_service"
}
},
{
"name": "https://github.com/kurron/spring-configuration-files.git/application.yml",
"source": {
"applications": "${spring.application.name}",
"endpoints.health.sensitive": false,
"endpoints.health.time-to-live": 1000,
"logging.config": "classpath:logback.xml",
"management.contextPath": "/operations",
"management.security.enabled": false,
"management.security.role": "admin",
"management.security.sessions": "stateless",
"netflix.atlas.uri": "http://localhost:7101",
"security.basic.enabled": false,
"security.basic.realm": "example",
"security.user.name": "developer",
"security.user.password": "developer",
"server.contextPath": "/",
"server.port": 8080,
"server.tomcat.portHeader": "X-Forwarded-Port",
"server.tomcat.protocolHeader": "X-Forwarded-Protocol-Header",
"server.tomcat.remoteIpHeader": "X-Forwarded-Remote-IP-Header",
"server.useForwardHeaders": true,
"spring.cloud.cluster.leader.enabled": true,
"spring.cloud.cluster.leader.id": "Sponge Bob",
"spring.cloud.cluster.leader.role": "Master Of The Universe",
"spring.cloud.config.allowOverride": true,
"spring.cloud.config.failFast": true,
"spring.cloud.config.overrideNone": false,
"spring.cloud.config.overrideSystemProperties": false,
"spring.cloud.consul.discovery.healthCheckInterval": "15s",
"spring.cloud.consul.discovery.healthCheckPath": "${management.contextPath}/health",
"spring.cloud.consul.discovery.instanceId": "${spring.application.name}:${random.value}",
"spring.cloud.consul.discovery.preferAgentAddress": true,
"spring.cloud.consul.discovery.preferIpAddress": true,
"spring.cloud.consul.host": "localhost",
"spring.cloud.consul.port": 8500,
"spring.cloud.stream.rabbit.binder.addresses[0]": "localhost",
"spring.cloud.stream.rabbit.binder.adminAdresses[0]": "localhost",
"spring.cloud.stream.rabbit.binder.password": "guest",
"spring.cloud.stream.rabbit.binder.username": "guest",
"spring.cloud.stream.rabbit.binder.vhost": "/",
"spring.groovy.template.check-template-location": false,
"spring.inetutils.defaultHostname": "localhost",
"spring.inetutils.ignoredInterfaces[0]": "docker0",
"spring.inetutils.localhost": "127.0.0.1",
"spring.inetutils.timeoutSeconds": 1,
"spring.jackson.serialization-inclusion": "non_empty",
"spring.jackson.serialization.indent_output": true,
"spring.main.banner-mode": "console",
"spring.oauth2.client.accessTokenUri": "https://github.com/login/oauth/access_token",
"spring.oauth2.client.clientAuthenticationScheme": "form",
"spring.oauth2.client.clientId": "a1c5113ec74d4fc16c69",
"spring.oauth2.client.clientSecret": "aac122e5814095dc3525f1a19e5274353330f09d",
"spring.oauth2.client.userAuthorizationUri": "https://github.com/login/oauth/authorize",
"spring.oauth2.resource.preferTokenInfo": false,
"spring.oauth2.resource.userInfoUri": "https://api.github.com/user",
"spring.rabbitmq.host": "localhost",
"spring.rabbitmq.password": "guest",
"spring.rabbitmq.port": 5672,
"spring.rabbitmq.username": "guest",
"spring.rabbitmq.virtualHost": "/",
"turbine.aggregator.clusterConfig": "${applications}",
"turbine.appConfig": "${applications}"
}
}
],
"version": "527c752ddbfdd5b9f2f2c4862883fc5fd83668df"
}
http localhost:8080/CONFIGSERVER/example-default.yml
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Date: Wed, 30 Mar 2016 01:21:32 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
spring:
cloud:
stream:
rabbit:
binder:
addresses:
- localhost
adminAdresses:
- localhost
password: guest
username: guest
vhost: /
bindings:
input:
consumer:
acknowledgeMode: AUTO
autoBindDlq: true
backOffInitialInterval: 1000
backOffMaxInterval: 10000
backOffMultiplier: 2.0
concurrency: 1
durableSubscription: true
maxAttempts: 3
maxConcurrency: 1
partitioned: false
prefetch: 1
prefix: ${spring.application.name}.
republishToDlq: true
requeueRejected: true
transacted: false
txSize: 1
contentType: text/plain
destination: rendezous
group: example
output:
contentType: text/plain
destination: rendezous
producer:
autoBindDlq: true
batchBufferLimit: 1000
batchSize: 100
batchTimeout: 5000
batchingEnabled: false
compress: false
deliveryMode: PERSISTENT
prefix: ${spring.application.name}.
cluster:
leader:
enabled: true
id: Sponge Bob
role: Master Of The Universe
config:
allowOverride: true
failFast: true
overrideNone: false
overrideSystemProperties: false
consul:
discovery:
healthCheckInterval: 15s
healthCheckPath: /operations/health
instanceId: ${spring.application.name}:${random.value}
preferAgentAddress: true
preferIpAddress: true
host: localhost
port: 8500
inetutils:
ignoredInterfaces:
- docker0
defaultHostname: localhost
localhost: 127.0.0.1
timeoutSeconds: 1
groovy:
template:
check-template-location: false
jackson:
serialization-inclusion: non_empty
serialization:
indent_output: true
main:
banner-mode: console
oauth2:
client:
accessTokenUri: https://github.com/login/oauth/access_token
clientAuthenticationScheme: form
clientId: a1c5113ec74d4fc16c69
clientSecret: aac122e5814095dc3525f1a19e5274353330f09d
userAuthorizationUri: https://github.com/login/oauth/authorize
resource:
preferTokenInfo: false
userInfoUri: https://api.github.com/user
rabbitmq:
host: localhost
password: guest
port: 5672
username: guest
virtualHost: /
applications: ${spring.application.name}
endpoints:
health:
sensitive: false
time-to-live: 1000
example:
deadLetterExchangeName: dead-letter
deadLetterQueueName: dead-letter
exchangeName: some-exchange
foo: default
inputChannelName: my-input-channel
messageRetryAttempts: 3
queueName: some-queue
logging:
config: classpath:logback.xml
management:
contextPath: /operations
security:
enabled: false
role: admin
sessions: stateless
netflix:
atlas:
uri: http://localhost:7101
security:
basic:
enabled: false
realm: example
user:
name: developer
password: developer
server:
contextPath: /
port: 8080
tomcat:
portHeader: X-Forwarded-Port
protocolHeader: X-Forwarded-Protocol-Header
remoteIpHeader: X-Forwarded-Remote-IP-Header
useForwardHeaders: true
sidecar:
health-uri: http://localhost:2020/operations/health
port: 2020
turbine:
aggregator:
clusterConfig: ${spring.application.name}
appConfig: ${spring.application.name}
zuul:
ignoredServices: '*'
routes:
first:
path: /first/**
url: http://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd
users:
path: /myusers/**
sensitiveHeaders: Cookie,Set-Cookie,Authorization
serviceId: users_service
http localhost:8080/CONFIGSERVER/example-bamboo.json
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Wed, 30 Mar 2016 01:22:31 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: configuration-service:8080
{
"applications": "${spring.application.name}",
"endpoints": {
"health": {
"sensitive": false,
"time-to-live": 1000
}
},
"example": {
"foo": "bamboo"
},
"logging": {
"config": "classpath:logback.xml"
},
"management": {
"contextPath": "/operations",
"security": {
"enabled": false,
"role": "admin",
"sessions": "stateless"
}
},
"netflix": {
"atlas": {
"uri": "http://localhost:7101"
}
},
"security": {
"basic": {
"enabled": false,
"realm": "example"
},
"user": {
"name": "developer",
"password": "developer"
}
},
"server": {
"contextPath": "/",
"port": 8080,
"tomcat": {
"portHeader": "X-Forwarded-Port",
"protocolHeader": "X-Forwarded-Protocol-Header",
"remoteIpHeader": "X-Forwarded-Remote-IP-Header"
},
"useForwardHeaders": true
},
"spring": {
"cloud": {
"cluster": {
"leader": {
"enabled": true,
"id": "Sponge Bob",
"role": "Master Of The Universe"
}
},
"config": {
"allowOverride": true,
"failFast": true,
"overrideNone": false,
"overrideSystemProperties": false
},
"consul": {
"discovery": {
"healthCheckInterval": "15s",
"healthCheckPath": "/operations/health",
"instanceId": "${spring.application.name}:${random.value}",
"preferAgentAddress": true,
"preferIpAddress": true
},
"host": "localhost",
"port": 8500
},
"stream": {
"rabbit": {
"binder": {
"addresses": [
"localhost"
],
"adminAdresses": [
"localhost"
],
"password": "guest",
"username": "guest",
"vhost": "/"
}
}
}
},
"groovy": {
"template": {
"check-template-location": false
}
},
"inetutils": {
"defaultHostname": "localhost",
"ignoredInterfaces": [
"docker0"
],
"localhost": "127.0.0.1",
"timeoutSeconds": 1
},
"jackson": {
"serialization": {
"indent_output": true
},
"serialization-inclusion": "non_empty"
},
"main": {
"banner-mode": "console"
},
"oauth2": {
"client": {
"accessTokenUri": "https://github.com/login/oauth/access_token",
"clientAuthenticationScheme": "form",
"clientId": "a1c5113ec74d4fc16c69",
"clientSecret": "aac122e5814095dc3525f1a19e5274353330f09d",
"userAuthorizationUri": "https://github.com/login/oauth/authorize"
},
"resource": {
"preferTokenInfo": false,
"userInfoUri": "https://api.github.com/user"
}
},
"rabbitmq": {
"host": "localhost",
"password": "guest",
"port": 5672,
"username": "guest",
"virtualHost": "/"
}
},
"turbine": {
"aggregator": {
"clusterConfig": "${spring.application.name}"
},
"appConfig": "${spring.application.name}"
}
}