Eureka服务注册与发现探究

2019-11-03 00:00:00 注册 发现 探究

一、为什么使用服务注册发现

拟定一个场景,假设我们已经有一个服务生产者为用户微服务,这个服务提供了可以通过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简介

Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。Spring Cloud将它集成在Netflix中, 从而实现微服务的注册与发现。当然Eureka的使用架构不能脱离服务发现的架构:
《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服务注册与发现探究》

注: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 注解标识开启服务组件客户端。
注:
《Eureka服务注册与发现探究》

  • 这些版本都是以开头字母顺序命名的, 开始的几个版本都是伦敦地铁站的名字, 每个版本的生产版本都是.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
《Eureka服务注册与发现探究》

http://192.168.30.161:8889/user/instance/1
《Eureka服务注册与发现探究》

[{
    "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就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  1. Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
  2. Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
  3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中

因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。
————————————————
版权声明:本文为CSDN博主「司青」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/neosmit…

七、结语

当然服务注册发现的中间件也有很多,本文只是以Eureka为例浅析,除此,Eureka 中的配置内容还有很多, 比如权限配置、健康检查、元数据配置以及分布式部署等内容需要深入学习。本文只是泛泛而谈,如果错误,还请读者指正。

相关文章