微服务之服务注册中心(EUREKA)

为什么要用服务注册中心

在微服务架构下,众多服务的相互调用,不可能每一个服务都掌握所有其他服务的ip端口等信息,并且还参杂着其他服务的临时加入和掉线.所以需要有一个统一的注册中心来保存所有提供服务的信息中心,提供其他调用

注册中心之eureka (已停止更新)

github地址: https://github.com/RexChengZhu/EurekaCluster.

eureka 概念

1. Eureka Server 提供一个场所让想要被人调用的服务可以在这里注册自己的信息
2. Eureka Provider 服务的提供方,将自己的服务api注册在Eureka Server 
3. Eureka Client  服务的调用方,去server中找provider调用

eureka 组件

1. eureka server 提供服务注册
	各个微服务节点启动后,会通过配置向eureka server 中注册,把自身信息注册到
	eureka server 中以供其他服务调用
2. eureka client  java客户端,简化和server的交互
	会向eureka server 发送心跳,默认30秒一次,如果eureka 在几个周期内没有收到
	心跳,则会把client 踢出注册表中,默认90秒

eureka 使用

创建eureka server

1.导入maven依赖

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

版本号已经在父工程中指定了

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

其中又依赖了

  <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-netflix-dependencies</artifactId>
        <version>2.2.3.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

在这里面指定了 spring-cloud-starter-netflix-eureka-server 的版本2.2.3.RELEASE

2 创建配置文件

server:
  port: 7001

eureka:
  instance:
    hostname: localhost

  client:
#    是否要自己注册到EUREKA 上
    register-with-eureka: false
#    不需要去服务注册中心获取其他服务地址
    fetch-registry: false
    service-url:
      defaultZone:  http://${eureka.instance.hostname}:${server.port}/eureka/

3 编写主函数

/**
 * 表示这是一个配置中心
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApp {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApp.class, args);
    }
}

4 创建服务提供方
注意,eureka需要spring-boot-starter-web 依赖才能启动成功, eureka-server中默认依赖,而client中没有依赖,所以得手动外部依赖,不然会启动失败

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

5 编写yml配置文件

server:
  port: 8001

spring:
  application:
    name: provider

eureka:
  client:
    #    自己注册到EUREKA 上
    register-with-eureka: true
    #    需要去服务注册中心获取其他服务地址
    fetch-registry: true
    service-url:
    # eureka 注册中心的地址
      defaultZone:  http://localhost:7001/eureka/

6 启动类上添加注解

/***
 * 声明自己是eureka 客户端
 */
@EnableEurekaClient
@SpringBootApplication
public class ProviderApp {

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

Euerka 集群搭建

集群搭建的思想就是多个eureka server 相互注册
拷贝一份工程目录,除了配置文件其他都一样
在host文件中定义 EUREKA01 和 EUREKA02
第一份配置文件

server:
  port: 7001

eureka:
  instance:
    hostname: EUREKA01
  client:
#    是否要自己注册到EUREKA 上
    register-with-eureka: false
#    不需要去服务注册中心获取其他服务地址
    fetch-registry: false
    service-url:
    # 注册到eureka02  7002上
      defaultZone:  http://EUREKA02:7002/eureka/

第二份配置 文件

server:
  port: 7002

eureka:
  instance:
    hostname: EUREKA02
  client:
#    是否要自己注册到EUREKA 上
    register-with-eureka: false
#    不需要去服务注册中心获取其他服务地址
    fetch-registry: false
    service-url:
    # 注册到 7001上
      defaultZone:  http://EUREKA01:7001/eureka/

生产者消费者注册进eureka集群

yml 配置

server:
  port: 8001

spring:
  application:
    name: provider

eureka:
  client:
    #    是否要自己注册到EUREKA 上
    register-with-eureka: true
    #    不需要去服务注册中心获取其他服务地址
    fetch-registry: true
    service-url:
      defaultZone:  http://EUREKA01:7001/eureka/,http://EUREKA02:7002/eureka/

如果是生产者也是集群,只需要在yml上修改端口即可,application.name 需要一样

集群模式消费者

server:
  port: 9001

spring:
  application:
    name: consumer

eureka:
  client:
    #    是否要自己注册到EUREKA 上
    register-with-eureka: false
    #    需要去服务注册中心获取其他服务地址,true才可以通过名称调用其他服务
    fetch-registry: true
    service-url:
      defaultZone:  http://EUREKA01:7001/eureka/,http://EUREKA02:7002/eureka/

因为 eureka 是rest 请求,所以需要创建一个resttemplate

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

@LoadBalanced 注解表示 同样名称的服务可以能有多个,如果只用名称调用生产者的话eureka 不知道调用的是哪一个,所以加了注解之后就会负载均衡的调所有服务.

 @Autowired
   private RestTemplate restTemplate;

