Eureka & Feign源码分析

总览

Eureka 分为 Server 和 Client。
而Eureka Server实例既作为server接受client(其它server实例以及feign客户端实例)的注册,又作为client向集群中的其他server实例注册自己。所以后文以Eureka Server为线索进行源码分析,既覆盖了server部分,也覆盖了client部分。
Feign客户端只有客户端功能,作为client只使用DiscoveryClient,向Eureka Server注册自己,并通过接口获取全量的其他微服务信息,和增量变化。

Eureka

配置相关

spring-cloud-netflix-eureka-client 中的 spring-configuration-metadata.json是关于配置的说明文件。
通过org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration类进行自动配置。
注意该类的一个注解,
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
即,如果没有配置eureka.client.enabled,那么默认为true,开启eureka的client功能。
此类也会实例化org.springframework.cloud.netflix.eureka.EurekaClientConfigBeanorg.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean两个配置实例。
分别对应了eureka.clienteureka.instance的相关配置。eureka.client内的配置是作为client的一些行为相关的配置,如server地址,是否向server注册本client等。eureka.instance内的配置是作为instance的一些相关配置,如本实例的instanceId,hostname等。

客户端

作为client,向server发送请求时,核心类是DiscoveryClient

  1. 程序启动后,实例化bean的过程中
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();

这其中会实例化com.netflix.discovery.DiscoveryClient
DiscoveryClient的构造函数执行过程中会fetch registry。即

