服务发现组件是微服务架构中非常关键的一个组件。SpringCloud 提供的服务发现有多种,如Eureka,Consul和Zookeeper等。本篇介绍的是Eureka的使用。
服务发现简介
服务提供者,服务消费者,服务发现组件这三者之间的关系大致如下:
服务提供者与服务消费者都需要向服务发现组件进行注册,服务消费者从服务发现组件中获取服务提供者的信息(如名称、地址、端口等)。在服务发现组件注册的微服务需要通过心跳机制来保持连接状态并更新注册信息,当服务发现组件长时间无法与某个微服务实例进行通信时,会注销这个实例。这种机制使得即使服务提供者信息发生变化,服务消费者也无须修改配置文件。
由以上得知,服务发现组件应具备以下功能:
服务注册表:用于记录各个微服务的注册信息,它还提供查询API与管理API。
服务注册与服务发现:服务注册是指微服务在启动是将自己的信息注册到服务发现组件的过程。服务发现是指查询可用的微服务列表及其网络地址的机制。
服务检查:服务发现组件应有一定的机制定时检测已注册的微服务,如长时间无法访问,就会从服务注册表中移除该实例。
Eureka原理
以下是Eureka官方的架构图,比较详细的描述了Erueka集群的工作原理:
我们可以把us-east-1c、us-east-1d与us-east-1e理解成独立的机房,而这整个图则是一个跨机房的Eureka集群。其中:
Application Service 相当于前面说的服务提供者
Application Client 相当于前面的服务消费者
Make Remote Call 可以理解成调用RESTful API的行为
由于图中所示,我们可以知道Eureka包含两个组件:Eureka Server 和 Eureka Client,它们的作用如下:
Eureka Server提供服务发现的能力
Eureka Client 是一个java客户端,用于简化与Eureka Server的交互
微服务在启动后,会同期性(默认30s)地向Eureka Server 发送心跳来续约自己的“租期”
如果Eureka Server 在一定时间内(默认90s)没有接收到某个微服务的实例心跳,Eureka Server将会注销该实例
默认情况下,Eureka Server同时也是Eureka Client。多个Eureka Server 实例,互相之间通过复制的方式,来实现服务注册表中的数据同步
Eureka Client 会缓存服务注册表的信息,所以微服务无须每次请求都查询Eureka Server,从而降低了Eureka Server的压力。另外,即使Eureka Server 所以节点都挂了,服务消费者仍然可以使用缓存中的信息找到服务提供者并完成调用
在springCloud微服务架构下,各个业务会被拆分为独立的微服务。那么我们如何解决服务间调用的问题,springCloud默认提供了两种方式:restTemplate和feignClient
以下的例子会针对restTemplate 和 feignClient分别进行说明
------------------- Spring Boot 集成 SpringCloud组件Eureka来实现 服务注册、服务发现 -----------------
一、Eureka Server实现,注册中心的实现
1、引入maven依赖
2、我们需要在启动类上加上 @EnableEurekaServer
注解,声明这是一个Eureka Server,开启Eureka服务
3、添加application.properties配置
#服务路径
server.servlet.context-path=/my
server.port=8097
# 此应用为注册中心,false:不向注册中心注册自己。
eureka.client.register-with-eureka=false
# 注册中心职责是维护服务实例,false:不检索服务。
eureka.client.fetch-registry=false
#设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址,多个地址可以使用 , 分隔
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8097/my/eureka
4、启动项目,访问http://localhost:8097/my 即可看到Eureka Server的首页。可以看到界面展示了实例的状态,可用与不可用的Eureka节点,注册的服务实例列表,常用信息等
二(1)、微服务注册(服务提供者)
Eureka Server建好后,我们的微服务就可以向这个Eureka Server进行注册了。
1、添加maven依赖
2、application.properties配置
#端口配置
server.servlet.context-path=/pageHelper
server.port=8098
#注册中心的注册地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8097/my/eureka
#服务名称--调用的时候根据名称来调用该服务的方法
spring.application.name=service-provider
3、修改启动类
同样的,在微服务的启动类上加上一个注解 @EnableEurekaClient 表示这是一个Eureka Client。上面也说到Eureka 包含Server 与 Client两个组件,Client本身是一个JAVA客户端,把它集成到微服务中,简化了微服务与Eureka Server 的交互
(注意:@EnableDiscoveryClient,一种为@EnableEurekaClient,用法上基本一致。
spring cloud中discovery service有许多种实现(eureka、consul、zookeeper等等),@EnableDiscoveryClient基于spring-cloud-commons, @EnableEurekaClient基于spring-cloud-netflix。
其实用更简单的话来说,就是如果选用的注册中心是eureka,那么就推荐@EnableEurekaClient,如果是其他的注册中心,那么推荐使用@EnableDiscoveryClient。)
4、编写服务提供方对外提供的方法
/**
* 假如这个客户端要提供一个getUser的方法
* @return
*/
@GetMapping(value = "/getUser")
@ResponseBody
public Map
Map
data.put("id",id);
data.put("userName","admin");
data.put("from","provider-A");
return data;
}
二(2)、微服务注册(服务消费者)
1、添加maven依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-dependencies
Finchley.RC2
pom
import
2、application.properties配置
#端口配置
server.servlet.context-path=/userHelper
server.port=8099
#注册中心的注册地址eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8097/my/eureka
#服务名称--调用的时候根据名称来调用该服务的方法
spring.application.name=consumer
3、修改启动类
同样的,在微服务的启动类上加上一个注解 @EnableEurekaClient 表示这是一个Eureka Client。上面也说到Eureka 包含Server 与 Client两个组件,Client本身是一个JAVA客户端,把它集成到微服务中,简化了微服务与Eureka Server 的交互
4、在启动类加入:注入远程访问模板类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
/**
* 实例化RestTemplate
* @return
*/
@Bean
@LoadBalanced
public RestTemplate rest() {
return new RestTemplate();
}
}5、调用已注册的远程服务
@RestController
@RequestMapping("/api/{edition}/user")
public class MyPageHelperController {
@Autowired
private HouseMovingService houseMovingService;
@Autowired
RestTemplate restTemplate;
/**
* Rest服务端使用RestTemplate发起http请求,然后得到数据返回给前端
* @param id
* @return
*/
@GetMapping(value = "/gotoUser")
@ResponseBody
public MapgetUser(@RequestParam Integer id){
Mapdata = new HashMap<>();
/**
* 小伙伴发现没有,地址居然是 http://service-provider
* 居然不是 http://127.0.0.1:8082/
* 因为他向注册中心注册了服务,服务名称service-provider,我们访问service-provider即可
*/
data = restTemplate.getForObject(" http://service-provider/pageHelper/api/v1/page/getUser?id="+id,Map.class);
return data;
}
}
以上就是SpringBoot集成SpringCloud的Eureka实现服务注册、服务发现
----------- 重要知识点
Eureka Server设计精妙的注册表存储结构
现在咱们假设手头有一套大型的分布式系统,一共100个服务,每个服务部署在20台机器上,机器是4核8G的标准配置。
也就是说,相当于你一共部署了100 * 20 = 2000个服务实例,有2000台机器。
每台机器上的服务实例内部都有一个Eureka Client组件,它会每隔30秒请求一次Eureka Server,拉取变化的注册表。
此外,每个服务实例上的Eureka Client都会每隔30秒发送一次心跳请求给Eureka Server。
那么大家算算,Eureka Server作为一个微服务注册中心,每秒钟要被请求多少次?一天要被请求多少次?
按标准的算法,每个服务实例每分钟请求2次拉取注册表,每分钟请求2次发送心跳
这样一个服务实例每分钟会请求4次,2000个服务实例每分钟请求8000次
换算到每秒,则是8000 / 60 = 133次左右,我们就大概估算为Eureka Server每秒会被请求150次
那一天的话,就是8000 * 60 * 24 = 1152万,也就是每天千万级访问量
好!经过这么一个测算,大家是否发现这里的奥秘了?
首先,对于微服务注册中心这种组件,在一开始设计它的拉取频率以及心跳发送频率时,就已经考虑到了一个大型系统的各个服务请求时的压力,每秒会承载多大的请求量。
所以各服务实例每隔30秒发起请求拉取变化的注册表,以及每隔30秒发送心跳给Eureka Server,其实这个时间安排是有其用意的。
按照我们的测算,一个上百个服务,几千台机器的系统,按照这样的频率请求Eureka Server,日请求量在千万级,每秒的访问量在150次左右。
即使算上其他一些额外操作,我们姑且就算每秒钟请求Eureka Server在200次~300次吧。
所以通过设置一个适当的拉取注册表以及发送心跳的频率,可以保证大规模系统里对Eureka Server的请求压力不会太大。
关键问题来了,Eureka Server是如何保证轻松抗住这每秒数百次请求,每天千万级请求的呢?
要搞清楚这个,首先得清楚Eureka Server到底是用什么来存储注册表的?三个字,看源码
接下来咱们就一起进入Eureka源码里一探究竟:
从代码中可以看到,Eureka Server的注册表直接基于纯内存,即在内存里维护了一个数据结构。
各个服务的注册、服务下线、服务故障,全部会在内存里维护和更新这个注册表。
各个服务每隔30秒拉取注册表的时候,Eureka Server就是直接提供内存里存储的有变化的注册表数据给他们就可以了。
同样,每隔30秒发起心跳时,也是在这个纯内存的Map数据结构里更新心跳时间。
一句话概括:维护注册表、拉取注册表、更新心跳时间,全部发生在内存里!这是Eureka Server非常核心的一个点。
搞清楚了这个,咱们再来分析一下registry这个东西的数据结构,大家千万别被它复杂的外表唬住了,沉下心来,一层层的分析!
首先,这个ConcurrentHashMap的key就是服务名称,比如“inventory-service”,就是一个服务名称。
value则代表了一个服务的多个服务实例。
举例:比如“inventory-service”是可以有3个服务实例的,每个服务实例部署在一台机器上。
再来看看作为value的这个Map:
Map
这个Map的key就是服务实例的id
value是一个叫做Lease的类,它的泛型是一个叫做InstanceInfo的东东,你可能会问,这俩又是什么鬼?
首先说下InstanceInfo,其实啊,我们见名知义,这个InstanceInfo就代表了服务实例的具体信息,比如机器的ip地址、hostname以及端口号。
而这个Lease,里面则会维护每个服务最近一次发送心跳的时间
三、Eureka Server端优秀的多级缓存机制
假设Eureka Server部署在4核8G的普通机器上,那么基于内存来承载各个服务的请求,每秒钟最多可以处理多少请求呢?
根据之前的测试,单台4核8G的机器,处理纯内存操作,哪怕加上一些网络的开销,每秒处理几百请求也是轻松加愉快的。
而且Eureka Server为了避免同时读写内存数据结构造成的并发冲突问题,还采用了多级缓存机制来进一步提升服务请求的响应速度。
在拉取注册表的时候:
首先从ReadOnlyCacheMap里查缓存的注册表。
若没有,就找ReadWriteCacheMap里缓存的注册表。
如果还没有,就从内存中获取实际的注册表数据。
在注册表发生变更的时候:
会在内存中更新变更的注册表数据,同时过期掉ReadWriteCacheMap,此过程不会影响ReadOnlyCacheMap提供人家查询注册表
一段时间内(默认30秒),各服务拉取注册表会直接读ReadOnlyCacheMap,30秒过后,Eureka Server的后台线程发现ReadWriteCacheMap已经清空了,也会清空ReadOnlyCacheMap中的缓存。下次有服务拉取注册表,又会从内存中获取最新的数据了,同时填充各个缓存
多级缓存机制的优点是什么?
尽可能保证了内存注册表数据不会出现频繁的读写冲突问题
并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高
为方便大家更好的理解,同样来一张图,大家跟着图再来回顾一下这整个过程:
四、总结
通过上面的分析可以看到,Eureka通过设置适当的请求频率(拉取注册表30秒间隔,发送心跳30秒间隔),可以保证一个大规模的系统每秒请求Eureka Server的次数在几百次
同时通过纯内存的注册表,保证了所有的请求都可以在内存处理,确保了极高的性能
另外,多级缓存机制,确保了不会针对内存数据结构发生频繁的读写并发冲突操作,进一步提升性能
Erueka高可用部署
前面我们写了一个Eureka Server,并将一个微服务注册到了Eureka Server,在实际环境中,Eureka 需要是一个高可用的集群环境。这样Eureka Server 宕机时,其它的Eureka节点还是能够继续提供服务,虽然Eureka Client有缓存注册表信息,也可以提供服务查询,但缓存不及时更新也会影响之后的服务调用。
前面在编写Eureka Server时配置了 eureka.client.register-with-eureka=false 和eureka.client.fetch-registry=false,现在在多节点的环境中,要实现Eureka实例之间相互注册彼此增量地同步信息,才能确保各节点数据一致,实现Eureka的高可用部署。所以下面的集群环境中,这两个配置应该为 true 或不配置默认为 true 。
修改hosts
因为是在本机实现多节点的Eureka Server集群,需要修改一下系统的hosts文件,添加以下配置:
127.0.0.1 peer1 peer2 peer3
如修改hosts无法保存,需要添加用户修改权限。
添加成功后可以ping peer1试试是否配置生效。
修改配置
将上面编写的Eureka Server 项目的application.yml修改如下 :
spring:
application:
name: microservice-discovery-eureka
---
spring:
profiles: peer1 # 指定profile=peer1
server:
port: 8761
eureka:
instance:
hostname: peer1 # 指定当profile=peer1时,主机名是peer1
prefer-ip-address: true
client:
serviceUrl:
defaultZone: http://peer2:8762/eureka/,http://peer3:8763/eureka/ # 将自己注册到peer2和peer3这两个Eureka上面去
register-with-eureka: true
fetch-registry: true
---
spring:
profiles: peer2
server:
port: 8762
eureka:
instance:
hostname: peer2
prefer-ip-address: false
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/,http://peer3:8763/eureka/
register-with-eureka: true
fetch-registry: true
---
spring:
profiles: peer3
server:
port: 8763
eureka:
instance:
hostname: peer3
prefer-ip-address: false
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/
register-with-eureka: true
fetch-registry: true
YAML 文件可以由一或多个文档组成(即相对独立的组织结构组成),文档间使用---(三个横线)在每文档开始作为分隔符。同时,文档也可以使用...(三个点号)作为结束符(可选)。
所以这里的配置文件实际上是三个配置文件组成,---分隔符不可少,否则编译出错。也可以把这几段配置写在三个YAML文件中。
启动
启动项目时通过给spring.profiles.active 传递不同的参数,以使用不同的配置。
在IDEA中我们可以在启动前配置参数:
添加三个Spring Boot Application,并选择启动类,在Active Profiles中分别配置peer1,peer2,peer3参数后,分别启动。
在启动时,先启动成功的Eureka 可能会报以下错误:
2018-07-05 14:32:10.983 ERROR 13768 --- [-target_peer2-8] c.n.e.cluster.ReplicationTaskProcessor : Network level connection to peer peer2; retrying after delay
com.sun.jersey.api.client.ClientHandlerException: java.net.SocketTimeoutException: Read timed out
这是因为先启动成功的peer1根据配置会向peer2和peer3进行注册请求,而此时这两个应用可能还没启动好,所以出现上面的错误,三个应用启动成功后就正常了。
正常情况下如下图:
显示了注册到该Eureka的实例信息,Eureka各节点地址。
在下面可以看到显示了两个注册的节点,同时这两个节点是可用的。
我之前测试时出现不可用(unavailable-replicas)的节点,如下:
解决情况是将 eureka.instance.prefer-ip-address 设置为 false ,因为是在本地配置集群环境,IP是相同的,因此使用IP注册可能会导致节点不可识别,另外,三个应用的spring.application.name不一致也会出现 unavailable-replicas 的情况
服务注册到Erueka 集群
前面我们将服务提供者注册到了一个单节点的Eureka Server ,在Eureka的集群环境中,只需要稍加修改即可。把服务提供者的 eureka.client.serviceUrl.defaultZone配置修改如下:
eureka:
client:
service-url:
defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/,http://peer3:8763/eureka/
其实可以继续使用localhost,因为是在本地测试。
另外,我们可以只写一个Eureka Server的地址,因为各Eureka Server节点之间会自动同步注册的实例信息,正常情况下这两种方式是一样。建议全部写上。
启动服务提供者,刷新三个Eureka Server可以看到服务已经注册了:
Eureka用户认证
在生产环境中Eureka Server需要登录才能访问,Eureka的用户认证可以使用Security实现。下面稍微修改一下Eureka Server
添加依赖
添加配置
添加以下配置在第一段中,作为三个Eureka Server的公共配置。
security:
basic:
enabled: true #开启HTTP basic的认证
user:
name: user #配置登录账号证
password: admin #配置登录密码
最后把Eureka以及服务提供者的配置文件中的 defaultZone 地址修改成如下格式:
http://user:admin@peer1:8761/eureka/
这样才能注册到需要认证的Eureka Server。
重新启动Eureka Server和服务提供者后,再次访问三个Eureka可以看到弹出了登录框,输入上面的账号与密码后登录后即可进行Eureka Server首页,服务已正常注册成功。
Eureka自我保护模式
在实现上面的练习过程中,打开Eureka可能会遇到如下情况:
这是Eureka开启自我保护模式时的警告。
前面说过Eureka Server在一定时间内没有接收到某个微服务实例心跳,Eureka Server将会注销该实例。但如果出现网络分区故障,微服务与Eureka Server之间无法正常通信,微服务其实是正常的,但由于无法接收到该微服务的通信,Eureka Server则会注销该服务。
为了避免这种情况的发生,Eureka Server 通过 自我保护模式 来解决这个问题。当Eureka Server在短时间内丢失过多客户端时,那么这个Eureka节点会进行自我保护模式。Eureka 会保护服务注册表中的信息,不再删除服务注册表中的数据,当网络故障恢复后,该Eureka节点会自动退出自我保护模式。
这种模式使得Eureka 集群更加健壮,稳定。
另外也可以选择禁用该模式,配置如下:
eureka.server.enable-self-preservation = false
Eureka健康检查
登录Eureka首页可以看到注册的微服务状态是 UP
表示应用程序状态正常,应用的状态还有其它取值,如DOWN,OUT_OF_SERVICE,UNKNOWN等。只有标记为 UP 的微服务会被请求。
默认情况下,服务器端与客户端的心跳保持正常,应用程序状态就会显示UP 状态。但这个机制并不能完全反应微服务的状态,举个栗子,微服务与Eureka Server心跳正常,但微服务的数据源出现了问题(如数据库被关闭)。这个微服务其实无法正常工作,但Eureka Server认为该微服务为UP状态。
为了解决这个问题,我们可以把微服务的健康状态传播到Eureka Server,只需要在微服务中作如下配置:
eureka:
client:
healthcheck:
enabled: true
这样,当微服务健康状态出问题时,Eureka Server能够实时反应其它真实状态。
--------------------------------------------------------- 加入网关 --------------------------------------------------
接着上面的服务注册发现,这里添加网关功能 ,这里的网关使用Zuul,在zuul中有两种路由配置:
1. 通过访问ip及端口号映射
2. 通过服务名称映射
1、创建一个项目,用作网关服务zuul-server
2、引入核心依赖
3、启动类添加注解
@SpringBootApplication
@EnableEurekaClient //开启服务注册发现
@EnableZuulProxy //开启网关
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4、配置文件
方式一:通过访问ip及端口号映射
server.servlet.context-path=/myZuul
server.port=8190
#注册中心的注册地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8097/my/eureka
#服务名称--调用的时候根据名称来调用该服务的方法
spring.application.name=myZuul
//userHelper是服务消费方的跟路径
zuul.routes.client.path = /userHelper/**
//client是标识,可以自定义
zuul.routes.client.url=http://localhost:8099/userHelper ;
方式二:通过服务名称映射
server.servlet.context-path=/myZuul
server.port=8190
#注册中心的注册地址
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8097/my/eureka
#服务名称--调用的时候根据名称来调用该服务的方法
spring.application.name=myZuul
//userHelper是服务消费方的跟路径
zuul.routes.client.path = /userHelper/**
//client是标识,可以自定义
zuul.routes.client.url=http://localhost:8099/userHelper ;
5、访问路径
http://localhost:8190/myZuul/userHelper/api/v1/user/gotoUser?id=1
或者
http://localhost:8099/userHelper/api/v1/user/gotoUser?id=1
参考资料:
https://blog.csdn.net/xm393392625/article/details/88819341
https://blog.csdn.net/FTDD_HW/article/details/80922134
SpringCloud服务注册+feign服务调用:https://blog.csdn.net/qq_21299835/article/details/81081455?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase