SpringCloud源码之注册中心Eureka

一、基本概念:

1、注册中心的基本原理如下图所示:

从图中可以看到,注册中心涉及到三端,其中Provider和Consumer端通常在一起,也即一个服务既是服务提供者,也会请求其他服务调用:

a、Registry端:也即Server端。Provider启动时自动注册到Registry,Provider关闭时从Registry中移除,Provider定期向Registry发送心跳,超过一定时间没收到Provider的心跳信号时就将该Provider移除。每当Registry发生新增或者移除的改变时都要通知所有的Consumer。

b、Provider端:启动时,向 Registry 注册自己为一个服务(Service)的实例(Instance)。关闭时,向 Registry 取消注册。定期向 Registry 发送心跳,告诉自己还存活。

c、Consumer端:启动时,向 Registry 订阅使用到的服务,并缓存服务的实例列表在内存中。关闭时,向 Registry 取消订阅。Consumer 向对应服务的 Provider 发起调用时,从内存中的该服务的实例列表选择一个,进行远程调用。

2、注册中心必须具备的功能:

a、Register(注册):服务提供方使用Provider注册自己到 Registry上,添加到注册表,成为服务的一个实例。

b、Renew(续租):服务提供方使用Provider 每 30 秒向 Registry 发起一次心跳,告诉 Registry当前服务实例还存活。如果 Registry 90 秒没有收到 Provider 的心跳,会认为它已经下线,将该服务实例从注册表移除。

c、Get Registry(获取注册信息):服务消费者使用 Consumer 从 Registry获取全量注册表,并缓存在本地内存。之后,服务消费者要远程调用服务提供者时,只需要从本地缓存的注册表查找对应的服务即可。考虑到 Registry的注册表是不断变化的,服务消费者每 30 秒从 Registry获取增量变化的部分,合并更新到本地缓存的注册表。

d、Cancel(下线):服务提供者准备下线时,向 Registry发起取消注册请求,从而从注册表中移除,避免后续服务消费者请求当前实例。

二、Demo:

1、registry端:

a、引入依赖:

xml 复制代码
<dependencies>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	</dependency>
</dependencies>

b、application.yml配置:

yaml 复制代码
server
	port: 8761 # 设置 Eureka-Server 的端口
spring:
	application:
		name: eureka-server
eureka:
	client:
		register-with-eureka: false # 不注册到 Eureka-Server,默认为 true
		fetch-registry: false # 不从 Eureka-Server 获取注册表,默认为 true

c、启动类添加@EnableEurekaServer注解:

less 复制代码
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
	public static void main(string[] args) {
		SpringApplication.run(EurekaServerApplication.class,args);
	}
}

2、Provider端:

a、引入依赖:

xml 复制代码
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

b、application.yml配置:

yaml 复制代码
spring:
	application:
		name: demo-provider # Spring 应用名
server:
	port: 18080 # 服务器端口。默认为 88
eureka:
	client:
		register-with-eureka: true # 注册到 Eureka-Server,默认为 true
		fetch-registry: true # 从 Eureka-Server 获取注册表,默认为 true
		service-url:
			defaultZone: http://127.0.0.1:8761/eureka/ # Eureka-Server 地址

3、Consumer端:

引入依赖:

xml 复制代码
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml配置:

yaml 复制代码
spring:
	application:
		name: demo-consumer # Spring 应用名
server:
	port:28989 # 服务器端口。默认为 8080
eureka:
	client:
		register-with-eureka:true
		fetch-registry: true
		service-url:
			defaultZone: http://127.0..1:8761/eurekal

向Provider请求服务代码:

typescript 复制代码
@RestController
static class TestController {
	@Autowired
	private Discoveryclient discoveryclient;
	
	Autowired
	private RestTemplate restTemplate;
	
	@Autowired
	private LoadBalancerClient loadBalancerclient;
	