if (clientConfig.shouldFetchRegistry()) {
    boolean primaryFetchRegistryResult = fetchRegistry(false);

url为http://{host}:{port}/eureka/apps/
常见url的请求和回复逻辑在文章末尾描述。这个接口返回Applications类,是全量的注册实例数据。
RetryableEurekaHttpClient#execute中,{host}:{port}会使用在集群节点列表逐个尝试直到成功发出请求。集群节点列表初始为配置中的所有节点配置,默认5分钟后会调用AsyncResolver#updateTask再次获取一次最新的集群节点列表。

然后initScheduledTasks();
这些定时任务包括了

  • if shouldFetchRegistry ,fetch registry (默认 30秒后执行一次,然后每隔30秒执行一次)
    fetchRegistry(boolean forceFullRegistryFetch)方法内根据applications.getRegisteredApplications().size() == 0来抉择使用哪个url来fetch,http://{host}:{port}/eureka/apps/(全量更新)
    或是http://{host}:{port}/eureka/apps/delta(增量更新)
  • if shouldRegisterWithEureka
    • heartbeat (默认30秒后执行一次,然后每隔30秒执行一次)
      url为http://{host}:{port}/eureka/apps/UNKNOWN/{instanceId}?status=UP&lastDirtyTimestamp=1610865104367
    • InstanceInfoReplicator(默认40秒后执行一次)
      com.netflix.discovery.InstanceInfoReplicator
    • 注册状态变化监听器
      applicationInfoManager.registerStatusChangeListener(statusChangeListener);
      监听器现在不会被调用,接到事件时,会调用instanceInfoReplicator.onDemandUpdate();
      (被调用的过程详见2)
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();

1中的实例化bean之后,会执行finishRefresh()。该方法会调用所有实现了Lifecycle接口的bean的Lifecycle.start()方法。然后依次调用
org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration#start
org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry#register
最后com.netflix.appinfo.ApplicationInfoManager#setInstanceStatus中调用监听器。

虽然EurekaAutoServiceRegistration也会响应WebServerInitializedEvent事件并执行start(),但是Lifecycle.start()WebServerInitializedEvent事件触发的更早。

1中注册的响应函数被调用com.netflix.discovery.InstanceInfoReplicator#onDemandUpdate
然后在新的线程中立即执行一次InstanceInfoReplicator.this.run();,然后每隔30秒周期执行一次本函数
函数中会执行discoveryClient.register();
发送http请求,url为http://{host}:{port}/eureka/apps/UNKNOWN,向server注册本实例信息。

注意
discoveryClient.register() 中的http请求发出后,eureka注册中心中就会注册上本服务,流量就会进入到服务器中
而springboot服务需要执行完TomcatWebServer.start(),才会暴露http端口号,才能实际接收http请求。见SpringBoot中Tomcat的源码分析一文
虽然二者是在不同的线程中执行的。但某些版本的springboot中,这两个函数触发顺序反了。有可能会先执行discoveryClient.register(),再执行TomcatWebServer.start()。即先注册至服务中,而此时http端口还不可用,最终导致服务的启动不平滑

  1. 发送请求的http client默认是com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient
    继承了com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient抽象类
    实现了 com.netflix.discovery.shared.transport.EurekaHttpClient接口
  1. 发送http请求时的{ip:port}选择
    多会经过com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute,这里candidateHosts = getHostCandidates();函数确定向哪个{ip:port}发送请求,这里是没有轮询选择域名的逻辑,只会从第一个开始尝试发送,失败时才会选择下一个。
    所以实践中eureka server应配置成域名。如果手动配置为多个{ip:port},RetryableEurekaHttpClient#execute函数内将只选择配置的第一个{ip:port}进行请求,会发生流量倾斜。
  1. 使用 /eureka/apps/ 获取全量数据,使用 /eureka/apps/delta 获取增量数据

服务端

作为server,接受请求时,内部数据主要来源于InstanceRegistry
使用 /eureka/peerreplication/batch/ 向其它server实例批量广播变更数据

  1. 启动后EurekaController负责接收Eureka网页请求
    eureka的web页面由EurekaController#status生成,通过com.netflix.eureka.registry.AbstractInstanceRegistry#registry获取微服务信息,使用templates/eureka/status.ftlh生成网页
  2. restfule接口
    https://github.com/Netflix/eureka/wiki/Eureka-REST-operations 文档中记录了restful接口实例。不过实际代码中使用的url与文档不同,url中不包含v2,虽然不包括v2,但默认版本是v2,所以逻辑一致。

2.1 eureka-core中的com.netflix.eureka.resources包内的*Resource类中包含了实例间通信的restful接口,功能类似controller。但又不完全相同,*Resource的函数返回值可以是一个新的*Resource,相当于controller返回值是新的controller,需要一层层传递达到最终的controller。
2.2 spring-cloud-netflix-eureka-server-3.0.0.jar 包中的org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration#jerseyApplication方法实例化了javax.ws.rs.core.Application这个Bean。实例化的过程中会扫描2.1中提到的部分*Resource类。这些类会在后续的构造出url及其对对应的

2.3 jersey-server.jar中的com.sun.jersey.server.impl.application.WebApplicationImpl#_initiate方法末尾处

RulesMap<UriRule> rootRules = new RootResourceUriRules(this,
        resourceConfig, wadlFactory, injectableFactory).getRules();

这是根据2.2中扫描出的*Resource类构造出RulesMap,是一种规则映射,可以将restful请求映射到对应的处理method。

2.4 jersey-servlet.jar中的com.sun.jersey.spi.container.servlet.WebComponent#service方法中的语句_application.handleRequest(cRequest, w);,将http request与2.3中的ruleMap进行匹配,找到对应的处理method。
jersey-server.jar中的com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider.ResponseOutInvoker#_dispatch方法中通过反射调用具体的处理method。

InstanceRegistry & ResponseCacheImpl

org.springframework.cloud.netflix.eureka.server.InstanceRegistry是核心类。
该类有一个缓存属性,是继承于父类的属性
com.netflix.eureka.registry.AbstractInstanceRegistry#responseCache,它默认使用的是com.netflix.eureka.registry.ResponseCacheImpl实现。
ResponseCacheImpl中有两个属性
ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>()LoadingCache<Key, Value> readWriteCacheMap。其中readWriteCacheMap是一个基于com.google.common.cache.LoadingCache构建的缓存。

/eureka/apps/
/eureka/apps/delta
/eureka/svips/{svipAddress}
/eureka/vips/{vipAddress}
/eureka/apps/{appId}
"/eureka/peerreplication/batch"
等接口的返回值优先从缓存中获取。因为有些请求需要遍历大量微服务实例数据,而且eureka客户端使用的轮询的方式获取增量变化,比如delta接口会被频繁调用。所以使用缓存。

配置shouldUseReadOnlyResponseCache默认为true。
优先从ResponseCacheImpl#readOnlyCacheMap中获取,它是一个ConcurrentHashMap。它是ResponseCacheImpl#readWriteCacheMap的对应的只读缓存。readWriteCacheMap如果有变化并不会主动更新readOnlyCacheMap。更新方式有以下两种。

  • 如果readOnlyCacheMap中获取结果为null,则从ResponseCacheImpl#readWriteCacheMap中获取并放入ResponseCacheImpl#readOnlyCacheMap。因为readOnlyCacheMap没有移除逻辑,所以最终会包含所有类型的key。

  • Eureka-CacheFillTimer线程每隔默认30s执行一次com.netflix.eureka.registry.ResponseCacheImpl#getCacheUpdateTask,遍历readOnlyCacheMap中的key,将readWriteCacheMap中的缓存信息更新至readOnlyCacheMap,如果readWriteCacheMap的key值过期了,生成最新的值。由于readOnlyCacheMap没有移除逻辑,所以最终会生成一遍全量的key。

ResponseCacheImpl#readWriteCacheMap是一个基于com.google.common.cache.LoadingCache构建的缓存。ResponseCacheImpl的构造函数中构建了readWriteCacheMap,写后默认3分钟失效,使用ResponseCacheImpl#generatePayload生成值并缓存。缓存key为com.netflix.eureka.registry.KeygeneratePayload方法内部根据不同的Key执行不同的加载逻辑。

ALL_APPS => InstanceRegistry#getApplications()
ALL_APPS_DELTA => InstanceRegistry#getApplicationDeltas
单独某个应用 => InstanceRegistry#getApplication(java.lang.String)

接口

/eureka/apps/

拉取实例信息列表
http method: GET

  1. 发送方
    调用链为依次为
getApplicationsInternal:189, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
getApplications:167, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
.....
getApplications:134, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
getAndStoreFullRegistry:1101, DiscoveryClient (com.netflix.discovery)
fetchRegistry:1014, DiscoveryClient (com.netflix.discovery)
<init>:441, DiscoveryClient (com.netflix.discovery)

server 调用方仅仅请求/eureka/apps/url,没有添加任何参数。
服务端返回的response是一个com.netflix.discovery.shared.Applications,然后将该对象处理一下之后,存储在com.netflix.discovery.DiscoveryClient#localRegionApps中。

  1. 接收方
    handle method: com.netflix.eureka.resources.ApplicationsResource#getContainers
    该方法使用(entityType:Application, entityName:ALL_APPS,等)构造com.netflix.eureka.registry.Key,通过这个key在responseCache中查找缓存值。缓存的相关信息见上面的InstanceRegistry一节。
    如果没有缓存,使用com.netflix.eureka.registry.AbstractInstanceRegistry#getApplications生成返回值并缓存。此方法调用com.netflix.eureka.registry.AbstractInstanceRegistry#getApplicationsFromMultipleRegions,返回com.netflix.eureka.registry.AbstractInstanceRegistry#registry中的全部已注册实例信息。
    Eureka Server启动后发出的第一个请求就是这个请求,但是这时候收到的response是空列表,因为还没有任何实例在server中注册。

/eureka/apps/{appName}

向服务器注册实例信息
http method: POST

  1. 发送方
    调用链为依次为
register:48, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
......
register:56, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
register:876, DiscoveryClient (com.netflix.discovery)
run:121, InstanceInfoReplicator (com.netflix.discovery)
run:101, InstanceInfoReplicator$1 (com.netflix.discovery)

server 调用方使用com.netflix.appinfo.InstanceInfo#getAppName作为url参数中的{appName},默认为UNKNOWN。POST body为InstanceInfo。

InstanceInfo实例化于org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration.RefreshableEurekaClientConfiguration#eurekaApplicationInfoManager,字段信息来源于org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean配置类。
而这个配置类又实例化于org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration#eurekaInstanceConfigBean
EurekaInstanceConfigBean的初始化过程会赋予一些一些关键字段默认值。

  • appName 默认为 UNKNOWN
  • hostInfo 默认来源于org.springframework.cloud.commons.util.InetUtils#findFirstNonLoopbackHostInfo,其中使用java.net.NetworkInterface#getNetworkInterfaces方法获取所有网卡信息。
  • ipAddress 默认为hostInfo.getIpAddress()
  • hostname 默认为hostInfo.getHostname()
  • instanceId 默认为{hostname}:{port}
    之后这些字段会被人工配置值覆盖掉。
  1. 接收方
    handle method
    首先根据路径/eureka/apps/{appName}匹配至方法com.netflix.eureka.resources.ApplicationsResource#getApplicationResource
    该方法实例化了一个com.netflix.eureka.resources.ApplicationResource对象并返回,然后继续匹配,匹配至该对象的方法com.netflix.eureka.resources.ApplicationResource#addInstance
    注册方法
    org.springframework.cloud.netflix.eureka.server.InstanceRegistry#register()
    注册到 <appName, <instanceId, Lease<InstanceInfo>>> 嵌套的map中。
    注册成功后responseCache.invalidate()重置缓存,此缓存曾在上面的 InstanceRegistry 一节中提到。
    然后执行replicateToPeers(),将信息同步至集群,使用类似下面的请求来同步信息。
    http://server-peer0:8080/eureka/peerreplication/batch/

/eureka/apps/{appName}/{id}

从服务器去注册(移除注册)实例信息
http method: DELETE

DiscoveryClient.shutdown时会发送这个消息。

服务端的处理路径为
com.netflix.eureka.resources.ApplicationsResource#getApplicationResource =>
com.netflix.eureka.resources.ApplicationResource#getInstanceInfo =>
com.netflix.eureka.resources.InstanceResource#cancelLease =>
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel

com.netflix.eureka.registry.AbstractInstanceRegistry#registry中直接删除实例信息,并广播至集群。

/eureka/apps/{appName}/{instanceId}?status={status}&lastDirtyTimestamp={lastDirtyTimestamp}

心跳信息
http method: PUT

  1. 发送方
sendHeartBeat:103, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
.....
sendHeartBeat:89, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
renew:893, DiscoveryClient (com.netflix.discovery)
run:1457, DiscoveryClient$HeartbeatThread (com.netflix.discovery)

该请求没有http body

  1. 接收方
    依次匹配
    com.netflix.eureka.resources.ApplicationsResource#getApplicationResource =>
    com.netflix.eureka.resources.ApplicationResource#getInstanceInfo =>
    com.netflix.eureka.resources.InstanceResource#renewLease =>
    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew

刷新存活时间,再通过PeerAwareInstanceRegistryImpl#replicateToPeers,也就是 /eureka/peerreplication/batch/ 转发至server集群内的其他实例

/eureka/apps/{appName}/{instanceId}/status?value=OUT_OF_SERVICE

可以用于手动摘除实例,将状态强制override为 OUT_OF_SERVICE
http method: PUT
使用 相同的url,http method改为 DELETE 用于移除这个状态override操作

  1. 发送方
    可以人工调用
  1. 接收方
    依次匹配
    com.netflix.eureka.resources.ApplicationsResource#getApplicationResource =>
    com.netflix.eureka.resources.ApplicationResource#getInstanceInfo =>
    com.netflix.eureka.resources.InstanceResource#statusUpdate

InstanceRegistry#statusUpdate() => PeerAwareInstanceRegistryImpl#statusUpdate() => AbstractInstanceRegistry#statusUpdate
AbstractInstanceRegistry#registry 中修改实例状态
AbstractInstanceRegistry#recentlyChangedQueue 加入这个实例变化的事件
AbstractInstanceRegistry#invalidateCache 缓存主动失效
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers 广播给其它server实例
接下来详细解析一下最后三个操作

2.1 AbstractInstanceRegistry#recentlyChangedQueue 加入这个实例变化的事件

AbstractInstanceRegistry#getDeltaRetentionTask 线程异步,默认每30秒检查一次AbstractInstanceRegistry#recentlyChangedQueue队列,移除队列中3分钟(默认)之前的事件。

AbstractInstanceRegistry#getApplicationDeltas会读取AbstractInstanceRegistry#recentlyChangedQueue里的信息。
"/eureka/apps/delta" => ApplicationsResource#getContainerDifferential => 缓存 => ResponseCacheImpl#generatePayload => AbstractInstanceRegistry#getApplicationDeltas
有且仅有这一条路径会调用到这里AbstractInstanceRegistry#getApplicationDeltas

2.2 AbstractInstanceRegistry#invalidateCache 缓存主动失效
=> ResponseCacheImpl#invalidate(java.lang.String, java.lang.String, java.lang.String)
清除com.netflix.eureka.registry.ResponseCacheImpl#readWriteCacheMap中的key,包括3个类型{appName}应用服务级别,ALL_APPS,ALL_APPS_DELTA

2.3 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers 广播给其它server实例
本节末尾统一描述这个函数

/eureka/apps/delta

增量更新
http method: GET

  1. 发送方
getApplicationsInternal:189, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
getDelta:172, AbstractJerseyEurekaHttpClient (com.netflix.discovery.shared.transport.jersey)
......
getDelta:149, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
getAndUpdateDelta:1135, DiscoveryClient (com.netflix.discovery)
fetchRegistry:1016, DiscoveryClient (com.netflix.discovery)
refreshRegistry:1531, DiscoveryClient (com.netflix.discovery)
run:1498, DiscoveryClient$CacheRefreshThread (com.netflix.discovery)

此请求的返回值和/eureka/apps/{appName}请求一样,是一个com.netflix.discovery.shared.Applications。然后通过com.netflix.discovery.DiscoveryClient#updateDelta方法将增量的变化信息存储至com.netflix.discovery.DiscoveryClient#localRegionApps中。

com.netflix.appinfo.InstanceInfo#actionType动作类型包括三种 增加,修改,删除
删除操作执行com.netflix.discovery.shared.Application#removeInstance(),从DiscoveryClient#localRegionApps中删除实例信息
增加,修改操作都是执行com.netflix.discovery.shared.Application#addInstance,将本地DiscoveryClient#localRegionApps中记录的实例信息完全有delta接口中传来的信息直接替换。

  1. 接收方
    com.netflix.eureka.resources.ApplicationsResource#getContainerDifferential
    该方法使用(entityType:Application, entityName:ALL_APPS_DELTA,等)构造com.netflix.eureka.registry.Key,通过这个key在responseCache中查找缓存值。缓存的相关信息见上面的InstanceRegistry一节。
    先从ResponseCacheImpl#readOnlyCacheMap中获取,如果为null则从ResponseCacheImpl#readWriteCacheMap中获取并放入ResponseCacheImpl#readOnlyCacheMap

如果没有缓存,使用com.netflix.eureka.registry.AbstractInstanceRegistry#getApplicationDeltas生成返回值并缓存。
该函数读取recentlyChangedQueue中的数据,也就是最近有变化的实例。这里只读取
recentlyChangedQueue保存的都是AbstractInstanceRegistry#registry内的实例信息引用,都是最新的数据。然后将这些实例数据封装返回。

一个公共方法 PeerAwareInstanceRegistryImpl#replicateToPeers

多个函数都会调用这个replicateToPeers函数,将变更广播至其它server实例。

函数内部通过peerEurekaNodes.getPeerEurekaNodes()获取所有其它server实例节点,目前看这个节点列表只来自于 eureka.client.service-url.defaultZone 配置的所有节点。

然后对每个节点调用PeerAwareInstanceRegistryImpl#replicateInstanceActionsToPeers
这个函数内根据Action action调用PeerEurekaNode node的不同方法。
虽然是不同的方法,但逻辑大致一致。都是通过com.netflix.eureka.util.batcher.TaskDispatcher#process => com.netflix.eureka.util.batcher.AcceptorExecutor#process 最终向AcceptorExecutor#acceptorQueue
TaskDispatchers#createBatchingTaskDispatcher中添加任务。
这里的com.netflix.eureka.util.batcher.TaskDispatcher#process是在TaskDispatchers#createBatchingTaskDispatcher函数末尾创建的匿名类。
TaskDispatchers#createBatchingTaskDispatcher中添加任务根据Action action各不相同,都在PeerEurekaNode node的不同调用方法内部定义,这些任务类都是InstanceReplicationTask
这些任务最终会通过ReplicationTaskProcessor#createReplicationInstanceOf转换成ReplicationInstance,打包成ReplicationList,通过"/eureka/peerreplication/batch/"接口批量发送出去。
需要注意InstanceReplicationTask覆写的execute方法只会被com.netflix.eureka.util.batcher.TaskExecutors.SingleTaskWorkerRunnable执行,但实际上都是批量发送任务,是被com.netflix.eureka.util.batcher.TaskExecutors.BatchWorkerRunnable消费的,所以不会执行execute方法,只会将InstanceReplicationTask的构造参数转化为批量任务一起发送出去。

AcceptorExecutor#acceptorQueue队列的消费在"/eureka/peerreplication/batch/"中的"server调用方"一节描述。

  1. 状态变更
    调用栈如下
process:125, AcceptorExecutor (com.netflix.eureka.util.batcher)
process:50, TaskDispatchers$2 (com.netflix.eureka.util.batcher)
statusUpdate:272, PeerEurekaNode (com.netflix.eureka.cluster)
replicateInstanceActionsToPeers:679, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
replicateToPeers:647, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
statusUpdate:441, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
statusUpdate:169, InstanceResource (com.netflix.eureka.resources)
......

最终向AcceptorExecutor#acceptorQueue中添加的PeerEurekaNode#statusUpdate()内定义的任务InstanceReplicationTask,调用com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#statusUpdate,也就是发送

  1. 心跳
    调用栈如下
process:125, AcceptorExecutor (com.netflix.eureka.util.batcher)
process:50, TaskDispatchers$2 (com.netflix.eureka.util.batcher)
heartbeat:227, PeerEurekaNode (com.netflix.eureka.cluster)
replicateInstanceActionsToPeers:672, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
replicateToPeers:647, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
renew:423, PeerAwareInstanceRegistryImpl (com.netflix.eureka.registry)
renew:114, InstanceRegistry (org.springframework.cloud.netflix.eureka.server)
renewLease:112, InstanceResource (com.netflix.eureka.resources)
......

/eureka/peerreplication/batch/

http method: POST

  1. 发送方
submitBatchUpdates:113, JerseyReplicationClient (com.netflix.eureka.transport)
process:80, ReplicationTaskProcessor (com.netflix.eureka.cluster)
run:190, TaskExecutors$BatchWorkerRunnable (com.netflix.eureka.util.batcher)
run:832, Thread (java.lang)

具体的调用来源是一个很长的链条,最初来自于各处调用的PeerAwareInstanceRegistryImpl#replicateToPeers
PeerAwareInstanceRegistryImpl#replicateToPeers是任务的生产方,该方法在上文描述过。
PeerAwareInstanceRegistryImpl#replicateToPeers经过层层调用,最终会向com.netflix.eureka.util.batcher.AcceptorExecutor#acceptorQueue中加入任务

com.netflix.eureka.util.batcher.AcceptorExecutor.AcceptorRunner#drainInputQueues中会不停的消费任务TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);
然后将任务传入com.netflix.eureka.util.batcher.AcceptorExecutor.AcceptorRunner#appendTaskHolder函数,函数内部又将任务放入一个map中com.netflix.eureka.util.batcher.AcceptorExecutor.AcceptorRunner#appendTaskHoldercom.netflix.eureka.util.batcher.AcceptorExecutor#pendingTasks
com.netflix.eureka.util.batcher.AcceptorExecutor.AcceptorRunner#assignBatchWork函数会消费pendingTasks这一map,函数末尾又把他们放到com.netflix.eureka.util.batcher.AcceptorExecutor#batchWorkQueue这个队列中。
这个队列又在com.netflix.eureka.util.batcher.TaskExecutors.BatchWorkerRunnable#getWork这里被消费

com.netflix.eureka.util.batcher.TaskExecutors.BatchWorkerRunnable中不停地执行BatchWorkerRunnable#getWork获取任务,然后处理任务com.netflix.eureka.cluster.ReplicationTaskProcessor#process() => com.netflix.eureka.transport.JerseyReplicationClient#submitBatchUpdates => 发送"/eureka/peerreplication/batch/"请求

请求体ReplicationList来自于ReplicationTaskProcessor#createReplicationInstanceOf函数转换的InstanceReplicationTask task,这里只转换一些基本的应用名称,实例id,状态等。只有在register注册实例任务时,才会传递AbstractInstanceRegistry#registry中实时的这个实例的信息。

  1. 接收方
    PeerReplicationResource#batchReplication
    => com.netflix.eureka.resources.PeerReplicationResource#dispatch
    内部根据不同的操作调用不同的函数。其实都是原操作的处理逻辑,区别是"isReplication"传入的是true。再次执行到PeerAwareInstanceRegistryImpl#replicateToPeers函数时,isReplication == true,不再向其它服务端实例进行广播了。当前广播消息就此终止。

比如
StatusUpdate => com.netflix.eureka.resources.PeerReplicationResource#handleStatusUpdate
=> com.netflix.eureka.resources.InstanceResource#statusUpdate
又回到了 /eureka/apps/{appName}/{instanceId}/status?value=OUT_OF_SERVICE statusUpdate接口的处理逻辑。
然后"isReplication"传入的是true。再次执行到PeerAwareInstanceRegistryImpl#replicateToPeers函数时,不再向其它服务端实例进行广播。

Client

通过上文对 Server 的流程可知,Eureka Client 节点的主要逻辑均在于com.netflix.discovery.DiscoveryClient类,集群内的节点信息存储于localRegionApps属性中,使用各种get方法获取。

Feign

Client的实例化

1.扫描并注册Feign client
@SpringBootApplication同一级别的注解@EnableFeignClients的源码中,包含了@Import(FeignClientsRegistrar.class)
该类的方法
org.springframework.cloud.openfeign.FeignClientsRegistrar.registerBeanDefinitions()
即为将Feign Client注册进beanFactory中的入口函数。
该方法调用的registerFeignClients()函数,执行扫描并注册进BeanDefinitionRegistry
1.1 扫描Bean
registerFeignClients()方法中会调用getBasePackages(),获取从哪些package中扫描bean。
该方法的调用栈如下:

getBasePackages:313, FeignClientsRegistrar (org.springframework.cloud.openfeign)
registerFeignClients:165, FeignClientsRegistrar (org.springframework.cloud.openfeign)
registerBeanDefinitions:137, FeignClientsRegistrar (org.springframework.cloud.openfeign)
registerBeanDefinitions:86, ImportBeanDefinitionRegistrar (org.springframework.context.annotation)
lambda$loadBeanDefinitionsFromRegistrars$1:396, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation)
accept:-1, 928734079 (org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$$Lambda$277)
forEach:684, LinkedHashMap (java.util)
loadBeanDefinitionsFromRegistrars:395, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation)
loadBeanDefinitionsForConfigurationClass:157, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation)
loadBeanDefinitions:129, ConfigurationClassBeanDefinitionReader (org.springframework.context.annotation)
processConfigBeanDefinitions:348, ConfigurationClassPostProcessor (org.springframework.context.annotation)
postProcessBeanDefinitionRegistry:252, ConfigurationClassPostProcessor (org.springframework.context.annotation)
invokeBeanDefinitionRegistryPostProcessors:285, PostProcessorRegistrationDelegate (org.springframework.context.support)
invokeBeanFactoryPostProcessors:99, PostProcessorRegistrationDelegate (org.springframework.context.support)
invokeBeanFactoryPostProcessors:751, AbstractApplicationContext (org.springframework.context.support)
refresh:569, AbstractApplicationContext (org.springframework.context.support)
refresh:144, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:767, SpringApplication (org.springframework.boot)
refresh:759, SpringApplication (org.springframework.boot)
refreshContext:426, SpringApplication (org.springframework.boot)
run:326, SpringApplication (org.springframework.boot)
run:1309, SpringApplication (org.springframework.boot)
run:1298, SpringApplication (org.springframework.boot)
main:22, XXXXXApplication (com.example.eurekaclient0)

