前言
上一篇文章介绍了Eureka Client
端的相关源码。这篇文章我们学习Eureka Server
是如何存储Client
注册过来的实例信息,以及Server
端如何与Client
端续约。相对于Client
端来说,Server
端要简单一些。
Eureka Server 启动
我们可以发现EurekaServerAutoConfiguration
类导入了EurekaServerInitializerConfiguration
@Import(EurekaServerInitializerConfiguration.class)
观察EurekaServerInitializerConfiguration
发现它也实现了SmartLifecycle
接口,在它的start()
中进行了初始化
initEurekaEnvironment();initEurekaServerContext();
这里主要是一些环境、基础信息的初始化,以及启动了一个定时剔除未发送心跳的服务实例任务EvictionTask
。
事件发布
我们还可以观察到在EurekaServerInitializerConfiguration.start()
方法中发布了两个事件EurekaRegistryAvailableEvent、EurekaServerStartedEvent
这和Client
端的初始化类似。实际上几乎所有组件在启动、销毁等一些关键节点都会发布一些事件,便于我们去扩展,如果我们想在某个节点做某些事,只需要监听该事件写我们自己的处理逻辑即可。
Client 实例注册入口
ApplicationResource.addInstance()
是客户端实例注册请求的入口,这里首先做了一些实例信息校验然后调用实例注册器的register()
方法开始注册
registry.register(info, "true".equals(isReplication));
该方法内部首先发布了一个EurekaInstanceRegisteredEvent
事件,随后开始调用AbstractInstanceRegistry.register()
开始执行真正的注册逻辑。该类维护了一个保存服务实例信息的ConcurrentHashMap
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
外层的key -> appName
,内层的key -> instanceId
。每次过来一个Client
注册请求,就会更新这个registry
。
在更新完AbstractInstanceRegistry.registry
之后有一行很重要的代码
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
从这个方法的名字我们可以猜到,失效缓存。点进去之后我们发现了一个类ResponseCacheImpl
,这个类有两个很重要的成员变量
private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();private final LoadingCache<Key, Value> readWriteCacheMap;
等会我们会提到这两个 Map。
Client 实例拉取入口
在 Client 篇中,我们提到了实例注册到Eureka Server
的同时会从Server
拉取其他微服务实例信息,而入口就在ApplicationsResource.getContainers()
,我们可以看到一段代码
Response response;if (acceptEncoding != null && acceptEncoding.contains(HEADER_GZIP_VALUE)) {response = Response.ok(responseCache.getGZIP(cacheKey)).header(HEADER_CONTENT_ENCODING, HEADER_GZIP_VALUE).header(HEADER_CONTENT_TYPE, returnMediaType).build();}
responseCache
是个接口,通过 debug 发现它的实现类就是ResponseCacheImpl
,这里查看getGZIP()
源码
Value getValue(final Key key, boolean useReadOnlyCache) {Value payload = null;try {if (useReadOnlyCache) {final Value currentPayload = readOnlyCacheMap.get(key);if (currentPayload != null) {payload = currentPayload;} else {payload = readWriteCacheMap.get(key);readOnlyCacheMap.put(key, payload);}} else {payload = readWriteCacheMap.get(key);}} catch (Throwable t) {logger.error("Cannot get value for key : {}", key, t);}return payload;}
这里逻辑很简单,如果readOnlyCacheMap
没有就从readWriteCacheMap
拿,然后更新到readOnlyCacheMap
。既然如此肯定有个地方去更新readWriteCacheMap
。很遗憾我找了很久都没有找到在哪更新的,迷茫之际我无意中发现readWriteCacheMap
并不是一个单纯的ConCurrentHashMap
,而是一个第三方缓存组件,本着多年 Redis 缓存中间件的使用流程的经验,我猜测也许是get()
发现没有这个值的时候会自动塞进去,于是我找到了readWriteCacheMap
初始化的代码。ResponseCacheImpl
的构造方法中
this.readWriteCacheMap =CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache()).expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS).removalListener(new RemovalListener<Key, Value>() {@Overridepublic void onRemoval(RemovalNotification<Key, Value> notification) {Key removedKey = notification.getKey();if (removedKey.hasRegions()) {Key cloneWithNoRegions = removedKey.cloneWithoutRegions();regionSpecificKeys.remove(cloneWithNoRegions, removedKey);}}}).build(new CacheLoader<Key, Value>() {@Overridepublic Value load(Key key) throws Exception {if (key.hasRegions()) {Key cloneWithNoRegions = key.cloneWithoutRegions();regionSpecificKeys.put(cloneWithNoRegions, key);}Value value = generatePayload(key);return value;}});
我在build()
打了个断点,发现果然是每次get()
的时候发现没有这个值就会走到这塞进去,那么是从哪里取值放进缓存的呢?查看generatePayload()
方法发现最终取值的地方就是我们上面说的AbstractInstanceRegistry.registry
。
也就是说他们三个的关系是readOnlyCacheMap → readWriteCacheMap → registry
。
现在有一个问题,就是readOnlyCacheMap
他是一个 Map 集合,元素不会自动过期,而readWriteCacheMap
是一个LoadingCache
,从初始化代码会发现,这个缓存时间是180S
后自动过期。那么我们可以猜测应该有一个定时任务定时置空readOnlyCacheMap
。
随后在该构造方法中还发现了一个定时任务getCacheUpdateTask()
,这里会根据默认配置每隔30S
将readWriteCacheMap
同步到readOnlyCacheMap
。
缓存之间的数据同步
现在我们来整理一下两个缓存readOnlyCacheMap、readWriteCacheMap
和一个注册表AbstractInstanceRegistry.registry
之间的数据同步关系。
微服务请求Eureka Server
注册,将数据存在registry
每次有微服务注册的时候会失效readWriteCacheMap
(因为它缓存的 key 不是具体微服务名,而是ALL_APPS、ALL_APPS_DELTA
)其他微服务拉取实例首先从readOnlyCacheMap
查readOnlyCacheMap
没有,从readWriteCacheMap
查,并将数据写到readOnlyCacheMap
readWriteCacheMap
也没有,从registry
查,并将数据写到readWriteCacheMap,readOnlyCacheMap
定时任务每隔30S
定时从readWriteCacheMap
同步数据到readOnlyCacheMap
Eureka Client 的感知
从上面的内容我们可以知道这些实例信息的数据并不能保证是实时正确的,这也正好反应了Eureka
是 AP 原则,对于强一致性是不支持的。我们可以计算服务上下线被感知到的临界时间
前面两个好理解。现在我们看在非正常下线的时候由于我们是每60S
定时剔除90S
未续约的服务,最大需要三次定时任务扫描到,如下图
然后再加上最后readOnlyCacheMap(responseCacheUpdateIntervalMs = 30)
,总共210S
。
值得注意的是我们上面的表格只是单纯的服务发现的时间,但通常我们并不会直接关心这个时间,因为我们都是在服务间调用的时候才会涉及到获取其他微服务实例,这需要用到负载均衡器。
前面我们提到两种负载均衡器,Ribbon
和LoadBalancer
,它们都会将服务实例信息缓存一份,ribbon
是30S
,不过在新一代负载均衡器LoadBalancer
出来之后,我们大多更新到了LoadBalancer
,通常我喜欢用现在流行的内存缓存中间件caffeine
,在LoadBalancerCacheProperties
中caffeine
默认的过期时间是35S
,所以在上面的数据基础上还要加上负载均衡器的缓存时间。详细流程我会在后面的SpringCloud LoadBalancer
文章中介绍。
结语
相对于Eureka Client
来说,Server
要简单一些,我们重点需要关注服务上下线被延迟感知的临界时间,在这上面做优化,尽可能减少由此导致的微服务之间调用失败。
如果觉得《SpringCloud 微服务注册中心 Eureka - Server》对你有帮助,请点赞、收藏,并留下你的观点哦!