	@GetMapping("/hello")
	public string hello(string name) {		
		//<1> 获得服务demo-provider 的一个实例
		ServiceInstance instance;
		if (true) {
			//获取服务demo-provider~ 对应的实例列表
			List<ServiceInstance> instances = discoveryclient.getInstances("demo-provider");
			// 选择第一个
			instance = instances.size() > 0 ? instances.get(0) : null; 
		} else {
			instance = loadBalancerclient.choose("demo-provider");
		}
		<2>发起调用
		if (instance == null) {
			throw new IllegalstateException("获取不到实例");
		}
		string targetUrl = instance.getUri() + "/echo?name="+ name;
		string response = restTemplate.getForobject(targetUrl, String.class);
		//返回结果
		return "consumer:" + response;
	}
}

三、Registry端:

Registry端的职责是提供服务的增删改查管理,并在服务列表发生变化时通知所有的Consumer端。

1、相关bean注入:

首先从注解@EnableEurekaServer开始,可以看到这个注解通过引入的EurekaServerMarkerConfiguration生命了一个名为Marker的bean。

less 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {

}

@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
	@Bean
	public Marker eurekaServerMarkerBean() {
		return new Marker();
	}

	class Marker {
	}
}

接下来查看Spring.factories文件,可以看到指定了配置类EurekaServerAutoConfiguration,我们来查看这个类:

kotlin 复制代码
@Configuration(proxyBeanMethods = false)
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
		InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
	//................
	@Bean
	@ConditionalOnProperty(prefix = "eureka.dashboard", name = "enabled",
			matchIfMissing = true)
	public EurekaController eurekaController() {
		return new EurekaController(this.applicationInfoManager);
	}
	
	@Bean
	public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
			ServerCodecs serverCodecs) {
		this.eurekaClient.getApplications(); // force initialization
		return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
				serverCodecs, this.eurekaClient,
				this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(),
				this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
	}

	@Bean
	@ConditionalOnMissingBean
	public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry,
			ServerCodecs serverCodecs,
			ReplicationClientAdditionalFilters replicationClientAdditionalFilters) {
		return new RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig,
				this.eurekaClientConfig, serverCodecs, this.applicationInfoManager,
				replicationClientAdditionalFilters);
	}

	@Bean
	@ConditionalOnMissingBean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}

	@Bean
	public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry,
			EurekaServerContext serverContext) {
		return new EurekaServerBootstrap(this.applicationInfoManager,
				this.eurekaClientConfig, this.eurekaServerConfig, registry,
				serverContext);
	}
	
	//..............
}

从上面的代码可以的得到如下信息:

a、这个配置bean依赖于上面声明的Marker。

b、引入了EurekaServer初始化启动配置类EurekaServerInitializerConfiguration。

c、激活配置属性类EurekaDashboardProperties和InstanceRegistryProperties。

d、定义了EurekaController的bean:Eureka管理页面的Controller。

e、定义了PeerAwareInstanceRegistry的bean:处理Eureka Client的register、renew、cancel等请求的类。

f、定义了PeerEurekaNodes的bean:集群节点管理的类。

g、定义了EurekaServerContext的bean:eureka Server上下文类。

h、定义了EurekaServerBootstrap的bean:eureka Server启动类。

I、ApplicationResource和InstanceResource:对外提供注册服务实例的增删改查的接口。

2、启动类:

首先从EurekaServerInitializerConfiguration类开始,由于其实现了EurekaServerInitializerConfiguration接口,SpringBoot在启动时必会调用其start方法,在start方法中又调用eurekaServerBootstrap.contextInitialized方法。

csharp 复制代码
public void start() {
	new Thread(() -> {
		//重点,调用eurekaServerBootstrap.contextInitialized
		eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);			

		publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
		EurekaServerInitializerConfiguration.this.running = true;
		publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
	}).start();
}

public void contextInitialized(ServletContext context) {
	//......省略部分代码............
	initEurekaEnvironment();
	//重点,调用initEurekaServerContext完成context的初始化
	initEurekaServerContext();
	context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
}

protected void initEurekaServerContext() throws Exception {
	//......省略部分代码............
	//从集群中其他节点同步服务实例信息到当前节点缓存
	int registryCount = this.registry.syncUp();
	this.registry.openForTraffic(this.applicationInfoManager, registryCount);
}

上面代码中initEurekaServerContext方法,this.registry.syncUp从集群中其他节点同步服务实例信息到当前节点缓存。this.registry.openForTraffic的实现中启动了一个定时任务EvictionTask,用来定时清理已经下线但还未清理掉的服务实例。代码如下:

