当前位置 博文首页 > MPolaris:三. SpringCloud服务注册与发现
什么是服务治理
Spring Cloud封装了Netflix
公司开发的Eurkeka模块来实现服务治理
在传统的rpc远程调用
框架中,管理每个服务与服务之间依赖关系比较复杂。管理比较复杂服务之间的依赖关系可以实现服务调用,负载均衡,容错等,实现服务发现与注册。
什么是服务注册与发现
Eureka采用了CS的设计架构,Eureka Server
作为服务注册功能的服务器,它是服务注册中心,而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持 心跳链接 。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候会把当前自己服务器的信息(比如:服务地址、通讯地址等)以别名方式注册到注册中心中。另一方(消费者/服务提供者)以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
下图左边是Eureka系统架构,右边是Dubbo系统架构
Eureka包含两个组件:Eureka Server 和 Eureka Client
Eureka Server
提供服务注册中心
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client
通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中表把这个服务节点移除(默认90秒)
IDEA生成EurekaServer端服务注册中心
类似物业公司
<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 公共模块 -->
<dependency>
<groupId>com.polaris</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- boot web actuator -->
<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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
SpringBoot 1x 和 SpringBoot 2x对比
<!-- SpringBoot1.X对应的SpringCloud eureka,不要再用了!!! -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- SpringBoot2.X对应的SpringCloud eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
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/
@SpringBootApplication
@EnableEurekaServer // 声明我是服务注册中心
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain.class);
}
}
运行该Eureka Server主启动类,访问 localhost:7001,就会看到下面的服务注册中心,可以发现目前还没有任何服务入驻进服务注册中心中,在应用中显示:No instances available
EurekaClient端 服务提供者cloud-provider-payment8001修改
<!-- 注意:与Eureka Server一样,这里SpringBoot2x不再使用spring-cloud-starter-eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
application:
name: cloud-payment-service # 入驻Eureka服务注册中心的服务名称
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 单机版
defaultZone: http://localhost:7001/eureka # 入驻的服务注册中心地址
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain {
public static void main(String[] args) {
SpringApplication.run(PaymentMain.class,args);
}
}
注意先要启动EurekaServer,因为有了服务注册中心具体的服务提供者才能后向其中注册自己的服务
可以发现注册到服务注册中心的服务名(图中蓝框)即为我们在yml配置文件中设置的服务名,下面页面中出现的 红字 是Eureka的 自我保护机制
EurekaClient端 服务消费者cloud-sonsumer-order80修改
与服务提供者cloud-provider-payment8001修改差不多,不再赘述
spring:
application:
name: cloud-order-service # 入驻Eureka服务注册中心的服务名称
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 单机
defaultZone: http://localhost:7001/eureka
总结
此时再回看最开始的Eureka系统架构,在服务注册中心和服务提供者没有集群的情况下,7001端口的微服务就对应了服务注册中心,而该服务不需要向服务注册中心注册自己,8001端口的微服务作为服务提供方入住到服务注册中心,8002端口的微服务作为服务消费方也同样注册到服务注册中心
集群Eureka原理
服务注册中心Eureka Server中分为 服务注册
和 服务发现
,服务注册过程将服务信息注册进服务注册中心,服务发现过程从服务注册中心上获取服务信息,而这个过程的实质就是:将服务名作为key存储,然后根据value取得服务的调用地址。
整个Eureka的过程如下:
那么微服务RPC远程服务调用最核心的是什么呢?
高可用!如果注册中心只有一个,而这个注册中心出现了故障那么整个微服务就直接GG了,整个微服务环境就不可用了,所以应该搭建Eureka注册中心集群, 实现 负载均衡 + 故障容错
那怎么实现Eureka注册中心的集群呢?用一句话总结就是 - 互相注册,相互守望。如下图所示,服务注册中心实现相互注册让彼此都知道对方的存在,也就是注册中心集群中的每一个注册中心都知道整个集群中的其他注册中心,比如如果有三个注册服务中心7001,7002,7003,那么就将7002和7003注册给7001, 将7002和7001注册给7003, 将7003和7001注册给7002, 以此类推,而这些个注册服务中心 作为一个整体对外看做一个注册服务中心。
Eureaka集群环境构建
参考cloud-eureka-server7001新建一个服务注册中心cloud-eureka-server7002
copy复制cloud-eureka-server7001的POM文件即可
找到C:\Windows\System32\drivers\etc路径下的hosts文件,将其内容修改成如下内容:
# learn-spring-cloud
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
以前是单机版配置,而现在已经有两个注册中心可以看做两台机器,显然 hostname 不能再叫localhost
更改了服务端的实例名称后,最重要的是在defaultZone中将自己注册给其他注册中心
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com # eureka服务端的实例名称
client:
register-with-eureka: false
fetch-registry: true
service-url:
# 互相注册,相互守望
defaultZone: http://eureka7002.com:7002/eureka/
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com
client:
register-with-eureka: false
fetch-registry: true
service-url:
# 互相注册,相互守望
defaultZone: http://eureka7001.com:7001/eureka/
两个服务中心已经完成了互相注册。主页面DS Replicas下面的信息就表示是这个Eureka Server相邻节点且这些节点加上自己互为一个集群。
修改其配置文件即可,就是将自己的微服务注册到每一个服务注册中心里去,见配置文件中的defaultZone。
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
服务提供者集群环境构建
参考8001服务新建8002服务
pom.xml
8002和8001的POM文件一样
将端口改为8002,其他和8001相同,两个微服务 对外暴露的服务名相同均为cloud-payment-service 从而构成集群。
server:
port: 8002
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://mpolaris.top:3306/cloud-test?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.polaris.springcloud.entities # 所有Entity别名类所在包
主启动类名字分别为PaymentMain8001与PaymentMain8002
修改controller,添加端口号以区分这两个具体的微服务:读取配置文件中设置的端口号。8002的修改同8001。
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@PostMapping("/save")
public CommonResult save(@RequestBody Payment payment) {
int result = paymentService.save(payment);
log.info("===> result: " + result);
if(result > 0) {
return new CommonResult(200,
"保存到数据库成功,端口号:" + serverPort,result);
}
return new CommonResult(400,"保存到数据库失败",null);
}
@GetMapping("/get/{id}")
public CommonResult<Payment> save(@PathVariable("id") Long id) {
Payment paymentById = paymentService.getPaymentById(id);
log.info("===> payment: " + paymentById);
if(paymentById != null) {
return new CommonResult(200,
"查询成功,端口号:" + serverPort,paymentById);
}
return new CommonResult(400,"查询失败",null);
}
}
如图可以看到此时服务注册中心构成集群,而相同名字的服务提供方的实际提供者已经出现了两个,分别是8001和8002,也就是说服务提供方微服务也实现了集群。
负载均衡
@LoadBalanced
注解赋予RestTemplate负载均衡能力,该负载均衡默认为轮询方式。所以将80服务的配置文件修改如下:@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //使用该注解赋予RestTemplate负载均衡的能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
CLOUD-PAYMENT-SERVICE
服务时,具体的微服务在8001和8002之间进行轮询切换。Ribbon
,在Ribbon和Eureka整合后,消费者可以直接调用服务而不用再关心地址和端口号,且该服务还有负载功能。总结
主机名称:服务名称修改
发现问题:在注册中心显示的微服务中,我们发现服务名含有主机名称,这显然不是我们希望看到的
怎么能解决这个问题呢,只需要修改服务提供方(8001和8002)的配置文件,向其中的eureka部分加入instance实例
即可配置该服务显示的服务名称
instance:
instance-id: payment8001
最终的整体配置文件如下:
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://mpolaris.top:3306/cloud-test?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 1234321
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 单机版
# defaultZone: http://localhost:7001/eureka
instance:
instance-id: payment8001
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.polaris.springcloud.entities
8002服务的修改同上,此时再访问注册中心,看到的服务具体名称中就没有主机名了,而是我们配置好的服务名称:
访问信息有IP信息提示
发现问题:我们在鼠标移动到具体服务时,提示的地址信息中并没有服务所在具体主机的IP地址,这在开发中不方便定位具体微服务。
解决方式仍然是通过配置文件,在配置文件中向其中的eureka部分加入优先ip地址
即可配置该服务访问路径可以显示IP地址:
instance:
prefer-ip-address: true # 访问路径可以显示IP地址
最终的配置文件如下:
eureka:
client:
# 表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true。
# 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001
prefer-ip-address: true #访问路径可以显示IP地址
对于注册进Eureka服务注册中心的微服务,可以通过服务发现来获取该服务的信息
。
修改微服务的Controller
向其中注入DiscoveryClient,并编写相应Controller方法
DiscoveryClient对象中的 getServices
方法用于获取服务列表的信息,也就是有哪些服务,如cloud-payment-service服务, getInstances
方法用于获取服务列表对应的具体服务实例,如cloud-payment-service服务对应的8001和8002服务。
import org.springframework.cloud.client.discovery.DiscoveryClient;
//...
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
//...
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/discovery")
public Object discovery() {
//获取服务列表的信息
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("===> service:" + service);
}
//根据微服务名称获取具体服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info("===> " + instance.getServiceId()
+ "\t" + instance.getHost()
+ "\t" + instance.getPort()
+ "\t" + instance.getUri());
}
return this.discoveryClient;
}
//...
}
修改主启动类
只需要在主启动类上添加注解@EnableDiscoveryClient
,修改后的主启动类:
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
测试
访问地址http://localhost:8001/payment/discovery,我们可以看到获取的服务信息,即完成了服务发现:
后台也对服务列表进行了日志打印:
<img src="https://gitee.com/mp2333/blog-img/raw/master/SpringCloud/20210126234348.png" alt="image-20210126234348515" https://gitee.com/mp2333/blog-img/raw/master/SpringCloud/20210126234854.png />
自我保护机制
保护模式主要用于一组客户端和EurekaServer之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。换句话说就是,某时刻某一个微服务不可用了,Eureka不会立刻清理,而是依旧会对该微服务的信息进行保存。
如果在Eureka Server的首页看到以下提示,说明Eureka进入了保护模式
产生原因
为什么会产生Eureka自我保护机制? => 为了防止 EurekaClient可以正常运行,但是与EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除。
什么是自我保护模式? => 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之前无法正常通信,以上行为可能变得非常危险 - 因为微服务本身是健康的,只是由于网络问题链接不到EurekaServer,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题 :当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障,网络延时),那么这个节点就会进入自我保护模式。在自我保护模式中,EurekaServer会保护服务注册表中的信息,不再注销任何服务实例,宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
怎么禁止自我保护
先在EurekaServer端修改配置文件即可设置关闭自我保护机制
eureka:
server:
# 关闭自我保护机制,保证不可用服务被及时剔除。默认为true开启
enable-self-preservation: false
# 时间间隔,单位ms
eviction-interval-time-in-ms: 2000
然后在EurekaClient端修改配置文件
eureka:
instance:
instance-id: payment8001
# Eureka客户单向服务端发送心跳的时间间隔,默然是30秒,这里改成1秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端在收到最后一次心跳后等待时间上限,默然为90秒,超时将剔除服务,这里改成2秒
lease-expiration-duration-in-seconds: 2
这样就会使EurekaClient客户端的微服务很快死亡。
https://github.com/Netflix/eureka/wiki
我们可以使用SpringCloud整合Zookeeper替代Eureka
Zookeeper是一个分布式协调工具,可以实现注册中心功能
安装Zookeeper
# 关闭Linux服务器防火墙(关闭默认端口2181也行)
# 2181 对Client端提供服务的端口
# 3888 选举Leader
# 2888 集群内的机器通讯使用。(Leader使用此端口)
systemctl stop firewalld
systemctl status firewalld
# 我这里使用的是zookeeper-3.4.11.tar.gz,解压即可
# bin目录下启动zookeeper服务器
./zkServer.sh start
# 连接zookeeper客户端
# 如果是连接同一台主机上的zk进程,那么直接运行bin/目录下的kCli.sh,即可连接上zk。
# 直接执行zkCli.sh命令默认以主机号 127.0.0.1,端口号 2181 来连接zk
# 如果要连接不同机器上的zk,可以使用 -server 参数,例如:
./zkCli.sh -server 192.168.0.1:2181
# 启动报错?
# grep: /usr/local/zookeeper-3.4.11/bin/../conf/zoo.cfg: No such file or directory
# 这里的原因是因为下载下来的zoo.cfg名字是zoo_sample.cfg,只需要改名字即可
mv zoo_sample.cfg zoo.cfg
Zookeeper服务器取代Eureka服务器,zk作为服务注册中心
新建cloud-provider-payment8004
pom.xml
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.polaris</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- boot web actuator -->
<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>
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!-- 通用配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml配置文件
server:
# 8004表示注册到zookeeper服务器的支付服务提供者端口号
port: 8004
spring:
application:
# 服务别名 - 注册zookeeper到注册中心的名称
name: cloud-provider-payment
cloud:
zookeeper:
# zookeeper访问地址
connect-string: mpolaris.top:2181
主启动类
@SpringBootApplication
//该注解用于向使用consul或zookeeper作为注册中心时注册服务
@EnableDiscoveryClient
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class,args);
}
}
controller
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/zk")
public String paymentZk() {
return "===> SpringCloud with zookeeper:"
+ serverPort
+ "\t"
+ UUID.randomUUID().toString();
}
}
启动8004注册进zookeeper
why?
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!-- 先排除自带的zookeeper3.5.3 -->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加zookeeper3.4.11版本zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.11</version>
</dependency>
验证测试1
访问 http://localhost:8004/payment/zk
验证测试2
获得json串后用在线工具查看如下
思考
服务节点是临时节点还是持久节点? => 临时节点
新建cloud-consumerzk-order80
pom.xml