Microservice 其實不是很好管理,可想而知會有非常多路由、組態、監控等問題要搞,但是如果你團隊都是用Java的話,基本上 SpringCloud 提供非常多組件,讓你使用一些簡單設定檔跟 Annotation 就可以搞定 Discovery、Synchronize Settings、Proxy、LoadBalance、Realtime Dashboards、LogAnalyzer 等機制,例如下圖。
選擇組件的話到這邊http://start.spring.io/勾選需要的組件後下載專案即可
這次練習選擇使用 SpringBoot1.3 & Gradle
使用 SpringBoot 建立 RestAPI
建立統一管理的 Config Server
新增自動發現服務
新增 Proxy 機制
增加 LoadBalance 機制
透過 redis 來轉發請求
增加服務中斷時的回覆訊息
即時監控
Log 收集
References
使用 SpringBoot 建立 RestAPI
先建立一個 reservation-service 專案
使用到的組件如下
Web | JPA | Config Client | Eureka Discovery | Zipkin | Stream Redis | H2 | Actuator |
Rest Repositories | - | - | - | - | - | - | - |
首先先把下面這幾個依賴註解起來,因為暫時用不到
build.gradle
dependencies {
/*
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
compile('org.springframework.boot:spring-boot-starter-actuator')
*/
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
application.properties
server.port=8025
ReservationServiceApplication.java
package com.example;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
@SpringBootApplication
public class ReservationServiceApplication {
//起動的時候預先塞測試資料
@Bean
CommandLineRunner runner(ReservationRepository rr){
return args -> {
Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
.forEach( x -> rr.save(new Reservation(x)));;
rr.findAll().forEach( System.out::println);
};
}
public static void main(String[] args) {
SpringApplication.run(ReservationServiceApplication.class, args);
}
}
//這個註解是把你的Repository直接變成 RESTful API
@RepositoryRestResource
interface ReservationRepository extends JpaRepository<Reservation, Long>{
@RestResource(path = "by-name")
Collection<Reservation> findByReservationName( @Param("rn") String rn);
}
@Entity
class Reservation{
@Id
@GeneratedValue
private Long id;
private String reservationName;
public Reservation(){}
public Reservation(String reservationName) {
this.reservationName = reservationName;
}
public Long getId() {
return id;
}
public String getReservationName() {
return reservationName;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Reservation{");
sb.append("id=").append(id);
sb.append(", reservationName='").append(reservationName).append("'}");
return sb.toString();
}
}
這是產生器幫我們建立的測試範本ReservationServiceApplicationTests.java
package com.example;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ReservationServiceApplication.class)
@WebAppConfiguration
public class ReservationServiceApplicationTests {
@Test
public void contextLoads() {
}
}
接著使用GET 向 http://localhost:8025/reservations 取得資料,得到這樣的結果
{
"_embedded": {
"reservations": [
{
"reservationName": "Dr. rod",
"_links": {
"self": {
"href": "http://localhost:8025/reservations/1"
},
"reservation": {
"href": "http://localhost:8025/reservations/1"
}
}
},
{
"reservationName": "Dr. Syer",
"_links": {
"self": {
"href": "http://localhost:8025/reservations/2"
},
"reservation": {
"href": "http://localhost:8025/reservations/2"
}
}
},
{
"reservationName": "Juergen",
"_links": {
"self": {
"href": "http://localhost:8025/reservations/3"
},
"reservation": {
"href": "http://localhost:8025/reservations/3"
}
}
},
{
"reservationName": "ALL THE COMMUNITY",
"_links": {
"self": {
"href": "http://localhost:8025/reservations/4"
},
"reservation": {
"href": "http://localhost:8025/reservations/4"
}
}
},
{
"reservationName": "Josh",
"_links": {
"self": {
"href": "http://localhost:8025/reservations/5"
},
"reservation": {
"href": "http://localhost:8025/reservations/5"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8025/reservations"
},
"profile": {
"href": "http://localhost:8025/profile/reservations"
},
"search": {
"href": "http://localhost:8025/reservations/search"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
簡單幾行就可以把資料庫轉成 RESTful API 主要是靠這個 @RepositoryRestResource
建立統一管理的 Config Server
建立一個 config-server 專案
使用到的組件如下
Config Server |
主要依賴是 spring-cloud-config-server
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-config-server')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
application.properties
spring.cloud.config.server.git.uri=D:/springcloud/config-repo server.port=8888
-
spring.cloud.config.server.git.uri 設定應用起動時從哪裡讀取設定檔,可以從Github,也可以從本地Git檔案
D:/springcloud/config-repo資料夾放的檔案
application.yml
server.port: ${PORT:${SERVER_PORT:0}}
info.id: ${spring.application.name}
debug: true
spring.sleuth.log.json.enabled: true
logging.pattern.console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"
reservation-service.properties
server.port=${PORT:8000} message=HELLO world! spring.cloud.stream.bindings.input=reservations
起動程式
ConfigServerApplication.java
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.server.EnableConfigServer; @EnableConfigServer @SpringBootApplication public class ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); } }
記得加上@EnableConfigServer就可以啟動ConfigServer的功能了
起動 Config-Server 後可以訪問 http://localhost:8888/reservation-service/master 就可以取得設定相關資料
{
"name": "reservation-service",
"profiles": [
"master"
],
"label": null,
"version": "b017cbcb47700df4ffd7e824614532dd18128040",
"propertySources": [
{
"name": "D:/springcloud/config-repo/reservation-service.properties",
"source": {
"server.port": "${PORT:8000}",
"spring.cloud.stream.bindings.input": "reservations",
"message": "HELLO world!"
}
},
{
"name": "D:/springcloud/config-repo/application.yml",
"source": {
"server.port": "${PORT:${SERVER_PORT:0}}",
"info.id": "${spring.application.name}",
"debug": true,
"spring.sleuth.log.json.enabled": true,
"logging.pattern.console": "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([trace=%X{X-Trace-Id:-},span=%X{X-Span-Id:-}]){yellow} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"
}
}
]
}
修改 reservation-service
把原先得設定檔改成依靠 Config-Server 提供的
把原先註解掉的依賴加回去 spring-cloud-starter-config 跟 spring-boot-starter-actuator
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-config')
/*
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
*/
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
記得更新一下依賴
把原本的application.properties重新命名為bootstrap.properties並改成以下內容
bootstrap.properties
spring.application.name=reservation-service spring.cloud.config.uri=http://localhost:8888
-
spring.application.name 應用自己的名稱,到時候可以從介面上看到,也必須對應到設定檔的名稱
-
spring.cloud.config.uri Config-Server的位置
增加個控制器可以顯示從Condif-Server得到的資料
@RefreshScope @RestController class MessageRestControler{ @Value("${message}") private String message; @RequestMapping("/message") String message(){ return this.message; } }
只要啟動後可以在 http://localhost:8000/message 取得 HELLO world! 的資料
注意 Port 變了喔,因為一開始就從 Config-Server 取得 reservation-service.properties 的內容,也取得了 message=HELLO world! 的內容來呈現。
加上 @RefreshScope 用意是當設定檔有變更時,你可以透過 URL 來觸發更新
curl -X POST 'http://localhost:8000/refresh'
但是要怎麼隨時保持同步請看另外一篇研究
使用 SpringCloud 同步所有節點設定
新增自動發現服務
建立一個 eureka-server 專案
使用到的組件如下
Config Client | Eureka Server |
新增 bootstrap.properties 然後把不需要的 application.properties 刪除,因為組態檔我們使用 Config Server 提供的
bootstrap.properties
spring.application.name=eureka-service spring.cloud.config.uri=http://localhost:8888
新增 eureka-service.properties 到 Config-Server 設定檔路徑底下
eureka-service.properties
server.port=${PORT:8761} eureka.client.register-with-eureka=false eureka.client.fetch-registry=false #eureka.client.enabled=false eureka.instance.hostname=localhost #eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
其實 hostname 理論上是可以不用加的,但是會出現以下的錯誤訊息,不過網路上是說這沒關係是沒有Client的錯誤
2015-11-09 14:53:30.685 ERROR 21880 --- [trace=,span=] [nio-8761-exec-1] c.n.eureka.resources.StatusResource : Could not determine if the replica is available
java.lang.NullPointerException: null
at com.netflix.eureka.resources.StatusResource.isReplicaAvailable(StatusResource.java:87)
at com.netflix.eureka.resources.StatusResource.getStatusInfo(StatusResource.java:67)
at org.springframework.cloud.netflix.eureka.server.EurekaController.status(EurekaController.java:68)
主程式部份
EurekaServerApplication.java
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
起動後就可以從 http://localhost:8761/ 觀察到目前有哪些服務
修改 reservation-service 新增自動發現的客戶端
把 spring-cloud-starter-eureka 依賴加回去
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
/*
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
*/
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
記得更新依賴後在起動類別加上 @EnableDiscoveryClient
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationServiceApplication {
//起動的時候預先塞測試資料
@Bean
CommandLineRunner runner(ReservationRepository rr){
return args -> {
Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
.forEach( x -> rr.save(new Reservation(x)));;
rr.findAll().forEach( System.out::println);
};
}
public static void main(String[] args) {
SpringApplication.run(ReservationServiceApplication.class, args);
}
}
起動後再回去 http://localhost:8761/ 觀察,就可以發現在 DS Replicas Instances currently registered with Eureka 列表中多了一個 RESERVATION-SERVICE 的應用名稱
新增 Proxy 機制
建立一個 reservation-client 專案
使用到的組件如下
HATEOAS | Config Client | Eureka Discovery | Zuul | Hystrix | Zipkin | Stream Redis | Actuator |
先暫時將 zipkin 跟 hateoas 註解起來練習 Proxy 機制
build.gradle
dependencies {
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile('org.springframework.cloud:spring-cloud-starter-hystrix')
/*
compile('org.springframework.boot:spring-boot-starter-hateoas')
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
*/
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
compile('org.springframework.cloud:spring-cloud-starter-zuul')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
把範例的 application.properties 刪掉並新增 bootstrap.properties
bootstrap.properties
spring.application.name=reservation-client spring.cloud.config.uri=http://localhost:8888
起動程式
ReservationClientApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {
public static void main(String[] args) {
SpringApplication.run(ReservationClientApplication.class, args);
}
}
在原先的範例程式上增加 @EnableZuulProxy 跟 @EnableDiscoveryClient
起動後即可 http://localhost:8050/reservation-service/reservations 取得原本 reservation-service 上的資料如下,可以很明顯的得出來多了一層 reservation-service 代理路徑
{
"_embedded": {
"reservations": [
{
"reservationName": "Dr. rod",
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations/1"
},
"reservation": {
"href": "http://localhost:8050/reservation-service/reservations/1"
}
}
},
{
"reservationName": "Dr. Syer",
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations/2"
},
"reservation": {
"href": "http://localhost:8050/reservation-service/reservations/2"
}
}
},
{
"reservationName": "Juergen",
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations/3"
},
"reservation": {
"href": "http://localhost:8050/reservation-service/reservations/3"
}
}
},
{
"reservationName": "ALL THE COMMUNITY",
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations/4"
},
"reservation": {
"href": "http://localhost:8050/reservation-service/reservations/4"
}
}
},
{
"reservationName": "Josh",
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations/5"
},
"reservation": {
"href": "http://localhost:8050/reservation-service/reservations/5"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8050/reservation-service/reservations"
},
"profile": {
"href": "http://localhost:8050/reservation-service/profile/reservations"
},
"search": {
"href": "http://localhost:8050/reservation-service/reservations/search"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
增加 LoadBalance 機制
修改 reservation-client 專案
先把依賴 hateoas 加回去
build.gradle
dependencies {
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
compile('org.springframework.cloud:spring-cloud-starter-hystrix')
compile('org.springframework.boot:spring-boot-starter-hateoas')
/*
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
*/
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
compile('org.springframework.cloud:spring-cloud-starter-zuul')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
然後新增一個控制器
@RestController
@RequestMapping("/reservations")
class ReservationApiGatewayRestController{
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
@RequestMapping("names")
public Collection<String> getReservationNames(){
ParameterizedTypeReference<Resources<Reservation>> ptr =
new ParameterizedTypeReference<Resources<Reservation>>(){};
ResponseEntity<Resources<Reservation>> responseEntity =
this.restTemplate.exchange("http://reservation-service/reservations",
HttpMethod.GET, null, ptr);
Collection<String> nameList = responseEntity
.getBody()
.getContent()
.stream()
.map(Reservation::getReservationName)
.collect(Collectors.toList());
return nameList;
}
}
這邊其實猜得出來它的使用方式,然後下面的部分是Java8的語法喔。
再啟動後從 http://localhost:8050/reservations/names 嘗試抓取資料,結果是可以取得預期的姓名清單
[ "Dr. rod", "Dr. Syer", "Juergen", "ALL THE COMMUNITY", "Josh" ]
透過 redis 來轉發請求
修改 reservation-client 專案
先修改 build.gradle 把 spring-cloud-starter-stream-redis 加進來
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
/*
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
*/
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
主程式上方新增 @EnableBinding (Source.class)
@EnableZuulProxy
@EnableBinding (Source.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {
public static void main(String[] args) {
SpringApplication.run(ReservationClientApplication.class, args);
}
}
在 Controller 中加上
@Autowired @Output(Source.OUTPUT) private MessageChannel messageChannel; @RequestMapping( method = RequestMethod.POST) public void write(@RequestBody Reservation r){ this.messageChannel.send(MessageBuilder.withPayload(r.getReservationName()).build()); }
修改 reservation-service
修改 build.gradle 把 spring-cloud-starter-stream-redis 加進來
build.gradle
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-config')
compile('org.springframework.cloud:spring-cloud-starter-eureka')
/*
compile('org.springframework.cloud:spring-cloud-starter-zipkin')
*/
compile('org.springframework.cloud:spring-cloud-starter-stream-redis')
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
然後在主程式加上 @EnableBinding(Sink.class)
@EnableBinding(Sink.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationServiceApplication {
//起動的時候預先塞測試資料
@Bean
CommandLineRunner runner(ReservationRepository rr){
return args -> {
Arrays.asList("Dr. rod,Dr. Syer,Juergen,ALL THE COMMUNITY,Josh".split(","))
.forEach( x -> rr.save(new Reservation(x)));;
rr.findAll().forEach( System.out::println);
};
}
public static void main(String[] args) {
SpringApplication.run(ReservationServiceApplication.class, args);
}
}
再增加一個訊息接入點
@MessageEndpoint
class MessageReservationReceiver{
@Autowired
private ReservationRepository reservationRepository;
@ServiceActivator(inputChannel = Sink.INPUT)
public void acceptReservation(String rn){
this.reservationRepository.save(new Reservation(rn));
}
}
然後再回到 Config-Server 的設定檔資料夾加上 spring.redis.host ,因為是透過 redis 來收送所以當然是要給位置才能用
application.yml
spring.redis.host: "localhost"
這邊測試而已,還要裝個 redis 就太大費周章了,直接用 Docker 來跑吧
Vagrantfile.proxy
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.provision "docker"
config.vm.provision "shell", inline:
"ps aux | grep 'sshd:' | awk '{print $2}' | xargs kill"
config.vm.provider :virtualbox do |vb|
vb.name = "redis"
vb.gui = $vm_gui
vb.memory = $vm_memory
vb.cpus = $vm_cpus
end
config.vm.network :forwarded_port, guest: 6379, host: 6379
config.ssh.username = "vagrant"
config.ssh.password = "vagrant"
end
Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Specify Vagrant version and Vagrant API version
#Vagrant.require_version ">= 1.6.0"
VAGRANTFILE_API_VERSION = "2"
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker'
$vm_gui = false
$vm_memory = 2048
$vm_cpus = 2
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.define "redis" do |v|
v.vm.provider "docker" do |d|
d.name = "redis"
d.image = "redis"
d.ports = ["6379:6379"]
d.vagrant_vagrantfile = "./Vagrantfile.proxy"
end
end
end
兩個檔案放同一個資料夾後接著執行
vagrant up redis --provider=docker
好啦,程式跟環境都好了,接著把程式都叫起來,接著透過 POST 新增資料從 reservation-client -> redis -> reservation-service 寫入資料庫
curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" -d '{"reservationName":"Red Johnson"}' 'http://localhost:8050/reservations'
再重新查看 http://localhost:8050/reservations/names 就可以看到多了 Red Johnson 的名字
為什麼要這樣做?我覺得是為了突發量,有時就算你的應用可以水平拓展,但是來不及拓也是GG...
增加服務中斷時的回覆訊息
有時候還是會有意外,但是出現問題如果跑出奇怪的錯有可能讓前端措手不及,或是稍微偽裝一下,可以讓客戶端無感異常
修改 reservation-client 專案
起 動程式增加 @EnableCircuitBreaker ,然後在需要此功能的方法上增加 @HystrixCommand(fallbackMethod = "getReservationNamesFallback") 當失敗時他就會使用你指定的方法 getReservationNamesFallback 來回覆前端
@EnableZuulProxy
@EnableBinding(Source.class)
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {
public static void main(String[] args) {
SpringApplication.run(ReservationClientApplication.class, args);
}
}
@RestController
@RequestMapping("/reservations")
class ReservationApiGatewayRestController{
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
@Autowired
@Output(Source.OUTPUT)
private MessageChannel messageChannel;
@RequestMapping( method = RequestMethod.POST)
public void write(@RequestBody Reservation r){
this.messageChannel.send(MessageBuilder.withPayload(r.getReservationName()).build());
}
public Collection<String> getReservationNamesFallback(){
return Collections.emptyList();
}
@HystrixCommand(fallbackMethod = "getReservationNamesFallback")
@RequestMapping("names")
public Collection<String> getReservationNames(){
ParameterizedTypeReference<Resources<Reservation>> ptr =
new ParameterizedTypeReference<Resources<Reservation>>(){};
ResponseEntity<Resources<Reservation>> responseEntity =
this.restTemplate.exchange("http://reservation-service/reservations",
HttpMethod.GET, null, ptr);
Collection<String> nameList = responseEntity
.getBody()
.getContent()
.stream()
.map(Reservation::getReservationName)
.collect(Collectors.toList());
return nameList;
}
}
reservation-client 起動後,把 reservation-service 關掉,這麼一來通常應用程式就會發生異常回傳 500 之類的,但是你可以試著呼叫 http://localhost:8050/reservations/names,你可以發現你只是得到一個空集合,不影響你的前端程式
[]
即時監控
建立一個 hystrix-dashboard 專案
使用到的組件如下
Config Client | Eureka Discovery | Hystrix Dashboard |
一樣移除 application.properties,因為主要設定我們現在都依靠 Config-Server 的提供,再新增bootstrap.properties
bootstrap.properties
spring.application.name=hystrix-dashboard spring.cloud.config.uri=http://localhost:8888
然後主程式啟用 @EnableHystrixDashboard
HystrixDashboardApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
然後在 Config-Server 設定檔資料夾中新增
hystrix-dashboard.properties
server.port=${PORT:8010}
然後起動 hystrix-dashboard ,接著訪問
http://localhost:8050/hystrix.stream
你可以看到我們 reservation-client 一直在吐資料
然後把上面網址貼在下面網頁中間欄位
http://localhost:8010/hystrix.html
然後按下 Monitor Stream 按鈕,你就可以看到一個監控的介面
當後端執行成功或是失敗你都可以即時的發現到
Log 收集
有時知道錯在哪一個環節,但是沒有記錄還是很難找問題, spring-cloud-starter-zipkin 就可以記錄每一個 service 之間的資料傳遞,官網 https://twitter.github.io/zipkin/Quickstart.html
這東西 twitter 做的,裝起來應該也是很煩人,改天再試看看,再研究一下怎麼用 Docker 頂一下
https://github.com/joshlong/cloud-native-workshop/blob/master/bin/zipkin/docker-compose.yml
先說程式部分
把 reservation-service 、 reservation-client 原先註解掉的依賴 spring-cloud-starter-zipkin 加回去
然後這兩個專案內註冊 @Bean ,程式端就完成了
@Bean AlwaysSampler alwaysSampler(){ return new AlwaysSampler(); }
http://my.oschina.net/xliangbo/blog/608910