获取package的逻辑为,将开发者在@EnableFeignClientsvalue,basePackages,basePackageClasses这些字段中声明的package路径收集并返回。
如果收集的结果是空列表(默认情况下不定义任何额外的包信息,就会是空列表),会添加@EnableFeignClients注解的类所在的包;但是收集的结果不为空就不会填加注解的类所在的包。
也就是说,如果不配置package,就会自动扫描@EnableFeignClients注解的类所在的包。如果手动配置了package,就会以配置为准,不再额外添加任何包。所以如果手动配置了当前工程外的package,记得还要配置当前工程的package。

1.2 注册Bean
之后,registerFeignClients()方法中会调用registerFeignClient方法生成BeanDefinition并注册进入registry中。
构建出的BeanDefinition中,beanClass属性是org.springframework.cloud.openfeign.FeignClientFactoryBean类,attributes中会记录被@FeignClient注解的类名,即定义了各个Feign调用接口的interface。

  1. 实例化Feign client
    调用栈如下
newInstance:64, ReflectiveFeign (feign)
target:269, Feign$Builder (feign)
target:30, DefaultTargeter (org.springframework.cloud.openfeign)
loadBalance:306, FeignClientFactoryBean (org.springframework.cloud.openfeign)
getTarget:335, FeignClientFactoryBean (org.springframework.cloud.openfeign)
getObject:315, FeignClientFactoryBean (org.springframework.cloud.openfeign)
doGetObjectFromFactoryBean:169, FactoryBeanRegistrySupport (org.springframework.beans.factory.support)
...
...

