Eureka源码分析一 -- 服务注册

一、meta-data信息收集

When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. Eureka receives heartbeat messages from each instance belonging to a service. If the heartbeat fails over a configurable timetable, the instance is normally removed from the registry.

通过EurekaClientAutoConfiguration配置类中的eurekaInstanceConfigBean方法,收集客户端的配置信息,包括host、port等。这些信息,会随着客户端进行注册时,传递给server端。

public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,
			ManagementMetadataProvider managementMetadataProvider) {
		String hostname = getProperty("eureka.instance.hostname");
		boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
		String ipAddress = getProperty("eureka.instance.ip-address");
		boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));

		String serverContextPath = env.getProperty("server.servlet.context-path", "/");
		int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));

		Integer managementPort = env.getProperty("management.server.port", Integer.class); // nullable.
		String managementContextPath = env.getProperty("management.server.servlet.context-path"); // nullable. should be wrapped  into optional
		Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port", Integer.class); // nullable
		EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);

		instance.setNonSecurePort(serverPort);
		instance.setInstanceId(getDefaultInstanceId(env));
		instance.setPreferIpAddress(preferIpAddress);
		instance.setSecurePortEnabled(isSecurePortEnabled);
		if (StringUtils.hasText(ipAddress)) {
			instance.setIpAddress(ipAddress);
		}

		if (isSecurePortEnabled) {
			instance.setSecurePort(serverPort);
		}

		if (StringUtils.hasText(hostname)) {
			instance.setHostname(hostname);
		}
		String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
		String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");

		if (StringUtils.hasText(statusPageUrlPath)) {
			instance.setStatusPageUrlPath(statusPageUrlPath);
		}
		if (StringUtils.hasText(healthCheckUrlPath)) {
			instance.setHealthCheckUrlPath(healthCheckUrlPath);
		}

		ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
				serverContextPath, managementContextPath, managementPort);

		if (metadata != null) {
			instance.setStatusPageUrl(metadata.getStatusPageUrl());
			instance.setHealthCheckUrl(metadata.getHealthCheckUrl());
			if (instance.isSecurePortEnabled()) {
				instance.setSecureHealthCheckUrl(metadata.getSecureHealthCheckUrl());
			}
			Map<String, String> metadataMap = instance.getMetadataMap();
			metadataMap.computeIfAbsent("management.port", k -> String.valueOf(metadata.getManagementPort()));
		}
		else {
			// without the metadata the status and health check URLs will not be set
			// and the status page and health check url paths will not include the
			// context path so set them here
			if (StringUtils.hasText(managementContextPath)) {
				instance.setHealthCheckUrlPath(
						managementContextPath + instance.getHealthCheckUrlPath());
				instance.setStatusPageUrlPath(
						managementContextPath + instance.getStatusPageUrlPath());
			}
		}

		setupJmxPort(instance, jmxPort);
		return instance;
	}

其中,针对于hostname、ip地址信息,内部先通过InetUtils帮助工具类获取。当外部通过eureka.instance.hostname和eureka.instance.ip-address指定是,会覆盖原获取到的数据信息。

public EurekaInstanceConfigBean(InetUtils inetUtils) {
	this.inetUtils = inetUtils;
	this.hostInfo = this.inetUtils.findFirstNonLoopbackHostInfo();
	this.ipAddress = this.hostInfo.getIpAddress();
	this.hostname = this.hostInfo.getHostname();
}

If your app wants to be contacted over HTTPS, you can set two flags in the EurekaInstanceConfig:
eureka.instance.[nonSecurePortEnabled]=[false]
eureka.instance.[securePortEnabled]=[true]
Doing so makes Eureka publish instance information that shows an explicit preference for secure communication. The Spring Cloud DiscoveryClient always returns a URI starting with https for a service configured this way. Similarly, when a service is configured this way, the Eureka (native) instance information has a secure health check URL.

在官方帮助手册中,存在如上描述,介绍如何开启Https传输。当使用https协议时,Spring会通过DefaultManagementMetadataProvider.get方法,重置healthCheckUrl以及statusPageUrl属性。

private String getUrl(EurekaInstanceConfigBean instance, int serverPort,
			String serverContextPath, String managementContextPath,
			Integer managementPort, String urlPath, boolean isSecure) {
	managementContextPath = refineManagementContextPath(serverContextPath, managementContextPath, managementPort);
	if (managementPort == null) {
		managementPort = serverPort;
	}
	String scheme = isSecure ? "https" : "http";
	return constructValidUrl(scheme, instance.getHostname(), managementPort, managementContextPath, urlPath);
}

二、DiscoveryClient创建。

