sfogo / spring-democloud

Simple Docker-ized demo with Config Server, Eureka, Zuul, Turbine, Hystrix dashboard, Rabbit MQ and a few services.
2 stars 2 forks source link

Spring Cloud Demo

Overview

This is a simple (not secured) demo that can be run with or without containers and that showcases a possible (if not typical) microservices Spring Cloud landscape where :

Applications

Summary

Application Context Path Port Comment
Configuration Server / 8888 Management context path is /admin
Gateway /gateway 8099 Routes /gateway/m1 to M1 Service
Routes /gateway/m2 to M2 Service
Turbine / 8989 Management port 8991
Eureka / 8761
Dashboard / 7980 Management port 7981
M1 Service / 8091 GET /items/{id} invokes both one outside resource and M3 (see interaction diagram)
M2 Service / 8092 Same as M1 with M2 tag
M3 Service / 8093 Counter service
POST /counters/{tag} increments counter
GET /counters/{tag} gets counter value
GET /counters retrieves all counters

Notes

Application level annotations

Application Annotations
Configuration Server @EnableConfigServer
Gateway @EnableZuulProxy
Turbine @EnableTurbineStream
Eureka @EnableEurekaServer
Dashboard @EnableHystrixDashboard
@EnableTurbineStream
M1 Service @EnableCircuitBreaker : some calls are wrapped with @HystrixCommand
@EnableFeignClients : invocations of M3 are feigned with @FeignClient("m3-service")
@RestController
M2 Service Same as M1
M3 Service @RestController

Interaction Diagram

<img src="https://cloud.githubusercontent.com/assets/13286393/17678268/36026ab8-62eb-11e6-9725-ac3e5d5564b1.png" border="0" width="60%" />

Client Side Load Balancing

Ribbon provides client-side load balancing. It will typically be used for Gateway Routing as well as with other App to App communication.
<img src="https://cloud.githubusercontent.com/assets/13286393/17674082/df849a7e-62d8-11e6-9c20-c9254f338c4a.png" border="0" width="40%" />

Using the Feign declaration, it is even easier to get a load-balanced invocation. Feign is an extremely handy shortcut that :

In this demo, M1 and M2 invocations of M3 are feigned.

Actuator

Spring Cloud emphasizes the importance of Spring Actuator endpoints as most participants must have them enabled to participate fully (especially for Hystrix streams). It also shows the extent of Spring configurability. Here are some stats (pulled from using the actuator demo app) for demo services that have almost no customization.

Application # of env props # of config props # of metrics
Configuration Server 149 262 37
Gateway 165 365 91
Turbine 159 380 135
Eureka 159 412 126
Dashboard 159 398 82
M1 Service 156 412 264
M2 Service 156 412 264
M3 Service 155 328 90

Run locally

Start all the pieces

$ ./run-all.sh 
Starting config-server...
config-server started PID:13325 Log:/tmp/democloud/config-server.pid.13325.txt
Starting eureka...
eureka started PID:13382 Log:/tmp/democloud/eureka.pid.13382.txt
Starting m3-service...
m3-service started PID:13483 Log:/tmp/democloud/m3-service.pid.13483.txt
Starting m2-service...
m2-service started PID:13576 Log:/tmp/democloud/m2-service.pid.13576.txt
Starting m1-service...
m1-service started PID:13649 Log:/tmp/democloud/m1-service.pid.13649.txt
Starting gateway...
gateway started PID:13754 Log:/tmp/democloud/gateway.pid.13754.txt
Starting turbine...
turbine started PID:13845 Log:/tmp/democloud/turbine.pid.13845.txt
Starting dashboard...
dashboard started PID:13926 Log:/tmp/democloud/dashboard.pid.13926.txt
Done.
You can shut it all down with : kill `cat /tmp/democloud/pids.txt`

Eureka

Configuration Server

Dashboard

<img src="https://cloud.githubusercontent.com/assets/13286393/17682185/c100f2c0-62fe-11e6-8297-9ea9a053a49a.png" border="0" width="90%" />

Add instances

M1 Service

curl http://localhost:8091 {"counter":{"name":"m1-service","value":0}, "message":"Hi! My name is m1.","config.uri":"http://localhost:8888"}


* Curl the gateway twice for m1 and you can see it alternates between M1 instances

curl http://localhost:8099/gateway/m1 {"counter":{"name":"m1-service","value":0}, "message":"I am M1 at port 8191","config.uri":"Not Applicable"}

curl http://localhost:8099/gateway/m1 {"counter":{"name":"m1-service","value":0}, "message":"Hi! My name is m1.","config.uri":"http://localhost:8888"}

