SpringCloud学习笔记(2)

服务注册中心

如果是上面只有两个微服务,通过 RestTemplate ,是可以相互调用的,但是当微服务项目的数量增大,就需要服务注册中心。目前没有学习服务调用相关技术,使用 SpringCloud 自带的 RestTemplate 来实现RPC

Eureka

什么是服务治理

SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理

在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

什么是服务注册与发现

Eureka采用了CS的设计结构,Eureka Server服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。这点和zookeeper很相似

1597291856582

单机版Eureka构建:

消费者端口80,提供者端口8001。

Eureka端口7001

1) Server模块

pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2022</artifactId>
<groupId>com.wzg.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>eureka-server7001</artifactId>

<dependencies>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!--一般通用配置-->
<dependency>
<groupId>com.wzg.springcloud</groupId>
<artifactId>api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>

</dependencies>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

</project>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 7001

eureka:
instance:
hostname: localhost # eureka 服务端的实例名称

client:
# false 代表不向服务注册中心注册自己,因为它本身就是服务中心
register-with-eureka: false
# false 代表自己就是服务注册中心,自己的作用就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与 Eureka Server 交互的地址,查询服务 和 注册服务都依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
# defaultZone: http://127.0.0.1:7001/eureka/

主启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.wzg.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
* @author whlie(true){learn}
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}

问题1:启动idea报错,提示你没有配置数据源

解决方法:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)禁止SpringBoot自动注入数据源配置

Eureka集群原理

注册中心集群搭建

根据上面创建的7001创建一份7002,除了yml文件其余全部相同

7001yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 7001

eureka:
instance:
hostname: eureka7001.com # eureka 服务器的实例地址

client:
register-with-eureka: false
fetch-registry: false
service-url:
## 一定要注意这里的地址,这是搭建集群的关键。反过来写,写的是集群中其他Eureka服务器的地址
defaultZone: http://eureka7002.com:7002/eureka/

7002yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 7002

eureka:
instance:
hostname: eureka7002.com # eureka 服务器的实例地址

client:
register-with-eureka: false
fetch-registry: false
service-url:
## 一定要注意这里的地址 这是搭建集群的关键
defaultZone: http://eureka7001.com:7001/eureka/

服务提供者集群搭建

根据8001创建一份8002

把yml统一改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 8002


spring:
application:
name: payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver #mysql驱动包
# driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wzg_db01?useUnicode=true&characterEncoding-utr-8&useSSL=false
username: root
password: 123
druid:
test-while-idle: true
validation-query: SELECT 1


eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url: # 提供者注册到多个eureka中
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/


mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.wzg.springcloud.entities #所有Entity别名类所在包

controller统一改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.wzg.springcloud.controller;


import com.wzg.springcloud.entities.CommonResult;
import com.wzg.springcloud.entities.Payment;
import com.wzg.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
* @author whlie(true){learn}
*/
@RestController
@Slf4j
public class PaymentController {

@Resource
private PaymentService paymentService;

@Value("${server.port}")
private String serverPort;

@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("插入结果:" + result);
if (result > 0) {
return new CommonResult<>(200, "插入成功,serverPort"+serverPort, result);
} else {
return new CommonResult<>(444, "插入失败", null);
}
}

@GetMapping(value = "/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);

log.info("查询结果:" + payment);

if (payment != null) {
return new CommonResult<>(200, "查询成功serverPort"+serverPort,payment);
} else {
return new CommonResult<>(444, "查询失败,查询ID:" + id, null);
}
}
}

负载均衡

更改消费者的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.wzg.springcloud.controller;

import com.wzg.springcloud.entities.CommonResult;
import com.wzg.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
* @author whlie(true){learn}
*/
@RestController
@Slf4j
@RequestMapping("consumer")
public class OrderController {

// public static final String PAYMENY_URL="http://localhost:8001";
public static final String PAYMENY_URL="http://PAYMENT-SERVICE";

@Resource
private RestTemplate restTemplate;

/**
* PAYMENY_URL+"/payment/create",//请求地址
* payment,//请求参数
* CommonResult.class//返回类型
*/

@PostMapping("/payment/create")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENY_URL+"/payment/create", payment, CommonResult.class);
}

@GetMapping("/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
return restTemplate.getForObject(PAYMENY_URL+"/payment/"+id,CommonResult.class);
}
}

config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.wzg.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* @author whlie(true){learn}
*/
@Configuration
public class ApplicationContextConfig {

/**
* RestTemplate提供了多种便捷访问远程http服务的方法,
* 是一种简单便捷的访问restful服务模板类,是spring提供的用于rest服务的客户端模板工具集
* @return
*/
@Bean
@LoadBalanced//这个注解,就赋予了RestTemplate 负载均衡的能力
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}

小总结

启动顺序—7001—7002—8001—8002—80

  • 1先启动eureka注册中心
  • 2启动服务提供者payment支付服务
  • 3支付服务启动后会把自身信息化 服务以别名方式注册进eureka
  • 4消费者order服务在要调用接囗时,使用服务别名去注册中心取实际的RPC远程调用地址
  • 5消费者获得调用地址后,底层实际是利用HttpClient技术实现远程调用
  • 6消费者获得服务地址后会存jvm内存中,默认每间隔30s更新一次服务调用地址

Eureka Server在设计的时候就考虑了高可用设计,在Eureka服务治理设计中,所有节点既是服务的提供方,也是服务的消费方,服务注册中心也不例外。

Eureka Server的高可用实际上就是将自己做为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。