scss 复制代码
public int syncUp() {
	//......省略部分代码............
	//eurekaClient.getApplications读取集群中其他节点的应用服务实例信息
    Applications apps = eurekaClient.getApplications();
    for (Application app : apps.getRegisteredApplications()) {
        for (InstanceInfo instance : app.getInstances()) {
        	//将服务实例信息注册到本地缓存
            register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
        }
    }    
}

public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
    //......省略部分代码............
    super.postInit();
}

protected void postInit() {
    //......省略部分代码............
    evictionTaskRef.set(new EvictionTask());
    evictionTimer.schedule(evictionTaskRef.get(),
            serverConfig.getEvictionIntervalTimerInMs(),
            serverConfig.getEvictionIntervalTimerInMs());
}

3、服务注册:

实现代码在ApplicationResource类里面的addInstance:

typescript 复制代码
//ApplicationResource.java
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
	//......省略部分代码............
	
    //校验实例信息的完整度

	//注册服务实例
    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build(); 
}

//InstanceRegistry.java
public void register(final InstanceInfo info, final boolean isReplication) {
	//利用Spring的事件机制发布一个EurekaInstanceRegisteredEvent的事件
	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));
}

//PeerAwareInstanceRegistryImpl.java
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
	//注册服务实例
    super.register(info, leaseDuration, isReplication);
	//同步到集群其他节点
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

private void replicateToPeers(Action action, String appName, String id,InstanceInfo info, InstanceStatus newStatus, boolean isReplication) {
    for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {        
        if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
            continue;
        }
        replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
    }
}

//同步到集群其他节点
private void replicateInstanceActionsToPeers(Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
    switch (action) {
        case Cancel:
            node.cancel(appName, id);
            break;
        case Heartbeat:
            InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
            infoFromRegistry = getInstanceByAppAndId(appName, id, false);
            node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
            break;
        case Register:
            node.register(info);
            break;
        case StatusUpdate:
            infoFromRegistry = getInstanceByAppAndId(appName, id, false);
            node.statusUpdate(appName, id, newStatus, infoFromRegistry);
            break;
        case DeleteStatusOverride:
            infoFromRegistry = getInstanceByAppAndId(appName, id, false);
            node.deleteStatusOverride(appName, id, infoFromRegistry);
            break;
    }
}

a、addInstance调用InstanceRegistry.register注册服务实例;

b、InstanceRegistry.register首先利用Spring的事件机制发布一个EurekaInstanceRegisteredEvent的事件,然后调用父类的register;

c、PeerAwareInstanceRegistryImpl.register同样将注册服务实例交给父类去完成,注册完之后将当前新增的服务实例同步到集群其他节点(见replicateInstanceActionsToPeers)

d、最后将注册实现交给AbstractInstanceRegistry.register来实现,见如下代码:

ini 复制代码
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
	//......省略部分代码............
	
	//首先查找appName下的实例信息map
	Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
	if (gMap == null) {
        final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
        gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);        
    }
	Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
	Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
    if (existingLease != null) {
        lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
    }
	//添加要注册的服务实例信息到gMap
    gMap.put(registrant.getId(), lease);
	
	if (InstanceStatus.UP.equals(registrant.getStatus())) {
        lease.serviceUp();
    }
	registrant.setActionType(ActionType.ADDED);
}

可以看到服务实例是保存在registry变量中的,而这个变量是一个两层的map结构,第一层的key是appName,value则是这个app下的服务实例的map。第二层的key是instanceId,value是InstanceInfo。其定义如下

swift 复制代码
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

4、服务续约:

typescript 复制代码
@PUT
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
	
	//..........省略部分代码............
}

public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
    	//同步到集群其他节点
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

public boolean renew(String appName, String id, boolean isReplication) {    
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
    if (leaseToRenew == null) {       
        return false;
    } else {
        InstanceInfo instanceInfo = leaseToRenew.getHolder();        
        leaseToRenew.renew();
        return true;
    }
}

public void renew() {
	lastUpdateTimestamp = System.currentTimeMillis() + duration;
}

