SpringCloud踩坑记(三)SpringCloud 注册中心Eureka

简介

Eureka是基于REST(代表性状态转移)的服务,主要在AWS云中用于定位服务,以实现负载均衡和中间层服务器的故障转移。它主要分为Server和Client两个部份。与Zookeeper和Consul类型,是作用于服务注册和发现的组件。

架构

Eureka 的基本架构下图所示

2019121601

其中主要包括以下3种角色

Register Service:服务注册中心,它是一个Eureka Server ,提供服务注册和发现的功能。

Provider Service:服务提供者,它是 Eureka Client,提供服务

Consumer Service:服务消费者,它是 Eureka Client,消费服务

服务消费的基本过程如下:首先前要 个服务注册中心 Eureka Server ,服务提供Eureka
Client 向服务注册中心 Eureka Server 注册,将自己的信息(比如服务名和服务的 IP 地址等)
通过REST API的形式提交给服务注册中心 Eureka Server 。同样 ,服务消费 Eureka Client
向服务注册Eureka Server 注册,同时服务消费者获取一份服务注册列表的信息 该列表
包含了所有向注册中心 Eureka Server注册的服务信息。获取服务注册列表信息之后,服
务消费者就知道就近服务提供者的 IP 地址,可以通过 Http远程调度来消费服务提供者的服务。

Eureka几个概念

服务注册(Register)

Eureka客户端将有关正在运行的实例的信息(比如服务名和服务的 IP 地址等)注册到Eureka服务器。

续约(Renew)

Eureka客户端需要通过每30秒发送一次心跳来续订租约。续订通知Eureka服务器该实例仍在运行。如果服务器在90秒钟内未看到续订,则会将实例从其注册表中删除。建议不要更改更新间隔,因为服务器使用该信息来确定客户端与服务器之间的通信是否存在广泛传播的问题。

获取服务注册列表信息(Fetch Registry)

Eureka客户端从服务器获取注册表信息,并将其本地缓存。之后,客户端使用该信息来查找其他服务。通过获取上一个获取周期与当前获取周期之间的增量更新,定期(每30秒)更新此信息。增量信息在服务器中保留的时间更长(大约3分钟),因此增量获取可能会再次返回相同的实例。Eureka客户端会自动处理重复信息。

获取增量后,Eureka客户端通过比较服务器返回的实例计数来与服务器协调信息,如果信息由于某种原因不匹配,则会再次获取整个注册表信息。Eureka服务器缓存增量的压缩有效负载,整个注册表以及每个应用程序以及该应用程序的未压缩信息。有效负载还支持两种JSON / XML格式。Eureka客户端使用jersey apache客户端以压缩的JSON格式获取信息。

服务下线(Cancel)

Eureka客户端在关闭时向Eureka服务器发送取消请求。这样会将实例从服务器的实例注册表中删除,从而有效地消除了实例的流量。

当Eureka客户端关闭时,将执行此操作,并且应用程序应确保在关闭过程中调用以下命令。

DiscoveryManager.getInstance().shutdownComponent()

时差(Time Lag)

来自Eureka客户端的所有操作可能需要一些时间才能反映到Eureka服务器上,然后反映到其他Eureka客户端上。这是因为eureka服务器上的有效负载缓存,它会定期刷新以反映新信息。Eureka客户端还定期地获取增量。因此,更改传播到所有Eureka客户机可能需要2分钟。

自我保护模式

默认情况下,如果Eureka服务端在一定时间内(默认90s)没收到客户端的心跳就会注销该客户端。但是如果是网络分区出现故障时,导致客户端和Eureka服务端无法正常通讯,这时候客户端是健康的,但是服务端
缺注销该客户端,这是非常危险的。

为了解决这个问题,Eureka提供自我保护模式。

如果Eureka服务器检测到数量超过预期的注册客户端已以不正当的方式终止了他们的连接,并且同时正等待逐出,则它们将进入自我保存模式。这样做是为了确保灾难性的网络事件不会清除eureka注册表数据,并将其传播到下游的所有客户端。

Eureka协议要求客户端永久离开时执行明确的注销操作。例如,在提供的Java客户端中,这是通过shutdown()方法完成的。任何连续3次心跳续订失败的客户端将被视为不正常的终止,并且将由后台驱逐过程逐出。只有当当前注册表的15%处于此更高状态时,才会启用自我保存。

在自我保存模式下,Eureka服务器将停止逐出所有实例,直到发生以下情况之一:

1.它看到的心跳续订次数又回到了预期的阈值之上

2.自我保护功能已禁用

默认情况下会启用自我保留,并且启用自我保留的默认阈值>当前注册表大小的15%。

需要禁用自我包含模式,可在配置文件中设置eureka.server.enable-self-preservation=false

自我保护时,服务端查看界面会出现以下内容,并显示成红色

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

编写Eureka Server

在IDEA创建maven主工程,并且重新包含EurekaServer、EurekaClientProducer1、EurekaClientProducer2三个Module。

工程目录如下

-EurekaDemo
  - EurekaServer
  - EurekaClientProducer1
  - EurekaClientProducer2
-pom.xml 

