不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

SpringCloud微服务系列(2) Eureka注册中心

注册中心Eureka

起源

Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。

基本介绍

为什么需要注册中心

微服务的早期,我们可以通过一些静态配置或者软件负载来完成服务之间的负载均衡调用,但随着微服务的不断增加,静态配置就会暴露出一些问题:

  • 静态配置维护越来越复杂
  • 集群规模越来越大,人工维护成本高
  • 服务的位置可能会发生变化,灵活性差,调整成本高
  • 服务的名称都有可能发生变化,难以维护

所以为了有效解决以上的问题,我们需要引入服务治理。服务治理一般包含三个角色:

  • 服务注册中心:每个服务都要在注册中心进行注册登记自己的服务(主机,端口,版本,协议等),服务中心会提供心跳维护。
  • 服务提供者:提供服务的一方,就是服务的被调用方
  • 服务消费者:服务的调用方,当然本身也可以是其他服务的提供者

也就是说,在服务治理的框架下,服务间的调用不再通过指定具体的实例地址来实现,而是通过服务名来实现。
服务的调用方在注册中心查询到可用的服务清单后,可以采用不同的负载均衡方式进行调用。

Spring Cloud Eureka能做什么

  • 既包含了服务端组件(作为注册中心),又包含了客户端组件(作为服务提供者,处理服务的注册与发现)
  • 服务端和客户端均以java实现,非常适合通过java实现的分布式系统或与JVM兼容的其他语言构成的系统
  • 提供了完备RESTful API,支持非java语言构建的微服务纳入进来,不过其他语言要实现自己的客户端程序(很多语言都已经有实现)

核心知识

搭建服务中心Eureka Server

依赖于:spring-cloud-starter-eureka-server (默认是服务端和客户端为一体的)

关键注解
  • 在启动类上加上@EnableEurekaServer注解,使其成为注册中心
关键配置
  • 注册中心服务端口
  • 注册中心服务实例名称
  • 默认客户端配置是打开的,注册中心也会将自己作为客户端来尝试注册自己,所以我们需要禁用它的客户端注册行为
  • 如果不是高可用的多中心配置,也不需要和其他注册中心进行同步,检索其他注册中心服务也要禁用

一个单中心注册服务配置示例:

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  # 每个Eureka server 也是一个client 所以把client相关配置关闭掉,只作为服务使用
  client:
    # 只作为服务端
    registerWithEureka: false
    # 不需要同其他的注册中心同步信息
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  # 关闭注册中心的保护模式,如果90秒收不到心跳信息,将销毁某个注册者信息,
  # 开启保护模式时,即使服务提供者宕机或无法提供服务,注册中心仍然会保留注册信息
  server:
    enable-self-preservation: false

客户端注册到注册中心

  • 客户端的依赖和服务端一样
  • 客户端可以使用@EnableEurekaClient注解,也可以使用@EnableDiscoveryClient注解注册为服务的客户端
  • 二者区别:当Eureka在classpath下的话,二者没有区别。@EnableDiscoveryClient可以支持其他的服务发现组件,比如zk
  • 最后客户端只要添加以下配置就可以注册到服务端注册中心了
eureka:
  client:
    service-url:
      #注册中心地址
      defaultZone: http://localhost:8761/eureka/

  # 以IP地址方式注册,默认是hostname
  instance:
    ip-address: true
server:
  port: 8762
# service name
spring:
  application:
    name: service-hi

我们给客户端添加一个简单的hello world服务

@RestController
@EnableEurekaClient
@SpringBootApplication
public class EurekaclientApplication {

   public static void main(String[] args) {
      SpringApplication.run(EurekaclientApplication.class, args);
   }

   @Value("${server.port}")
   String port;

   @RequestMapping("/hi")
   public String home(@RequestParam String name){

      return "hi " + name +", i am from port" + port;
   }
}

#### 简单测试

分别启动服务端和客户端

服务端,端口:8761
java -jar eureka-server-1.0.0.jar 

客户端,端口:8762
java -jar eureka-client-1.0.0.jar 

访问:localhost:8761可以查看服务端注册中心的监控页面,看到我们的客户端服务已经注册到注册中心了

尝试通过url,http://localhost:8762?name=hziquxm 来访问下服务,客户端的服务则会返回

hi hzqiuxm, i am from port8762

Eureka Server 的高可用

在分布式服务应用中,高可用是必须考虑的事情,我们注册中心也需要具备高可用才行,所以下面简单介绍下注册中心的高可用

基本思想
  • 实现思想:所有节点是服务提供方,也是服务消费方,服务中心也一样,多个Eureka之间相互注册实现高可用

  • 建议按照优先级命名方式比如:profile:primary ,secondary, tertiary,当然也可以采用自己喜欢的简单命名方式,比如:ha1,ha2......

高可用注册中心架构图:

注意:Eureka微服务客户端只要注册到其中一个服务端即可

主要配置
  • 配置关键点:每个Eureka名字相同, 实例名称不同,端口号只要不在一台主机上建议都设置相同,便于统一管理
  • 不同中心启动的时候指定对应的配置文件中不同的段,例如:java -jar eureka-server-1.0.0.jar -- spring.profiles.active=ha1