同样,续约也是先在registry根据appName查找,再根据id查找,续约其实就是更新节点的lastUpdateTimestamp时间戳。当前节点续约完成后同步到集群其他节点进行同样的续约。

5、服务下线:

typescript 复制代码
@DELETE
public Response cancelLease(@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
	//..........省略部分代码............
    boolean isSuccess = registry.cancel(app.getName(), id, "true".equals(isReplication));
}

public boolean cancel(String appName, String serverId, boolean isReplication) {
	handleCancelation(appName, serverId, isReplication);
	return super.cancel(appName, serverId, isReplication);
}

private void handleCancelation(String appName, String id, boolean isReplication) {		
	publishEvent(new EurekaInstanceCanceledEvent(this, appName, id, isReplication));
}

public boolean cancel(final String appName, final String id, final boolean isReplication) {
    if (super.internalCancel(appName, id, isReplication)) {
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);

        return true;
    }
    return false;
}

protected boolean internalCancel(String appName, String id, boolean isReplication) {
	//..........省略部分代码............
	
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);    
    if (gMap != null) {
        gMap.remove(id);
    }

    return true;
}

服务下线的核心代码就是将服务实例信息从registry中移除。

6、获取指定服务详细信息:

typescript 复制代码
@GET
public Response getInstanceInfo() {
    InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id);
    if (appInfo != null) {        
        return Response.ok(appInfo).build();
    } else {        
        return Response.status(Status.NOT_FOUND).build();
    }
}

public InstanceInfo getInstanceByAppAndId(String appName, String id, boolean includeRemoteRegions) {
    Map<String, Lease<InstanceInfo>> leaseMap = registry.get(appName);
    Lease<InstanceInfo> lease = null;
    if (leaseMap != null) {
        lease = leaseMap.get(id);
    }
    return decorateInstanceInfo(lease);
}

四、Provider端:

Provider端作为服务提供者,需要在启动时将自身注册到Registry端,退出时将其从Registry端下线,服务运行期间需要给Registry发送心跳来避免Registry端将自己给剔除掉。Provider实现这些都是靠EurekaClient接口来实现的,这个接口有一个单例实现类DiscoveryClient,这个类成为Provider端和Consumer端实现的核心。

1、加载DiscoveryClient的bean:

DiscoveryClient有一个派生子类CloudEurekaClient,注入到Spring容器的bean其实是CloudEurekaClient的实例。那么这个bean是如何注入到Spring容器中的了。

在Spring.factories中定义了EurekaClientAutoConfiguration:

ini 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration

在EurekaClientAutoConfiguration中有如下定义:

less 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingRefreshScope
protected static class EurekaClientConfiguration {

	@Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
    public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {
        return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);
    }
}

可以看到在这个配置类中创建了CloudEurekaClient实例并添加到Spring容器中。

2、注册自身到远端:

在DiscoveryClient的构造函数中有下面一段代码:

scss 复制代码
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
	//........省略部分代码..........
	
	//创建相关线程池对象
	scheduler = Executors.newScheduledThreadPool(2,
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-%d")
                    .setDaemon(true)
                    .build());
    //初始化心跳线程池对象
    heartbeatExecutor = new ThreadPoolExecutor(
            1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                    .setDaemon(true)
                    .build()
    );  
    //初始化缓存刷新线程池对象
    cacheRefreshExecutor = new ThreadPoolExecutor(
            1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                    .setDaemon(true)
                    .build()
    );  

	//fetchRegistry从eureka registry端上拉取服务实例信息
	if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
        fetchRegistryFromBackup();
    }
	
	//将自身注册到eureka registry端
	if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
        register();
    }

	//初始化相关定时任务
    initScheduledTasks();    
}

private void initScheduledTasks() {
	//初始化刷新缓存的定时任务
    int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
    int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
    cacheRefreshTask = new TimedSupervisorTask(
            "cacheRefresh",
            scheduler,
            cacheRefreshExecutor,
            registryFetchIntervalSeconds,
            TimeUnit.SECONDS,
            expBackOffBound,
            new CacheRefreshThread()
    );
    scheduler.schedule(cacheRefreshTask, registryFetchIntervalSeconds, TimeUnit.SECONDS);
	
	//初始化心跳的定时任务
	int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();            
    heartbeatTask = new TimedSupervisorTask(
            "heartbeat",
            scheduler,
            heartbeatExecutor,
            renewalIntervalInSecs,
            TimeUnit.SECONDS,
            expBackOffBound,
            new HeartbeatThread()
    );
    scheduler.schedule(heartbeatTask,renewalIntervalInSecs, TimeUnit.SECONDS);    
}