从栈底向上看,触发Feign client的实例化后,调用FeignClientFactoryBean类内的函数获取bean,大部分实例化的逻辑都在该类内完成。入口是FeignClientFactoryBean的getObject()方法,该方法来自于Spring的FactoryBean接口,然后调用getTarget()方法。
2.1 getTarget()方法
首先调用feign()方法构建feign.Feign.Builder实例。
然后会调用loadBalance()方法构建最终的bean。
2.2 loadBalance()方法
该方法内首先获取实现了feign.Client接口的实例,默认为org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient类的对象。
默认情况下,该对象的

  • delegate属性为feign.Client.Default
  • loadBalancerClient属性为org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient

然后获取实现了org.springframework.cloud.openfeign.Targeter接口的实例,默认为org.springframework.cloud.openfeign.DefaultTargeter类的对象。
最后调用Targeter.target()方法构建最终的bean。

2.3 Targeter.target()方法
该方法内部使用2.1中获取的feign.Feign.Builder实例,调用它的feign.Feign.Builder#target(feign.Target<T>)方法构建最终的bean。
2.4 Feign.Builder.target()方法。
该方法内部首先调用feign.Feign.Builder.build()方法构造继承了抽象类feign.Feignfeign.ReflectiveFeign,构造的过程主要是属性的赋值。
然后调用feign.ReflectiveFeign.newInstance()方法构建最终的bean。

