失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > SpringCloud 微服务注册中心 Eureka - Server

SpringCloud 微服务注册中心 Eureka - Server

时间:2022-02-28 07:45:58

相关推荐

SpringCloud 微服务注册中心 Eureka - Server

前言

上一篇文章介绍了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(),这里会根据默认配置每隔30SreadWriteCacheMap同步到readOnlyCacheMap

缓存之间的数据同步

现在我们来整理一下两个缓存readOnlyCacheMap、readWriteCacheMap和一个注册表AbstractInstanceRegistry.registry之间的数据同步关系。

微服务请求Eureka Server注册,将数据存在registry每次有微服务注册的时候会失效readWriteCacheMap(因为它缓存的 key 不是具体微服务名,而是ALL_APPS、ALL_APPS_DELTA)其他微服务拉取实例首先从readOnlyCacheMapreadOnlyCacheMap没有,从readWriteCacheMap查,并将数据写到readOnlyCacheMapreadWriteCacheMap也没有,从registry查,并将数据写到readWriteCacheMap,readOnlyCacheMap定时任务每隔30S定时从readWriteCacheMap同步数据到readOnlyCacheMap

Eureka Client 的感知

从上面的内容我们可以知道这些实例信息的数据并不能保证是实时正确的,这也正好反应了Eureka是 AP 原则,对于强一致性是不支持的。我们可以计算服务上下线被感知到的临界时间

前面两个好理解。现在我们看在非正常下线的时候由于我们是每60S定时剔除90S未续约的服务,最大需要三次定时任务扫描到,如下图

然后再加上最后readOnlyCacheMap(responseCacheUpdateIntervalMs = 30),总共210S

值得注意的是我们上面的表格只是单纯的服务发现的时间,但通常我们并不会直接关心这个时间,因为我们都是在服务间调用的时候才会涉及到获取其他微服务实例,这需要用到负载均衡器。

前面我们提到两种负载均衡器,RibbonLoadBalancer,它们都会将服务实例信息缓存一份,ribbon30S,不过在新一代负载均衡器LoadBalancer出来之后,我们大多更新到了LoadBalancer,通常我喜欢用现在流行的内存缓存中间件caffeine,在LoadBalancerCachePropertiescaffeine默认的过期时间是35S,所以在上面的数据基础上还要加上负载均衡器的缓存时间。详细流程我会在后面的SpringCloud LoadBalancer文章中介绍。

结语

相对于Eureka Client来说,Server要简单一些,我们重点需要关注服务上下线被延迟感知的临界时间,在这上面做优化,尽可能减少由此导致的微服务之间调用失败。

如果觉得《SpringCloud 微服务注册中心 Eureka - Server》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。