   @GetMapping("/test")
   public String test(){
       ResponseEntity<String> forEntity = restTemplate.getForEntity("http://PROVIDER/test", String.class);
       String body = forEntity.getBody();
       return body;
   }

测试可以得出,消费者负载均衡的请求生产者

eureka 自我保护模式

保护模式存在于eureka client 和 eureka server 存在网络分区情况下场景保护,一旦进入保护模式
eureka server 将不会再把eureka client 移除 !!!!!

故障的原因

eureka client 注册到server 中后,会定时向server 发送心跳包,如果server 在一定时间内(默认90秒)
没有收到心跳包,就会把这个服务从列表中删除。但是如果短时间内没有收到大量服务的心跳,server就会认为是发生了网络故障,认为client还是好的,不会删除。

禁用保护模式

eureka:
	server:
	#    关闭保护模式
		enable-self-preservation: false
	instance:
		# eureka 客户端向服务端发送心跳的间隔时间
		lease-renewal-interval-in-seconds: 1
		# eureka 服务端在最后一次收到心跳后等待上限,超过就剔除
		lease-expiration-duration-in-seconds: 2

如果是正常推出的话,eureka client 会向server发送一条退出指令,server会直接剔除client.但是如果是 kill -9 的话,server就不会收到指令,这时候关闭保护模式的才会生效,把服务剔除,否则服务就一直存在

源码解析

项目会有经常的服务上线下限,导致的就是服务经常请求到已经下限的服务.需要完成能及时感知.
需要了解几个方面

配置文件实体映射

EurekaClientConfigBean 客户端可以配置的字段

  1. registryFetchIntervalSeconds: 从server端获取服务列表的频率,默认30秒
  2. instanceInfoReplicationIntervalSeconds: 客户端定时扫描自身状态的频率,默认30秒. 如果扫描的状态和上一次不一致的时候,会通过监听者模式上报自身给DiscoveryClient 然后发送状态到server. 原本认为这是一个没用的状态,因为服务挂了还会发送其他请求,如果kill -9也没有机会上报自身状态,但是后来发现服务状态还有一个是OUT_OF_SERVICE 状态,被人强制下限,这样就可以扫描到这个状态上报给服务端.

EurekaInstanceConfigBean eureka实体都可以配置的字段

  1. leaseRenewalIntervalInSeconds: 客户端发送心跳给服务端的频率,默认30秒. 被 Eureka 的 InstanceResource 类 renewLease 方法接收.
  2. leaseExpirationDurationInSeconds 服务端最后一次收到心跳后超过多少秒就将其踢出,默认90秒

EurekaServerConfigBean 服务端可以配置的字段

  1. responseCacheUpdateIntervalMs 一级缓存和二级缓存的同步频率,默认30s
  2. responseCacheAutoExpirationInSeconds 一级缓存的失效时间,默认180s
  3. retentionTimeInMSInDeltaQueue 最近更改队列的存活时间 默认180s
  4. deltaRetentionTimerIntervalInMs 扫描最近更改队列定时器的频率,30s

客户端注册自身到服务端的流程

  1. 客户端 DiscoveryClient 的 register() 方法 会通过http上报自身情况到server端
  2. ApplicationResource 类的addInstance 方法收到请求. 最终由AbstractInstanceRegistry 的register 方法处理请求,将 实例添加到
        try {
            read.lock();
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
        } finally {
            read.unlock();
        }

由源码可知, 服务端将实例对象存储到一个 ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> 结构和一个
ConcurrentLinkedQueue<RecentlyChangedItem> 结构中

ConcurrentHashMap 中第一个key 存储的是服务名称,第二个储存的是实例名称
ConcurrentLinkedQueue 存储的是最近变更记录,增加删除都会存储在里面

客户端心跳机制

DiscoveryClient 类中 initScheduledTasks 初始化方法里定义里心跳的方法

 int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            // Heartbeat timer
            heartbeatTask = new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            );
            scheduler.schedule(
                    heartbeatTask,
                    renewalIntervalInSecs, TimeUnit.SECONDS);

服务端接收到心跳之后做的事情就是拿到对应的实例,把最近更新时间修改而已

public boolean renew(String appName, String id, boolean isReplication) {
        RENEW.increment(isReplication);
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = gMap.get(id);
        }
            renewsLastMin.increment();
            leaseToRenew.renew();
            return true;
        }

客户端从服务端获取服务列表的流程(全量更新 增量更新)

  1. 客户端初次加载的时候会全量更新,获取服务端的列表后存在localRegionApps 变量中.
  2. 服务端 的 ApplicationsResource 类的 getContainers 方法中
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            if (useReadOnlyCache) {
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                payload = readWriteCacheMap.get(key);
            }
        } catch (Throwable t) {
            logger.error("Cannot get value for key : {}", key, t);
        }
        return payload;
    }

其中 readOnlyCacheMap 是一个ConcurrentMap 作为二级缓存.
定时找一级缓存同步数据.
readWriteCacheMap 是一级缓存,由guava提供的map,缓存的失效时间默认为180s
3. 客户端开启定时器定时从服务端获取增量数据,默认频率30秒.
客户端从服务端获得的数据包含
3.1 Eureka-Server 近期变化( 注册、下线 )的应用集合
3.2 Eureka-Server 应用集合一致性哈希码
客户端获得数据之后,将服务端的应用集合和本地的应用集合合并之后,计算出来的一致性哈希,和服务端的一致性哈希码对比,如果一致,则表示增量成功,否则将全量更新一次
4. 服务端收到增量更新请求, 会遍历 recentlyChangedQueue

