总览
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.EurekaClientConfigBean
和org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean
两个配置实例。
分别对应了eureka.client
和eureka.instance
的相关配置。eureka.client
内的配置是作为client的一些行为相关的配置,如server地址,是否向server注册本client等。eureka.instance
内的配置是作为instance的一些相关配置,如本实例的instanceId,hostname等。
客户端
作为client,向server发送请求时,核心类是DiscoveryClient
- 程序启动后,实例化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)
- heartbeat (默认30秒后执行一次,然后每隔30秒执行一次)
// 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端口还不可用,最终导致服务的启动不平滑
- 发送请求的http client默认是
com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient
继承了com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient
抽象类
实现了com.netflix.discovery.shared.transport.EurekaHttpClient
接口
- 发送http请求时的{ip:port}选择
多会经过com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute
,这里candidateHosts = getHostCandidates();
函数确定向哪个{ip:port}发送请求,这里是没有轮询选择域名的逻辑,只会从第一个开始尝试发送,失败时才会选择下一个。
所以实践中eureka server应配置成域名。如果手动配置为多个{ip:port},RetryableEurekaHttpClient#execute
函数内将只选择配置的第一个{ip:port}进行请求,会发生流量倾斜。
- 使用 /eureka/apps/ 获取全量数据,使用 /eureka/apps/delta 获取增量数据
服务端
作为server,接受请求时,内部数据主要来源于InstanceRegistry
使用 /eureka/peerreplication/batch/ 向其它server实例批量广播变更数据
- 启动后
EurekaController
负责接收Eureka网页请求
eureka的web页面由EurekaController#status
生成,通过com.netflix.eureka.registry.AbstractInstanceRegistry#registry
获取微服务信息,使用templates/eureka/status.ftlh
生成网页 - 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.Key
。generatePayload
方法内部根据不同的Key执行不同的加载逻辑。
ALL_APPS => InstanceRegistry#getApplications()
ALL_APPS_DELTA => InstanceRegistry#getApplicationDeltas
单独某个应用 => InstanceRegistry#getApplication(java.lang.String)
接口
/eureka/apps/
拉取实例信息列表
http method: GET
- 发送方
调用链为依次为
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
中。
- 接收方
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
- 发送方
调用链为依次为
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}
之后这些字段会被人工配置值覆盖掉。
- 接收方
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
- 发送方
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
- 接收方
依次匹配
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操作
- 发送方
可以人工调用
- 接收方
依次匹配
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
- 发送方
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接口中传来的信息直接替换。
- 接收方
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调用方"一节描述。
- 状态变更
调用栈如下
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
,也就是发送
- 心跳
调用栈如下
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
- 发送方
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
中实时的这个实例的信息。
- 接收方
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的逻辑为,将开发者在@EnableFeignClients
的value
,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。
- 实例化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.Feign
的feign.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
对象,该对象内包含的是connectTimeout
和readTimeout
等。
接下来调用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#serviceInstances
是reactor
api。先从cache取,
DefaultLoadBalancerCache
中DefaultLoadBalancerCache#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"和心跳信息。
关键的断点位置
- 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(); }
- 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()))
- Feign 中的
DiscoveryClient
发送请求
默认Feign 中的DiscoveryClient
使用RestTemplate
发送请求。
org.springframework.web.client.RestTemplate#doExecute
log使用以下代码
LocalTime.now() + " send: " + url
- Feign client 发送请求
可以在这个函数内
feign.SynchronousMethodHandler#executeAndDecode
打断点。
这一行代码会执行http请求,这里的client
的具体实现由配置决定
response = client.execute(request, options);
多个缓存机制
后文 "一些问题" 中的 "手动摘除实例流量,需要很久才能生效" 一节总结了会遇到的多级缓存。
一些问题
- 由于与eureka共用客户端代码,每个微服务的内存中都保留着全量的注册中心微服务实例信息,无论自己是否会调用到那些服务,浪费内存
- 手动摘除实例流量,需要很久才能生效
通过"/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秒,从readWriteCacheMap
至ResponseCacheImpl#readOnlyCacheMap
更新一次信息。这个环节最多30秒延迟。Feign client中的
DiscoveryClient
默认每隔30秒执行一次com.netflix.discovery.DiscoveryClient.CacheRefreshThread
,向eureka server 发送"/eureka/apps/delta"请求,读取readOnlyCacheMap
中的信息。这个环节最多30秒延迟。Feign Client 优先从
DefaultLoadBalancerCache
中读取实例信息,默认35秒的缓存逐出时间。这个环节最多35秒延迟。
全部配置为默认值的情况下,最终可能有长达95秒的延迟生效时间。
-
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 状态,没测试过,不清楚。
- Feign客户端的注册时机
ServletWebServerApplicationContext#finishRefresh
中
先执行Lifecycle.start()
,在执行WebServer webServer = startWebServer();
实现了Lifecycle
接口EurekaAutoServiceRegistration
,在Lifecycle.start()
中让新线程开始异步向eureka注册中心注册当前实例。
但之后才执行的WebServer webServer = startWebServer();
中tomcat才会开始监听http请求端口。
新线程的注册任务的开启是早于tomcat监听端口。
由于是两个线程同时进行,所以会有概率先注册至注册中心,再开始本地监听端口。
不过实际中由于eureka注册中心的机制,其他调用方会稍后才能发现新实例,导致实践中较少遇到问题。但如果使用一个反应极其灵敏的注册中心,feign客户端的这个问题出现的概率就会提高。