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
先建立一個 reservation-service 專案
使用到的組件如下
Web | Data | Cloud Config | Cloud Discovery | Cloud Tracing | Cloud Messaging | Database | Ops |
---|---|---|---|---|---|---|---|
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
使用到的組件如下
Cloud Config |
---|
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" } } ] }
把原先得設定檔改成依靠 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 同步所有節點設定
使用到的組件如下
Cloud Config | Cloud Discovery |
---|---|
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/ 觀察到目前有哪些服務
把 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 的應用名稱
使用到的組件如下
Web | Cloud Config | Cloud Discovery | Cloud Routing | Cloud Circuit Breaker | Cloud Tracing | Cloud Messaging | Ops |
---|---|---|---|---|---|---|---|
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 } }
先把依賴 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" ]
先修改 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()); }
修改 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...
有時候還是會有意外,但是出現問題如果跑出奇怪的錯有可能讓前端措手不及,或是稍微偽裝一下,可以讓客戶端無感異常
起動程式增加 @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,你可以發現你只是得到一個空集合,不影響你的前端程式
[]
使用到的組件如下
Cloud Config | Cloud Discovery | Cloud Circuit Breaker |
---|---|---|
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 按鈕,你就可以看到一個監控的介面
當後端執行成功或是失敗你都可以即時的發現到
有時知道錯在哪一個環節,但是沒有記錄還是很難找問題, 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(); }