图解+源码讲解 Eureka Client 拉取注册表流程

1,039 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

Eureka Client 拉取注册表流程

成功永远属于那些爱拼搏的人 相关文章
eureka-server 项目结构分析
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析
图解+源码讲解 Eureka Server 服务剔除逻辑
图解+源码讲解 Eureka Server 集群注册表同步机制

客户端拉取注册流程逻辑

    客户端拉取注册表的逻辑就是,如果开启了只读缓存那么就先从只读缓存中获取注册表,如果只读注册表中不存在的情况下从读写缓存中获取,如果读写缓存也没有的话那么就进行从注册中心拉取之后同步到读写缓存,在同步到只读缓存中

代码核心逻辑

image.png

从哪里开始分析

    在 new DiscoveryClient 客户端的时候如果配置了拉取注册表的情况下,那么就会走拉取注册表的方法fetchRegistry,参数传的是一个false

if (clientConfig.shouldFetchRegistry()) {
    /**
     * 客户端拉取注册表核心逻辑
     */
    boolean primaryFetchRegistryResult = fetchRegistry(false);
}

拉取核心逻辑

    如果是第一次拉取注册表的话那么就会走全量拉取逻辑 getAndStoreFullRegistry() ,如果不是第一次拉取的话那么就会走增量拉取逻辑,getAndUpdateDelta(applications)

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    try {
    /**
     * 如果增量拉取没有开放或者第一次拉取注册表的时候是全量拉取得注册表
     */
    Applications applications = getApplications();// 获取本地的所有实例
    // clientConfig.shouldDisableDelta() 默认是false
    if (clientConfig.shouldDisableDelta()
       || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
       || forceFullRegistryFetch // 传进来的时候是 false
       || (applications == null) // 不是null
       || (applications.getRegisteredApplications().size() == 0) // 第一次启动的时候是0
       || (applications.getVersion() == -1))
    {
        /**
         * 拉取并且缓存全量注册表
         */
        getAndStoreFullRegistry();
    } else {
        /**
         * 如果配置了增量拉取,并且本地localRegionApps缓存不为空的情况下走 增量拉取注册表方法
         */
        getAndUpdateDelta(applications);
    }
}

全量拉取逻辑

    通过之前创建好的 eurekaTransport.queryClient 客户端进行访问拉取注册表,设置到本地的缓存中 localRegionApps

private void getAndStoreFullRegistry() throws Throwable {
    /**
     * 获取所有实例注册信息从 eureka-server
     */
    Applications apps = null;
    // clientConfig.getRegistryRefreshSingleVipAddress()默认是空的,
    // 所以走 eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
    EurekaHttpResponse<Applications> httpResponse = 
        eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        apps = httpResponse.getEntity();
    }
    /**
     * 如果不为空的话从缓存中读取到数据的话,那么就设置到本地缓存中
     */
    localRegionApps.set(this.filterAndShuffle(apps));
}

    拉取全量注册表的方法走的是 AbstractJersey2EurekaHttpClient 中的 getApplicationsInternal 接口,serviceUrl 自己配置的,通过 jersey框架进行访问的接口,所以在eureka-core的resource的 ApplicationsResource 里面找带有 路径加+版本号+/apps的get请求

private EurekaHttpResponse<Applications> 
                getApplicationsInternal(String urlPath, String[] regions) {
Response response = null;
    // serviceUrl 自己配置的,通过 jersey框架进行访问的接口,
    // 所以在eureka-core的resource的 ApplicationsResource 里面找带有
    // 路径加+版本号+/apps的get请求
    WebTarget webTarget = jerseyClient.target(serviceUrl).path(urlPath);
    if (regions != null && regions.length > 0) {
        webTarget = webTarget.queryParam("regions", StringUtil.join(regions));
    }
    Builder requestBuilder = webTarget.request();
    addExtraProperties(requestBuilder);
    addExtraHeaders(requestBuilder);
    response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get();

    Applications applications = null;
    if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) {
        applications = response.readEntity(Applications.class);
    }
    return anEurekaHttpResponse(response.getStatus(), applications).
    headers(headersOf(response)).build();
}

    去eureka-core的resource的 ApplicationsResource 里面找带有 路径加+版本号+/apps的get请求,这个里面是真正的获取注册表的方法

@GET
public Response getContainers(@PathParam("version") String version,
      @HeaderParam(HEADER_ACCEPT) String acceptHeader,
      @HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
      @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
      @Context UriInfo uriInfo,
      @Nullable @QueryParam("regions") String regionsStr) {
    ......
    /**
     * 构建缓存 key
     */
    Key cacheKey = new Key(Key.EntityType.Application, ResponseCacheImpl.ALL_APPS,
          keyType, CurrentRequestVersion.get(), EurekaAccept.fromString(eurekaAccept), 
                           regions);
    /**
     * 在缓存里面获取 cacheKey 对应的value
     */
    response = Response.ok(responseCache.get(cacheKey)).build();
    }
    return response;
}

    从缓存中获取注册表信息 responseCache.get(cacheKey) ,如果开启了只读缓存那么就先从只读缓存中获取注册表,如果只读注册表中不存在的情况下从读写缓存中获取

