Eureka Server源码解析回顾

一 概述

Eureka Server作为一个开箱即用的服务注册中心,提供了满足与Eureka Client交互需求的功能:

  • 服务注册
  • 接受服务心跳
  • 服务剔除
  • 服务下线
  • 集群同步
  • 获取注册表中服务实例信息

Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为的时候,它会向它配置文件中的其他Eureka Server进行拉取注册表,服务注册和发送心跳等操作。

二 服务实例注册表

InstanceRegistry是Eureka Server中注册表管理的核心接口。在InstanceRegistry的类图中可以发现存在两个InstanceRegistry。一个为InstanceRegistry接口,一个为InstanceRegistry类。

InstanceRegistry接口是Eureka Server注册表的最核心接口,其职责是在内存中管理注册到Eureka Server中的服务信息。它分别继承了LeaseManager接口和LookupService接口。

LeanseManager接口对注册到Eureka Server中的服务实例租约进行管理,包括服务注册,服务下线,服务租约更新以及服务剔除等操作。

public interface LeaseManager<T> {
    void register(T var1, int var2, boolean var3);

    boolean cancel(String var1, String var2, boolean var3);

    boolean renew(String var1, String var2, boolean var3);

    void evict();
}

LeaseManager中管理的对象是Lease,Lease代表一个Eureka Client服务实例信息的租约,它提供了对其内特有的类的时间有效性操作。Lease持有的类是代表服务实例信息的InstanceInfo。Lease中定义了租约的操作类型,分别是注册,下线,更新,同时提供了对租约中时间属性的各项操作。租约默认有效时长(duration)为90秒。

LookupService提供了对服务实例进行检索的功能。

public interface LookupService<T> {

    Application getApplication(String appName);

    Applications getApplications();

    List<InstanceInfo> getInstancesById(String id);

    InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}

三 服务注册

Eureka Client在发起注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供Eureka Client进行服务发现。

服务注册的主要实现位于AbstractInstanceRegistry的registry方法中

public abstract class AbstractInstanceRegistry implements InstanceRegistry {
        public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        //获取锁
        read.lock();

        //这里的registry是ConcurrentHashMap<String,Map<String,Lease<InstanceInfo>>>
          registry,根据appName对服务实例集群进行分类
        try {
            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>>();
        //这里有一个比较严谨的操作,防止在添加新的服务实例集群租约时把已由的其他线程添加的集群租约覆盖掉,如果存在该键值,直接返回已存在的值,否则添加该键值对,返回null。
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);

                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
           
            //根据instanceId获取实例的租约
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());

            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();

                //如果该实例的租约已经存在,比较最后更新时间戳的大小,取最大值的注册信息为有效
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
              
                    registrant = existingLease.getHolder();
                }
            } else {
                //如果租约不存在,这时一个新的注册实例
                synchronized (lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        //自我保护机制
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
            }
            //创建新的租约
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                //如果租约存在,继承租约的服务上线初始时间
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            //保存租约
            gMap.put(registrant.getId(), lease);
            //添加最近注册队列
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
           
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
              
                                + "overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                 
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());

            if (overriddenStatusFromMap != null) {
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            //根据覆盖状态规则得到服务实例的最终状态,并设置服务实例的当前状态
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);
            //如果服务实例状态为UP,设置租约的服务上线时间,只有第一次设置有效
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            //添加最近租约变更记录队列,标识ActionType为ADDED
            //这将用于Eureka Client增量式获取注册表信息
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            //设置服务实例信息更新时间
            registrant.setLastUpdatedTimestamp();
            //设置response缓存过期,这将用于Eureka Client全量获取注册表信息
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
        } finally {
            //释放读锁
            read.unlock();
        }
    }
}

根据源码可知,在AbstractInstanceRegister类的register方法中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。然后从registry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。

如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。

之后将进行一系列缓存操作并根据覆盖状态规则覆盖设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效是封建,释放读锁并完成服务注册。

四 接受服务心跳

在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。

Eureka Server处理心跳请求的核心逻辑位于AbstractIntanceRegistry中的renew方法中。renew方法是对Eureka Client位于注册表中的租约的续约操作,不像register方法需要服务实例信息,仅仅是根据服务实例的服务名和服务实例id即可更新对一个你租约的有效时间。

public boolean renew(String appName, String id, boolean isReplication) {
       
        RENEW.increment(isReplication);
        //根据appName获取服务集群的租约集合
        Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
        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);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    //如果得到的服务实例最后状态时UNKNOWN,取消续约
                    RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) { 
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
                }
            }
            //统计每分钟续租的次数,用于自我保护
            renewsLastMin.increment();
            //更新租约中的有效时间
            leaseToRenew.renew();
            return true;
        }
    }

在renew方法中,不关注InstanceInfo,仅关注租约本身以及租约的服务实例状态。如果更具服务实例的appName和instanceInfoId查询服务实例的租约,并且根据getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN,那么跟新租约中的有效时间,即更新租约Lease中的lastUpdateTimestamp,达到续约的目的;如果租约不存在,那么返回续租失败的结果。

