spring cloud項目01:服務注冊與發現


Java 8

spring boot 2.5.2

spring cloud 2020.0.3

---

 

目錄

EurekaServer項目(standalone)

EurekaClient項目

使用Eureka注冊中心注冊的服務

        方法1:使用 RestTemplate

        方式2:使用OpenFeign

        斷路器的一個問題

 

EurekaServer項目(standalone)

建立 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 端點 就會暴露出來了。

 

EurekaClient項目

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 

 

使用Eureka注冊中心注冊的服務

在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:使用 RestTemplate

 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。

 

方式2:使用OpenFeign

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注冊中心原理及其搭建

3、FeignClient超時配置

4、Resilience4j+Feign實現熔斷,fallback

5、Spring Cloud Feign(第四篇)之Fallback

6、

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM