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、

原文地址:https://www.cnblogs.com/luo630/p/15043547.html