* Refresh Eureka `http://localhost:8761`. M1 is now multi-instances.  
<img src="https://cloud.githubusercontent.com/assets/13286393/17723727/3d1b9728-63f1-11e6-8082-455215d96b59.png"
     border="0" width="80%" />

#### M2 Service
* Test file contains a JSON structure, value for `spring.application.json`

cd m2-service cat ../testing/m2-instance-at-8192.txt { "demo":{"message":"M2 Service at port 8192","resource":"http://vachement.net/api/items"}, "eureka.client.serviceUrl.defaultZone":"http://localhost:8761/eureka/", "server":{"port":8192}, "spring":{ "application":{"name":"m2-service"}, "rabbitmq":{"host":"localhost","port":5672}, "cloud.config.uri":"Not Applicable" }, "endpoints":{"cors":{ "allowedOrigins":"*", "allowedMethods":"POST, GET, OPTIONS, DELETE", "maxAge":"3600", "allowedHeaders":"x-requested-with, authorization"} } }

* Flatten JSON structure (hence the sed and tr). Set value for `spring.application.json`

mvn spring-boot:run \ -Dspring.cloud.bootstrap.enabled=false \ -Dspring.application.json="cat ../testing/m2-instance-at-8192.txt | sed 's/^[ \t]*//' | tr -d '\n'"

* Check home endpoint

curl http://localhost:8192 {"counter":{"name":"m2-service","value":0}, "message":"M2 Service at port 8192","config.uri":"Not Applicable"}