一个高可用注册中心服务配置示例:

# ha service name
spring:
  application:
    name: service-hi-ha
---
spring:
  profiles: ha1
server:
  port: 8761
eureka:
  instance:
    # profiles = ha1
    hostname: ha1
  client:
    serviceUrl:
      # 将自己注册到ha2
      defaultZone: http://ha2:8771/eureka/

---
spring:
  profiles: ha2
server:
  port: 8771
eureka:
  instance:
    # profiles = ha2
    hostname: ha2
  client:
    serviceUrl:
      # 将自己注册到ha1
      defaultZone: http://ha1:8761/eureka/

简单示例

我们分别启动二个注册中心(其实从数学理论角度看,3个是最佳的,3个的配置也很简单,就是在其中一个中心注册地址defaultZone后面加上另外2个中心地址即可,用逗号分开):
为了演示方便,我这里就举2个中心的例子

注册中心1,端口:8761
java -jar eureka-server-1.0.0.jar -- spring.profiles.active=ha1

注册中心2,端口:8771
java -jar eureka-server-1.0.0.jar -- spring.profiles.active=ha2

客户端服务,端口:8762,只注册到8761
java -jar eureka-client-1.0.0.jar 

我们访问8761和8771的注册中心监视界面:
下面是8761

数字1:此列出了注册到8761上的实例,发现除了客户端服务外,注册中心本身
数字2:此处指出了8761注册中心注册到了8771端口的注册中心
数字3:此处指出了8761的备份节点是8771

下面是8771

数字1:可以发现我们之前客户端只是注册到了8761端口的注册中心,但在此处,也可以发现此实例
数字2:此处指出了8771注册中心注册到了8761端口的注册中心
数字3:此处指出了8771的备份节点是8761

组件详解

基础架构与通信行为

Eureka作为服务治理框架,其基础架构主要包含了三个核心要素

  • 服务注册中心:就是本节中的Eureka服务端,又称之为注册中心
  • 服务生产者:就是本节中的Eureka客户端,扮演作用是服务的提供者
  • 服务消费者:本节上面没有演示,其实也是Eureka客户端,扮演的作用是服务的消费者

注意:上面的服务提供者和服务消费者在实际应用中并不是单一职责的,服务B可能是服务A的提供者,同时也可能是服务C的消费者,是一个相对的概念。

下图是三者关系调用图:

服务生产者:主要有三种操作,服务注册、服务续约、服务下线

服务注册,服务的提供者在启动的时候通过发送REST请求将自己注册到Eureka Server上。Eureka Server接收到这个信息后,
会把发送请求中关于服务提供者的元数据信息存放在一个双层的Map中。

类似下面的结构:第一层key是服务名,value是这个服务下的所有实例;第二层key是实例名,value是具体实例元数据信息

因为上图中的架构是高可用的架构,所以注册中心之间还会有个服务同步的操作,在一方注册的服务提供者信息会被同步到另一方的注册中心。

通过服务同步,服务提供者的服务就可以从这两台注册中心中的任意一台上获得,从而实现了高可用。

服务续约,在注册完服务后,服务提供者会维护一个心跳来持续告诉注册中心它还活着,以防止注册中心从服务列表中剔除没有心跳的服务实例。

服务续约的两个重要属性是

EurekaInstanceConfigBean类
private int leaseRenewalIntervalInSeconds = 30; //续约服务调用间隔时间
private int leaseExpirationDurationInSeconds = 90;//定义服务失效时间

服务下线,在服务关闭时候,会触发一个服务下线的REST请求给注册中心,注册中心收到请求后,将该服务状态置为下线,
并把该下线事件传播出去(同步给其他注册中心或以及通知服务消费者)。

服务的消费者:主要操作二个,获取服务、服务调用。

获取服务,服务消费者在启动的时候会发送一个REST请求给服务注册中心,获取在上面注册的服务清单。
出于性能考虑,注册中心Eureka Server只会维护一份只读的清单缓存用来返回给客户端使用,清单默认每隔30秒刷新一次

EurekaClientConfigBean类
private int registryFetchIntervalSeconds = 30; //刷新时间配置的属性与默认值

服务调用,服务消费者在获取了服务清单后,通过服务名可以获得具体提供服务的实例名和元数据信息(参考上面的双层Map图)。
客户端可以根据自己的需要来决定具体调用哪个,所以一般我们会在服务消费者端集成类似Ribbon,Feign这样的负载工具。

这里需要补充的是对于访问实例的选择,Eureka中有Region和Zone概念,它们的关系如下图所示:

一个Region中可以包含多个Zone,Zone中包含了服务的实例。细心的读者会发现我们之前的配置文件中有这么一段

  client:
    serviceUrl:
      # 将自己注册到ha1
      defaultZone: http://ha1:8761/eureka/

这里的defaultZone就是服务默认注册的Zone,我们也可以自己设置Region和Zone

EurekaClientConfigBean类
private String region = "us-east-1";//默认的region名字
private Map<String, String> availabilityZones = new HashMap<>();//多个Zone用逗号分开