@Deprecated
    public Applications getApplicationDeltas() {

        try {
            write.lock();
            Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator();
            logger.debug("The number of elements in the delta queue is : {}",
                    this.recentlyChangedQueue.size());
            while (iter.hasNext()) {
                Lease<InstanceInfo> lease = iter.next().getLeaseInfo();
                InstanceInfo instanceInfo = lease.getHolder();
           }
         return apps;            

这个最近变更队列里面存储着服务的上线和下限情况.
retention-time-in-m-s-in-delta-queue 参数又代表着每个状态在队列里面存活的时间, 默认存活180秒

    private TimerTask getDeltaRetentionTask() {
        return new TimerTask() {

            @Override
            public void run() {
                Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
                while (it.hasNext()) {
                    if (it.next().getLastUpdateTime() <
                            System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
                        it.remove();
                    } else {
                        break;
                    }
                }
            }

        };
    }

一致性哈希code 可能引发的问题

增量更新的准确性是靠着服务端和客户端计算出的一致性哈希code来维护的,举个例子.

  1. 服务端现在有 3个A 和 2个B 两个服务注册上去.这时候一个不会注册上去的客户端C启动,全量拉取了一次数据. 本地也保存着 3个A 和 2个B的实例. 这时服务端和客户端的哈希code 都为 5_UP 表示5个在线
  2. A 和 B 各自下线一个实例,服务端收到下线请求后,将下线的对象存入recentlyChangedQueue 中. 这时服务端的一致性哈希code为 3_UP_2_DOWN 为3个在线2个下线. 这时客户端发送增量更新请求,服务端把队列中的两个下线实例发送给客户端,客户端收到两个下线请求之后,和本地的服务列表进行合并,同样计算出 3_UP_2_DOWN 表示此次增量更新有效.
场景1

参数设置

eureka:
  server:
	# recentlyChangedQueue 中数据存活时间 1毫秒
    retention-time-in-m-s-in-delta-queue: 1
    # 检查recentlyChangedQueue队列的时间间隔
    delta-retention-timer-interval-in-ms: 1
    # 禁用二级缓存
    use-read-only-response-cache: false
  1. 开启 服务端
  2. 开启应用A 9001 端口
  3. 开启客户端,此时客户端已经全量获取到服务端数据 哈希code= 1_UP
  4. 关闭9001 端口,启动9002端口的应用A.
  5. 客户端定时获取服务端实例列表的定时器生效,从服务端的 recentlyChangedQueue 中获取数据,但是由于配置导致 队列中永远都是空的,获取下来的数据也为空,所以增量获取数据之后,本地的服务列表里的9001端口实例还是存在本地中,计算出来的哈希code 为 1_UP. 和服务端的状态相同.流程结束.
    结果就是客户端里面永远都保存着 9001的数据
场景2
  1. 开启服务端
  2. 开启 应用 A,B 注册到服务端 2_UP
  3. 开启客户端, 拉取数据 2_UP
  4. A 下线, C 上线 并且recentlyChangedQueue 数据由于网络问题,客户端没有及时来同步,导致过期
  5. D 服务上线. 此时服务端总共上线的实例数量为 B C D 3_UP
  6. 客户端拉取定时服务列表,获得 队列中的 D 数据, A,B,D 3_UP, 哈希code相同,流程结束

结论:

默认情况下, 最近变化队列中保存的是180秒内的数据,客户端拉取一次的频率为30秒,可能导致上述bug发生的情况为,客户端前面5次请求都没有获取到数据,第六次的时候,服务端在前30秒内的变更失效,并且之后的变化也和30秒的增减总和不变. 小概率事件

客户端异常退出之后服务端的发现流程

客户端的心跳在上面已经提到了,每个服务的最近更新时间都保存在服务端.
前提是一定要把enable-self-preservation自我保护模式关闭
EvictionTask 类的 evict方法定时判断每个实例的最新更新时间是否超过了设定的 lease-expiration-duration-in-seconds 的值,默认90秒。如果超过则认为是掉线。踢出服务.

结论

对于当前业务场景,生产环境下服务可能频繁的上下线,服务端配置为非保护模式,10秒钟扫描一次过期实例,实例超过30秒没有发送心跳就踢出。客户端关闭增量更新,定时30秒拉取一次,5秒发送一次心跳。

eureka:
	server:
		# 超时队列检查频率
		eviction-interval-timer-in-ms: 10000
		# 关闭自我保护模式
		enable-self-preservation: false
	instance:
		# 超过30秒没有接受到心跳就踢出服务
		lease-expiration-duration-in-seconds: 30
eureka:
	client:
		# 30秒拉取一次实例信息
		registry-fetch-interval-seconds: 30
		# 关闭增量更新,每次都拉取全量数据
		disable-delta: true
	instance:
		# 发送心跳的频率,5秒发送一次
		lease-renewal-interval-in-seconds: 5
	
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值