上一篇文章中有介紹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