Eureka服务注册与发现探究

32次阅读

共计 8240 个字符,预计需要花费 21 分钟才能阅读完成。

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

拟定一个场景,假设我们已经有一个服务生产者为用户微服务,这个服务提供了可以通过 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 就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

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

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

七、结语

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

正文完
 0