消费者在进行服务调用时,优先访问同一个Zone的服务,若访问不到就会访问其他Zone。

最佳实践提醒:利用上面这个特点我们可以用一个Zone代表一个物理区域(物理主机或集群),设计出具备区域故障容错的微服务集群。

关键源码分析

我们把一个普通的SpringBoot应用注册到Eureka Server注册中心时,主要做了两件事:

  • 在应用主类中配置了@EnableDiscoveryClient或者@EnableEurekaClient注解
  • 在配置文件中用eureka.client.serviceUrl.defaultZone参数指定了服务中心的位置

那这一切是如何发生的呢?我们顺着这两个线索,一起去看看它们背后的实现原理。

首先我们看下@EnableDiscoveryClient注解的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

   /**
    * If true, the ServiceRegistry will automatically register the local server.
    */
   boolean autoRegister() default true;
}

从上面的代码中我们可以看出来,它主要用来开启DiscoveryClient实例。由这个类我们梳理下和它相关的类,以及他们之间的关系如下:

为了让大家看清楚它们之间的关系我用两种不同的颜色区分了Netflix包下的类和SpringCloud包下面的类。真正实现发现服务的
则是Netflix 包中的com.netflix.discovery.DiscoveryClient 类,我们就来详细看看DiscoveryClient 类功能吧,它的主要作用就是与注册中心Eureka Server进行交互,上一节中我们说了它的功能主要有:向注册中心注册服务实例、向注册中心服务租约、服务关闭时取消租约、查询注册中心服务实例列表。

DiscoveryClient 中提供下非常多的方法,在这里就不一一说明,举上面的注册来说说吧,希望大家可以举一反三。

通过查看它的构造类, 可以找到它调用了下面这个函数:

private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        ...

    if (clientConfig.shouldRegisterWithEureka()) { //注释一
        ...

        // InstanceInfo replicator
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize
    ...
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

大家可以看到在注释一处判断了是否要注册到注册中心,条件为真后创建了一个InstanceInfoReplicator实例,它实现了Runnable接口,所以会启动一个线程来处理。

果然在后面的代码中InstanceInfoReplicator实例启动了start方法,我们赶紧去它的Run方法里看下,它启动后干了点什么:

public void run() {
    try {
        discoveryClient.refreshInstanceInfo();

        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
            discoveryClient.register();//注释二
            instanceInfo.unsetIsDirty(dirtyTimestamp);
        }
    } catch (Throwable t) {
        logger.warn("There was a problem with the instance info replicator", t);
    } finally {
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

不负所望,在它的run方法里注释二处,我们看到了discoveryClient.register(),这一行真正触发了注册的动作。我们再进入注册方法中看看:

/**
 * Register with the eureka service by making the appropriate REST call.
 */
boolean register() throws Throwable {
    logger.info(PREFIX + appPathIdentifier + ": registering service...");
    EurekaHttpResponse<Void> httpResponse;
    try {
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == 204;
}

相信一切都真相大白了,注册操作通过REST请求方式进行,注册时传入的instanceInfo就是客户端给服务端的元数据,如果你对元数据看兴趣的话,就进入InstanceInfo中去看看吧。

配置详解

服务注册类

这部分主要负责配置注册中心的地址、服务获取的间隔时间、可用区域等。
服务注册类的配置,我们可以查看源码中的org.springframemwork.cloud.netflix.eureka.EurekaClientConfigBean类,该类中的属性基本都是可以进行配置的,比看官方的文档还要全。
比如上面我们提到的属性:registryFetchIntervalSeconds = 30,用来设置缓存中服务清单刷新时间,30表示默认值。我们如果配置成100秒的话可以这么配置:

eureka.client.registry-fetch-interval-seconds=100
服务实例类

这部分主要负责配置服务实例的名称、IP地址、端口号、健康检查路径等。
服务实例类配置我们可以查看源码中的org.springframemwork.cloud.netflix.eureka.EurekaInstanceConfigBean类。
比如我们上面提到的属性:leaseRenewalIntervalInSeconds = 30,用来设置续约时间,30表示默认值。我们如果要配置成90秒的话,可以:

eureka.client.lease-renewal-interval-in-seconds=90

跨平台支持

其他语言客户端

因为采用了HTTP的REST接口方式,使得Eureka Server下注册的微服务不限于使用Java开发。
除了Java实现了Eureka的客户端外,有JS的实现eureka-js-client,Python的实现python-eureka,即使是你自己来为某门语言来开一个客户端,
也并不是十分复杂,只需要根据上面提到的那些用户服务协调的通信请求实现就能实现服务的注册与发现,有兴趣的同学可以参考官方的API。

通信协议

默认情况下,Eureka使用Jersey和XStream配合JSON作为Server和Clinet之间的通信协议。
Jersey是JAX-RS规范的参考实现,主要包含:核心服务器,核心客户端,集成三个部分。
XStream是用来将对象序列化或反序列化操作一个Java类库。

未经允许不得转载:菡萏如佳人 » SpringCloud微服务系列(2)

欢迎加入极客江湖

进入江湖关于作者