一、基本概念:
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/。