主工程pom.xml配置如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.smallstep</groupId>
    <artifactId>EurekaDemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>


    <modules>
        <module>EurekaServer</module>
        <module>EurekaClientProducer1</module>
        <module>EurekaClientProducer2</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR4</spring-cloud.version>
    </properties>

    <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>

</project>

编写Eureka Server模块下pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>EurekaDemo</artifactId>
        <groupId>com.smallstep</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>EurekaServer</artifactId>
    <groupId>com.smallstep</groupId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

编写Eureka Server模块下启动类

package com.smallstep;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

编写Eureka Server模块下application.yml配置文件

server:
  port: 9000

spring:
  application:
    name: register-server

eureka:
  instance:
    hostname: register-server
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:${server.port}/eureka/

由于Eureka服务端因此无需注册,因此配置eureka.client.registerWithEureka和eureka.client.fetchRegistry为false。

配置完成启动服务,通过地址:http://localhost:9000/ ,访问主界面即可查看服务注册情况

编写Eureka Client

EurekaClientProducer1下pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>EurekaDemo</artifactId>
        <groupId>com.smallstep</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>EurekaClientProducer1</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

EurekaClientProducer1下启动类

package com.smallstep;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author 李俊
 * @Description
 * @Date 2019/12/13 15:33
 */
@SpringBootApplication
public class EurekaClientProducer1Application {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientProducer1Application.class, args);
    }

}

从Spring Cloud Edgware开始,@EnableDiscoveryClient 或@EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。

如不想将服务注册到Eureka Server,如不想将服务注册到Eureka Server,只需设置spring.cloud.service-registry.auto-registration.enabled=false ,或@EnableDiscoveryClient(autoRegister = false) 即可。

EurekaClientProducer1下application.yml

server:
  port: 8001


spring:
  application:
    name: producer

eureka:
  instance:
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

复制一份EurekaClientProducer1为EurekaClientProducer2并修改EurekaClientProducer2下application.yml为以下内容

server:
  port: 8002


spring:
  application:
    name: producer

eureka:
  instance:
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

配置完成后,启动EurekaClientProducer1和EurekaClientProducer2服务。

默认注册到服务端的的clientId为spring.application.name。

再次访问http://localhost:9000/,可看到注册内容

#源码分析

服务注册

客户端是基于spring的SPI机制进行初始化客户端。因此首先查看spring-cloud-netflix-eureka-client.jar下META-INF/spring.factories,
内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

直接查看启动配置文件EurekaDiscoveryClientConfigServiceBootstrapConfiguration,根据内容看到Import了EurekaClientAutoConfiguration。
查看EurekaClientAutoConfiguration内容,找到初始CloudEurekaClient代码如下

...//省略无关代码
@Bean(
    destroyMethod = "shutdown"
)
@ConditionalOnMissingBean(
    value = {EurekaClient.class},
    search = SearchStrategy.CURRENT
)
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {
    return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);
}

CloudEurekaClient继承了DiscoveryClient,DiscoveryClient实现了EurekaClient,类图如下:

2019121701

查看CloudEurekaClient构造函数,调用了DiscoveryClient构造,
接这查看DiscoveryClient构造函数,看到关键代码,初始话一些定时任务,如下:

...//省略多余代码
 this.initScheduledTasks();//

查看initScheduledTasks函数内容,内容如下:

private void initScheduledTasks() {
        
    ...//省略非关键代码
            
            this.statusChangeListener = new StatusChangeListener() {
                public String getId() {
                    return "statusChangeListener";
                }

                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
                        DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                    } else {
                        DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
                    }

                    DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
                }
            };
            if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
                this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
            }

            this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        

    }

其中instanceInfoReplicator.start开启注册任务线程并延迟启动,再跟下去可看到instanceInfoReplicator的
run方法调用了discoveryClient.register()方法。
register方法内容如下

boolean register() throws Throwable {
        logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);

        EurekaHttpResponse httpResponse;
        try {
            httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
        } catch (Exception var3) {
            logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
            throw var3;
        }

        if (logger.isInfoEnabled()) {
            logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
        }

        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

其中this.eurekaTransport.registrationClient.register就是调用REST接口向服务端注册信息。

延迟注册问题

前面说了注册时会开启注册任务线程并延迟启动,默认延迟时长40s,默认方法见

this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

public int getInitialInstanceInfoReplicationIntervalSeconds() {
       return this.configInstance.getIntProperty(this.namespace + "appinfo.initial.replicate.time", 40).get();
   }

但是实际使用下未延迟注册,服务一启动就会注册。再查看代码发现初始化线程时,服务也会开启改变状态监听器StatusChangeListener,代码在initScheduledTasks内,
如果状态改为UP时就会调用DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate()函数,onDemandUpdate会马上去执行InstanceInfoReplicator.this.run()去注册信息,
onDemandUpdate方法内容如下

