SpringCloud服务治理——Eureka

Eureka——Someone might say “eureka” when they suddenly find or realize something, or when they solve a problem.

准备

需要准备三个SpringBoot服务:eureka-server服务注册中心,hello-service服务提供者,service-consumer服务消费者。

eureka-server

pom.xml:

<project >
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springcloudaction</groupId>
    <artifactId>eureka-server</artifactId>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

EurekaServerApplication:

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
	psvm(String[] args){ SpringApplication.run(EurekaServerApplication.class, args); }
}

application.properties:

spring.application.name=eureka-server
eureka.client.fetch-registery=false

application-peer1.properties:

server.port=1111
eureka.client.service-url.defaultZone=http://127.0.0.1:1112/eureka

application-peer2.properties:

server.port=1112
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka
hello-service

pom.xml:

<project >
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springcloudaction</groupId>
    <artifactId>hello-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

HelloServiceApplication.java:

@SpringBootApplication
@EnableDiscoveryClient
// 如果不加@EnableDiscoveryClient注解,该服务仍能在注册中心注册成功,管理页面的Instances currently registered with Eureka中仍有该服务,但其他消费者调用该服务会失败
// 不加@EnableDiscoveryClient注解,client.getInstances("hello-service")得到的为空结果
public class HelloServiceApplication {
	psvm(String[] args) { SpringApplication.run(HelloServiceApplication.class, args); }
}

HelloController.java:

@RestController
@Slf4j
public class HelloController {
	@Autowired
	private DiscoveryClient client;
	@RequestMapping("/hello")
	public String hello(){
		List<String> services = client.getServices();
		List<ServiceInstance> serviceInstances = client.getInstances("hello-service");
		String description = client.description();
		return "HelloWorld!";
	}
}

application.properties

server.port=8080
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka,http://127.0.0.1:1112/eureka

application-peer1.properties

server.port=8081
service-consumer

pom.xml

<project >
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springcloudaction</groupId>
    <artifactId>service-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

ServiceConsumerApplication.java

@SpringBootApplication
// @EnableDiscoveryClient
// 只在pom中添加了spring-cloud-netflix-eureka-client的依赖即可向注册中心注册,消费其他服务
// 如果想要作为服务提供方,需要加上@EnableDiscoveryClient注解
public class ServiceConsumerApplication {
	psvm(String[] args) { SpringApplication.run(ServiceConsumerApplication.class, args); }
	@Bean
	@LoadBalanced
	// 如果不加@LoadBalanced注解,将会报unknownhost异常,无法识别hello-service
	RestTemplate restTemplate() { return new RestTemplate(); }
}

ConsumerController.java

@RestController
public class ConsumerController {
	@Autowired
	private RestTemplate restTemplate;
	@RequestMapping("/consumer")
	public String helloConsumer() {
		return restTemplate.getForEntity("http://hello-service/hello", String.class).getBody();
	}
}

application.properties

server.port=9000
spring.application.name=service-consumer
eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka

运行

  1. 部署eureka-server,执行命令java -jar eureka-server.jar
    通过添加参数--server.port=1111或1112,分别部署到端口1111和1112上,也可以添加参数--spring.profiles.active=peer1或peer2指定不同的配置文件,来指定不同的端口。
  2. 部署hello-service
  3. 部署service-consumer
  4. 打开localhost:1111或localhost:1112,可以看到Instances currently registered with Eureka下出现了三个服务:
    • eureka-server,因为没有加eureka.client.register-with-eureka=false,默认为true,所以服务中心也会相互注册,实现高可用
    • hello-service,在配置文件里加了eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka,http:127.0.0.1:1112/eurekahello-service被注册到了两个注册中心
    • service-consumer,配置文件中只有eureka.client.service-url.defaultZone=http://127.0.0.1:1111/eureka,除1111端口之外,1112端口也有service-consumer服务,即服务同步

在这里插入图片描述

架构

Eureka的服务治理体系中有三个主要角色:服务注册中心,服务提供者,服务消费者。
在这里插入图片描述
服务注册中心之间:服务同步
服务提供者与服务注册中心:服务注册、服务续约、服务下线
服务消费者与服务注册中心:获取服务清单
服务消费者与服务提供者:服务调用

解析

加载服务注册中心

服务需要添加@EnableDiscoveryClient注解,标明是一个eureka的客户端,代码如下:

package org.springframework.cloud.client.discovery;
/** Annotation to enable a DiscoveryClient implementation. */
public @interface EnableDiscoveryClient {
	boolean autoRegister() default true;
}

注释里说该注解用来开启DiscoveryClient的实例,那么org.springframework.cloud.client.discovery.DiscoveryClient接口代码如下:

package org.springframework.cloud.client.discovery;
/** Represents read operations commonly available to discovery services such as Netflix Eureka or consul.io. */
public interface DiscoveryClient extends Ordered {
	// A human-readable desceription of the implementation, used in HealthIndicator.
	String description();
	// Gets all ServiceInstances associated with a particular serviceId.
	List<ServiceInstance> getInstances(String serviceId);
	// return all known service IDs.
	List<String> getServices();  
	// Default implementation for getting order of discovery clients.
	@Override
	default int getOrder() {return DEFAULT_ORDER; }
	int DEFAULT_ORDER = 0;
}

DiscoveryClient接口有四个实现类:EurekaDiscoveryClientCompositeDiscoveryClientNoopDiscoveryClientSimpleDiscoveryClient

package org.springframework.cloud.client.discovery.composite;
/** A DiscoveryClient that is composed of other discovery clients and delegates calls to each of them in order. */
public class CompositeDiscoveryClient implements DiscoveryClient {
	private final List<DiscoveryClient> dcs;
	String description() { return "Composite Discovery Client"; }
	List<ServiceInstance> getInstances(String serviceId) {
		for (DiscoveryClient dc: this.dcs) {
			List<ServiceInstances> instances = dc.getInstances(serviceId);
			if (instances != null && !instances.isEmpty()) { return instances; }
		}
		return Collections.emptyList();
	}
	
	List<String> getServices() {
		Set<String> services = new LinkedHashSet<>();
		for (DiscoveryClient dc: this.dcs) {
			List<String> s = dc.getServices();
			if (s != null) { services.addAll(s); }
		}
		return new ArrayList<>(services);
	}
}
package org.springframework.cloud.netflix.eureka;
public class EurekaDiscoveryClient implements DiscoveryClient {
	psfs DESCRIPTION = "Spring Cloud Eureka Discovery Client";
	final EurekaClient eurekaClient;
	final EurekaClientConfig clientConfig;

	List<ServiceInstance> getInstances(String serviceId) {
		List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId, false);
		List<ServiceInstance> instances = new ArrayList<>();
		for (InstanceInfo info : infos) { instances.add(new EurekaServiceInstance(info)); }
		return instances;
	}

	List<String> getServices() {
		Applications applications = this.eurekaClient.getApplications();
		List<Application> registered = applications.getRegisteredApplications();
		List<String> names = new ArrayList<>();
		for (Application app : registered) {
			if (app.getInstances().isEmpty()) { continue; }
			names.add(app.getName().toLowerCase());
		}
		return names;
	}
}

DiscoveryClientgetInstances(serviceId)返回对应serviceId的ServiceInstance列表,ServiceInstance接口代码如下:

package org.springframework.cloud.client;
/** Represents an instance of a service in a discovery system. */
public interface ServiceInstance {
	// 获取instanceId,serviceId,host,port,是否https,uri,元数据metadata等
}

EurekaDiscoveryClient持有一个EurekaClient的实例,EurekaClient位于com.netflix.discovery包下,不是springframework.cloud的一部分,代码如下:

package com.netflix.discovery;
@ImplementedBy(DiscoveryClient.class)  // 这个DiscoveryClient是com.netflix.discovery下的,不是SpringCloud的
public interface EurekaClient extends LookupService {
	// EurekaClient定义了几类方法
	// 一、用于获取InstanceInfo,以getApplications开头或getInstances开头
	// 二、用于获取元数据,如获取region,实例的status,serviceUrls
	// 三、与健康检查相关,注册HealthCheckHandler、EurekaEventListener等
	// 四、其他方法,shutdown,获取EurekaClientConfig
	// 第二类中获取serviceUrl的几个方法标记了@Deprecated,替代类是com.netflix.discovery.endpoint.EndpointUtils
}

EurekaClient有一个实现类com.netflix.discovery.DiscoveryCilent,该实现类的类注释:

/**
 * The class that is instrumental for interactions with <tt>Eureka Server</tt>.
 * Eureka Client is responsible for 
 * a) Registering the instance with Eureka Server 
 * b) Renewal of the lease with Eureka Server
 * c) Cancellation of the lease from Eureka Server during shutdown
 * d) Querying the list of services/instances registered with Eureka Server
 * 
 * Eureka Client needs a configured list of Eureka Server java.net.URLs to talk to.These java.net.URLs are 
 * typically amazon elastic eips which do not change. All of the functions defined above fail-over to other
 * java.net.URLs specified in the list in the case of failure.
 * 
*/

EurekaClient负责1.向EurekaServer注册,2.向EurekaServer续约,3.服务关闭期间取消租约,4.查询注册到EurekaServer的服务/实例列表。最后,EurekaClient需要配置EurekaServer的url列表。
我们的serviceUrl是在properties配置文件中配置的,据此找到EndpointUtils里的getServiceUrlsMapFromConfig方法:

/** 从properties文件获取eureka service urls列表,用于eurekaclient talk to
*/
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
	String region = getRegion(clientConfig); // 获取clientConfig.getRegion(),默认为DEFAULT_REGION="default"
	String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); // 从EurekaClientConfigBean里的availabilityZones这个map里获取region对应的zone,用逗号分隔,默认为defaultZone
	int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); // 获取instanceZone在availZones中的索引
	String zone = availZones[myZoneOffset]; // 由于preferSameZone选项,instanceZone不一定等于availZones[myZoneOffset]
	List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); // 从zone中获取serviceUrl列表
	List<String> orderedUrls;
	orderedUrls.addAll(serviceUrls);
	return orderedUrls;
}

getServiceUrlsFromConfig做了三件事:加载region,加载zone,加载serviceUrl。

EurekaClientConfig有两个实现类:Netflix的DefaultEurekaClientConfig和Spring的EurekaClientConfigBean
查看EurekaClientConfigBean,Zone来自于Map:availabilityZones根据Region得到的数组,即一个region对应一个zone[],serviceUrls来自于Map:serviceUrl根据选择的Zone得到的url数组。serviceUrl默认存储了defaultZone和defaultUrl。

psf String DEFAULT_ZONE = "defaultZone";
psf String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX + "/";
psf String DEFAULT_PREFIX = "/eureka";
{
	this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
@Override
public String[] getAvailabilityZones(String region) {
	String value = this.availabilityZones.get(region);
	value = value == null ? DEFAULT_ZONE : value;
	return value.split(",");
}
@Override
public List<String> getEurekaServerServiceUrls(String myZone) {
	String serviceUrls = this.serviceUrl.get(myZone);
	serviceUrls = serviceUrls == null ? this.serviceUrl.get(DEFAULT_ZONE) : serviceUrls;
	String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
	return Arrays.asList(serviceUrlsSplit);
}
服务注册与续约

EurekaClient的实现类com.netflix.discovery.DiscoveryClient中,有两个方法:register() 和 renew():

/** Register with eureka service by making the appropriate REST call. */
boolean register() throws Throwable {
	EurekaHttpResponse<Void> httpResponse;
	try {
		httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
	} catch (Exp){}
	return httpResponse.getStatusCode() == Status.NO_CONTENT; // NO_CONTENT=204
}
/** Renew with eureka service by making the appropriate REST call. */
boolean renew() {
	EurekaHttpResponse<InstanceInfo> httpResponse;
	try {
		httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
		if (httpResponse.getStatusCode == Status.NOT_FOUND) { // NOT_FOUND=404
			REREGISTER_COUNTER.increment();
			long timestamp = instanceInfo.setIsDirtyWithTime();
			boolean success = register();
			if (success) { instanceInfo.unsetIsDirty(timestamp); }
			return success;
		}
		return httpResponse.getStatusCode() == Status.OK; // OK=200
	} catch (Thr) { return false; }
}

register()方法中的httpResponse = eurekaTransport.registrationClient.register(instanceInfo);负责实施注册行为。
renew()方法中的httpResponse = eurekaTransport.registrationClient.sendHeartBeat(appName, id, instanceInfo, null),负责实施续约行为。
registrationClient是EurekaHttpClient的实例,EurekaHttpClient的继承结构如下:

EurekaHttpClient
	EurekeHttpClientDecorator
		MetricsCollectiongEurekaHttpClient
		SessionedEurekaHttpClient
		RedirectingEurekaHttpClient
		RetryableEurekaHttpClient
	AbstractJerseyEurekaHttpClient
		JerseyApplicationClient

只有AbstractJerseyEurekaHttpClient真正实现了EurekaHttpClient中定义的register(InstanceInfo)sendHeartBeat(appName, id, info, overriddenStatus)方法,其他类的register()sendHeartBeat()方法采用了委托的方式。由AbstractJerseyEurekaHttpClient的实现可知,register操作通过发送post请求的方式,向serviceUrl发送自身的InstanceInfo数据,sendHeartBeat通过发送put请求的方式,向serviceUrl发送心跳包。

/** AbstractJerseyEurekaHttpClient、JerseyApplicationClient
*/
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
	String urlPath = "apps/" + info.getAppName(); // urlPath为apps/SERVICE-CONSUMER
	ClientResponse response = null;
	try {
		Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
		// serviceUrl= http://127.0.0.1/eureka/ 即在eureka.client.service-url.defaultZone里配置的serviceUrl地址
		addExtraHeaders(resourceBuilder);
		response = resourceBuilder.header(xx).type(xx).accept(x)
				.post(ClientResponse.class, info);
		return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
	} finally {
		if (response != null) { response.close(); }
	}
}
@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
	String urlPath = "apps/" + appName + "/" + id; // appName为SERVICE-CONSUMER, id为172.11.22.100:service-consumer:9000
	ClientResponse response = null;
	try {
		// serviceUrl为http://127.0.0.1/eureka/
		WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath) 
				.queryParam("status", info.getStatus().toString())
				.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
		Builder requestBuilder = webResource.getRequestBuilder();
		addExtraHeaders(requestBuilder);
		response = requestBuilder.put(ClientResponse.class);
		EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
		if (response.hasEntity()) {
			eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
		}
		return eurekaResponseBuilder.build();
	} finally {
		if (response != null) { response.close(); }
	}
}
注册过程