五 服务剔除

当Eureka Client在注册后,既没有续约,由于服务崩溃或者网络异常等原因也没有下线,那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要将该服务剔除。AbstractInstanceRegistry中的evict方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除。

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

    public void evict(long additionalLeaseMs) {
        
        //自我保护相关,如果出现该状态,不允许剔除服务。
        if (!isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
            return;
        }
        
        //遍历注册表register,一次性获取所有的过期租约。
        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);
                    }
                }
            }
        }
        
        //计算最大允许剔除的租约的数量,获取注册表租约总数
        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);
            }
        }
    }

服务剔除将会遍历registry注册表,找出其中所有的过期租约,然后根据配置文件中续租百分比阈值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数减去当前注册表租约表阈值),分批次剔除过期的服务实例租约。对过期的服务实例租约调用internalCancel服务下线的方法将其从注册表中清除掉。

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

    protected boolean internalCancel(String appName, String id, boolean isReplication) {
        //获取读锁,防止被其他线程进行修改
        read.lock();
        try {
            CANCEL.increment(isReplication);
            //根据appName获取服务实例的集群
            Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
            Lease<InstanceInfo> leaseToCancel = null;
            //移除服务实例的租约
            if (gMap != null) {
                leaseToCancel = gMap.remove(id);
            }
            //将服务实例信息添加到最近下线服务实例统计队列
            recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
            InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
            if (instanceStatus != null) {
     
            }
            //租约不存在,返回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) {
                    instanceInfo.setActionType(ActionType.DELETED);
                    //添加最近租约变更记录队列,标识ActionType为DELETED
                    //这将用于Eureka Client增量式获取注册表信息
                    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
                    instanceInfo.setLastUpdatedTimestamp();
                    vip = instanceInfo.getVIPAddress();
                    svip = instanceInfo.getSecureVipAddress();
                }
                //设置response缓存过期
                invalidateCache(appName, vip, svip);
            }
        } finally {
            //释放读锁
            read.unlock();
        }

        synchronized (lock) {
            if (this.expectedNumberOfClientsSendingRenews > 0) {
                this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
                updateRenewsPerMinThreshold();
            }
        }
        //下线成功
        return true;
    }

internalCancel方法与register方法的行为过程很类似,首先通过registery根据服务名和服务实例id查询关于服务实例的租约Lease是否存在,统计最近请求下线的服务实例用于Eureka Server主页展示。如果租约不存在,返回下线失败;如果租约存在,从registery注册表中移除,设置租约的下线时间,同时在最近租约变更记录队列中添加新的下线记录,以用于Eureka Client的增量式获取注册表信息,最后设置response缓存过期。

internalCancel方法中国同样通过读锁保证registery注册表中数据的一致性,避免脏读。

源码阅读至此,并未完全理解通透,待续!