2.5 ReflectiveFeign.newInstance()方法
首先使用feign.Contract分析@GetMapping()等注解,构造Map<Method, MethodHandler>这样一个map。
然后构造出实例feign.ReflectiveFeign.FeignInvocationHandler,该实例实现了InvocationHandler接口,最终使用java的动态代理机制,构建出最终的bean。
动态代理的逻辑在feign.ReflectiveFeign.FeignInvocationHandler类中。

Client的调用

我们已经知道Feign client的具体实现使用的是java的动态代理机制。
方法调用的内部逻辑在feign.ReflectiveFeign.FeignInvocationHandler.invoke()方法中。
FeignInvocationHandler对象中的dispatch属性就是Client的实例化小节的2.5中提到的Map<Method, MethodHandler>,然后通过查询此map,找到对应的MethodHandler,并调用。
默认情况下调用的是feign.SynchronousMethodHandler.invoke()方法。
该方法中会先构建feign.RequestTemplate对象,该对象内包含的是http请求的报文内容,如"GET /echo?hi=aa HTTP/1.1"等信息。
然后构建feign.Request.Options对象,该对象内包含的是connectTimeoutreadTimeout等。
接下来调用feign.SynchronousMethodHandler.executeAndDecode()方法。
该方法内部调用org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient.execute()方法执行http请求。
该方法内部会调用org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.choose()方法选择一个合适的服务提供方实例。

