spring-cloud-eureka (二) Client - Server 接口交互(消息發送)源碼分析

上一篇文章中有介紹spring-cloud-eureka的原因,以及一部分源碼分析了服務在啓動時是如何加載並運行spring-cloud-eureka的,這一篇文章將從源碼的角度來分析spring-cloud-eureka是如何進行服務治理的。


服務註冊

服務註冊的真正入口在com.netflix.discovery.DiscoveryClient#register()

public class DiscoveryClient implements EurekaClient {
    ...
    boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            //發送註冊信息
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }
    ...
}

從代碼中可以看到,eureka註冊時,是將實例信息傳遞給eurekaTransport.registrationClient.register(InstanceInfo)方法,eurekaTransport.registrationClient是一個EurekaHttpClient接口,它的註釋中說明這是一個底層的eureka http 通信api,也就是說,服務註冊消息、服務續約消息(心跳)、服務下線、服務獲取等,都是由這個接口發送的。往上翻看源碼,發現eurekaTransport.registrationClient是一個com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient實例,也就是註冊信息是由該類發送的。

com.netflix.discovery.DiscoveryClient

public class DiscoveryClient implements EurekaClient {

    ...
    //構造方法
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
    ...
    //創建EurekaTransport,並初始化
    eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);
    ...
    }
    ...
    private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
                                            AbstractDiscoveryClientOptionalArgs args) {
     ...
        if (clientConfig.shouldRegisterWithEureka()) {
            EurekaHttpClientFactory newRegistrationClientFactory = null;
            EurekaHttpClient newRegistrationClient = null;
            try {
                //創建註冊客戶端工廠類
                newRegistrationClientFactory = EurekaHttpClients.registrationClientFactory(
                        eurekaTransport.bootstrapResolver,
                        eurekaTransport.transportClientFactory,
                        transportConfig
                );
                //創建註冊客戶端
                newRegistrationClient = newRegistrationClientFactory.newClient();
            } catch (Exception e) {
                logger.warn("Transport initialization failure", e);
            }
            //引用
            eurekaTransport.registrationClientFactory = newRegistrationClientFactory;
            eurekaTransport.registrationClient = newRegistrationClient;
        }
        ...
    }
}

com.netflix.discovery.shared.transport.EurekaHttpClients

public final class EurekaHttpClients {
    ...
    public static EurekaHttpClientFactory registrationClientFactory(ClusterResolver bootstrapResolver,
                                                                    TransportClientFactory transportClientFactory,
                                                                    EurekaTransportConfig transportConfig) {
        return canonicalClientFactory(EurekaClientNames.REGISTRATION, transportConfig, bootstrapResolver, transportClientFactory);
    }

    static EurekaHttpClientFactory canonicalClientFactory(final String name,
                                                          final EurekaTransportConfig transportConfig,
                                                          final ClusterResolver<EurekaEndpoint> clusterResolver,
                                                          final TransportClientFactory transportClientFactory) {
        //一個客戶端工廠匿名類
        return new EurekaHttpClientFactory() {
            @Override
            public EurekaHttpClient newClient() {
                //創建eureka http客戶端並返回
                return new SessionedEurekaHttpClient(
                        name,
                        /* 新建了一個RetryableEurekaHttpClient工廠 */ RetryableEurekaHttpClient.createFactory(
                                name,
                                transportConfig,
                                clusterResolver,
                                RedirectingEurekaHttpClient.createFactory(transportClientFactory),
                                ServerStatusEvaluators.legacyEvaluator()),
                        transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
                );
            }

            @Override
            public void shutdown() {
                wrapClosable(clusterResolver).shutdown();
            }
        };
    }
    ...
}

註冊客戶端(eurekaTransport.registrationClient)的創建過程是這樣的

DiscoveryClient.init()
- scheduleServerEndpointTask()
- - EurekaHttpClients.registrationClientFactory()
- - - canonicalClientFactory()
- - EurekaHttpClientFactory.newClient()

這裏寫圖片描述


發送註冊消息

你以爲最終的註冊消息是由SessionedEurekaHttpClient發送的麼?我一開始也是這樣以爲的,最後發現太天真了,看看調用鏈,看看這其中包裝了一大堆EurekaHttpClient實現類。
這裏寫圖片描述

可以發現,最終調用的是com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient對象。

這裏寫圖片描述