public boolean onDemandUpdate() {
        if (this.rateLimiter.acquire(this.burstSize, (long)this.allowedRatePerMinute)) {
            if (!this.scheduler.isShutdown()) {
                this.scheduler.submit(new Runnable() {
                    public void run() {
                        InstanceInfoReplicator.logger.debug("Executing on-demand update of local InstanceInfo");
                        Future latestPeriodic = (Future)InstanceInfoReplicator.this.scheduledPeriodicRef.get();
                        if (latestPeriodic != null && !latestPeriodic.isDone()) {
                            InstanceInfoReplicator.logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
                            latestPeriodic.cancel(false);
                        }

                        InstanceInfoReplicator.this.run();
                    }
                });
                return true;
            } else {
                logger.warn("Ignoring onDemand update due to stopped scheduler");
                return false;
            }
        } else {
            logger.warn("Ignoring onDemand update due to rate limiter");
            return false;
        }
    }

服务续约

服务续约也跟服务注册一样,在初始化会启动时候会开启一个30s的循环定时器去向服务端发起续约请求。

启动定时跟需注册一样在initScheduledTasks方法内

private void initScheduledTasks() {
   ...//省略多余代码
   this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
}

其中HeartbeatThread代码内容如下:

private class HeartbeatThread implements Runnable {
        private HeartbeatThread() {
        }

        public void run() {
            if (DiscoveryClient.this.renew()) {
                DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }

        }
    }

其中调用了DiscoveryClient.this.renew()的方法,renew的实现如下

boolean renew() {
        try {
            EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
            logger.debug("DiscoveryClient_{} - Heartbeat status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
                this.REREGISTER_COUNTER.increment();
                logger.info("DiscoveryClient_{} - Re-registering apps/{}", this.appPathIdentifier, this.instanceInfo.getAppName());
                long timestamp = this.instanceInfo.setIsDirtyWithTime();
                boolean success = this.register();
                if (success) {
                    this.instanceInfo.unsetIsDirty(timestamp);
                }

                return success;
            } else {
                return httpResponse.getStatusCode() == Status.OK.getStatusCode();
            }
        } catch (Throwable var5) {
            logger.error("DiscoveryClient_{} - was unable to send heartbeat!", this.appPathIdentifier, var5);
            return false;
        }
    }

其中this.eurekaTransport.registrationClient.sendHeartBeat就是去调用续约接口。

构建高可用的Eureka Server集群

在实际项目中我们的客户端可能是有数十个或者上百个,这时候就需要对Eureka Server进行高可用集群。

Eureka的高可用架构图(来源:https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance )如下:

2019121705

上面的架构描述了Eureka在Netflix上的部署方式,
这就是您通常运行它的方式。每个区域有一个Eureka集群并且每个
区域至少有一个Eureka Server以处理区域故障以防服务器瘫痪。

服务在Eureka注册,然后发送心跳以每30秒更新其租约。
如果客户端几次无法续签租约,则大约90秒内会将租约从
服务器注册表中删除。注册信息和续订将复制到集群中的
所有eureka节点。任何区域的客户端都可以查找注册表信息
(每30秒发生一次)以查找其服务(可能在任何区域)
并进行远程调用。

编辑Eureka Server集群

新增EurekaServer1模块,内容与EurekaServer一致。然后修改配置文件进行服务互相注册。

修改配置文件内容如下

EurekaServer下application.yml

server:
  port: 9000

spring:
  application:
    name: register-server

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:9001/eureka/

EurekaServer1下application.yml

server:
  port: 9001

spring:
  application:
    name: register-server

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

然后启动两个Eureka Server。

修改EurekaClientProducer1和EurekaClientProducer2的application.yml中的内容如下,即可。

eureka:
  instance:
    prefer-ip-address: true
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/,http://localhost:9001/eureka/

启动Eureka Client后,查看两个Eureka Server的注册客户端列表都已注册了Eureka Client,

Eureka的健康检查

Eureka Client注册后在Status一栏有个UP,表示应用程序状态正常。应用状态还有其他取值:DOWN、OUT_OF_SERVICE、UNKONWN等。只有标记为“UP”的微服务会被请求。
前面讲过,Eureka Server和Eureka Client通过心跳机制确定Eureka Client的状态。
默认情况下,服务器端与客户端的心跳保持正常,应用程序就会始终保持“UP”状态,但是该机制并不能完全反应应用程序的状态。
比如:微服务与Eureka Server的心跳正常,但是该微服务的数据源发生了问题(比如网络抖动,连不上数据源),根本无法正常工作。

要实现这一点,只须启动Eureka的健康检查即可。这样应用程序就会降自己的健康状态传播到Eureka Sever。

Eureka Client的application.yml中配置以下内容

eureka:
  client:
    healthcheck:
      enabled: true

某些场景下,可能希望更细粒度地控制健康检查,此时可实现 com.netflix.appinfo.HealthCheckHandler接口。

Spring Boot Actuator提供了/actuator/health端点,展示应用程序的健康信息。那么如果将该端点的健康状态传播到Eureka Server中,会使得状态更加准确。

pom增加Spring Boot Actuator依赖

<!--健康检查依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

附录

源代码地址:https://gitee.com/LeeJunProject/spring_cloud_learning/tree/master/eureka/EurekaDemo

END

欢迎扫描下图关注公众号 IT李哥,公众号经常推送一些优质的技术文章

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值