a、DiscoveryClient的构造方法首先初始化相关线程池对象,包括缓存刷新线程池和心跳线程池;

b、接着调用fetchRegistry从eureka registry端上拉取服务实例信息;

c、再将自身注册到eureka registry端;

d、最后调用initScheduledTasks初始化相关定时任务,在initScheduledTasks的实现中可以看到初始化了缓存刷新定时任务cacheRefreshTask和心跳定时刷新任务heartbeatTask。

接下来看看register是如何将自身注册到eureka Registry端的:

scss 复制代码
boolean register() throws Throwable {
	//交由EurekaHttpClient去注册instanceInfo
	httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
	return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

public EurekaHttpResponse<Void> register(final InstanceInfo info) {
    return execute(new RequestExecutor<Void>() {
        @Override
        public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
	        //交由EurekaHttpClient去注册instanceInfo
            return delegate.register(info);
        }

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

//AbstractJerseyEurekaHttpClient.java
//发起http请求进行注册InstanceInfo
public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();    
    //此处可以看到请求的URL为:http://127.0.0.1:8800/eureka/apps/xxx
    Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
    addExtraHeaders(resourceBuilder);
    ClientResponse 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();
}

register方法中将注册交由EurekaHttpClient去注册instanceInfo,最后还是交给AbstractJerseyEurekaHttpClient.register去注册,可以看到请求的URL地址为http://127.0.0.1:8800/eureka/apps/xxx,并且是调用的post方法。

3、下线远端服务实例:

EurekaHttpClient在退出销毁前会调用shutdown方法(@PreDestroy注解标注了),然后调用registrationClient.cancel,向eureka Server提交请求:delete http://127.0.0.1:8800/eureka/apps/xxx/id

scss 复制代码
@PreDestroy
public synchronized void shutdown() {
	//........省略部分代码..........
	
	cancelScheduledTasks();
	unregister();    
}

void unregister() {
	//........省略部分代码..........
	
    EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
}

public EurekaHttpResponse<Void> cancel(String appName, String id) {
    String urlPath = "apps/" + appName + '/' + id;
	//请求的URL为:delete http://127.0.0.1:8800/eureka/apps/xxx/id
    Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
    addExtraHeaders(resourceBuilder);
    ClientResponse response = resourceBuilder.delete(ClientResponse.class);
    return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
}

4、发送心跳:

在上面介绍的DiscoveryClient的构造函数中会初始化心跳的定时任务heartbeatTask,这个任务的执行是在HeartbeatThread线程中进行的,这个线程中调用renew方法发送心跳:

scss 复制代码
private class HeartbeatThread implements Runnable {
    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    //发送心跳请求到eureka
    httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);        
    if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) { 
    	//如果心跳失败,并且错误码是该服务实例没有注册,则发送将自身注册到eureka上
        boolean success = register();            
        return success;
    }
    return httpResponse.getStatusCode() == Status.OK.getStatusCode();
}

//发送心跳的请求
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    //请求的URL为:put http://127.0.0.1:8800/eureka/apps/xxx/${id}
    WebResource webResource = jerseyClient.resource(serviceUrl)
            .path(urlPath)
            .queryParam("status", info.getStatus().toString())
            .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());    
    Builder requestBuilder = webResource.getRequestBuilder();
    addExtraHeaders(requestBuilder);
    ClientResponse response = requestBuilder.put(ClientResponse.class);
    EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));    
    return eurekaResponseBuilder.build();
}