上面是EurekaHttpClient的類圖結構,最終真正發消息的是由AbstractJerseyEurekaHttpClient發送的,它有兩個子類,分別是JerseyApplicationClient和JerseyReplicationClient,JerseyReplicationClient類是用於服務同步的,後面再說,先看JerseyApplicationClient類。

package com.netflix.discovery.shared.transport.jersey;
...
public class JerseyApplicationClient extends AbstractJerseyEurekaHttpClient {

    private final Map<String, String> additionalHeaders;

    public JerseyApplicationClient(Client jerseyClient, String serviceUrl, Map<String, String> additionalHeaders) {
        super(jerseyClient, serviceUrl);
        this.additionalHeaders = additionalHeaders;
    }

    @Override
    protected void addExtraHeaders(Builder webResource) {
        if (additionalHeaders != null) {
            for (String key : additionalHeaders.keySet()) {
                webResource.header(key, additionalHeaders.get(key));
            }
        }
    }
}

在JerseyReplicationClient 類中並沒有發送消息的邏輯,那麼就是在AbstractJerseyEurekaHttpClient類中了,查看該類的源碼,可以看到相關的方法。

package com.netflix.discovery.shared.transport.jersey;
...
public abstract class AbstractJerseyEurekaHttpClient implements EurekaHttpClient {
    ...
    //發送註冊消息
    @Override
    public EurekaHttpResponse<Void> register(InstanceInfo info) {
        String urlPath = "apps/" + info.getAppName();
        ClientResponse response = null;
        try {
            Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder
                    .header("Accept-Encoding", "gzip")
                    .type(MediaType.APPLICATION_JSON_TYPE)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, info);
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }
    ...
}

AbstractJerseyEurekaHttpClient 類中方法對應的功能

method description
register(InstanceInfo) 服務註冊
cancel(String, String) 服務下線
sendHeartBeat(String, String, InstanceInfo, InstanceStatus) 服務續約
statusUpdate(String, String, InstanceStatus, InstanceInfo) 服務狀態更新

假設服務實例的eureka配置是這樣的

spring:
  application:
    name: eureka-client
server:
  port: 10101
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    serviceUrl:
      defaultZone: http://localhost:10002/eureka/

那麼請求的路徑就是http://localhost:10002/eureka/apps/EUREKA-CLIENT,並以post的方法將InstanceInfo對象提交過去,然後將註冊中心響應的狀態碼和head信息包裝起來返回。


註冊中心接收註冊消息

Eureka Server 通過jersey發佈了一些web接口,資源類在eureka-core-{version}.jar!com.netflix.eureka.resources包下,服務註冊的代碼就在com.netflix.eureka.resources.ApplicationResource中。

com.netflix.eureka.resources.ApplicationResource

package com.netflix.eureka.resources;
...
@Produces({"application/xml", "application/json"})
public class ApplicationResource {
    ...
    @POST
    @Consumes({"application/json", "application/xml"})
    public Response addInstance(InstanceInfo info,
                                @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        ...

        //註冊中心服務登記的入口,isReplication判斷是否是其它Eureka Server廣播過來的。
        registry.register(info, "true".equals(isReplication));
        return Response.status(204).build();  // 204 to be backwards compatible
    }
    ...
}

這些接口包括:

url method class description
/apps/{appId} POST ApplicationResource 服務註冊
/apps/{appId} GET ApplicationResource 獲取服務信息
/instances/{id} GET InstancesResource 獲取服務實例信息
/apps/{appId}/{id} GET InstanceResource 獲取服務實例信息
/apps/{appId}/{id} PUT InstanceResource 服務實例續約
/apps/{appId}/{id} DELETE InstanceResource 服務實例下線
/apps/{appId}/{id}/status PUT InstanceResource 服務實例狀態變更

上一章spring-cloud-eureka (一) 原理分析 主要分析服務治理的功能以及Eureka Client和Eureka Server在啓動時做的處理,這一章分析了Eureka Client與 Eureka Server接口交互的邏輯,還有會下一章分析註冊中的服務註冊、續約、下線、以及服務剔除的邏輯。

下面幾張圖描述了服務註冊、服務續約、服務下線的調用鏈

服務註冊
這裏寫圖片描述

服務續約
這裏寫圖片描述

服務下線
這裏寫圖片描述

參考資料
《spring cloud 微服務實戰》
深度剖析服務發現組件Netflix Eureka

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章