Eureka源码分析--服务注册(2)

112 阅读8分钟

Eureka源码分析--服务注册(2)

服务注册

client端发送注册请求

初看eureka client端源码,想找找服务是怎么注册的,在DiscoveryClient方法中顺着看的时候,看到一行代码,

 if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                if (!register() ) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable th) {
                logger.error("Registration error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }

这里面不就有个很眼熟的的单词吗,register(),这不就是要找的东西吗,点进去看看,源码如下:

  /**
     * Register with the eureka service by making the appropriate REST call.
     */
//通过进行适当的 REST 调用向 eureka 服务注册。
    boolean register() throws Throwable {
        logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
    }

这英文注释不是清清楚楚吗,这不就是我要找的注册流程吗,但是,回过头看找到register的入口时发现,这里有一个clientConfig.shouldEnforceRegistrationAtInit()这样的配置项,点进去看发现默认就是false,翻译了一下这个配置的注解意思也没有太理解,

image20220427205854190.png

而且在我们项目中也并没有使用过这个注解。这是我几乎可以肯定的确定这里并不是register方法的真正入口。

后续通过反查找,找到了这样几个方法

image20220427210030267.png

473行是我第一次看到的地方,之前我尝试打断点并未进入,所以目标只有907行和121行,当我点到907行时发现调用是在renew的方法中,而根据这个方法所提供的的注释来看,描述的是通过进行适当的 REST 调用来更新 eureka 服务

image20220427210315300.png

并且该方法开始会先去调用一个sendHeartBeat,抛开具体执行的方法,单从字面上来看这里是做心跳续约的,并且调用register时会接收一个返回值。而凭感觉来说,正常的注册流程调用完就完了嘛,我要你返回参数干啥。

所以自然而然的确定了最后一处调用,121行。

public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

看到这里可以更加确定这里就是client进行注册时调用的地方,但是还是需要向上找,该方法属于InstanceInfoReplicator类,并且该方法为run,而该类实现了Runnable,所以可以断定的是有一个地方一定是

InstanceInfoReplicator.run()。继续向上查找,在该类找到onDemandUpdate()方法中调用了this.run().

public boolean onDemandUpdate() {
        if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
            if (!scheduler.isShutdown()) {
                scheduler.submit(new Runnable() {
                    @Override
                    public void run() {
                        logger.debug("Executing on-demand update of local InstanceInfo");
    
                        Future latestPeriodic = scheduledPeriodicRef.get();
                        if (latestPeriodic != null && !latestPeriodic.isDone()) {
                            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;
        }
    }

而该方法继续向上找便会找到Discovery方法中initScheduledTasks()。到目前为止算是找到真正的入口了,那么便从入口在重新梳理一次。 image20220427223345499.png

完整的流程如下:

1、调用initScheduledTasks方法

2、实例化InstanceInfoReplicator(实例信息复制器(直译)),调用onDemandUpdate方法

3、onDemandUpdate方法中调用run方法进行真正的注册

4、run方法中会先判断一个dirtyTimestamp是否为空,而dirtyTime会在InstanceInfoReplicator.start()方法中调用setIsDirty()设置

5、调用register()方法发送请求至server端进行注册

整体的注册流程就结束了,但是这里面有一个非常有意思的问题,在InstanceInfoReplicator.start(),将自己放入里一个定时任务的线程池中,并且第一次初始化的时候会延迟40s(默认值,可以通过配置修改)

通过查找官方文档,找到官方解释

image20220427234145711.png

再看一下register的请求源码

@Override
    public EurekaHttpResponse<Void> register(InstanceInfo info) {
        //这里会拼接一个url ,如果服务名为ServerA 那么 当前urlPath的路径即为 apps/ServerA
        String urlPath = "apps/" + info.getAppName();
        Response response = null;
        try {
            //构建请求信息
            Builder resourceBuilder = jerseyClient.target(serviceUrl).path(urlPath).request();
            addExtraProperties(resourceBuilder);
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder
                    .accept(MediaType.APPLICATION_JSON)
                    .acceptEncoding("gzip")
                    .post(Entity.json(info));
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey2 HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }

server端接收client发送的请求

其实当client发送出请求以后,就已经不知道发到何处了。。。

经过再三的查找,在eureka-server的项目下发现了一个webapp,里面有个web.xml,这个就很神奇,名字叫server,还是个web.xml,这里面不就是拦截器啊

image20220428001602771.png

这里的拦截器分为四种:1、负责状态相关的处理逻辑 2、对请求进行授权认证的处理 3、负责限流相关的逻辑 4、负责压缩、编码相关的处理

最后都会走jersey的servlet,这个servlet会根据请求的路径,将请求转发给eureka core里面的resource,继续找core里的resource。

根据请求路径分析,最终会找到ApplicationResource中addInstance方法上去

在这个方法中上来就是一堆的if..else.. 看的人头皮发麻,不过简单了解都是些校验之类的东西,没有深究的必要,后续又执行了一个对我们无关紧要的东西,直接略过,再道最后出现了关键的方法register();

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

继续进入register方法中查看,发现调用了一个super.register

@Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }
        super.register(info, leaseDuration, isReplication);
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }

进入super.register,便是注册的核心流程

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        //加读锁,表示可以多个实例同时来注册
        read.lock();
        try {
            //从根据注册的服务名取出当前server存储的所有该服务名的实例信息
            //注册表结构
           /** {
                “ServiceA”: {
                “001”: Lease<InstanceInfo>,
                “002”: Lease<InstanceInfo>,
                “003”: Lease<InstanceInfo>
                },
                “ServiceB”: {
                “001”: Lease<InstanceInfo>
                }
            } **/
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            //如果当前服务没有其他实例信息,说明该服务是第一次注册,构造一个新的并发newMap放如gMap中去,而registry也是一个ConcurrentHashMap,能够保证并发的安全性
            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;
                }
            }
            //取出注册服务的当前实例信息,如果是第一次注册,一定为null
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            //判断是否重复注册,并且如果已经存在当前实例信息,说明实例信息发生变动,需要再次注册
            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.
                //这是一个 > 而不是 >= 因为如果时间戳相等,我们仍然采用远程传输的 InstanceInfo 而不是服务器本地副本。
                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中,并放入本地注册表中
            Lease<InstanceInfo> lease = new Lease<>(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() + ")"));
            // 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
            //如果租约注册为 UP 状态,则设置租约服务上线时间戳
            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();
        }
    }

服务注册的方法主要为几步:

1、获取原始注册表中当前实例服务名称的所有信息

2、根据当前注册实例的id判断是否为第一次注册,分别走不同的逻辑

3、将注册信息封装到gMap中

4、根据实例状态来做不同的事情,如果为up状态,则将信息放入最近变更的队列中去

5、清除缓存

总结

整体上服务的注册流程已经完成,其中逻辑大体上比较清晰顺畅,但是比较坑爹的是client的服务注册的入口比较隐秘,不易查找,通过反推法最终也可以找到。