在EurekaClientAutoConfiguration配置类中,存在RefreshableEurekaClientConfiguration内部类,在该内部类中,完成了EurekaClient Bean的创建。针对于CloudEurekaClient对象的构造方法,使用的4个入参,appManager, config这前两个入参,在EurekaClientAutoConfiguration配置类中,能够找到创建方法。而 context属性则为Spring上下文。其中只有optionalArgs这一个参数(对应于AbstractDiscoveryClientOptionalArgs),比较特殊,它用于控制基于何种方式,进行请求的发送与处理。这个属性的来源,在"三、服务注册"环节介绍。

@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager,
		EurekaClientConfig config, EurekaInstanceConfig instance,
		@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
	// If we use the proxy of the ApplicationInfoManager we could run into a problem
	// when shutdown is called on the CloudEurekaClient where the
	// ApplicationInfoManager bean is
	// requested but wont be allowed because we are shutting down. To avoid this
	// we use the object directly.
	ApplicationInfoManager appManager;
	if (AopUtils.isAopProxy(manager)) {
		appManager = ProxyUtils.getTargetObject(manager);
	}
	else {
		appManager = manager;
	}
	CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
	cloudEurekaClient.registerHealthCheck(healthCheckHandler);
	return cloudEurekaClient;
}

通过类图,来查看CloudEurekaClient和EurekaClient之间的关系。
在这里插入图片描述
在CloudEurekaClient构造方法处,先调用父类的构造方法,然后收集父类的eurekaTransport属性,通过eurekaTransport属性,可以获取其内部的EurekaHttpClient属性,用于后续的getInstanceInfo方法,关于DiscoveryClient的这两个属性,后续会有介绍。

public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
			EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args,
			ApplicationEventPublisher publisher) {
	super(applicationInfoManager, config, args);
	this.applicationInfoManager = applicationInfoManager;
	this.publisher = publisher;
	this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class, "eurekaTransport");
	ReflectionUtils.makeAccessible(this.eurekaTransportField);
}

紧接着,Spring覆写了onCacheRefreshed方法,在该方法中,基于Spring的事件发布机制,增加了HeartbeatEvent事件的发布操作。

protected void onCacheRefreshed() {
	super.onCacheRefreshed();

	if (this.cacheRefreshedCount != null) { // might be called during construction and
		// will be null
		long newCount = this.cacheRefreshedCount.incrementAndGet();
		log.trace("onCacheRefreshed called with count: " + newCount);
		this.publisher.publishEvent(new HeartbeatEvent(this, newCount));
	}
}

在EurekaClient体系中,最为核心的是DiscoveryEurekaClient对象。在该对象中,完成了EurekaHttpClient对象的创建(服务注册操作的基础)等操作。现在,我们主要介绍一下EurekaHttpClient对象的创建。
EurekaHttpClient作为EurekaTransport对象的一个属性存在,对应于registrationClient。

private static final class EurekaTransport {
    private ClosableResolver bootstrapResolver;
    private TransportClientFactory transportClientFactory;

    private EurekaHttpClient registrationClient;
    private EurekaHttpClientFactory registrationClientFactory;

    private EurekaHttpClient queryClient;
    private EurekaHttpClientFactory queryClientFactory;
    ……
}    

EurekaTransport对象创建以及属性完善,通过DiscoveryEurekaClient构造方法处的如下两行代码完成。

eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);

其中截取与registrationClient属性有关的如下代码。

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

在通过EurekaHttpClients.registrationClientFactory方法,跟踪到如下处理代码,发现首次创建的EurekaHttpClient对象,为SessionedEurekaHttpClient类型。真正用于完成服务注册的EurekaHttpClient对象,后续介绍。

static EurekaHttpClientFactory canonicalClientFactory(final String name,
                                                          final EurekaTransportConfig transportConfig,
                                                          final ClusterResolver<EurekaEndpoint> clusterResolver,
                                                          final TransportClientFactory transportClientFactory) {

        return new EurekaHttpClientFactory() {
            @Override
            public EurekaHttpClient newClient() {
                return new SessionedEurekaHttpClient(
                        name,
                        RetryableEurekaHttpClient.createFactory(
                                name,
                                transportConfig,
                                clusterResolver,
                                RedirectingEurekaHttpClient.createFactory(transportClientFactory),
                                ServerStatusEvaluators.legacyEvaluator()),
                        transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
                );
            }

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

三、服务注册。

在DiscoveryEurekaClient中,通过register方法,完成服务注册操作。内部对应于EurekaHttpClient的register方法。

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();
}

通过上述分析,我们找到了SessionedEurekaHttpClient这个EurekaHttpClient类。在EurekaHttpClient接口外,还存在EurekaHttpClientDecorator抽象类,在该抽象类中,针对于register方法进行了封装。后续操作,暴露了execute抽象方法。后续介绍的几个EurekaHttpClient实现类,都是通过实现execute方法,完成其注册逻辑。

@Override
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
	return execute(new RequestExecutor<Void>() {
		@Override
		public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
	        return delegate.register(info);
        }

        @Override
        public RequestType getRequestType() {
	        return RequestType.Register;
        }
	});
}

