【源码】Spring Cloud —— Eureka Server 1 服务注册、服务续约、服务下线、服务剔除
前言
Spring Cloud,基于 Spring Boot 实现的云应用开发工具,其下有(支持)大量的子工程,提供了 微服务 架构下的各种 组件,比如
- 服务注册与发现组件:Eureka、Zookeeper、Consul 等
- 服务调用组件:Ribbon、OpenFeign、Hystrix 等
- 路由和过滤组件:Zuul、Spring Cloud Gateway 等
- 配置中心组件:Spring Cloud Config
- 消息组件:Spring Cloud Stream、Spring Cloud Bus
- 安全控制组件:Spring Cloud Security
- 链路监控组件:Spring Cloud Sleuth
- 其他
本系列章节结合源码解读 Spring Cloud Eureka Server 组件,该组件提供了 服务注册中心 的功能
版本
Spring Cloud Netflix 版本:2.2.3.RELEASE
对应 Netflix-Eureka 版本:1.9.21
核心类
我们从 顶层接口 出发,先大体了解所有相关的 接口 和 类
LookupService
该接口提供对 服务实例 进行检索的相关方法,因为 Eureka Server 同时也可以是一个 Eureka Client
LeaseManager
该接口提供了 服务注册、服务续约、服务下线、服务剔除 等方法
LeaseManager 管理的对象时 Lease,Lease 代表一个 Eureka Client 服务实例信息的租约
InstanceRegistry
该接口继承了 LookupService 和 LeaseManager,拓展了一些方法可以更为简单地管理 服务实例租约 和查询注册表中的 服务实例信息
PeerAwareInstanceRegistry
该接口继承了 InstanceRegistry,在其基础上添加了 Eureka Server 集群同步的操作
AbstractInstanceRegistry
InstanceRegistry 的抽象实现类,十分重要,我们会在后文重点解读
PeerAwareInstanceRegistryImpl
继承了 AbstractInstanceRegistry,同时实现了 PeerAwareInstanceRegistry,在对 本地注册表 操作的基础上添加了对其 peer
节点的 同步复制 操作,使得 Eureka Server 集群中的注册表信息保持一致
InstanceRegistry
以上的类都由 com.netflix.eureka
提供,而 InstanceRegistry 则是 Spring Cloud 对 PeerAwareInstanceRegistryImpl 的拓展,以适配 Spring Cloud
类图
图中 InstanceRegistry 接口以
netflixInstanceRegistry 标识,
因为该接口由 com.netflix.eureka 提供
接下来我们结合源码对其中的若干类、方法进行解读,以了解 Eureka Server 功能的实现
AbstractInstanceRegistry
AbstractInstanceRegistry 提供了 服务注册、服务续约、服务下线、服务剔除 的实现
服务注册
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
// 读锁
read.lock();
/**
* registrant:
* key 集群名称 value gMap
* gMap:
* key 实例Id value 实例(InstanceInfo)
*/
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
// 获取实例
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// 如果实例存在
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
// registrant 取时间戳大的
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
// ...
registrant = existingLease.getHolder();
}
} else {
// 更新自我保护阈值
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
// ...
}
// 创建新的 Lease
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
// 上线时间
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 保存
gMap.put(registrant.getId(), lease);
// recentRegisteredQueue 用于调试、统计
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
// 状态更新等,略
} finally {
read.unlock();
}
}
用 ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
来维护实例信息,key
为 集群名称,value
是一个 Map<String, Lease<InstanceInfo>> gMap
,gMap
以 key
为 实例ID,value
为 InstanceInfo 维护着实例信息
服务续约
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
// gMap 获取
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
// Lease 获取
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = gMap.get(id);
}
// 如果没找到租约,则返回 false
if (leaseToRenew == null) {
RENEW_NOT_FOUND.increment(isReplication);
// ...
return false;
} else {
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
// 实例状态为 UNKNOWN,返回 false
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
// ...
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
// 状态更新
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
// ...
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
renewsLastMin.increment();
// 续约
leaseToRenew.renew();
return true;
}
}
根据 Eureka Client 发送的 appName
和 id
给 服务实例 续约
服务下线
@Override
public boolean cancel(String appName, String id, boolean isReplication) {
return internalCancel(appName, id, isReplication);
}
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
// 读锁
read.lock();
CANCEL.increment(isReplication);
// gMap 获取
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
// 移除指定 id 的实例
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
// ...
// leaseToCancel 不存在则返回 false
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
// ...
return false;
} else {
// 租约下线
leaseToCancel.cancel();
InstanceInfo instanceInfo = leaseToCancel.getHolder();
String vip = null;
String svip = null;
if (instanceInfo != null) {
// 标识实例状态为 DELETED,用于增量更新
instanceInfo.setActionType(ActionType.DELETED);
recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
// 缓存清除
invalidateCache(appName, vip, svip);
// ...
}
} finally {
read.unlock();
}
// 更新自我保护阈值
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
updateRenewsPerMinThreshold();
}
}
return true;
}
Eureka Client 在应用销毁时,会向 Eureka Server 发送服务下线请求,清除注册表中关于本应用的 租约,避免无效的服务调用
服务剔除
@Override
public void evict() {
evict(0l);
}
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 判断是否允许剔除(比如:自我保护机制是否开启)
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 获取所有过期的 Lease
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
/**
* 此处为了避免大规模的服务下线
* 会取下线服务阈值(总服务 - 总服务 * 在线服务百分比阈值)
* 和 当前过期服务数 中的更小值,
* 以此值进行随机的服务下线,调用 internalCancel 方法
*
*/
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
// ...
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
// 剔除
internalCancel(appName, id, false);
}
}
}
为了避免服务大规模的下线,保证 可用性,Eureka Server 会分批下线过期的服务,自我保护 期间不会剔除实例,AbstractInstanceRegistry 定义了 evictionTimer
定时进行 服务剔除
总结
AbstractInstanceRegistry 实现了 服务注册、服务续约、服务下线、服务剔除 的主要逻辑,下章节解读 Eureka Server 关于 服务集群 的相关操作
下一篇:【源码】Spring Cloud —— Eureka Server 2 集群相关
参考
《Spring Cloud 微服务架构进阶》 —— 朱荣鑫 张天 黄迪璇