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/。

相关推荐
大学生资源网20 分钟前
基于springboot的万亩助农网站的设计与实现源代码(源码+文档)
java·spring boot·后端·mysql·毕业设计·源码
苏三的开发日记29 分钟前
linux端进行kafka集群服务的搭建
后端
苏三的开发日记1 小时前
windows系统搭建kafka环境
后端
爬山算法1 小时前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai1 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌1 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
9号达人2 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net