Eureka服务注册与发现探究
一、为什么使用服务注册发现
拟定一个场景,假设我们已经有一个服务生产者为用户微服务,这个服务提供了可以通过Id获得用户的RESTful方法:
@GetMapping("/{id}")
public User findUserById(@PathVariable Long id){
User user = this.userMapper.findById(id);
return user;
}
另外我们再提供一个服务消费者为 电影微服务, 使用RestTemplate 调用服务生产者的API:
@GetMapping("/user/{id}")
public User findUserById(@PathVariable Long id){
return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class);
}
通过这样简单的编码, 就可以实现服务之间的简单调用了,其中服务消费中使用的ip+端口, 我们还可以写到application.yml中,这样就完美的实现了服务的调用。
但是,这样真的就完美了吗?以上的硬编码有存在哪些问题呢?
(1)适应场景有限:
如果服务的提供者ip端口发生变化, 那么我们的服务消费者就必须同时修改代码或者配置,并重新发布。
(2)无法动态伸缩:
在生产环境中,每个微服务一般都会部署多个实例,从而实现容灾和负载均衡。动态增减节点, 而硬编码无法适应这种需求。
为了解决以上问题,我们需要使用服务注册与发现。
二、服务发现简介
服务发现架构图如下:
服务提供者、服务消费者、服务发现者之间的关系如下:
- 在各个微服务启动时, 将自己的网络地址等信息注册到服务发现组件,服务发现组件会存储这些信息。
- 服务消费者会从服务发现组件获取服务提供者的网络地址,并使用该地址调用服务提供者的接口。
- 各个微服务与服务发现组件使用一定的机制通信(例如心跳)。若长时间无法与某个实例通信,服务发现组件就会注销这个实例。
- 各个微服务的网络地址发生变化时, 会重新注册到服务发现组件。
三、Eureka简介
Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。Spring Cloud将它集成在Netflix中, 从而实现微服务的注册与发现。当然Eureka的使用架构不能脱离服务发现的架构:
由此图可知,Eureka 分成 Client 和 Server 两部分。
- Eureka Server 提供服务发现的能力, 各个微服务启动时,会向Eureka Server 注册自己的信息, Eureka 会存储这些信息。
- Eureka Client 是一个java客户端,微服务启动后, 会周期性的(默认30s)地向 Eureka Server 发送心跳以续约自己的“租期”。
- 如果 Eureka Server 在一定时间内没有收到某个微服务实例的心跳, Eureka Server 将会注销该实例(默认90s)。
四、Eureka原理浅析
1、 程序的构成
(1)Eureka 是一个 servlet 应用;
(2)使用了 Jersey 框架实现自身的 RESTful HTTP接口;
(3)Eureka 之间的同步与服务的注册全部通过 HTTP 协议实现;
(4)定时任务(发送心跳、定时清理过期服务、节点同步等)通过 JDK 自带的 Timer 实现;
(5)内存缓存使用Google的guava包实现。
注:开发RESTful WebService意味着支持在多种媒体类型以及抽象 底层的客户端-服务器通信细节,如果没有一个好的工具包可用,这将是一个困难的任务
为了简化使用JAVA开发RESTful WebService及其客户端,一个轻量级的标准被提出:JAX-RS API
Jersey RESTful WebService框架是一个开源的、产品级别的JAVA框架,支持JAX-RS API并且是一个JAX-RS(JSR 311和 JSR 339)的参考实现。
2、Eureka的注册表
Eureka 是通过内存和缓存来实现服务的注册功能的,在Eureka中 PeerAwareInstanceRegistry 用这样一个接口, 用来保存所有的服务,这个也是所谓的服务注册表。注册的服务列表保存在一个hashmap中。
除此之外Eureka还做了服务的多级缓存来, 如下图所示:
注:Eureka Client对已经获取到的注册信息也做了30s缓存。即服务通过eureka客户端第一次查询到可用服务地址后会将结果缓存,下次再调用时就不会真正向Eureka发起HTTP请求了。
五、Eureka安装及部署
1、首先来构建Eureka Server这个服务注册发现的Server端。本文采用Maven来构建项目。
(1)添加以下依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
(2)编写启动类,在启动类上添加@EnableEurekaServer注解, 声明这是一个Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class MicroserviceDiscoveryEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(MicroserviceDiscoveryEurekaApplication.class, args);
}
}
(3)在配置文件application.yml中添加以下内容。
server:
port: 8761
eureka:
client:
register-with-eureka: false # 表示是否将自己注册到Eureka Server
fetch-registry: false # 表示是否从别的Eureka Server 获取注册信息,因为是单点部署, 所以设置为false
service-url:
# 设置与Eureka Server交互的地址,查询服务和注册服务都要依靠这个地址。默认是当前这个,多个地址间可以用 “,” 分隔
default-zone: http://localhost:8761/eureka/
本地服务器部署地址:
http://192.168.30.161:8761/
注:如果Eureka A的peer指向了B, B的peer指向了C,那么当服务向A注册时,B中会有该服务的注册信息,但是C中没有。也就是说,如果你希望只要向一台Eureka注册其它所有实例都能得到注册信息,那么就必须把其它所有节点都配置到当前Eureka的peer属性中。
2、其次构建一个服务提供者的微服务。
(1)添加以下依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
(2)在配置文件中添加以下配置
spring:
application:
# 注册到Eureka Server 的应用名称
name: microservice-provider-user
eureka:
client:
service-url:
# 注册到Eureka 的地址
default-zone: http://192.168.30.161:8761/eureka/
instance:
# 表示将自己的ip注册到 Eureka Server, false 表示将自己所在操作系统的hostname 注册到Eureka Server
prefer-ip-address: true
如果是SpringCloud Edgware 版本以前,还需要在Application.java 中加上@EnableEurekaClient 注解 或 @EnableDiscoverClient 注解标识开启服务组件客户端。
注:
- 这些版本都是以开头字母顺序命名的, 开始的几个版本都是伦敦地铁站的名字, 每个版本的生产版本都是.GA。
- 在单例模式下,eureka.instance.hostname必须是localhost,而且defaultZone不能使用ip,要使用eureka.instance.hostname且走域名解析才可以。 这里我们配置的是localhost,不需要修改hosts文件。
(3)编写简单的接口
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/{id}")
public User findById(@PathVariable Long id) {
Optional<User> findOne = userRepository.findById(id);
return findOne.orElse(null);
}
}
这样就有了一个可以测试的接口了。
http://192.168.30.161:8888/user/1
3、构建一个服务消费者的微服务
这个服务的消费者只需要和服务提供者的配置相同就可以了。下面我们部署一个microservice-consumer-movie 用来消费提供者提供的接口。
部署地址是:http://192.168.30.161:8889/
@RestController
@RequestMapping(value = "/user")
public class MovieController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* Method Description: Created by whx
* 〈从8888 获取user 信息〉
*
* @param id user id
* @return com.ultrapower.consumermovie.pojo.User
* @date 11/02/2019 15:08
*/
@GetMapping("/{id}")
public User findById(@PathVariable Long id) {
return this.restTemplate.getForObject("http://192.168.30.161:8888/user/" + id, User.class);
}
/**
* Method Description: Created by whx
* 〈从服务注册中心 获取 microservice-provider-user 服务提供的信息〉
*
* @param id user id
* @return com.ultrapower.consumermovie.pojo.User
* @date 11/02/2019 15:09
*/
@GetMapping("/instance/{id}")
public User findByUserInstanceId(@PathVariable Long id) {
List<ServiceInstance> list = discoveryClient.getInstances("microservice-provider-user");
String url = list.get(0).getUri().toString();
return this.restTemplate.getForObject(url + "/user/" + id, User.class);
}
/**
* Method Description: Created by whx
* 〈获取 microservice-provider-user 服务的 instance 信息 〉
*
* @return java.util.List<org.springframework.cloud.client.ServiceInstance>
* @date 11/02/2019 15:10
*/
@GetMapping("/instance")
public List<ServiceInstance> findByUserInstance() {
return discoveryClient.getInstances("microservice-provider-user");
}
}
以下是该服务的获取结果:
http://192.168.30.161:8889/user/1
http://192.168.30.161:8889/user/instance/1
[{
"metadata": {
"management.port": "8888"
},
"secure": false,
"uri": "http://192.168.30.161:8888",
"instanceId": "localhost:microservice-provider-user:8888",
"serviceId": "MICROSERVICE-PROVIDER-USER",
"instanceInfo": {
"instanceId": "localhost:microservice-provider-user:8888",
"app": "MICROSERVICE-PROVIDER-USER",
"appGroupName": null,
"ipAddr": "192.168.30.161",
"sid": "na",
"homePageUrl": "http://192.168.30.161:8888/",
"statusPageUrl": "http://192.168.30.161:8888/actuator/info",
"healthCheckUrl": "http://192.168.30.161:8888/actuator/health",
"secureHealthCheckUrl": null,
"vipAddress": "microservice-provider-user",
"secureVipAddress": "microservice-provider-user",
"countryId": 1,
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
},
"hostName": "192.168.30.161",
"status": "UP",
"overriddenStatus": "UNKNOWN",
"leaseInfo": {
"renewalIntervalInSecs": 30,
"durationInSecs": 90,
"registrationTimestamp": 1572502252210,
"lastRenewalTimestamp": 1572502972612,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1572502251658
},
"isCoordinatingDiscoveryServer": false,
"metadata": {
"management.port": "8888"
},
"lastUpdatedTimestamp": 1572502252211,
"lastDirtyTimestamp": 1572502251644,
"actionType": "ADDED",
"asgName": null
},
"host": "192.168.30.161",
"port": 8888,
"scheme": null
}]
六、Eureka与Zookeeper对比
著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。
1. Zookeeper保证CP
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集羣都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集羣失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
2. Eureka保证AP
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
- Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
- Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
- 当网络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。
————————————————
版权声明:本文为CSDN博主「司青」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/neosmit…
七、结语
当然服务注册发现的中间件也有很多,本文只是以Eureka为例浅析,除此,Eureka 中的配置内容还有很多, 比如权限配置、健康检查、元数据配置以及分布式部署等内容需要深入学习。本文只是泛泛而谈,如果错误,还请读者指正。
相关文章