Eureka Server的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。可以采用两两注册的方式实现集群中节点完全对等的效果,实现最高可用性集群,任何一台注册中心故障都不会影响服务的注册与发现。

注意:

  • 集群中多个提供者的spring:application:name:要一致
  • 启动类添加@EnableDiscoveryClient或者@EnableEurekaClient
    1,@EnableDiscoveryClient注解是基于spring-cloud-commons依赖,并且在classpath中实现;
    2,@EnableEurekaClient注解是基于spring-cloud-netflix依赖,只能为eureka作用;
    如果你的classpath中添加了eureka,则它们的作用是一样的。

actuator微服务信息完善

修改服务提供者的yml:设置服务名称+显示ip地址、

1
2
3
4
eureka:
instance:
instance-id: payment8001
prefer-ip-address: true

服务发现Discovery

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。(即我们前面可视化页面的信息)

  1. 在主启动类上添加注解:@EnableDiscoveryClient
  2. 在 服务提供者的Controller 里面打印信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Resource // 自动注入
private DiscoveryClient discoveryClient;

@GetMapping("/payment/discovery")
public Object discovery(){
//获得服务清单列表
List<String> services = discoveryClient.getServices();
for(String service: services){
log.info("*****service: " + service);
}
// 根据具体服务进一步获得该微服务的信息
List<ServiceInstance> instances = discoveryClient.getInstances("PAYMENT-SERVICE");
for(ServiceInstance serviceInstance:instances){
log.info(serviceInstance.getServiceId() + "\t" + serviceInstance.getHost()
+ "\t" + serviceInstance.getPort() + "\t" + serviceInstance.getUri());
}
return this.discoveryClient;
}

Eureka 自我保护机制

某时刻某一个微服务不可用了,Eureka不会立即清理,依旧会对该微服务的信息进行保存。属于CAP里的AP(高可用)分支

保护模式主要用于一组客户和Eureka Server之间存在网络分区场景下保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中固定信息,也就是不会注销任何微服务。

如果在Eureka Server的首页看到以下这段提示,则说明Eureka讲入了保护模式:

EMERGENCY!EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE

为什么会产生Eureka自我保护机制?

为了防止Eureka Client可以正常运行但是与Eureka Server网络不通情况下,Eureka Server不会立刻将Eureka Client服务剔除

什么是自我保护模式?

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了—因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过”自我保护模式”来解决这个问题—当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

自我保护机制:默认情况下Eureka CIient定时向Eureka Server端发送心跳包。

如果Eureka在server端在一定时间内(默认90秒)没有收到Eureka Client发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间(90秒内)内丢失了大量的服务实例心跳,这时Eureka Server会开启自我保护机制,不会剔除该服务(该现象可能出现如果网络不通但是Eureka Client出现宕机,此时如果别的注册中心如果一定时间内没有收到心跳会将剔除该服务这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问题而产生的

在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注册任何服务实例。

它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。

禁止自我保护:(如果想)

在 Eureka Server 的模块中的 yml 文件进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 7001

eureka:
instance:
hostname: eureka7001.com # eureka 服务器的实例地址

client:
register-with-eureka: false
fetch-registry: false
service-url:
## 一定要注意这里的地址,这是搭建集群的关键。反过来写,写的是集群中其他Eureka服务器的地址
defaultZone: http://eureka7002.com:7002/eureka/
server: # 与client平行
# 关闭自我保护机制,保证不可用该服务被及时剔除
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000

修改 Eureka Client 模块的 心跳间隔时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server:
port: 8001


spring:
application:
name: payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource #当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver #mysql驱动包
# driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wzg_db01?useUnicode=true&characterEncoding-utr-8&useSSL=false
username: root
password: 123
druid:
test-while-idle: true
validation-query: SELECT 1


eureka:
instance:
instance-id: payment8001
prefer-ip-address: true
# Eureka客户端像服务端发送心跳的时间间隔,单位s,默认30s
lease-renewal-interval-in-seconds: 1
# Rureka服务端在收到最后一次心跳后等待时间上线,单位为s,默认90s,超时将剔除服务
lease-expiration-duration-in-seconds: 2
client:
register-with-eureka: true
fetch-registry: true
service-url: # 提供者注册到多个eureka中
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/


mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.wzg.springcloud.entities #所有Entity别名类所在包
eureka配置项解读:

在注册服务之后,服务提供者会维护一个心跳用来持续高速Eureka Server,“我还在持续提供服务”,否则Eureka Server的剔除任务会将该服务实例从服务列表中排除出去。我们称之为服务续约。
面是服务续约的两个重要属性:

(1)eureka.instance.lease-expiration-duration-in-seconds
leaseExpirationDurationInSeconds,表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance。
默认为90秒
如果该值太大,则很可能将流量转发过去的时候,该instance已经不存活了。
如果该值设置太小了,则instance则很可能因为临时的网络抖动而被摘除掉。
该值至少应该大于leaseRenewalIntervalInSeconds

(2)eureka.instance.lease-renewal-interval-in-seconds
leaseRenewalIntervalInSeconds,表示eureka client发送心跳给server端的频率。如果在leaseExpirationDurationInSeconds后,server端没有收到client的心跳,则将摘除该instance。除此之外,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。
默认30秒

Eureka停更说明:

2.0后停更了。


SpringCloud学习笔记(2)
https://yztldxdz.top/2022/09/17/SpringCloud学习笔记(2)/
发布于
2022年9月17日
许可协议