【源码】Spring Cloud —— Eureka Server 1 服务注册、服务续约、服务下线、服务剔除

前言

Spring Cloud,基于 Spring Boot 实现的云应用开发工具,其下有(支持)大量的子工程,提供了 微服务 架构下的各种 组件,比如

  • 服务注册与发现组件EurekaZookeeperConsul
  • 服务调用组件RibbonOpenFeignHystrix
  • 路由和过滤组件ZuulSpring Cloud Gateway
  • 配置中心组件Spring Cloud Config
  • 消息组件Spring Cloud StreamSpring Cloud Bus
  • 安全控制组件Spring Cloud Security
  • 链路监控组件Spring Cloud Sleuth
  • 其他

本系列章节结合源码解读 Spring Cloud Eureka Server 组件,该组件提供了 服务注册中心 的功能

版本

Spring Cloud Netflix 版本:2.2.3.RELEASE
对应 Netflix-Eureka 版本:1.9.21

核心类

我们从 顶层接口 出发,先大体了解所有相关的 接口

LookupService

该接口提供对 服务实例 进行检索的相关方法,因为 Eureka Server 同时也可以是一个 Eureka Client

LeaseManager

该接口提供了 服务注册、服务续约、服务下线、服务剔除 等方法

LeaseManager 管理的对象时 LeaseLease 代表一个 Eureka Client 服务实例信息的租约

InstanceRegistry

该接口继承了 LookupServiceLeaseManager,拓展了一些方法可以更为简单地管理 服务实例租约 和查询注册表中的 服务实例信息

PeerAwareInstanceRegistry

该接口继承了 InstanceRegistry,在其基础上添加了 Eureka Server 集群同步的操作

AbstractInstanceRegistry

InstanceRegistry 的抽象实现类,十分重要,我们会在后文重点解读

PeerAwareInstanceRegistryImpl

继承了 AbstractInstanceRegistry,同时实现了 PeerAwareInstanceRegistry,在对 本地注册表 操作的基础上添加了对其 peer 节点的 同步复制 操作,使得 Eureka Server 集群中的注册表信息保持一致

InstanceRegistry

以上的类都由 com.netflix.eureka 提供,而 InstanceRegistry 则是 Spring CloudPeerAwareInstanceRegistryImpl 的拓展,以适配 Spring Cloud

类图

Eureka Server

图中 InstanceRegistry 接口以
netflixInstanceRegistry 标识,
因为该接口由 com.netflix.eureka 提供

接下来我们结合源码对其中的若干类、方法进行解读,以了解 Eureka Server 功能的实现

AbstractInstanceRegistry

AbstractInstanceRegistry 提供了 服务注册、服务续约、服务下线、服务剔除 的实现

服务注册

	public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
        	// 读锁
            read.lock();

			/**
			 * registrant:
			 * key 集群名称 value gMap 
			 * gMap:
			 * key 实例Id value 实例(InstanceInfo)
			 */
            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;
                }
            }

			// 获取实例
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
		
			// 如果实例存在
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();

				// registrant 取时间戳大的
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    
                    // ...
                    
                    registrant = existingLease.getHolder();
                }
            } else {
                // 更新自我保护阈值
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
                
                // ...
                
            }

			// 创建新的 Lease
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);

			// 上线时间
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
	
			// 保存
            gMap.put(registrant.getId(), lease);

			// recentRegisteredQueue 用于调试、统计
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
		
            // 状态更新等,略
            
        } finally {
            read.unlock();
        }
    }

ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry 来维护实例信息,key 为 集群名称,value 是一个 Map<String, Lease<InstanceInfo>> gMapgMapkey 为 实例ID,valueInstanceInfo 维护着实例信息

服务续约

	public boolean renew(String appName, String id, boolean isReplication) {
        RENEW.increment(isReplication);
	
		// gMap 获取
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);

		// Lease 获取
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = gMap.get(id);
        }

		// 如果没找到租约,则返回 false
        if (leaseToRenew == null) {
            RENEW_NOT_FOUND.increment(isReplication);
            
            // ...
            
            return false;
        } else {
            InstanceInfo instanceInfo = leaseToRenew.getHolder();
            if (instanceInfo != null) {
                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                        instanceInfo, leaseToRenew, isReplication);
				
				// 实例状态为 UNKNOWN,返回 false
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    
                    // ...
                    
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
		
				// 状态更新
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                
                    // ...
                    
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);

                }
            }
            renewsLastMin.increment();

			// 续约
            leaseToRenew.renew();
            return true;
        }
    }