注意
需要引入spring-cloud-starter-netflix-eureka-client依赖,RoundRobinLoadBalancer才能使用EurekaDiscoveryClient来获取eureka中注册的服务列表。

LoadBalancerClient中获取到ServiceInstance对象,该对象中包含具体的服务提供者ip:port,然后将请求url中的服务名替换为ip:port。

比如
BlockingLoadBalancerClient.choose() => RoundRobinLoadBalancer#choose => CachingServiceInstanceListSupplier.get() => CachingServiceInstanceListSupplier#serviceInstances
CachingServiceInstanceListSupplier#serviceInstancesreactor api。先从cache取,
DefaultLoadBalancerCacheDefaultLoadBalancerCache#evictMs默认35秒的逐出时间。
如果cache miss,则是.onCacheMissResume(delegate.get().take(1))delegate.get() => DiscoveryClientServiceInstanceListSupplier#get => DiscoveryClientServiceInstanceListSupplier#serviceInstances
DiscoveryClientServiceInstanceListSupplier#serviceInstances又是一个reactor api。调用delegate.getInstances(serviceId),也就是CompositeDiscoveryClient#getInstances => EurekaDiscoveryClient#getInstances => DiscoveryClient#getInstancesByVipAddress() => DiscoveryClient#localRegionApps

DiscoveryClient#localRegionApps 这里eureka的服务端、客户端逻辑均一致,都是保存着全量的微服务实例信息。然后通过Applications#virtualHostNameAppMap这个map获取实例地址信息。