查找调用DiscoveryClientregister()方法的地方,有三处:

  1. DiscoveryClient的构造函数里,if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) { register(); },即初始化时注册
  2. renew()里404时的注册
  3. InstanceInfoReplicatorrun()方法里的注册,该类实现了Runnable接口。
// com.netflix.discovery.InstanceInfoReplicator
public void run() {
	try {
		discoveryClient.refreshInstanceInfo();
		Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
		if (dirtyTimestamp != null) {
			discovery.register();
			instance.unsetIsDirty(dirtyTimestamp);
		}
	}
}
public void start(int initialDelayMs) {
	if (started.compareAndSet(false, true)) {
		instanceInfo.setIsDirty();
		Future next = scheduler.schedule(this, initialDelayMs, SECONDS);
		scheduledPeriodicRef.set(next);
	}
}

DiscoveryClient持有InstanceInfoReplicator的实例,但是没有通过线程池去调度它,而是在initScheduledTasks()方法里调用InstanceInfoReplicatorstart()方法去调度,InstanceInfoReplicator持有一个ScheduledExecutorService实例,调度自身scheduler.schedule(this, initialDelayMs, SECONDS)
initScheduledTasks()DiscoveryClient的构造函数中被调用,查看DiscoveryClientinitScheduledTasks()

private void initScheduledTasks() {
	if (clientConfig.shouldFetchRegistry()) {
		// 服务获取
		scheduler.schedule(new TimedSupervisorTask(..., new CacheRefreshThread()), registryFetchIntervalSeconds, TimeUnit.SECONDS);
	}
	if (clientConfig.shouldRegisterWithEureka()) {
		// 服务续约
		scheduler.schedule(new TimedSupervisorTask(..., new HeartbeatThread()), renewalIntervalInSecs, TimeUnit.SECONDS);
		// 服务注册
		instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
	}
}
// 定时服务续约任务
private class HeartbeatThread implements Runnable {
	public void run() { if (renew()) { lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); }}
}
// 定时服务获取任务
class CacheRefreshThread implements Runnable {
	public void run() { refreshRegistry(); }
}

可以看到,DiscoveryClient在构造函数中,根据配置项register-with-eureka,判断是否需要注册。
续约的处理流程与注册类似。

服务注册中心处理注册请求

服务注册的请求在com.netflix.eureka.resources.ApplicationResource中进行处理,