针对于SessionedEurekaHttpClient类,其execute方法如下。可以发现,第一次进来的时候,由于eurekaHttpClientRef没有EurekaHttpClient对象,因此,真正的EurekaHttpClient对象,是通过clientFactory.newClient()方法,进行创建。

@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    long now = System.currentTimeMillis();
    long delay = now - lastReconnectTimeStamp;
    if (delay >= currentSessionDurationMs) {
		logger.debug("Ending a session and starting anew");
	    lastReconnectTimeStamp = now;
        currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
        TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
    }

	EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
    if (eurekaHttpClient == null) {
        eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
	}
    return requestExecutor.execute(eurekaHttpClient);
}

在整套EurekaHttpClient体系中,其内部采用类似于责任链的设计模式,完成最终EurekaHttpClient对象的创建操作。即SessionedEurekaHttpClient,通过RetryableEurekaHttpClientFactory创建RetryableEurekaHttpClient,RetryableEurekaHttpClient又是通过RedirectingEurekaHttpClientFactory完成。刚开始查看代码的时候,比较绕,容易弄晕。多看几次,了解其内部处理运转逻辑,会好一点。整个体系的数据流转,在EurekaHttpClients.registrationClientFactory方法,完成指定。

static EurekaHttpClientFactory canonicalClientFactory(final String name,
                                                          final EurekaTransportConfig transportConfig,
                                                          final ClusterResolver<EurekaEndpoint> clusterResolver,
                                                          final TransportClientFactory transportClientFactory) {

        return new EurekaHttpClientFactory() {
            @Override
            public EurekaHttpClient newClient() {
                return new SessionedEurekaHttpClient(
                        name,
                        RetryableEurekaHttpClient.createFactory(
                                name,
                                transportConfig,
                                clusterResolver,
                                RedirectingEurekaHttpClient.createFactory(transportClientFactory),
                                ServerStatusEvaluators.legacyEvaluator()),
                        transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
                );
            }

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

通过上述分析,可以发现,真正的注册操作所需的EurekaHttpClient,通过RedirectingEurekaHttpClient.createFactory(transportClientFactory)创建的factory中的transportClientFactory完成。那么这个属性又是怎么来的呢?
通过一步步倒退,发现该属性顶层来源于DiscoveryEurekaClient构造方法处。

TransportClientFactories argsTransportClientFactories = null;
if (args != null && args.getTransportClientFactories() != null) {
	argsTransportClientFactories = args.getTransportClientFactories();
}
        
// Ignore the raw types warnings since the client filter interface changed between jersey 1/2
@SuppressWarnings("rawtypes")
TransportClientFactories transportClientFactories = argsTransportClientFactories == null
	? new Jersey1TransportClientFactories()
	: argsTransportClientFactories;

而其中的args,对应于构造方法处AbstractDiscoveryClientOptionalArgs类型的入参。现在来回答一下"二、DiscoveryClient创建"提到的AbstractDiscoveryClientOptionalArgs来源问题。

@Configuration
public class DiscoveryClientOptionalArgsConfiguration {

	@Bean
	@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
	@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
	public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() {
		return new RestTemplateDiscoveryClientOptionalArgs();
	}

	@Bean
	@ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter")
	@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
	public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
		return new MutableDiscoveryClientOptionalArgs();
	}
}

通过DiscoveryClientOptionalArgsConfiguration配置类,发现,classpath下存在ClientFilter类时,使用MutableDiscoveryClientOptionalArgs,否则使用RestTemplateDiscoveryClientOptionalArgs。
现,我们通过RestTemplateDiscoveryClientOptionalArgs完成注册请求发送逻辑分析。

public class RestTemplateDiscoveryClientOptionalArgs
		extends AbstractDiscoveryClientOptionalArgs<Void> {

	public RestTemplateDiscoveryClientOptionalArgs() {
		setTransportClientFactories(new RestTemplateTransportClientFactories());
	}

}

在RestTemplateTransportClientFactories存在如下,代码块。顿时明白,Spring引入了RestTemplateTransportClientFactory以及RestTemplateEurekaHttpClient完成注册请求的发送操作。

@Override
public TransportClientFactory newTransportClientFactory(
	EurekaClientConfig clientConfig, Collection<Void> additionalFilters,
	InstanceInfo myInstanceInfo) {
	return new RestTemplateTransportClientFactory();
}

在RestTemplateEurekaHttpClient的register方法,通过RestTemplate发送注册请求,从而完成服务注册操作。

@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
	String urlPath = serviceUrl + "apps/" + info.getAppName();

	HttpHeaders headers = new HttpHeaders();
	headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip");
	headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

	ResponseEntity<Void> response = restTemplate.exchange(urlPath, HttpMethod.POST,
			new HttpEntity<>(info, headers), Void.class);

	return anEurekaHttpResponse(response.getStatusCodeValue())
			.headers(headersOf(response)).build();
}

到此,已完成Eureka注册分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要做个有钱人2020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值