接下来调用org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing()方法向服务端发送请求。
该方法内部默认情况下调用feign.Client.Default.execute()方法执行请求,这个默认的client不会维护连接池,使用最基本的jdk中的URL.openConnection()方法获得java.net.HttpURLConnection对象。然后调用HttpURLConnection.getResponseCode()方法实际建立连接并获得返回值,此方法内部会调用java.net.URLConnection.connect()方法。

HttpURLConnection类继承了java.net.URLConnection抽象类,调用URLConnection.connect()方法才会实际建立http连接。

spring-cloud-loadbalancer中的LoadBalancerClientFactory.contexts是一个map,key是下游服务名称,value是一个的ApplicationContext,相当于每个服务对应一个单独的ApplicationContext
feign client的调用最好预热,因为在第一次feign调用时,调用LoadBalancerClientFactory#getContext,这里才会第一次创建远程服务对应的ApplicationContext NamedContextFactory#createContext

Feign 中的 DiscoveryClient

初始化时获取全量服务信息。

getApplications:135, RestTemplateEurekaHttpClient (org.springframework.cloud.netflix.eureka.http)
execute:137, EurekaHttpClientDecorator$6 (com.netflix.discovery.shared.transport.decorator)
executeOnNewServer:121, RedirectingEurekaHttpClient (com.netflix.discovery.shared.transport.decorator)
execute:80, RedirectingEurekaHttpClient (com.netflix.discovery.shared.transport.decorator)
getApplications:134, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
execute:137, EurekaHttpClientDecorator$6 (com.netflix.discovery.shared.transport.decorator)
execute:120, RetryableEurekaHttpClient (com.netflix.discovery.shared.transport.decorator)
getApplications:134, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
execute:137, EurekaHttpClientDecorator$6 (com.netflix.discovery.shared.transport.decorator)
execute:77, SessionedEurekaHttpClient (com.netflix.discovery.shared.transport.decorator)
getApplications:134, EurekaHttpClientDecorator (com.netflix.discovery.shared.transport.decorator)
getAndStoreFullRegistry:1101, DiscoveryClient (com.netflix.discovery)
fetchRegistry:1014, DiscoveryClient (com.netflix.discovery)
<init>:441, DiscoveryClient (com.netflix.discovery)
<init>:283, DiscoveryClient (com.netflix.discovery)
<init>:279, DiscoveryClient (com.netflix.discovery)
<init>:66, CloudEurekaClient (org.springframework.cloud.netflix.eureka)
eurekaClient:290, EurekaClientAutoConfiguration$RefreshableEurekaClientConfiguration (org.springframework.cloud.netflix.eureka)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
instantiate:154, SimpleInstantiationStrategy (org.springframework.beans.factory.support)
instantiate:653, ConstructorResolver (org.springframework.beans.factory.support)
instantiateUsingFactoryMethod:638, ConstructorResolver (org.springframework.beans.factory.support)
instantiateUsingFactoryMethod:1336, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBeanInstance:1179, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:571, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:531, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$1:374, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 977972728 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$844)
getBean:381, GenericScope$BeanLifecycleWrapper (org.springframework.cloud.context.scope)
get:184, GenericScope (org.springframework.cloud.context.scope)
doGetBean:371, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:208, AbstractBeanFactory (org.springframework.beans.factory.support)
getTarget:35, SimpleBeanTargetSource (org.springframework.aop.target)
getTargetObject:127, EurekaRegistration (org.springframework.cloud.netflix.eureka.serviceregistry)
getEurekaClient:115, EurekaRegistration (org.springframework.cloud.netflix.eureka.serviceregistry)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethod:282, ReflectionUtils (org.springframework.util)
invoke:490, GenericScope$LockedScopedProxyFactoryBean (org.springframework.cloud.context.scope)
proceed:186, ReflectiveMethodInvocation (org.springframework.aop.framework)
proceed:749, CglibAopProxy$CglibMethodInvocation (org.springframework.aop.framework)
intercept:691, CglibAopProxy$DynamicAdvisedInterceptor (org.springframework.aop.framework)
getEurekaClient:-1, EurekaRegistration$$EnhancerBySpringCGLIB$$987ad7f5 (org.springframework.cloud.netflix.eureka.serviceregistry)
maybeInitializeClient:54, EurekaServiceRegistry (org.springframework.cloud.netflix.eureka.serviceregistry)
register:38, EurekaServiceRegistry (org.springframework.cloud.netflix.eureka.serviceregistry)
start:83, EurekaAutoServiceRegistration (org.springframework.cloud.netflix.eureka.serviceregistry)
doStart:178, DefaultLifecycleProcessor (org.springframework.context.support)
access$200:54, DefaultLifecycleProcessor (org.springframework.context.support)
start:356, DefaultLifecycleProcessor$LifecycleGroup (org.springframework.context.support)
accept:-1, 955875766 (org.springframework.context.support.DefaultLifecycleProcessor$$Lambda$843)
forEach:75, Iterable (java.lang)
startBeans:155, DefaultLifecycleProcessor (org.springframework.context.support)
onRefresh:123, DefaultLifecycleProcessor (org.springframework.context.support)
finishRefresh:940, AbstractApplicationContext (org.springframework.context.support)
refresh:591, AbstractApplicationContext (org.springframework.context.support)
refresh:144, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:767, SpringApplication (org.springframework.boot)
refresh:759, SpringApplication (org.springframework.boot)
refreshContext:426, SpringApplication (org.springframework.boot)
run:326, SpringApplication (org.springframework.boot)
run:1309, SpringApplication (org.springframework.boot)
run:1298, SpringApplication (org.springframework.boot)
......

Feign 中的DiscoveryClient内部不像server,不使用 jersey 发送http请求。默认在org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient中使用RestTemplate发送请求。

DiscoveryClient 初始化时调用"/eureka/apps/"获取全量微服务信息。
运行中默认每隔30秒发送"/eureka/apps/delta"和心跳信息。