根据 Eureka Client 发送的 appNameid服务实例 续约

服务下线

	@Override
    public boolean cancel(String appName, String id, boolean isReplication) {
        return internalCancel(appName, id, isReplication);
    }

    protected boolean internalCancel(String appName, String id, boolean isReplication) {
        try {
        	// 读锁
            read.lock();
            CANCEL.increment(isReplication);

			// gMap 获取
            Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
            Lease<InstanceInfo> leaseToCancel = null;

			// 移除指定 id 的实例
            if (gMap != null) {
                leaseToCancel = gMap.remove(id);
            }
            
            // ...
            
            // leaseToCancel 不存在则返回 false
            if (leaseToCancel == null) {
                CANCEL_NOT_FOUND.increment(isReplication);
                
                // ...
                
                return false;
            } else {
				// 租约下线
                leaseToCancel.cancel();
                InstanceInfo instanceInfo = leaseToCancel.getHolder();
                String vip = null;
                String svip = null;
                if (instanceInfo != null) {
					// 标识实例状态为 DELETED,用于增量更新                 
                    instanceInfo.setActionType(ActionType.DELETED);
                    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                    instanceInfo.setLastUpdatedTimestamp();
                    vip = instanceInfo.getVIPAddress();
                    svip = instanceInfo.getSecureVipAddress();
                }
				
				// 缓存清除
                invalidateCache(appName, vip, svip);
                
                // ...
                
            }
        } finally {
            read.unlock();
        }

		// 更新自我保护阈值
        synchronized (lock) {
            if (this.expectedNumberOfClientsSendingRenews > 0) {
                this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
                updateRenewsPerMinThreshold();
            }
        }

        return true;
    }

Eureka Client 在应用销毁时,会向 Eureka Server 发送服务下线请求,清除注册表中关于本应用的 租约,避免无效的服务调用

服务剔除

	@Override
    public void evict() {
        evict(0l);
    }

    public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");

		// 判断是否允许剔除(比如:自我保护机制是否开启)
		if (!isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
            return;
        }
        
        // 获取所有过期的 Lease
        List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
        for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
            Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
            if (leaseMap != null) {
                for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                    Lease<InstanceInfo> lease = leaseEntry.getValue();
                    if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                        expiredLeases.add(lease);
                    }
                }
            }
        }

        /**
         * 此处为了避免大规模的服务下线
         * 会取下线服务阈值(总服务 - 总服务 * 在线服务百分比阈值)
         * 		和 当前过期服务数 中的更小值,
         * 		以此值进行随机的服务下线,调用 internalCancel 方法
         * 		
         */
        int registrySize = (int) getLocalRegistrySize();
        int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
        int evictionLimit = registrySize - registrySizeThreshold;

        int toEvict = Math.min(expiredLeases.size(), evictionLimit);
        if (toEvict > 0) {
           
            // ...

            Random random = new Random(System.currentTimeMillis());
            for (int i = 0; i < toEvict; i++) {
                int next = i + random.nextInt(expiredLeases.size() - i);
                Collections.swap(expiredLeases, i, next);
                Lease<InstanceInfo> lease = expiredLeases.get(i);

                String appName = lease.getHolder().getAppName();
                String id = lease.getHolder().getId();
                EXPIRED.increment();
                logger.warn("DS: Registry: expired lease for {}/{}", appName, id);

				// 剔除
                internalCancel(appName, id, false);
            }
        }
    }

为了避免服务大规模的下线,保证 可用性Eureka Server 会分批下线过期的服务,自我保护 期间不会剔除实例,AbstractInstanceRegistry 定义了 evictionTimer 定时进行 服务剔除

总结

AbstractInstanceRegistry 实现了 服务注册、服务续约、服务下线、服务剔除 的主要逻辑,下章节解读 Eureka Server 关于 服务集群 的相关操作

下一篇:【源码】Spring Cloud —— Eureka Server 2 集群相关

参考

《Spring Cloud 微服务架构进阶》 —— 朱荣鑫 张天 黄迪璇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值