Java 8
spring boot 2.5.2
spring cloud 2020.0.3
---
目錄
建立 Eureka Server項目,僅添加 spring-cloud-starter-netflix-eureka-server 依賴包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
給應用添加注解 @EnableEurekaServer:
1 @EnableEurekaServer 2 @SpringBootApplication 3 public class ServiceRegistrationAndDiscoveryServiceApplication { 4 5 public static void main(String[] args) { 6 SpringApplication.run(ServiceRegistrationAndDiscoveryServiceApplication.class, args); 7 } 8 9 }
添加配置:
server.port=8761
啟動應用:出現了一些異常信息,服務在8761端口啟動
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8761 (http) iguration$LoadBalancerCaffeineWarnLogger : Spring Cloud LoadBalancer is currently working with the default cache.
You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/},
exception=java.net.ConnectException: Connection refused: no further information stacktrace=com.sun.jersey.api.client.ClientHandlerException:
java.net.ConnectException: Connection refused: no further information ... Caused by: java.net.ConnectException: Connection refused: no further information c.n.d.s.t.d.RetryableEurekaHttpClient : Request execution failed with message: java.net.ConnectException: Connection refused: no further information com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/DESKTOP-BDNTQQ3:8761 - was unable to refresh its cache!
This periodic background refresh will be retried in 30 seconds. status = Cannot execute request on any known server stacktrace =
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server com.netflix.discovery.DiscoveryClient : Initial registry fetch from primary servers failed com.netflix.discovery.DiscoveryClient : Using default backup registry implementation which does not do anything. c.n.eureka.cluster.PeerEurekaNodes : Adding new peer nodes [http://localhost:8761/eureka/] c.n.eureka.cluster.PeerEurekaNodes : Replica node URL: http://localhost:8761/eureka/ c.n.eureka.DefaultEurekaServerContext : Initialized com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/DESKTOP-BDNTQQ3:8761: registering service... o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8761 (http) with context path '' .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8761
可以訪問 http://localhost:8761/ :
發現注冊了一個 UNKNOWN 應用,點擊 Status下的超鏈接,返回:
將鏈接 desktop-bdntqq3改為 localhost,仍然如此。
根據官網指南,Eureka Server會 注冊其本身,因此,添加下面的配置 1)禁止其注冊自身 和 2)禁止獲取配置:
eureka.client.register-with-eureka=false eureka.client.fetch-registry=false
啟動成功,沒有報錯,訪問 http://localhost:8761/ ,沒有發現注冊服務:
檢查 spring-cloud-starter-netflix-eureka-server 包的結構:原來,它已經包含了 spring-boot-starter-web/actuator、還包含了 spring-cloud-netflix-eureka-client 等包,難怪它會提供Web接口,也會注冊自己呢。
不過,其中 actuator 的 info接口 不能正常訪問,需要添加下面的配置:
management.endpoints.web.exposure.include=info,health
這樣,/actuator/info 端點 就會暴露出來了。
Eureka Server建立好了,接下來,通過Eureka Client注冊服務。
在 https://start.spring.io/ 建立項目——web-first,依賴包:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
上面建立了一個 Web服務。
導入Eclipse,啟動(啟動前,,Eureka Server已經啟動)。部分日志如下:
iguration$LoadBalancerCaffeineWarnLogger : Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using
Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath. com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1 c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration ... com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1626934699656 with initial instances count: 1 o.s.c.n.e.s.EurekaServiceRegistry : Registering application UNKNOWN with eureka with status UP com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1626934699658, current=UP, previous=STARTING] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/DESKTOP-BDNTQQ3: registering service... com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/DESKTOP-BDNTQQ3 - registration status: 204
訪問Eureka Server的 http://localhost:8761/ :
注冊成功,但是,Application 為 UNKNOWN。在Status中,超鏈接的名字為 電腦的 計算機名,地址為 http://desktop-bdntqq3:8080/actuator/info ,但是,訪問失敗——需要配置hosts才可以,
其中的 /actuator/info 像是 actuator的端點,但是,項目沒有引入 actuator,因此無法訪問。
疑問:
1)沒有任何配置,怎么就注冊成功了呢?一定是 依賴包spring-cloud-starter-netflix-eureka-client 自動做了一些事情;
2)注冊中心,Status下的超鏈接怎么不能訪問,要怎么配置?
解決Application為UNKNOWN的問題
在web-first項目中添加配置:
spring.application.name=web-first
再次啟動,部分日志:
com.netflix.discovery.DiscoveryClient : DiscoveryClient_WEB-FIRST/DESKTOP-BDNTQQ3:web-first: registering service... com.netflix.discovery.DiscoveryClient : DiscoveryClient_WEB-FIRST/DESKTOP-BDNTQQ3:web-first - registration status: 204
注冊中心顯示:
解決注冊中心下Status超鏈接地址為主機名稱的問題
在web-first項目中添加下面的配置:
eureka.instance.prefer-ip-address=true
再次啟動項目,此時,注冊中心下Status的超鏈接現實為IP地址:
http://192.168.125.197:8080/actuator/info
但這個接口 還是無法訪問。
更改注冊中心的端口
更改Eureka Server注冊服務地址為 8769,再次啟動 服務器、客戶端兩個服務,此時,客戶端 web-first項目 無法注冊成功,啟動時報錯:
c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/},
exception=I/O error on GET request for "http://localhost:8761/eureka/apps/": Connect to localhost:8761 [localhost/127.0.0.1,
localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException:
Connect to localhost:8761 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect
stacktrace=org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8761/eureka/apps/":
Connect to localhost:8761 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect; nested exception is
org.apache.http.conn.HttpHostConnectException: Connect to localhost:8761 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1]
failed: Connection refused: connect
web-first項目 啟動了,但是,注冊失敗。啟動后,仍然會 每隔30秒 執行一次注冊:
[nfoReplicator-0] c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/},
exception=I/O error on POST request for "http://localhost:8761/eureka/apps/WEB-FIRST": Connect to localhost:8761 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1]
failed: Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException: Connect to localhost:8761
[localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect stacktrace=org.springframework.web.client.ResourceAccessException:
I/O error on POST request for "http://localhost:8761/eureka/apps/WEB-FIRST": Connect to localhost:8761 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed:
Connection refused: connect; nested exception is org.apache.http.conn.HttpHostConnectException: Connect to localhost:8761 [localhost/127.0.0.1,
localhost/0:0:0:0:0:0:0:1] failed: Connection refused: connect [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_WEB-FIRST/DESKTOP-BDNTQQ3:web-first - registration failed Cannot execute
request on any known server [freshExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_WEB-FIRST/DESKTOP-BDNTQQ3:web-first - was unable to refresh its cache!
This periodic background refresh will be retried in 30 seconds. status = Cannot execute request on any known server stacktrace =
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
啟動期間的線程時 main,項目啟動后,使用異步線程執行注冊了。
解決這個問題的方法是,添加下面的配置:
eureka.client.service-url.defaultZone=http://localhost:8769/eureka/
再次啟動web-first項目,沒有報錯,注冊成功。
注:在注冊中心中,有一個 DS Replicas 項目,其下為 localhost 超鏈接,其仍然指向 8761端口:http://localhost:8761/eureka/
頁面上還有其它現實 8761端口的地方。
因此,此時需要修改Eureka Server的配置,同 web-first的 新增配置:
eureka.client.service-url.defaultZone=http://localhost:8769/eureka/
再次啟動 Eureka Server,服務啟動成功:,上面的 DS Replicas 下 超鏈接正常了:
http://localhost:8769/eureka/
特別提醒:
配置中的 defaultZone 必須是 駝峰格式,不能寫成 default-zone,否則,無效。
因為,eureka.client.service-url 是一個 Map類型。
更多細節還需 深挖。
服務器的 /eureka 端點 不能訪問
雖然配置了 /eureka 端點,但是,其不能訪問。
可以通過 /eureka/apps 來訪問 注冊到注冊中心的 應用:
/eureka/apps/ + 服務名稱 還可以訪問 服務的信息:
更進一步:
/eureka/apps/ + 服務名稱 + / + instance名稱 還可以訪問實例詳情,略。
啟動兩個同名的服務
上面注冊 WEB-FIRST 服務的一個實例,接下來使用 端口9090 啟動一個 web-first服務。
-Dserver.port=9090
啟動后的注冊中心頁面:
注:同一台主機,端口需要不同;如果是不同主機運行,則無妨。一般來說,同一個 application 的多個實例 是 運行在不同主機上。
更多配置,TODO
在web-first新建接口:
1 @RestController 2 class HelloController { 3 // GET請求
4 @GetMapping(path="getTime") 5 public String getTime() { 6 return new Date().toGMTString(); 7 } 8 // POST請求
9 @PostMapping(path="updateUser") 10 public String updateUser(@RequestBody User u) { 11 System.out.println("參數 User u = " + u); 12 return "OK"; 13 } 14
15 } 16
17 class User { 18 private String name; 19 private Integer age; 20 // getter, setter, toString...
21
22 }
新建Web項目 web-second,注冊到注冊中心,pom.xml同 web-first。
修改 web-second 的配置:端口為 10000
server.port=10000 spring.application.name=web-second eureka.instance.prefer-ip-address=true eureka.client.service-url.defaultZone=http://localhost:8769/eureka/
啟動 web-second。注冊中心頁面現實多了 WEB-SECOND 服務:
接下來,通過 WEB-SECOND 項目調用 WEB-FIRST 的兩個接口。
1 @Configuration 2 class MyConfiguration { 3
4 @LoadBalanced 5 @Bean 6 RestTemplate restTemplate() { 7 return new RestTemplate(); 8 } 9
10 } 11
12 @Component 13 class MyRunner implements CommandLineRunner { 14
15 @Autowired 16 private RestTemplate restTemplate; 17
18 @Override 19 public void run(String... args) throws Exception { 20 String results = restTemplate.getForObject("http://web-first/getTime", 21 String.class); 22 System.out.println("GET請求 results=" + results); 23
24 User user = new User(); 25 user.setName("web-second"); 26 user.setAge(1); 27 ResponseEntity<String> rt = restTemplate.postForEntity("http://web-first/updateUser", user, String.class); 28 System.out.println("POST請求 rt=" + rt + ", rt.body=" + rt.getBody()); 29 } 30
31 } 32
33 class User { 34 private String name; 35 private Integer age; 36 // getter, setter, toString...
37 }
測試結果:調用成功。
GET請求 results=22 Jul 2021 08:27:11 GMT POST請求 rt=<200,OK,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"2", Date:"Thu, 22 Jul 2021 08:27:11 GMT", Keep-Alive:"timeout=60",
Connection:"keep-alive"]>, boty=OK
增加一個 WEB-FIRST 服務——端口9090,再多次執行 WEB-SECOND 中的調用,檢查是否有 負載均衡:
修改 WEB-SECOND 的代碼:每隔2秒執行一輪,共10輪。
1 @Override 2 public void run(String... args) throws Exception { 3
4 for (int i=0; i<10; i++) { 5 String results = restTemplate.getForObject("http://web-first/getTime", 6 String.class); 7 System.out.println("GET請求 results=" + results); 8
9 User user = new User(); 10 user.setName("web-second"); 11 user.setAge(i); 12 ResponseEntity<String> rt = restTemplate.postForEntity("http://web-first/updateUser", user, String.class); 13 System.out.println("POST請求 rt=" + rt + ", rt.body=" + rt.getBody()); 14
15 System.out.println(); 16
17 // 每隔2秒執行一輪
18 TimeUnit.SECONDS.sleep(2); 19 } 20 }
檢查 兩個WEB-FIRST 服務的日志,是否 均衡地 收到了請求?NO!所有請求由一個服務器處理了。
更正WEB-FIRST的測試程序:GET、POST都輸出日志(之前只有 updateUser輸出了信息,誤判 沒有做負載均衡)
@RestController class HelloController { @Autowired private HttpServletRequest req; @GetMapping(path="getTime") public String getTime(Integer i) { System.out.println("GET url=" + req.getRequestURL()); System.out.println("GET 參數i=" + i + ", url=" + req.getRequestURL()); return new Date().toGMTString(); } @PostMapping(path="updateUser") public String updateUser(@RequestBody User u) { System.out.println("POST url=" + req.getRequestURL()); System.out.println("POST 參數 User u = " + u); return "OK"; } }
啟動兩個WEB-FIRST服務,啟動后,再啟動WEB-SECOND服務。檢查 WEB-FIRST服務 的日志,有做負載均衡了:其中的GET、POST請求分別由 兩個 WEB-FIRST服務 處理。
在啟動一台WEB-FIRST服務,就可以看到明顯的負載均衡現象了:
注:
參考 SPRING CLOUD文檔的 “Spring RestTemplate as a Load Balancer Client” 一節實現。
“Spring WebClient as a Load Balancer Client” 一節還介紹了 使用 WebClient 訪問,類似 RestTemplate。
feign 翻譯:
vt. 假裝,偽裝; 捏造(借口、理由等); 裝作; 創造或虛構; vi. 假裝; 裝作; 作假; 佯作; 變形 過去分詞: feigned 過去式: feigned 現在分詞: feigning 第三人稱單數: feigns
Feign是一個 Web服務客戶端,它讓Web服務客戶端的編寫變得很容易。
注:本節參考 spring cloud文檔的 “Spring Cloud OpenFeign” 一章。
添加依賴包:
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
啟動類添加注解:
1 @EnableFeignClients 2 @SpringBootApplication 3 public class WebSecondApplication { 4 ... 5 }
定義FeignClient訪問 WEB-FIRST 的接口:
1 @FeignClient(value="web-first") // WEB-FIRST 服務名 2 public interface WebFirstFeignClient { 3 4 @GetMapping(value="/getTime") 5 public String getTime(@RequestParam Integer i); // @RequestParam 必須! 6 7 @PostMapping(value="/updateUser") 8 public String updateUser(User user); 9 10 } 11 12 @Component 13 class MyRunner2 implements CommandLineRunner { 14 15 @Autowired 16 private WebFirstFeignClient client; 17 18 @Override 19 public void run(String... args) throws Exception { 20 21 for (int i=0; i<10; i++) { 22 System.out.println("GET 返回:" + client.getTime(i)); 23 24 User user = new User(); 25 user.setName("lib"); 26 user.setAge(i); 27 System.out.println("POST 返回:" + client.updateUser(user)); 28 } 29 30 } 31 32 }
啟動WEB-SECOND服務,調用成功。
WEB-FIRST 服務 掛了,此時,執行WEB-SECOND會怎樣呢?啟動失敗,報錯:
Caused by: feign.FeignException$ServiceUnavailable: [503] during [GET] to [http://web-first/getTime?i=0]
[WebFirstFeignClient#getTime(Integer)]: [Load balancer does not contain an instance for the service web-first]
WEB-SECOND服務沒有成功啟動。
怎么避免 WEB-FIRST 掛掉 導致 消費方 WEB-SECOND 啟動不了的情況呢?
進一步配置 @FeignClient ,添加斷路器:
feign.circuitbreaker.enabled=true
導入依賴包spring-cloud-starter-circuitbreaker-resilience4j:
<!-- resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
添加resilience4j 的 Customize bean配置:
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
return factory -> factory.configureDefault(id -> new
Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4
)).build())
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
.build());
}
添加 WebFirstFeignFallback:
@Component public class WebFirstFeignFallback implements WebFirstFeignClient { @Override public String getTime(Integer i) { return "getTime熔斷"; } @Override public String updateUser(User user) { return "updateUser熔斷"; } }
配置 @FeignClient :
1 @FeignClient(value="web-first", fallback = WebFirstFeignFallback.class) 2 public interface WebFirstFeignClient { 3 ... 4 }
此時,啟動 WEB-SECOND 服務:服務啟動成功,日志輸出如下,調用接口返回 WebFirstFeignFallback 中的內容。
2021-07-22 18:34:41.167 WARN 19484 --- [oundedElastic-1] o.s.c.l.core.RoundRobinLoadBalancer : No servers available for service: web-first 2021-07-22 18:34:41.167 WARN 19484 --- [pool-1-thread-1] .s.c.o.l.FeignBlockingLoadBalancerClient :
Load balancer does not contain an instance for the service web-first GET 返回:getTime熔斷 2021-07-22 18:34:41.169 WARN 19484 --- [oundedElastic-1] o.s.c.l.core.RoundRobinLoadBalancer : No servers available for service: web-first 2021-07-22 18:34:41.169 WARN 19484 --- [pool-1-thread-1] .s.c.o.l.FeignBlockingLoadBalancerClient :
Load balancer does not contain an instance for the service web-first POST 返回:updateUser熔斷
為什么 不使用spring-cloud-starter-hystrix 呢?因為版本啊!
本文的spring boot是2.5.2,而在 2.5.0之后就不支持 spring-cloud-starter-hystrix,需要使用 resilience4j 的斷路器。
除了resilience4j,還有 Sentinel、Spring Retry 兩種。
更多詳情,請看 spring cloud官方文檔之 “ Spring Cloud Circuit Breaker -> Configuring Resilience4J Circuit Breakers”一章。
注:Alibaba Sentinel 是面向雲原生微服務的流量控制,熔斷降級組件,監控保護你的微服務。
參考資料:
1、官方-Service Registration and Discovery
2、SpringCloud之Eureka注冊中心原理及其搭建
4、Resilience4j+Feign實現熔斷,fallback
5、Spring Cloud Feign(第四篇)之Fallback
6、