关键的断点位置

  1. Eureka Server接受请求
    jersey内部
    com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider.ResponseOutInvoker#_dispatch
    log使用以下代码
if(context instanceof WebApplicationContext){     WebApplicationContext c = (WebApplicationContext)context;     return LocalTime.now() + " receive: " + c.request.method + " " +c.request.requestUri.toString(); }
  1. Eureka Server 发送请求
    jersey内部
    com.sun.jersey.api.client.WebResource#handle(java.lang.Class<T>, com.sun.jersey.api.client.ClientRequest)
    log使用以下代码
LocalTime.now() + " send: " + ro.getMethod() + " " + ro.getURI() + " " + (ro.getEntity() == null? "" : new JsonMapper().writeValueAsString(ro.getEntity()))
  1. Feign 中的DiscoveryClient发送请求
    默认Feign 中的DiscoveryClient使用RestTemplate发送请求。
    org.springframework.web.client.RestTemplate#doExecute
    log使用以下代码
LocalTime.now() + " send: " + url
  1. Feign client 发送请求
    可以在这个函数内
    feign.SynchronousMethodHandler#executeAndDecode打断点。
    这一行代码会执行http请求,这里的client的具体实现由配置决定
    response = client.execute(request, options);

多个缓存机制

后文 "一些问题" 中的 "手动摘除实例流量,需要很久才能生效" 一节总结了会遇到的多级缓存。

一些问题

  1. 由于与eureka共用客户端代码,每个微服务的内存中都保留着全量的注册中心微服务实例信息,无论自己是否会调用到那些服务,浪费内存
  1. 手动摘除实例流量,需要很久才能生效
    通过"/eureka/apps/{appName}/{instanceId}/status?value=OUT_OF_SERVICE"api从Eureka手动摘除实例流量,该实例可能需要很久才没有新流量流入。
    因为
  • Feign Client是每隔一段时间轮询eureka的"/eureka/apps/delta"接口,才能感知到服务状态变化,eureka没有主动推送的机制

  • eureka server 以及 Feign Client 内部都有多个缓存机制,导致服务状态变化生效时间进一步延长

  • 发送 "/eureka/apps/{appName}/{instanceId}/status?value=OUT_OF_SERVICE" 请求,AbstractInstanceRegistry#registry内的实例状态,立刻变更,ResponseCacheImpl#readWriteCacheMap缓存立刻主动失效。

  • ResponseCacheImpl#getCacheUpdateTask默认每隔30秒,从readWriteCacheMapResponseCacheImpl#readOnlyCacheMap更新一次信息。这个环节最多30秒延迟。

  • Feign client中的DiscoveryClient默认每隔30秒执行一次com.netflix.discovery.DiscoveryClient.CacheRefreshThread,向eureka server 发送"/eureka/apps/delta"请求,读取readOnlyCacheMap中的信息。这个环节最多30秒延迟。

  • Feign Client 优先从DefaultLoadBalancerCache中读取实例信息,默认35秒的缓存逐出时间。这个环节最多35秒延迟。

全部配置为默认值的情况下,最终可能有长达95秒的延迟生效时间。

  1. RefreshScope.refreshAll触发DiscoveryClient销毁并重建
    一些配置中心,如apollo和nacos,可能会使用RefreshScope.refreshAll机制热更新配置字段。
    DiscoveryClient也是可以被Refresh的。因此会触发DiscoveryClient.shutdown和重新实例化。
    DiscoveryClient.shutdown内部会在本地将自己设置为 DOWN 状态。
    DiscoveryClient会在数十毫秒内向eureka注册中心连续发送 deregister 和 register消息。deregister是将自己从实例列表中删除。

生产实践中,会发生eureka注册中心显示部分实例(小部分,约小于三分之一)变为 DOWN 状态。
但自行本地测试时未能复现,不知道是否和某个版本有关。DiscoveryClient.shutdown内部只会在本地将自己设置为 DOWN 状态,不会将这个状态发送给server。看代码也是先停止心跳任务,在设置 DOWN 状态。deregister 消息也只是把自己从registry中直接删除,不会变更状态。从代码逻辑中粗看也没找到eureka注册中心会显示 DOWN 状态的可能性。但生产中确实会出现,且出现概率较高。暂时没能从代码逻辑层面找到原因。

临时解决方法就是不使用RefreshScope.refreshAll的方式更新配置,配置中心都有其他的方式实现配置热更新,比如直接修改被注解字段的值。

还可以从配置上关闭DiscoveryClient的refresh功能,或者使用RefreshScope.refresh(String name)。但这两种方式都会触发RefreshScopeRefreshedEvent,进而触发EurekaDiscoveryClientConfiguration.EurekaClientConfigurationRefresher#onApplicationEvent。这里虽然不会触发DiscoveryClient销毁并重建,但也会在本地将自己设置为 DOWN 状态,然后再次设置为 UP 状态。不确定这种方式是否还会导致eureka注册中心显示部分实例为 DOWN 状态,没测试过,不清楚。

  1. Feign客户端的注册时机
    ServletWebServerApplicationContext#finishRefresh
    先执行Lifecycle.start(),在执行WebServer webServer = startWebServer();
    实现了Lifecycle接口EurekaAutoServiceRegistration,在Lifecycle.start()中让新线程开始异步向eureka注册中心注册当前实例。
    但之后才执行的WebServer webServer = startWebServer();中tomcat才会开始监听http请求端口。
    新线程的注册任务的开启是早于tomcat监听端口。
    由于是两个线程同时进行,所以会有概率先注册至注册中心,再开始本地监听端口。
    不过实际中由于eureka注册中心的机制,其他调用方会稍后才能发现新实例,导致实践中较少遇到问题。但如果使用一个反应极其灵敏的注册中心,feign客户端的这个问题出现的概率就会提高。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。