1.服务注册与发现
注册表数据结构
arduino
// <应用名,Map<实例ID,租约<实例信息>>>
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
//租约信息
public class Lease<T> {
//实例信息
private T holder;
//服务下线时间
private long evictionTimestamp;
//注册时间
private long registrationTimestamp;
//服务上线时间
private long serviceUpTimestamp;
//最新续约时间
private volatile long lastUpdateTimestamp;
private long duration;
}
//实例信息
public class InstanceInfo {
//实例ID
private volatile String instanceId;
//服务名称
private volatile String appName;
//分组名称
private volatile String appGroupName;
//服务ip地址
private volatile String ipAddr;
//服务端口
private volatile int port = DEFAULT_PORT;
... ...
}
注册
- 客户端发送一个 REST 请求到 Eureka 服务器,包含应用程序的元数据信息,如应用名称、主机地址、端口等。
- Eureka 服务器接收到注册请求后,给该应用程序生成一个租约信息,将租约信息、服务信息存储在注册表中。
发现
客户端应用程序会周期性地从 EurekaServer 拉取注册表信息,缓存到本地
拉取方式:
- 全量拉取:拉取注册表数据
- 增量拉取:增量数据维护recentlyChangedQueue队列中,定时任务每3分钟清除最新变更时间之前的数据
这两种方式都先尝试查询响应缓存(ResponseCache)
2.服务续约
一旦服务注册成功,Eureka 客户端会周期性地向 Eureka 服务器发送心跳续约请求,以表明该服务仍然处于运行状态。
- 客户端定期发送心跳请求,告知 Eureka 服务器该服务的存活状态。
- Eureka 服务器在接收到心跳请求后,更新最新续约时间。
3.服务下线
服务下线代码:
typescript
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
CANCEL.increment(isReplication);
//将服务冲注册表中移除
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
... ...
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
return false;
} else {
//租期取消
leaseToCancel.cancel();
... ...
//清除缓存
invalidateCache(appName, vip, svip);
}
} finally {
read.unlock();
}
... ...
return true;
}
下线方式
主动下线
调用EurekaServer接口强制某个服务下线
定时剔除
EurekaServer在启动时,会开启一个EvictionTask 定时任务,定期去清理租约过期的服务
定时剔除相关配置:
yaml
eureka:
server:
# 剔除过期服务的时间间隔
eviction-interval-timer-in-ms: 1000
# 自我保护开启
enable-self-preservation: true
# 自我保护-续约阈值
renewal-percent-threshold: 0.85
定时剔除时的自我保护机制
由于网络波动导致一部分服务短时间内无法续约,保护这部分服务不会被剔除,提高Eureka服务注册中心的可用性和稳定性。
自我保护生效要同时满足:
- enable-self-preservation = true
- 在续约期最后一分钟内,已经续约的服务数量低于 renewal-percent-threshold 这个比例(默认为 0.85,即 85%)
4.响应缓存
Eureka Server 将请求的响应结果缓存起来。这样,当下一个相同的请求到达时,可以直接返回缓存的响应,而无需再次处理请求。这样可以大大减少网络通信和服务器资源的消耗,提高性能和可伸缩性。
缓存分类
缓存以压缩和非压缩的形式维护三类请求:
- 所有应用
- 增量更改
- 单个应用
就网络流量而言,压缩后的效率比较高,尤其是在查询所有应用程序时。
go
//单个应用缓存
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.compact),
//所有应用缓存
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.compact),
//增量更改缓存
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.full),
new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.compact)
缓存实现
使用了两级缓存策略来处理响应。一个带有过期策略的读写缓存,以及一个不带过期的只读缓存。
一级缓存:readWriteCacheMap
- 缓存写入:从注册表中获取
- 缓存更新:发生注册、下线、服务状态变更时会清除缓存
二级缓存:readOnlyCacheMap
从一级缓存中同步
代码实现:
ini
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
//LoadingCache 是基于 Guava 缓存库的一种缓存框架
this.readWriteCacheMap =
CacheBuilder.newBuilder()
//配置缓存容量
.initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
//配置缓存过期时间
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
//配置缓存删除时的监听器
.removalListener(new RemovalListener<Key, Value>() {
@Override
public 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>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
//是否使用二级缓存,根据配置定期从一级缓存中同步
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
}
}
二级缓存配置
yaml
# 是否开启readOnly读缓存
use-read-only-response-cache: true
# readOnly 刷新时间间隔,默认为30秒
response-cache-update-interval-ms: 1000
5.集群同步
当服务有以下操作时会进行集群间数据同步:
- Cancel:取消(服务下线)
- Heartbeat:服务心跳
- Register:服务注册
- StatusUpdate:服务状态变更
- DeleteStatusOverride:删除缓存状态
typescript
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}