### Actuator Data
* Deploy [actuator app](https://github.com/sfogo/spring-actuator-data)  
`mvn package`  
`java -jar target/dependency/webapp-runner.jar --port 7070 target/gs-actuator-service-0.1.0`
* Go to `http://localhost:7070/app/actuate/index.html` (credentials are config / config) and change the actuator URL to one of the demo apps (for instance `http://localhost:8092` or `http://localhost:8099/gateway`)  
_(this is possible because all participants [enable CORS](config-server/src/main/resources/shared/application.yml))_  
<img src="https://cloud.githubusercontent.com/assets/13286393/17682184/c0ef47b4-62fe-11e6-8d04-64282f332ad1.png"
     border="0" width="80%" />
* Environment  
<img src="https://cloud.githubusercontent.com/assets/13286393/17682182/c0ecd52e-62fe-11e6-831e-c5eaa9388fb2.png"
     border="0" width="80%" />
<img src="https://cloud.githubusercontent.com/assets/13286393/17682181/c0e9bbd2-62fe-11e6-80ca-15d57a10e0d4.png"
     border="0" width="80%" />

## Run in Docker containers
### Build and run
* Package all modules  
`mvn clean package`  
Set `SPRING_PROFILES_ACTIVE` environment variable to select `docker` profile  
`export SPRING_PROFILES_ACTIVE=docker`  
Build Docker images and start containers  
`docker-compose -f ./docker-compose.yml up -d --build`  

* All services still go by the [Config First Bootstrap](http://cloud.spring.io/spring-cloud-static/spring-cloud.html#config-first-bootstrap) and the [fail fast](http://projects.spring.io/spring-cloud/spring-cloud.html#config-client-fail-fast) options. No starting order is mandated and therefore the Configuration Server may not yet be ready when a service starts up : it will fail but the `restart: always` option present in [Docker Compose file](docker-compose.yml) will restart the container. It may then take a few `Spring fail fast / Docker restart` cycles until the Configuration Server is found at boot time. On my system, it takes at least 3 to 4 minutes for all pieces to be up and running.

### Composition
In [Docker Compose file](docker-compose.yml), [Spring profile](http://docs.spring.io/spring-boot/docs/current/reference/html/howto-properties-and-configuration.html#howto-change-configuration-depending-on-the-environment) named `docker` is enabled with environment variable `SPRING_PROFILES_ACTIVE`. Profiles are used in application configuration files (see [example](m1-service/src/main/resources/bootstrap.yml)), enabling configuration properties to be segegrated by [profile](http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-profile-specific-properties) to work in different environments (_for instance dev vs. prod_). There are multiple [ways](http://docs.spring.io/spring-boot/docs/current/reference/html/howto-properties-and-configuration.html#howto-set-active-spring-profiles) to set profiles active (i.e. select one or more profiles) and using an environment variable is just one of them.

[Docker Compose file](docker-compose.yml) builds new images except for RabbitMQ whose image is pulled from the [hub](https://hub.docker.com/_/rabbitmq/). Containers internally use the same ports as with the demo without containers (_but they could internally all use the same port_). Only the following pieces are externally exposed :

|Component|Externally|Container|
|---|---|---|
|Configuration Server|`8888`|`8888`|
|Eureka|`8761`|`8761`|
|Gateway|`80`|`8099`|
|Dashboard|`7980`|`7980`|
|Rabbit MQ Console|`15672`|`15672`|
* m1, m2 and m3 services can only be accessed through the gateway.
* Turbine stream at port `8989` is not externally exposed but the [Hystrix dashboard](http://localhost:7980/hystrix) can simply use `http://turbine:8989`. As in `docker` profile sections of configuration files, hostnames [**automatically created**](https://docs.docker.com/compose/networking/) by Docker compostion can be used for inter-container communication.

### Examples
* `curl http://localhost/gateway/m1/items/123-abc-456`  
`{"item":"123-abc-456","server":"vachement.net","time":{"millis":1473011217,"text":"2016-09-04T10:46:57-07:00","day":"Sun","week":"35"},"counter":{"name":"m1-service","value":268},"instance-counter":{"name":"m1-service-00000156-f64a-32eb-0000-00002a65fe7c","value":267},"message":"Hi! My name is m1."}`

* `curl http://localhost/gateway/m2/items/321-xyz-123`  
`{"item":"321-xyz-123","server":"vachement.net","time":{"millis":1473011296,"text":"2016-09-04T10:48:16-07:00","day":"Sun","week":"35"},"counter":{"name":"m2-service","value":268},"instance-counter":{"name":"m2-service-00000156-f64a-3d3e-0000-00002a65fe7c","value":266},"message":"Hi! My name is m2."}`

* `curl http://localhost/gateway/m3/counters`  
`[{"name":"m1-service-00000156-f64a-32eb-0000-00002a65fe7c","value":267},{"name":"m2-service","value":268},{"name":"m2-service-00000156-f64a-3d3e-0000-00002a65fe7c","value":266},{"name":"m1-service","value":268}]`

* Generate some traffic  
`./generate-traffic.py 91 -docker`

### Images

docker images REPOSITORY TAG IMAGE ID CREATED SIZE springdemocloud_config latest 1f05f50f151f About an hour ago 188.2 MB springdemocloud_dashboard latest 3c6f07155076 About an hour ago 209.1 MB springdemocloud_gateway latest f5d7f966fef0 About an hour ago 204.7 MB springdemocloud_m3 latest 501b7a83d8a1 About an hour ago 202 MB springdemocloud_m1 latest bfc521741e09 About an hour ago 208.2 MB springdemocloud_turbine latest 57ff57564521 About an hour ago 207 MB springdemocloud_m2 latest e896f1acf388 About an hour ago 208.2 MB springdemocloud_registry latest 7e1a8ea78c57 About an hour ago 205.7 MB rabbitmq 3-management cb479f313c93 11 days ago 180.8 MB frolvlad/alpine-oraclejdk8 slim ea24082fc934 6 weeks ago 167.1 MB

### Add container instances
* Check m1 containers  

docker ps | grep springdemocloud_m1 6cc07ca3b191 springdemocloud_m1 "java -Xmx200m -jar /" About an hour ago Up About an hour 8091/tcp springdemocloud_m1_1

* Scale by 1 (_make sure you still have SPRING&#95;PROFILES&#95;ACTIVE env variable set so that docker compose picks it up_)  

docker-compose scale m1=2 Creating and starting springdemocloud_m1_2 ... done

* Re-check m1 containers  

docker ps | grep springdemocloud_m1 046c2aa1e0df springdemocloud_m1 "java -Xmx200m -jar /" 9 seconds ago Up 6 seconds
8091/tcp springdemocloud_m1_2 6cc07ca3b191 springdemocloud_m1 "java -Xmx200m -jar /" About an hour ago Up About an hour 8091/tcp springdemocloud_m1_1

* Eureka view. Note that we now have 2 instances at 2 different Docker network addresses but using the same port while with demo without containers we have 2 localhost instances using different ports.  
<img src="https://cloud.githubusercontent.com/assets/13286393/18227525/423d4f0a-71dc-11e6-915d-dc6b2cf1e419.png"
     border="0" width="80%" />
* Curl m1 multiple times and responses come from different instances  

curl http://localhost/gateway/m1/items/xyz {"item":"xyz","server":"vachement.net","time":{"millis":1472947637,"text":"2016-09-03T17:07:17-07:00","day":"Sat","week":"35"},"counter":{"name":"m1-service","value":53},"instance-counter":{"name":"m1-service-00000156-f278-8df3-0000-00002a65fe7c","value":28},"message":"Hi! My name is m1."} curl http://localhost/gateway/m1/items/123 {"item":"123","server":"vachement.net","time":{"millis":1472947644,"text":"2016-09-03T17:07:24-07:00","day":"Sat","week":"35"},"counter":{"name":"m1-service","value":54},"instance-counter":{"name":"m1-service-00000156-f27c-b25f-0000-00002a65fe7c","value":27},"message":"Hi! My name is m1."}