SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理
在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册
Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心,而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持 心跳链接 。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息(比如:服务地址、通讯地址等)以别名方式注册到注册中心中。另一方(消费者/服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
Eureka Server提供服务注册中心
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中表把这个服务节点移除(默认90秒)
1 建Module
2 改POM
1.X和2.X的对比说明:
SpringBoot1.X对应的SpringCloud中,不要再用了!!!
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
dependency>
SpringBoot2.X对应的SpringCloud中
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
3 写配置文件YML
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/
4 SpringBoot应用主启动类
package cn.sher6j.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author sher6j
*/
@SpringBootApplication
@EnableEurekaServer//声明我是服务注册中心
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class);
}
}
5 测试
完成以上步骤后,运行该Eureka Server主启动类,访问 localhost:7001,就会看到下面的服务注册中心:
可以发现,目前还没有任何服务入驻进服务注册中心中,在应用中显示:No instances available
1 建module
2 改POM
1.X和2.X的对比说明:
SpringBoot1.X对应的SpringCloud中,不要再用了!!!
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
dependency>
SpringBoot2.X对应的SpringCloud中
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
3 写配置文件YML
server:
port: 8001
spring:
application:
name: cloud-payment-service # 入驻Eureka服务注册中心的服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/cloud20?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机版
defaultZone: http://localhost:7001/eureka # 入驻的服务注册中心地址
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: cn.sher6j.springcloud.entities # 所有Entity别名类所在包
4 SpringBoot应用主启动类
package cn.sher6j.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 主启动类
* @author sher6j
*/
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class);
}
}
5 测试
先要启动EurekaServer,因为有了服务注册中心,具体的服务提供者才能后向其中注册自己的服务,如下图:
可以发现,注册到服务注册中心的服务名(图中蓝框)即为我们在yml配置文件中设置的服务名,上面页面中出现的 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的自我保护机制
1 建module
2 改POM
3 写配置文件
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: false
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机
defaultZone: http://localhost:7001/eureka
4 主启动类
package cn.sher6j.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author sher6j
*/
@SpringBootApplication
@EnableEurekaClient
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class);
}
}
5 测试
如图此时80,8001两个微服务都已经注册到Eureka服务注册中心
此时,再回看最开始的Eureka系统架构,在服务注册中心和服务提供者没有集群的情况下,7001端口的微服务就对应了服务注册中心,而该服务不需要向服务注册中心注册自己,8001端口的微服务作为服务提供方入住到服务注册中心,8002端口的微服务作为服务消费方也同样注册到服务注册中心
服务注册中心Eureka Server中分为 服务注册 和 服务发现,服务注册过程将服务信息注册进服务注册中心,服务发现过程从服务注册中心上获取服务信息,而这个过程的实质就是:将服务名作为key存储,然后根据value取得服务的调用地址。
整个Eureka的过程如果:
那么微服务RPC远程服务调用最核心的是什么呢???
高可用!!!,如果注册中心只有一个,而这个注册中心出现了故障,那么整个微服务就直接GG了,整个微服务环境就不可用了,所以应该搭建Eureka注册中心集群, 实现 负载均衡 + 故障容错
那怎么实现Eureka注册中心的集群呢?用一句话总结就是——互相注册,相互守望,如下图所示:
服务注册中心实现相互注册,让彼此都知道对方的存在,也就是注册中心集群中的每一个注册中心都知道整个集群中的其他注册中心,比如如果有三个注册服务中心7001,7002,7003,那么就将7002和7003注册给7001, 将7002和7001注册给7003, 将7003和7001注册给7002, 以此类推,而这些个注册服务中心作为一个整体对外看做一个注册服务中心。
参考cloud-eureka-server7001新建一个服务注册中心cloud-eureka-server7001。
2 改POM,copy复制cloud-eureka-server7001的POM文件即可。
3 修改映射配置
找到C:\Windows\System32\drivers\etc路径下的hosts文件,将其内容修改成如下内容:
# learn-spring-cloud
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
当然我是用的SwitchHosts对该文件进行的修改。这样的话就可以通过一台主机(127.0.0.1即localhost)的不同端口模拟实现不同的主机(127.0.0.1:7001和127.0.0.1:7002)。
4 写配置文件YML(区别于单机)
以前单机的时候,注册中心的配置文件是这样的:
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/
而现在已经有两个注册中心,可以看做两台机器,显然 hostname 不能再叫localhost,而集群不能再这样,对于7001端口的注册中心,修改其配置文件:
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己
register-with-eureka: false
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 互相注册,相互守望
defaultZone: http://eureka7002.com:7002/eureka/
首先更改了其服务端的实例名称,最重要的是在defaultZone中将自己注册给7002,举一反三,7002的配置文件显然如下:
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
#false表示不向注册中心注册自己
register-with-eureka: false
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 互相注册,相互守望
defaultZone: http://eureka7001.com:7001/eureka/
这样就实现了两个服务注册中心的相互注册。
主启动类
7002的主启动类和7001一样
package cn.sher6j.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @author sher6j
*/
@SpringBootApplication
@EnableEurekaServer//声明我是服务注册中心
public class EurekaMain7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7002.class);
}
}
测试
如图:两个服务中心已经完成了互相注册。DS Replicas这个下面的信息就表示是这个Eureka Server相邻节点,且这些节点加上自己互为一个集群。
修改其配置文件即可
server:
port: 8001
spring:
application:
name: cloud-payment-service # 入驻Eureka服务注册中心的服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/cloud20?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
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: cn.sher6j.springcloud.entities # 所有Entity别名类所在包
也就是将自己的微服务注册到每一个服务注册中心里去,见配置文件中的defaultZone。
和上面的是一样的,只需要修改其配置文件,将自己注册进两个服务注册中心中。
下面进行测试:首先 要启动EurekaServer服务,即7001和7002服务,这样就有了服务注册中心,然后 再启动服务提供方即8001, 最后 启动服务消费方即80。如图:7001和7002已经构成集群,且都可以拉取到80和8001两个服务:
参考8001服务新建8002服务
改POM:和8001的POM文件一样
写配置文件:将端口改为8002,其他和8001相同,两个微服务 对外暴露的服务名相同 均为cloud-payment-service 从而构成集群。
server:
port: 8002
spring:
application:
name: cloud-payment-service # 入驻Eureka服务注册中心的服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/cloud20?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 单机版
# defaultZone: http://localhost:7001/eureka # 入驻的服务注册中心地址
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 入驻的服务注册中心地址
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: cn.sher6j.springcloud.entities # 所有Entity别名类所在包
主启动类、业务类:直接从8001里copy即可
package springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 主启动类
* @author sher6j
*/
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8002.class);
}
}
修改controller添加端口号已区分这两个具体的微服务:读取配置文件中设置的端口号。8002的修改同8001。
package cn.sher6j.springcloud.controller;
import cn.sher6j.springcloud.entities.CommonResult;
import cn.sher6j.springcloud.entities.Payment;
import cn.sher6j.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
/**
* @author sher6j
*/
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
/**
* 插入payment
* @param payment
* @return
*/
@PostMapping("/payment/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("=======插入结果:" + result);
if (result > 0) {
return new CommonResult(200, "插入数据库成功, 端口号:" + serverPort, result);
} else {
return new CommonResult(444, "插入数据库失败", null);
}
}
/**
* 根据id查询订单
* @param id
* @return
*/
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("=======查询结果:" + payment);
if (payment != null) {
return new CommonResult(200, "查询数据库成功, 端口号:" + serverPort, payment);
} else {
return new CommonResult(444, "没有对应记录,查询ID:" + id, null);
}
}
}
测试
如图,可以看到此时服务注册中心构成集群,而相同名字的服务提供方的实际提供者已经出现了两个,分别是8001和8002,,也就是说服务提供方微服务也实现了集群。
此时再回看最开始的Eureka微服务架构图,其集群架构图对应本实例应为:
此时,我们使用80端口的服务消费方来访问 CLOUD-PAYMENT-SERVICE 服务,输入网址http://localhost/consumer/payment/get/1,但是我们每次得到的数据都是:
{
code: 200,
message: "查询数据库成功, 端口号:8001",
data: {
id: 1,
serial: "aaaa001"
}
}
也就是说每次访问的具体微服务都是8001端口的CLOUD-PAYMENT-SERVICE服务,这明显是不符合业务逻辑的,原因就是在消费方代码中我们将服务访问地址写死了,没有实现负载均衡,这显然是不对的,所以我们应该让80访问服务名,而不是具体的服务,同时在配置文件中通过 @LoadBalanced
注解赋予RestTemplate负载均衡能力,该负载均衡默认为轮询方式,所以讲80服务的配置文件修改如下:
package cn.sher6j.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 sher6j
*/
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced//使用该注解赋予RestTemplate负载均衡的能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
//applicationContext.xml
然后重启80端口,发现每次访问 CLOUD-PAYMENT-SERVICE 服务时,具体的微服务在8001和8002之间进行轮询切换:
当然此时负载均衡我们还没有用到Ribbon,再Ribbon和Eureka整个后消费者可以直接调用服务而不用再关心地址和端口号,且该服务还有负载功能。
当前问题:在注册中心显示的微服务中,我们发现服务名含有主机名称,这显然不是我们希望看到的
怎么能解决这个问题呢,只需要修改服务提供方(8001和8002)的配置文件,向其中的eureka部分加入即可配置该服务显示的服务名称
instance:
instance-id: payment8001
最终的整体配置文件如下:
server:
port: 8001
spring:
application:
name: cloud-payment-service # 入驻Eureka服务注册中心的服务名称
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/cloud20?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 单机版
# defaultZone: http://localhost:7001/eureka # 入驻的服务注册中心地址
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 入驻的服务注册中心地址
instance:
instance-id: payment8001
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: cn.sher6j.springcloud.entities # 所有Entity别名类所在包
8002服务的修改同上,此时再访问注册中心,看到的服务具体名称中就没有主机名了,而是我们配置好的服务名称:
当前问题:我们在鼠标移动到具体服务时,提示的地址信息中并没有服务所在具体主机的IP地址,这在开发中不方便定位具体微服务。
解决方式仍然是通过配置文件,在配置文件中向其中的eureka部分加入即可配置该服务访问路径可以显示IP地址:
instance:
prefer-ip-address: true # 访问路径可以显示IP地址
最终的配置文件如下:
server:
port: 8001
......
eureka:
client:
......
instance:
instance-id: payment8001
prefer-ip-address: true # 访问路径可以显示IP地址
......
对于注册进Eureka服务注册中心的微服务,可以通过服务发现来获取该服务的信息。
package cn.sher6j.springcloud.controller;
......
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
......
/**
* @author sher6j
*/
@RestController
@Slf4j
public class PaymentController {
.......
@Autowired
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("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info("=======" + instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri() + "=======");
}
return this.discoveryClient;
}
}
DiscoveryClient对象中的 getServices
方法用于获取服务列表的信息,也就是有哪些服务,如cloud-payment-service服务, getInstances
方法用于获取服务列表对应的具体服务实例,如cloud-payment-service服务对应的8001和8002服务。
只需要在主启动类上添加注解@EnableDiscoveryClient
,修改后的主启动类:
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class);
}
}
访问地址http://localhost:8001/payment/discovery,我们可以看到获取的服务信息,即完成了服务发现:
后台也对服务列表进行了日志打印:
保护模式主要用于一组客户端和EurekaServer之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下提示,说明Eureka进入了保护模式(上面2. 单机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自我保护机制???
为了防止 EurekaClient可以正常运行,但是与EurekaServer网络不通 情况下,EurekaServer不会立刻将EurekaClient服务剔除。
什么是自我保护模式?
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之前无法正常通信,以上行为可能变得非常危险——因为微服务本身是健康的,只是由于网络问题链接不到EurekaServer,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障,网络延时),那么这个节点就会进入自我保护模式。
在自我保护模式中,EurekaServer会保护服务注册表中的信息,不再注销任何服务实例,宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
在EurekaServer端修改配置文件即可设置关闭自我保护机制
eureka:
server:
# 关闭自我保护机制,保证不可用服务被及时剔除
enable-self-preservation: false
# 时间间隔
eviction-interval-time-in-ms: 2000
在EurekaClient端修改配置文件
eureka:
instance:
instance-id: payment8001
# Eureka客户单向服务端发送心跳的时间间隔,默然是30秒
lease-renewal-interval-in-seconds: 1
# Eureka服务端在收到最后一次心跳后等待时间上限,默然为90秒,超时将剔除服务
lease-expiration-duration-in-seconds: 2
这样就会使EurekaClient客户端的微服务很快死亡。