(一)Eureka源码学习之eureka-server服务注册

  • 前言:由于自己学习过于浮躁,很多知识学过就忘,因此准备转换学习方式,记录一下自己的学习过程,博文有出错的地方,欢迎留言批评指正。
  • 微服务概述及eureka使用就不再概述,直接进入eureka源码部分
  • 环境:SpringCloud Hoxton.SR1,SpringBoot2.2.2(官方推荐),jdk8,Maven3.5,IDEA
MVC架构层

Eureka分为eureka-client端与eureka-server端(C/S架构)。作为服务注册与发现的模块,eureka内部采用基于Rest的网络通讯。在我们平时进行开发web应用时,要接受请求、处理请求,大多数采用了SpringMVC的MVC封装,eureka里面也是有一个MVC的架构层,但不是SpringMVC,这里它采用了Jersey框架,关于这个框架,可以点击这里Jersey入门进行大致了解。

@EnableEurekaServer注解

表示是一个注册中心
@EnableEurekaServer注解表示该SpringBoot服务作为一个注册中心,为什么有这个注解皆可以成为一个注册中心呢?让我们点击进去这个注解进行观察。
在这里插入图片描述
点开这个注解发现,前三个注解是java元注解,@Import注解给Spring容器注册一个EurekaServerMarkerConfiguration配置类,这个类是干什么的?为什么要给Spring容器导入这样一个类呢?打开这个类,看一下这个类到底是干嘛的。
在这里插入图片描述
点进去这个配置类,发现它只做了一件事,实例化一个Marker类,到这里看的一头雾水,这个Marker好像没有任何作用,实际上marker是标记的意思,在这里也只是仅用来实现标记功能。具体这个Marker在哪被使用的?要从SpringBoot自动配置原理来看了,自动配置原理这里不作赘述,打开springcloud整合eureka的maven依赖包,如下,注意是是spring-cloud-netflix-eureka-server包,不是含有starter的那个包,不要找错了。
在这里插入图片描述
这个包里有一个META-INF目录,找到spring.factories文件,springboot在启动时会自动实例化配置在这里的类。
在这里插入图片描述
这里有一个EurekaServerAutoConfiguration类,eureka的自动配置都是由这个类实现的。
暂时不去看eureka是怎么去配置的,我们先考虑一个问题,在平时我们使用SpringMVC时,要先初始化DispatchServlet分发器,之后才可以为我们拦截请求进行处理,Eureka使用的Jersey框架也是这样,不过它底层是通过过滤器拦截请求,我们要让它工作起来,要将过滤器初始化。这里类比一下SpringMVC就好理解了。
所以要让eureka-server工作起来,要初始化eureka工作需要的bean以及将Jersey的filter给注入到Spring容器中。我们打开EurekaServerAutoConfiguration看一下。
在这里插入图片描述
这个类实现了WebMvc接口,上面有几个注解,@Import注解导入EurekaServer初始化的配置(这个配置之后会讲到,本章节暂且不去讨论),@ConditionalOnBean这个注解表示如果容器中有Marker类,才会注入标注此注解的bean,即EurekaServerAutoConfiguration才可以正常工作。到这里,@EnableEurekaServer注解才算解读完毕,我们接下来去看一下EurekaServerAutoConfiguration做的几件比较核心的事情:

  • 读取配置,通过@PropertySource注解读取;
  • Jersey框架实例化,即初始化filter,设置拦截路径(了解即可,不必深究);
  • 初始化eureka初始化filter
服务注册(责任链模式)

为了追踪服务注册的过程,以debug的方式启动eureka-server,还需要打断点,这个断点要打在哪里?类比SpringMVC,我们可以在controller层去追踪,Jersey里与之类似的是Resources,我们打开maven依赖里的eureka-core,找到ApplicationResource,这个类是用来拦截微服务的请求。
既然是服务注册,POST方法,我们去寻找相关的方法,发现addInstance()方法,在这里打断点,启动一个eureka-client(即一个微服务)
在这里插入图片描述
info:client注册信息
isReplication:是否来自于集群同步(来自客户端的注册信息,此项默认null,只有作集群信息同步时,才会携带此参数,这个参数要稍微注意一下,与集群同步有关)

	@POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
        // validate that the instanceinfo contains all the necessary required fields
        if (isBlank(info.getId())) {
            return Response.status(400).entity("Missing instanceId").build();
        } else if (isBlank(info.getHostName())) {
            return Response.status(400).entity("Missing hostname").build();
        } else if (isBlank(info.getIPAddr())) {
            return Response.status(400).entity("Missing ip address").build();
        } else if (isBlank(info.getAppName())) {
            return Response.status(400).entity("Missing appName").build();
        } else if (!appName.equals(info.getAppName())) {
            return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
        } else if (info.getDataCenterInfo() == null) {
            return Response.status(400).entity("Missing dataCenterInfo").build();
        } else if (info.getDataCenterInfo().getName() == null) {
            return Response.status(400).entity("Missing dataCenterInfo Name").build();
        }

        // handle cases where clients may be registering with bad DataCenterInfo with missing data
        DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
        if (dataCenterInfo instanceof UniqueIdentifier) {
            String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
            if (isBlank(dataCenterInfoId)) {
                boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                if (experimental) {
                    String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                    return Response.status(400).entity(entity).build();
                } else if (dataCenterInfo instanceof AmazonInfo) {
                    AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                    String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                    if (effectiveId == null) {
                        amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                    }
                } else {
                    logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                }
            }
        }

        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }

首先要经过一系列的参数校验,当校验通过以后,还要处理客户端可能注册错误的DataCenterInfo而丢失数据的情况(可以直接忽略,一般不会进入此方法),真正执行服务注册的是register方法,我们进入此方法。
在这里插入图片描述
发现进入InstanceRigistry中,这个类并没有真正做服务注册,handleRegistration()追踪进去,发布了一个服务注册的事件,之后交给父类进行注册,打开InstanceRigistry的继承树看一下:
在这里插入图片描述
也就是说,InstranceRegistry交给PeerAwareInstanceRegistryImpl去注册,自己只是发布了一个注册事件,继续追踪下去,
在这里插入图片描述
这边出现了一个新的类Lease(租债器),leaseDuration表示Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒,可以在eureka.instance.leaseExpirationDurationInSeconds进行配置。PeerAwareInstanceRegistryImpl类也没有真正去进行服务注册,它调用了它的父类AbstractInstanceRegistry的register功能进行服务注册,等待注册完,调用自己的replicateToPeers()方法进行集群信息同步,关于集群同步源码,之后再进行解读。我们继续跟着程序走进下去,追踪注册信息。
友情提示:以下代码先大致过一遍,之后再具体分析

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {   // 注意,这里已经是父类AbstractInstanceRegistry的注册方法了
            read.lock(); //读锁,可以并发
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication); // 做统计用
            if (gMap == null) { //如果没有查到该微服务的集群信息,就新增一个map用来存放该微服务的集群信息,因为可能有其他相同名称的微服务也会注册进eureka
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                // putIfAbsent   如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {  
                    gMap = gNewMap;
                }
                // 此时 gMap 是该微服务名下的Map<String,Lease>  
            }
            // 根据传过来的注册信息ID去查找有没有对应的租债器
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            // 如果已经有租债器了,就保留最近的一个,即比较服务端存在的该实例与本次注册的实例留下最新的那个,这里的if是为了解决并发注册的冲突
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);

                // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
                // InstanceInfo instead of the server local copy.
                // 这里官方解释是如果两个时间相等,也用本次注册的实例信息,不用服务端保存的信息
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                            " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = existingLease.getHolder();
                }
            } else {
                // The lease does not exist and hence it is a new registration
                // 判断该实例的租债器不存在,本次是新注册
                synchronized (lock) {
                	// 与自我保护功能相关,本节不进行解读
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        // Since the client wants to register it, increase the number of clients sending renews
                        this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                        updateRenewsPerMinThreshold();
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            // 新建一个本次注册实例的租债器,用来进行本次微服务实例的注册
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            // 这里真正保存了注册信息
            gMap.put(registrant.getId(), lease);
            // 服务注册已经完毕,之后的内容不在本次解读范围之内
            synchronized (recentRegisteredQueue) {
                recentRegisteredQueue.add(new Pair<Long, String>(
                        System.currentTimeMillis(),
                        registrant.getAppName() + "(" + registrant.getId() + ")"));
            }
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                                + "overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            read.unlock();
        }
    }

可以发现,AbstractInstanceRegistry真正执行了注册方法gMap.put(registrant.getId(), lease);,我们来看一下eureka服务器是怎么样来保存客户端的注册信息的,先看一下携带的参数:

  • InstanceInfo registrant eureka-client的注册信息
  • int leaseDuration 租债器租用时间(s),表示服务端多久没有收到客户端的心跳,就将其移除
  • boolean isReplication 注册信息是否来自于集群同步

服务注册这里eureka加了一把读锁,关于这里为什么加读锁,请参考eureka-server服务发现源码解读。
我们来看一下eureka是怎么保存微服务的注册信息的,它在本类维护了一个ConcurrentHashMap。

ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

最外层的String表示微服务的name,内层的Map表示该微服务(名称一致)对应的集群,这也是为什么在搭建微服务集群的时候,要把spring.application.name设置一致的原因。现在讲解一下内层的Map,String表示微服务实例的id,Lease是维护了具体实例的一个租债器,租债器的结构如下,作用就是记录该实例的各个时间(注册时间、剔除时间、服务上线时间,最后操作时间以及租约有效时间),holder就是具体维护的实例对象。
在这里插入图片描述
明白了这些概念,我们回过头来分析一下注册代码。
友情提示:现在可以跟着注释去分析上面的服务注册代码。

尾声:

  • eureka服务注册代码其实很简单,就是在eureka-server内存里维护了一个map,相当于,之后心跳续约相当于,服务下架、服务剔除相当于,服务发现相当于,学习源码就是要学习源码的思想。
  • 第一次写博客,如有不正确的地方,请批评指正。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值