public String get(final Key key) {
// 是否开启只读缓存,默认是开启的
    return get(key, shouldUseReadOnlyResponseCache);
}

String get(final Key key, boolean useReadOnlyCache) {
    Value payload = getValue(key, useReadOnlyCache);
    if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
        return null;
    } else {
         // 返回注册表
        return payload.getPayload();
    }
}

Value getValue(final Key key, boolean useReadOnlyCache) {
    Value payload = null;
// 如果是true的话那么从 readOnlyCacheMap【ConcurrentHashMap】 只读缓存中获取key对应的缓存值
    if (useReadOnlyCache) {
        final Value currentPayload = readOnlyCacheMap.get(key);
      // 如果读到的话那么就直接赋值返回缓存中的值
        if (currentPayload != null) {
            payload = currentPayload;
        } else {
            // 如果读不到的话那么就直接从读写LoadingCache中获取缓存中对应的值,
                //并且放到 readOnlyCacheMap只读缓存中
            payload = readWriteCacheMap.get(key);
            readOnlyCacheMap.put(key, payload);
        }
    } else {
// 如果 useReadOnlyCache 是false的话,那么从读写LoadingCache中获取缓存中对应的值
    payload = readWriteCacheMap.get(key);
    }
return payload;
}

增量拉取逻辑

    getAndUpdateDelta(delta); 是增量拉取的方法,通过之前创建好的 eurekaTransport.queryClient 客户端进行访问拉取注册表,设置到本地的缓存中

private void getAndUpdateDelta(Applications applications) throws Throwable {
    Applications delta = null;
    /**
     * 这块会走EurekaHttpClient的getDelta()方法,
     * http://localhost:8080/v2/apps/delta,get请求
     */
    EurekaHttpResponse<Applications> httpResponse = 
        eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        delta = httpResponse.getEntity();
    }
}

    走的是 AbstractJersey2EurekaHttpClient 的getDelta()方法,调用的是eureka-core的resource的 ApplicationsResource 里面找带有 路径加+版本号+/apps/delta请求 @Path("delta")

public Response getContainerDifferential(
        @PathParam("version") String version,
        @HeaderParam(HEADER_ACCEPT) String acceptHeader,
        @HeaderParam(HEADER_ACCEPT_ENCODING) String acceptEncoding,
        @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept,
        @Context UriInfo uriInfo, @Nullable @QueryParam("regions") String regionsStr) {
    // 构建缓存key,key的类型是 ResponseCacheImpl.ALL_APPS_DELTA
    Key cacheKey = new Key(Key.EntityType.Application, ResponseCacheImpl.ALL_APPS_DELTA, 
                           keyType, CurrentRequestVersion.get(), 
                           EurekaAccept.fromString(eurekaAccept), regions);
    // 从缓存中获取注册表信息
    response = Response.ok(responseCache.get(cacheKey)).build();
  
    return response;
}

    responseCache.get(cacheKey) 这个方法和获取全量走的从缓存中获取注册表的逻辑是一样,后续根据获取到的增量注册表更新本地的缓存,如果获取的增量注册表是null的话那么就走全量拉取逻辑

if (delta == null) {
    // 如果增量拉取为空那么走全量拉取
    getAndStoreFullRegistry();
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, 
                                                 currentUpdateGeneration + 1)) {
String reconcileHashCode = "";
if (fetchRegistryUpdateLock.tryLock()) {
    try {
        // 更新增量拉取逻辑
        updateDelta(delta);
        reconcileHashCode = getReconcileHashCode(applications);
    } finally {
        fetchRegistryUpdateLock.unlock();
    }
}

    通过获取的增量数据进行本地注册表更新,遍历获取到的增量信息,如果是添加类型那么就加入到本地的注册表中,本地没有的情况下,如果是修改类型那么就加入到本地的注册表中,本地没有的情况下,如果是删除类型那么就从本地将其移除,本地如果有的情况下

private void updateDelta(Applications delta) {
    int deltaCount = 0;
    for (Application app : delta.getRegisteredApplications()) {
        for (InstanceInfo instance : app.getInstances()) {
            Applications applications = getApplications();
    if (ActionType.ADDED.equals(instance.getActionType())) {
          // 如果是添加类型那么就加入到本地的,本地没有的情况下
            applications.addApplication(app);
    } else if (ActionType.MODIFIED.equals(instance.getActionType())) {
          // 如果是修改类型那么就加入到本地的,本地没有的情况下
            applications.addApplication(app);
    } else if (ActionType.DELETED.equals(instance.getActionType())) {
           // 如果是删除类型那么就从本地将其移除,本地如果有的情况下
        Application existingApp = applications.
            getRegisteredApplications(instance.getAppName());
        if (existingApp != null) {
            existingApp.removeInstance(instance);
        }
    }
}

小结

  1. 客户端拉取注册表,如果配置了只读缓存那么就走只读缓存,只读缓存没有的情况下走读写缓存,读写缓存没有的话就去服务端拉取
  2. 如果是第一次拉取或者本地缓存没有的情况下,那么就走全量拉取
  3. 如果不是第一次拉取或者本地有的情况下就走增量拉取之后放入本地缓存中