@POST
@Consume({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
	// 校验instanceInfo参数
	// 处理info.getDataCenterInfo()的DataCenterInfo的一些特殊情况
	// 服务注册
	registry.register(info, "true".equals(isReplication)); // registry:PeerAwareInstanceRegistry
	return Response.status(204).build();
}

会调用InstanceRegistryregister()方法进行注册

// org.springframework.cloud.netflix.eureka.server.InstanceRegistry
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
	handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
	super.register(info, isReplication);
}
private void handleRegistration(InstanceInfo info, int leaseDuration, boolean isReplication) {
	publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
}
private void publishEvent(ApplicationEvent applicationEvent) {
	this.ctxt.publishEvent(applicationEvent); // ctxt:AbstractApplicationContext,调用其publishEvent方法,将服务注册事件广播
}

InstanceRegistry的继承关系:

InstanceRegistry	// com.netflix.eureka.registry
	PeerAwareInstanceRegistry	// com.netflix.eureka.registry
		PeerAwareInstanceRegistryImpl	// com.netflix.eureka.registry
			InstanceRegistry	// org.springframework.cloud.netflix.eureka.server
			AwsInstanceRegistry		// com.netflix.eureka.registry
	AbstractInstanceRegistry	// com.netflix.eureka.registry
		PeerAwareInstanceRegistryImpl	
			InstanceRegistry
			AwsInstanceRegistry

InstanceRegistrysuper.register()最终调用到AbstractInstanceRegistryregister()方法。

AbstractInstanceRegistry中有一个ConcurrentHashMapprivate final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry,可以看到这是一个双层Map,调用该Map的put方法的地方只有一处,即register()方法:

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
	try {
		read.lock();
		Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName()); // gMap是根据appName从双层map——registry获取的内层map,即外层map的key是服务名
		REGISTER.increment(isReplication);
		if (gMap == null) { // 如果没有appName对应的Map,则为其关联一个new Map
			final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<>();
			gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
			if (gMap == null) { gMap = gNewMap; }
		}

		Lease<InstanceInfo> existingLease = gMap.get(registrant.getId()); // 根据id从内层map获取Lease,即内层map的key是id
		// Lease翻译为租约,里面有几个时间戳和一个泛型对象holder,一个duration(默认90秒)
		if (existingLease != null && (existingLease.getHolder() != null)) { // getHolder()返回id对应InstanceInfo对象
			Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
			Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
			// 比较已有租约的时间戳和传入的时间戳
			if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
				registrant = existingLease.getHolder(); // 已有的租约的时间戳大于传入的,则仍然使用已有的
			}
		} else {
			// 不存在已有租约,是一个新注册
			synchronized (lock) {
				if (this.expectedNumberOfClientsSendingRenews > 0) {
					this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
					updateRenewsPerMinThreshold();
				}
			}
		}
		Lease<InstanceInfo> lease = new Lease<>(registrant, leaseDuration);
		if (existingLease != null) {
			// serviceUpTimestamp仍沿用已有租约的时间戳
			lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
		}
		gMap.put(registrant.getId(), lease); // 内层map使用id作为key,新的租约作为value
		synchronized (recentRegisteredQueue) {
			recentRegisteredQueue.add(new Pair<Long, String>(System.currentTimeMillis(), registrant.getAppName() + "(" + registrant.getId() + ")"));
		}
		// 以下是对overridden status的处理,如果overriddenInstanceStatusMap的key中不存在传入id,则保存传入的id和overriddenStatus,用已有的overriddenStatus更新传入的registrant。相当于如果有已有值,则用已有值更新传入的对象,类似的处理方式很多。
		if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
			if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
				overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
			}
		}
		InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
		if (overriddenStatusFromMap != null) { registrant.setOverriddenStatus(overriddenStatusFromMap); }
		
		InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
		registrant.setStatusWithoutDirty(overriddenInstanceStatus);
		
		if (InstanceStatus.UP.equals(registrant.getStatus())) { 
			// 传入的状态是UP,则更新serviceUpTimestamp,结合前处可知,如果是其他状态,则沿用已有租约的serviceUpTimestamp
			lease.serviceUp(); // serviceUp()里更新了Lease的serviceUpTimestamp
		}
		registrant.setActionType(ActionType.ADDED);
		recentlyChangeQueue.add(new RecentlyChangedItem(lease));
		registrant.setLastUpdatedTimestamp();
		invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
	} finally {
		read.unlock();
	}
}

在这个实验中,registry存储的数据为:

{
    "EUREKA-SERVER":{
        "172.22.33.44:eureka-server:1111":Lease<InstanceInfo1>,
        "172.22.33.44:eureka-server:1112":Lease<InstanceInfo2>
    },
    "SERVICE-CONSUMER":{
        "172.22.33.44:service-consumer:9000":Lease<InstanceInfo3>
    }
}

配置进阶

eureka-clienteureka-core中,可以找到几个与配置相关的接口:

  • com.netflix.discovery.EurekaClientConfig,EurekaClient注册实例到EurekaServer所需的配置信息
  • com.netflix.eureka.EurekaServerConfig,EurekaServer所需的配置信息
  • com.netflix.appinfo.EurekaInstanceConfig,实例instance注册到EurekaServer所需配置信息
  • com.netflix.discovery.shared.transport.EurekaTransportConfig,管理传输层相关配置

在搭建了高可用的注册中心之后,注册中心、服务提供者、消费者、配置中心、网关等服务都可以看做是Eureka服务治理体系中的一个微服务,所以重点关注Eureka客户端的配置,即EurekaClientConfig。

EurekaClientConfig——服务注册相关配置

查看实现类EurekaClientConfigBean,配置项的前缀PREFIX="eureka.client"
将服务注册,需要指定注册中心地址:

# 单节点注册中心
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
# 双节点高可用注册中心
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/,http://localhost:1112/eureka/ 
# 带安全校验的注册中心,加入用户名/密码
eureka.client.service-url.defaultZone=http://<username>:<password>@localhost:1111/eureka/

其他参数:

参数名默认值说明
enabledtrue是否启用eureka客户端
registry-fetch-interval-seconds30获取注册信息的间隔时间
instance-info-replication-interval-seconds30反馈实例信息到eureka-server的间隔时间
initial-instance-info-replication-interval-seconds40初始化实例信息到eureka-server的间隔时间
eureka-service-url-poll-interval-seconds300轮询eureka-server更改的间隔时间
eureka-server-read-timeout-seconds8读取eureka-server的超时时间
eureka-server-connect-timeout-seconds5连接eureka-server的超时时间
eureka-server-total-connections200从eureka-client到所有eureka-server的最大连接数
eureka-server-total-connections-per-host50从eureka-client到一个eureka-server的最大连接数
eureka-connection-idle-timeout-seconds30eureka-server连接保持空闲的时间,直到关闭
heartbeat-executor-thread-pool-size2心跳线程池的初始化大小
heartbeat-executor-exponential-back-off-bound10心跳超时重试时,延迟时间的最大乘数值(multiplier)
cache-refresh-executor-thread-pool-size2缓存刷新线程池的初始化大小
cache-refresh-executor-exponential-back-off-bound10缓存刷新重试时,延迟时间的最大乘数值(multiplier)
use-dns-for-fetching-service-urlsfalseeureka-client是否使用dns来获取eureka-server列表
register-with-eurekatrue是否注册到eureka-server,从而被其他服务发现
prefer-same-zone-eurekatrue是否优先使用相同zone中的eureka-server
filter-only-up-instancestrue只保留UP状态的实例
fetch-registrytrue是否从eureka-server获取注册信息

backup-registry-impl
proxy-host
proxy-port
proxy-user-name
proxy-password
g-zip-content
eureka-server-url-context
eureka-server-port
eureka-server-dns-name
unregister-on-shutdown
allow-redirects
log-delta-diff
disable-delta
fetch-registry-for-remote-regions
region
fetch-remote-regions-registry
availability-zones
eureka-server-service-urls
registry-refresh-single-vip-address
dollar-replacement
escape-char-replacement
on-demand-update-status-change
enforce-registration-at-init
encoder-name
decoder-name
client-data-accept
experimental
transport-config

EurekaInstanceConfig——服务实例相关配置

查看EurekaInstanceConfig的实现类EurekaInstanceConfigBean,配置项的前缀eureka.server
参数列表:

instance-id
appname
app-group-name
instance-enable-onit
non-secure-port
secure-port
non-secure-port-enabled
secure-port-enabled
lease-renewal-interval-in-seconds
lease-expiration-duration-in-seconds
virtual-host-name
secure-virtual-host-name
a-s-g-name
host-name
metadata-map
data-center-info
ip-address
status-page-url-path
status-page-url
home-page-url-path
home-page-url
health-check-url-path
health-check-url
secure-health-check-url
default-address-resolution-order
namespace

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值