<think>嗯,用户之前让我介绍了微服务的面试题和答案,现在他们又让我详细解释之前提到的几个关键点:CAP定理、BASE理论、服务拆分原则,以及Spring Cloud Alibaba框架。我需要先确定用户的需求是什么。看起来他们可能在准备面试,或者在深入学习微服务架构,想要更深入地理解这些核心概念和工具。 首先,我要回顾一下之前的回答,确保这次的内容能衔接上。用户可能已经对微服务的基础有了一定的了解,现在需要更深入的知识。他们提到CAP定理和BASE理论,这两个都是分布式系统中的重要理论,对于设计微服务架构至关重要。服务拆分原则是实际应用中的指导方针,而Spring Cloud Alibaba则是具体的工具,用户可能想知道如何应用这些理论到实际框架中。 接下来,我需要考虑如何将这些概念解释清楚。CAP定理涉及到一致性、可用性和分区容忍性,需要明确三者之间的关系以及在实际中的取舍。BASE理论是对CAP的延伸,强调最终一致性和柔性状态,可能需要用例子来说明。服务拆分原则可能包括业务边界、高内聚低耦合等,需要给出具体的拆分方法和注意事项。至于Spring Cloud Alibaba,需要介绍其核心组件,比如Nacos、Sentinel、Seata等,以及它们如何帮助实现微服务架构。 用户可能还希望了解这些理论在实际项目中的应用,比如如何在Spring Cloud Alibaba中处理服务发现、配置管理、熔断限流等问题。另外,他们可能想知道学习这些内容的资源或步骤,以便进一步自学。 需要注意避免过于技术化的术语,用通俗易懂的语言解释概念,并结合实际例子帮助理解。同时,结构要清晰,分点说明每个主题,让用户能够一步步深入理解。还要检查是否有遗漏的重要点,例如CAP的具体应用场景、BASE与ACID的区别,服务拆分时的常见错误,以及框架的具体使用案例等。 最后,确保回答全面覆盖用户提到的所有内容,并且逻辑连贯,帮助用户建立起从理论到实践的知识体系。可能还需要提供一些学习建议,比如推荐文档、教程或实践项目,以帮助用户更好地掌握这些知识。</think>以下是针对 **CAP定理、BASE理论、服务拆分原则** 以及 **Spring Cloud Alibaba框架** 的详细解析,帮助您深入理解微服务架构的核心理论与实战工具: -- ### **一、CAP定理:分布式系统的基石** #### **1. 定义** - **C(Consistency,一致性)**:所有节点在同一时刻看到的数据是相同的。 - **A(Availability,可用性)**:每个请求都能得到响应(不保证数据最新)。 - **P(Partition Tolerance,分区容忍性)**:系统在遇到网络分区(节点间通信中断)时仍能继续运行。 #### **2. CAP的取舍** - **只能满足其中两项**:在分布式系统中,网络分区(P)不可避免,因此实际需在 **CP** 或 **AP** 之间权衡。 - **CP系统**(如ZooKeeper、Etcd):保证一致性,但可能牺牲可用性(如选举期间服务不可用)。 - **AP系统**(如Eureka、Cassandra):优先可用性,允许短暂数据不一致。 #### **3. 应用场景** - **支付系统**:选择CP,确保资金数据强一致。 - **社交网络**:选择AP,允许短时间数据延迟(如点赞数显示不一致)。 --- ### **二、BASE理论:最终一致性的柔性方案** #### **1. 核心思想** - **BA(Basically Available,基本可用)**:允许部分功能降级(如限流、返回缓存数据)。 - **S(Soft State,软状态)**:允许系统存在中间状态(如订单“处理中”)。 - **E(Eventually Consistent,最终一致)**:数据在一段时间后达成一致(如通过消息队列同步)。 #### **2. 与ACID的对比** - **ACID**(传统数据库事务):强一致性,但扩展性差。 - **BASE**:牺牲强一致性,换取高可用性和可扩展性。 #### **3. 实战案例** - **电商库存扣减**: - 用户下单后,库存服务预扣库存(软状态)。 - 若支付超时,通过定时任务回滚库存(最终一致)。 --- ### **三、服务拆分原则:从单体到微服务的关键** #### **1. 拆分策略** - **业务边界优先**:按领域驱动设计(DDD)的 **限界上下文** 划分服务(如订单服务、用户服务)。 - **高内聚低耦合**:服务内部功能紧密相关,对外暴露明确接口。 - **单一职责原则**:一个服务只解决一个核心问题(如支付服务仅处理支付逻辑)。 #### **2. 拆分步骤** 1. **识别核心业务域**(如电商中的商品、订单、支付)。 2. **定义服务接口**(REST API或事件)。 3. **逐步拆分**:优先拆分高频变更或性能敏感模块。 4. **数据分离**:每个服务拥有独立数据库,通过事件同步数据。 #### **3. 常见误区** - **过度拆分**:服务粒度太细,导致运维复杂度激增。 - **事务依赖**:跨服务事务未妥善处理,引发数据不一致。 --- ### **四、Spring Cloud Alibaba:微服务实战框架** #### **1. 核心组件** | 组件 | 功能 | 对标Spring Cloud原生组件 | |---------------------|---------------| | **Nacos** | 服务注册与发现、配置中心 | Eureka + Config | | **Sentinel** | 流量控制、熔断降级、系统保护 | Hystrix | | **Seata** | 分布式事务(AT、TCC、Saga模式) | 无直接对标 | | **RocketMQ** | 消息队列(异步通信、削峰填谷) | RabbitMQ/Kafka | #### **2. 典型应用场景** - **服务注册与配置管理**: ```yaml # Nacos配置示例 spring: cloud: nacos: discovery: server-addr: localhost:8848 # 服务注册地址 config: server-addr: localhost:8848 # 配置中心地址 ``` - **流量控制与熔断**(Sentinel): ```java // 注解方式实现熔断降级 @SentinelResource(value = "orderService", fallback = "fallbackMethod") public Order getOrder(String id) { ... } ``` - **分布式事务**(Seata): ```java // 使用@GlobalTransactional注解 @GlobalTransactional public void createOrder() { // 调用库存服务、支付服务等 } ``` #### **3. 优势** - **国产化支持**:中文文档丰富,社区活跃。 - **云原生友好**:与Kubernetes、Dubbo等生态无缝集成。 - **功能全面**:覆盖服务治理、配置、消息、事务全链路。 -- ### **五、学习建议** 1. **理论结合实践**: - 通过开源项目(如Spring Cloud Alibaba官方Demo)动手搭建微服务。 - 模拟分布式场景(如网络延迟、服务宕机),验证CAP和BASE理论。 2. **深入源码**: - 阅读Nacos服务注册、Sentinel限流算法的实现逻辑。 - 理解Seata的AT模式如何通过全局锁保证事务一致性。 3. **扩展学习**: - 服务网格(Istio):了解如何通过Sidecar代理实现流量控制。 - 云原生技术栈:Kubernetes + Docker + Prometheus(监控)。 --- 掌握这些内容后,您将能够: ✅ 设计符合业务需求的微服务架构。 ✅ 解决分布式环境下的数据一致性、高可用性问题。 ✅ 熟练使用Spring Cloud Alibaba构建生产级微服务系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值