从renew的实现中可以看到如果心跳失败,并且错误码是eureka server上没有当前服务实例时,则调用registert将当前服务实例注册到eureka server上。从sendHeartBeat上可以看到请求的URL为:put [http://127.0.0.1:8800/eureka/apps/xxx/${id}](https://link.juejin.cn?target=http%3A%2F%2F127.0.0.1%3A8800%2Feureka%2Fapps%2Fxxx%2F%24%257Bid%257D "http://127.0.0.1:8800/eureka/apps/xxx/${id}")

五、Consumer端:

Consumer端作为服务请求方,启动时从Registry端拉取所有的服务实例信息并缓存在本地,当服务实例有变化时进行增量获取服务实例数据并更新本地缓存,调用服务时从本地缓存中找到指定服务的URL地址并进行远程调用。

1、从远端拉取服务实例信息:

从上面的DiscoveryClient构造函数中可以看到会调用fetchRegistry从eureka server拉取所有服务实例并缓存到本地:

scss 复制代码
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
	//........省略部分代码..........
	
    Applications applications = getApplications();
	if (clientConfig.shouldDisableDelta()
            || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
            || forceFullRegistryFetch
            || (applications == null)
            || (applications.getRegisteredApplications().size() == 0)
            || (applications.getVersion() == -1)) 
    {
		//全量获取服务实例列表
        getAndStoreFullRegistry();
    } else {
		//增量获取服务实例列表
        getAndUpdateDelta(applications);
    }
	
	onCacheRefreshed();

    return true;
}

private void getAndStoreFullRegistry() throws Throwable {
	//........省略部分代码..........
	
	Applications apps = null;
	//发送http请求获取全部的服务实例列表
    EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getApplications(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        apps = httpResponse.getEntity();
    }
	
	if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
		//将获取到的服务实例列表保存到localRegionApps中
        localRegionApps.set(this.filterAndShuffle(apps));        
    }    
}

public EurekaHttpResponse<Applications> getApplications(String... regions) {
    return getApplicationsInternal("apps/", regions);
}

//请求的url地址为:get http://127.0.0.1:8800/eureka/apps/
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
	WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath);
    if (regions != null && regions.length > 0) {
        regionsParamValue = StringUtil.join(regions);
        webResource = webResource.queryParam("regions", regionsParamValue);
    }
    Builder requestBuilder = webResource.getRequestBuilder();
    addExtraHeaders(requestBuilder);
    response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class);
	return anEurekaHttpResponse(response.getStatus(), Applications.class)
            .headers(headersOf(response))
            .entity(applications)
            .build();
}

上面的代码fetchRegistry中,getApplications是从localRegionApps中读取数据,在第一次请求时其内容为空,因此在下面就会调用getAndStoreFullRegistry进行全量获取,并将获取到的服务实例列表保存到localRegionApps中,这样下次再调用时,由于localRegionApps已经有数据,就会走增量获取服务实例列表的流程。

getApplicationsInternal会进行http调用向eureka server请求服务实例数据列表,请求的URL地址为:get http://127.0.0.1:8800/eureka/apps/。

相关推荐
小马爱打代码6 小时前
SpringBoot:封装 starter
java·spring boot·后端
STARSpace88886 小时前
SpringBoot 整合个推推送
java·spring boot·后端·消息推送·个推
Marktowin7 小时前
玩转 ZooKeeper
后端
蓝眸少年CY7 小时前
(第十二篇)spring cloud之Stream消息驱动
后端·spring·spring cloud
码界奇点8 小时前
基于SpringBoot+Vue的前后端分离外卖点单系统设计与实现
vue.js·spring boot·后端·spring·毕业设计·源代码管理
lindd9119118 小时前
4G模块应用,内网穿透,前端网页的制作第七讲(智能头盔数据上传至网页端)
前端·后端·零基础·rt-thread·实时操作系统·项目复刻
Loo国昌9 小时前
【LangChain1.0】第八阶段:文档处理工程(LangChain篇)
人工智能·后端·算法·语言模型·架构·langchain
vx_bisheyuange9 小时前
基于SpringBoot的海鲜市场系统
java·spring boot·后端·毕业设计
李慕婉学姐10 小时前
【开题答辩过程】以《基于Spring Boot和大数据的医院挂号系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·spring boot·后端
源代码•宸10 小时前
Leetcode—3. 无重复字符的最长子串【中等】
经验分享·后端·算法·leetcode·面试·golang·string