系列文章:
SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化
服务注册
实例信息注册器初始化
服务注册的代码位置不容易发现,我们看 DiscoveryClient 初始化调度任务的这个方法,这段代码会去初始化一个实例信息复制器 InstanceInfoReplicator,这个复制器就包含了实例的注册(明明是注册却叫 Replicator 感觉怪怪的)。
1、DiscoveryClient 初始化调度器的流程
- 先基于 DiscoveryClient、InstanceInfo 构造 InstanceInfoReplicator,然后还有两个参数为实例信息复制间隔时间(默认30秒)、并发的数量(默认为2)。
- 创建了一个实例状态变更监听器,并注册到 ApplicationInfoManager。当实例状态变更时,就会触发这个监听器,并调用 InstanceInfoReplicator 的
onDemandUpdate
方法。 - 启动 InstanceInfoReplicator,默认延迟40秒,也就是说服务启动可能40秒之后才会注册到注册中心(SpringCloud集成后是启动立即注册)。
private void initScheduledTasks() {
// 省略定时刷新注册表的任务...
if (clientConfig.shouldRegisterWithEureka()) {
// 省略定时心跳的任务...
// 实例信息复制器,用于定时更新自己状态,并向注册中心注册
instanceInfoReplicator = new InstanceInfoReplicator(this, instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
// 实例状态变更的监听器
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
// 通知实例重新注册
instanceInfoReplicator.onDemandUpdate();
}
};
// 向 ApplicationInfoManager 注册状态变更监听器
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 启动实例信息复制器,默认延迟时间40秒
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
复制代码
2、InstanceInfoReplicator 的构造方法
- 创建了一个单线程的调度器
- 设置
started
为 false - 创建了以分钟为单位的限流器,每分钟默认最多只能调度4次
InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
this.discoveryClient = discoveryClient;
this.instanceInfo = instanceInfo;
// 单线程的调度器
this.scheduler = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d").setDaemon(true).build());
this.scheduledPeriodicRef = new AtomicReference<Future>();
// started 设置为 false
this.started = new AtomicBoolean(false);
// 以分钟为单位的限流器
this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
// 间隔时间,默认为30秒
this.replicationIntervalSeconds = replicationIntervalSeconds;
this.burstSize = burstSize;
// 允许每分钟更新的频率 60 * 2 / 30 = 4
this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
}
复制代码
3、启动 InstanceInfoReplicator
- 将
started
设置为 true,代表已经启动了 - 调用
instanceInfo.setIsDirty()
方法,将实例设置为dirty=true
,并更新了最后一次设置 dirty 的时间戳 - InstanceInfoReplicator 实现了 Runnable,它本身被当成任务来调度,然后延迟40秒开始调度当前任务,并将 Future 放到本地变量中
public void start(int initialDelayMs) {
// 启动时 started 设置为 true
if (started.compareAndSet(false, true)) {
// 设置为 dirty,便于下一次心跳时同步到 eureka server
instanceInfo.setIsDirty();
// 延迟40秒后开始调度当前任务
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
// 将 Future 放到本地变量中
scheduledPeriodicRef.set(next);
}
}
public synchronized void setIsDirty() {
isInstanceInfoDirty = true;
lastDirtyTimestamp = System.currentTimeMillis();
}
复制代码
客户端实例注册
1、实现注册的run方法
接着看 InstanceInfoReplicator 的 run
方法,这个方法就是完成注册的核心位置。
- 首先会更新实例的信息,如果有变更就会设置
dirty=true
- 如过是 dirty 的,就会调用 DiscoveryClient 的
register
方法注册实例 - 实例注册后,就把 dirty 设置为 false
- 最后在 finally 中继续下一次的调度,默认是每隔
30秒
调度一次,注意他这里是把调度结果 Future 放到本地变量中
public void run() {
try {
// 更新本地实例信息,如果实例信息有变更,则 dirty=true
discoveryClient.refreshInstanceInfo();
// 设置为 dirty 时的时间戳
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 注册实例
discoveryClient.register();
// 设置 dirty=false
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 30秒之后再调度
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
复制代码
2、实例信息刷新
再来细看下 refreshInstanceInfo
刷新实例信息的方法:
- 首先刷新了数据中心的信息
- 然后刷新续约信息,主要就是将
EurekaClientConfig
的续约配置与本地的续约配置做对比,如果变更了就重新创建续约信息,并设置实例为 dirty。这种情况一般就是运行期间动态更新实例的配置,然后重新注册实例信息。 - 接着使用健康检查器检查实例健康状况,从
getHealthCheckHandler
这段代码进去不难发现,我们可以自定义健康检查器,例如当本地的一些资源未创建成功、某些核心线程池down了就认为实例不可用,这个时候就可以自定义健康检查器。如果没有自定义健康检查器,那就直接返回实例当前的状态。我们可以实现HealthCheckHandler
接口自定义健康检查器。 - 最后就会调用 ApplicationInfoManager 的
setInstanceStatus
设置实例状态,会判断如果状态发生变更,就会发出状态变更的通知,这样就会触发前面定义的状态变更监听器,然后调用 InstanceInfoReplicator 的onDemandUpdate
方法。
void refreshInstanceInfo() {
// 如果有必要,就更新数据中心的信息
applicationInfoManager.refreshDataCenterInfoIfRequired();
// 如果有必要,就更新续约信息,比如动态更新了配置文件,这时就更新续约信息 LeaseInfo,并将实例设置为 dirty
applicationInfoManager.refreshLeaseInfoIfRequired();
// 用监控检查器检查实例的状态
InstanceStatus status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
if (null != status) {
// 设置实例状态,实例状态变了会触发状态变更的监听器
applicationInfoManager.setInstanceStatus(status);
}
}
public void refreshLeaseInfoIfRequired() {
// 当前实例续约信息
LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
// 从配置中获取续约信息
int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();
// 如果续约信息变了,就重新创建续约信息,并设置实例为 dirty
if (leaseInfo.getDurationInSecs() != currentLeaseDuration || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) {
// 利用构造器模式创建 LeaseInfo
LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
.setRenewalIntervalInSecs(currentLeaseRenewal)
.setDurationInSecs(currentLeaseDuration)
.build();
instanceInfo.setLeaseInfo(newLeaseInfo);
instanceInfo.setIsDirty();
}
}
public HealthCheckHandler getHealthCheckHandler() {
HealthCheckHandler healthCheckHandler = this.healthCheckHandlerRef.get();
if (healthCheckHandler == null) {
// 可以自定义 HealthCheckHandler 实现健康检查
if (null != healthCheckHandlerProvider) {
healthCheckHandler = healthCheckHandlerProvider.get();
} else if (null != healthCheckCallbackProvider) {
// 可以自定义 HealthCheckCallback 实现健康检查,HealthCheckCallback 已过期,建议使用 HealthCheckHandler
healthCheckHandler = new HealthCheckCallbackToHandlerBridge(healthCheckCallbackProvider.get());
}
if (null == healthCheckHandler) {
// 没有自定义的就是用默认的桥接类
healthCheckHandler = new HealthCheckCallbackToHandlerBridge(null);
}
this.healthCheckHandlerRef.compareAndSet(null, healthCheckHandler);
}
return this.healthCheckHandlerRef.get();
}
public synchronized void setInstanceStatus(InstanceStatus status) {
InstanceStatus next = instanceStatusMapper.map(status);
// 如果状态变更了,才会返回之前的状态,然后触发状态变更监听器
InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
for (StatusChangeListener listener : listeners.values()) {
listener.notify(new StatusChangeEvent(prev, next));
}
}
}
复制代码
3、向 eureka-server 注册
在 run 方法里调用了 discoveryClient.register()
方法实现了客户端实例向注册中心的注册,进入到 register 方法可以看到,他就是使用前面构造的 EurekaTransport
来发起远程调用。
一层层进去,很容易发现就是调用了 eureka-server 的 POST /apps/{appName}
接口,后面我们就从 eureka-core 中找这个接口就可以找到注册中心实现服务注册的入口了。
boolean register() throws Throwable {
EurekaHttpResponse<Void> httpResponse;
// registrationClient => JerseyReplicationClient
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
public EurekaHttpResponse<Void> register(InstanceInfo info) {
// 调用的是 POST apps/{appName} 接口
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
// post 方法
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
response.close();
}
}
复制代码
4、注册中心设置实例状态为已启动
再回想下注册中心的初始化流程,在最后调用 openForTraffic
方法时,最后也会调用 ApplicationInfoManager 的 setInstanceStatus
方法,将实例状态设置为已启动,这个时候就会触发客户端注册到注册中心的动作。
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
复制代码
5、完成监听实例变更的方法
状态变更器会调用 onDemandUpdate
方法来完成实例状态变更后的逻辑。
- 它这里一个是用到了限流器来限制每分钟这个方法只能被调用 4 次,即避免了频繁的注册行为
- 然后在调度时,它会从本地变量中取出上一次调度的
Future
,如果任务还没执行完,它会直接取消掉 - 最后就是调用
run
方法,完成服务的注册
public boolean onDemandUpdate() {
// 限流控制
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
if (!scheduler.isShutdown()) {
scheduler.submit(new Runnable() {
@Override
public void run() {
// 如果上一次的任务还没有执行完,直接取消掉,然后执行注册的任务
Future latestPeriodic = scheduledPeriodicRef.get();
if (latestPeriodic != null && !latestPeriodic.isDone()) {
latestPeriodic.cancel(false);
}
InstanceInfoReplicator.this.run();
}
});
return true;
} else {
logger.warn("Ignoring onDemand update due to stopped scheduler");
return false;
}
} else {
logger.warn("Ignoring onDemand update due to rate limiter");
return false;
}
}
复制代码
6、限流器 RateLimiter
最后简单看下限流器 RateLimiter
的设计:
- 从它的注释中可以看出,eureka 的 RateLimiter 是基于令牌桶算法实现的限流器
acquire
方法有两个参数:burstSize
:允许以突发方式进入系统的最大请求数averageRate
:设置的时间窗口内允许进入的请求数
/**
* Rate limiter implementation is based on token bucket algorithm. There are two parameters:
* burst size - maximum number of requests allowed into the system as a burst
* average rate - expected number of requests per second (RateLimiters using MINUTES is also supported)
*/
public class RateLimiter {
private final long rateToMsConversion;
private final AtomicInteger consumedTokens = new AtomicInteger();
private final AtomicLong lastRefillTime = new AtomicLong(0);
@Deprecated
public RateLimiter() {
this(TimeUnit.SECONDS);
}
public RateLimiter(TimeUnit averageRateUnit) {
switch (averageRateUnit) {
case SECONDS:
rateToMsConversion = 1000;
break;
case MINUTES:
rateToMsConversion = 60 * 1000;
break;
default:
throw new IllegalArgumentException("TimeUnit of " + averageRateUnit + " is not supported");
}
}
public boolean acquire(int burstSize, long averageRate) {
return acquire(burstSize, averageRate, System.currentTimeMillis());
}
public boolean acquire(int burstSize, long averageRate, long currentTimeMillis) {
if (burstSize <= 0 || averageRate <= 0) { // Instead of throwing exception, we just let all the traffic go
return true;
}
refillToken(burstSize, averageRate, currentTimeMillis);
return consumeToken(burstSize);
}
private void refillToken(int burstSize, long averageRate, long currentTimeMillis) {
// 上一次填充 token 的时间
long refillTime = lastRefillTime.get();
// 时间差
long timeDelta = currentTimeMillis - refillTime;
// 固定生成令牌的速率,即每分钟4次
// 例如刚好间隔15秒进来一个请求,就是 15000 * 4 / 60000 = 1,newTokens 代表间隔了多少次,如果等于0,说明间隔不足15秒
long newTokens = timeDelta * averageRate / rateToMsConversion;
if (newTokens > 0) {
long newRefillTime = refillTime == 0
? currentTimeMillis
// 注意这里不是直接设置的当前时间戳,而是根据 newTokens 重新计算的,因为有可能同一周期内同时有多个请求进来,这样可以保持一个固定的周期
: refillTime + newTokens * rateToMsConversion / averageRate;
if (lastRefillTime.compareAndSet(refillTime, newRefillTime)) {
while (true) {
// 调整令牌的数量
int currentLevel = consumedTokens.get();
int adjustedLevel = Math.min(currentLevel, burstSize);
// currentLevel 可能为2,重置为了 0 或 1
int newLevel = (int) Math.max(0, adjustedLevel - newTokens);
if (consumedTokens.compareAndSet(currentLevel, newLevel)) {
return;
}
}
}
}
}
private boolean consumeToken(int burstSize) {
while (true) {
int currentLevel = consumedTokens.get();
// 突发数量为2,也就是允许15秒内最多有两次请求进来
if (currentLevel >= burstSize) {
return false;
}
if (consumedTokens.compareAndSet(currentLevel, currentLevel + 1)) {
return true;
}
}
}
public void reset() {
consumedTokens.set(0);
lastRefillTime.set(0);
}
}
复制代码
Eureka Server 接收注册请求
1、找到实例注册的API入口
从前面的分析中,我们知道服务端注册的API是 POST /apps/{appName}
,由于 eureka 是基于 jersey
来通信的,想找到API入口还是有点费劲的,至少没有 springmvc 那么容易。
先看 ApplicationsResource
这个类,可以找到 getApplicationResource
这个方法的路径是符合 /apps/{appName}
这个规则的。然后可以看到它里面创建了 ApplicationResource
,再进入到这个类里面,就可以找到 @Post
标注的 addInstance
方法,这就是注册的入口了。可以看到它是调用了注册表的 register
方法来注册实例的。
@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
// 符合规则 /apps/{appName}
@Path("{appId}")
public ApplicationResource getApplicationResource(@PathParam("version") String version, @PathParam("appId") String appId) {
// 真正的入口
return new ApplicationResource(appId, serverConfig, registry);
}
}
@Produces({"application/xml", "application/json"})
public class ApplicationResource {
private final PeerAwareInstanceRegistry registry;
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 调用注册表的注册方法来注册实例
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
}
复制代码
addInstance
接口有两个参数:
InstanceInfo
:服务实例,主要有两块数据:- 基本信息:主机名、IP地址、端口号、URL地址
- 租约信息:保持心跳的间隔时间、最近心跳的时间、服务注册的时间、服务启动的时间
isReplication
:这个参数是从请求头中取的,表示是否是在同步 server 节点的实例。在集群模式下,因为客户端实例注册到注册中心后,会同步到其它 server节点,所以如果是eureka-server之间同步信息,这个参数就为 true,避免循环同步。
2、实例注册
进入到注册表的 register
方法,可以看到主要就是调用父类的 register 方法注册实例,然后同步到 eureka server 集群中的其它 server 节点。集群同步放到后面来看,现在只需要知道注册实例时会同步到其它server节点即可。
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
// 如果实例中没有周期的配置,就设置为默认的 90 秒
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
// 注册实例
super.register(info, leaseDuration, isReplication);
// 复制到集群其它 server 节点
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
复制代码
接着看父类的注册方法,它的主要流程如下:
- 首先可以看到 eureka server 保存注册表(registry)的数据结构是
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
,key 就是服务名称,value 就是对应的实例,因为一个服务可能会部署多个实例。 - 根据服务名称从注册表拿到实例表,然后根据实例ID拿到实例的租约信息
Lease<InstanceInfo>
。 - 如果租约信息存在,说明已经注册过相同的实例了,然后就对比已存在实例和新注册实例的最后更新时间,如果新注册的是旧的,就替换为已存在的实例来完成注册。
- 如果租约信息不存在,说明是一个新注册的实例,这时会更新两个阈值:
- 期望续约的客户端数量
+1
- 每分钟续约次数的阈值,如果低于这个值,说明有很多客户端没有发送心跳,这时eureka就认为可能网络出问题了,就会有另一些机制,这个后面再说
- 期望续约的客户端数量
- 然后就根据注册的实例信息和续约周期创建新的租约,并放入注册表中去。
- 接着根据当前时间戳、服务名称、实例ID封装一个 Pair,然后放入到最近注册的队列中
recentRegisteredQueue
,先记住这个队列就行了。 - 根据实例的
overriddenStatus
判断,不为空的话,可能就只是要更新实例的状态,这个时候就会只变更实例的状态,而不会改变 dirty。 - 然后是设置了实例的启动时间戳,设置了实例的
ActionType 为 ADDED
。 - 将租约加入到最近变更的队列 recentlyChangedQueue,先记住这个队列,这个队列就是增量抓取注册表的来源。
- 最后一步失效缓存,一步步进去可以发现,主要就是将读写缓存 readWriteCacheMap 中与这个实例相关的缓存失效掉,这个缓存后面分析抓取注册表的时候再来细看。
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
read.lock();
try {
// registry 结构 => ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
// 初次注册时,创建一个 ConcurrentHashMap,key 为 appName
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());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
if (existingLease != null && (existingLease.getHolder() != null)) {
// 已存在的实例的最后更新时间
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
// 新注册的实例的最后更新时间
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
// 如果存在的实例比新注册尽量的实例后更新,就直接把新注册的实例设置为已存在的实例
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = existingLease.getHolder();
}
} else {
// 新注册时,续约信息不存在
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to register it, increase the number of clients sending renews
// 期望续约的客户端数量 + 1
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
// 更新每分钟续约请求次数的阀值,这个阀值在后面很多地方都会用到
updateRenewsPerMinThreshold();
}
}
}
// 创建新的续约
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
// 放入最近注册的队列
recentRegisteredQueue.add(new Pair<Long, String>(System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
// 覆盖状态
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
// 仅仅是变更实例状态,不会设置为 dirty
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
if (InstanceStatus.UP.equals(registrant.getStatus())) {
// UP 时设置 Lease 的时间戳
lease.serviceUp();
}
// 设置动作是 ADDED,这个在后面会做 switch 判断
registrant.setActionType(ActionType.ADDED);
// 添加到最近变更的队列
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
// 设置最后更新时间
registrant.setLastUpdatedTimestamp();
// 失效缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
read.unlock();
}
}
复制代码
更新每分钟续约次数的阈值:
-
每分钟续约阈值 = 期望续约的客户端数量 * (60 / 续约间隔时间) * 续约百分比
。 -
例如,一共注册了 10 个实例,那么期望续约的客户端数量为 10,间隔时间默认为 30秒,就是每个客户端应该每30秒发送一次心跳,续约百分比默认为 0.85,那么每分钟续约次数阈值 = 10 * (60.0 / 30) * 0.85 = 17,也就是说每分钟至少要接收到 17 次续约请求。
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
复制代码
这就是注册表 registry 缓存服务实例信息的结构,可以看出 eureka 是基于内存来组织注册表的,使用的是 ConcurrentHashMap
来保证多线程并发安全。
Eureka Server 控制台
前面已经将服务实例注册上去了,现在来看下 eureka server 的控制台页面是怎么获取这些数据的。
前面已经分析过 eureka-server 的 web.xml
中配置了欢迎页为 status.jsp
,这就是控制台的页面。
从 status.jsp 可以看出,其实就是从 EurekaServerContext
上下文获取注册表,然后读取注册表注册的服务实例,然后遍历展示到表格中。
// 获取 eureka server 上下文 EurekaServerContext
EurekaServerContext serverContext = (EurekaServerContext) pageContext.getServletContext()
.getAttribute(EurekaServerContext.class.getName());
// 从上下文中取出注册表,获取 Application
for(Application app : serverContext.getRegistry().getSortedApplications()) {
out.print("<tr><td><b>" + app.getName() + "</b></td>");
Map<String, Integer> amiCounts = new HashMap<String, Integer>();
Map<InstanceStatus,List<Pair<String, String>>> instancesByStatus = new HashMap<InstanceStatus, List<Pair<String,String>>>();
Map<String,Integer> zoneCounts = new HashMap<String, Integer>();
// 实例信息
for(InstanceInfo info : app.getInstances()){
String id = info.getId();
String url = info.getStatusPageUrl();
InstanceStatus status = info.getStatus();
String ami = "n/a";
}
}
复制代码
服务注册的整体流程图
面通过一张图来看看服务实例注册的整个流程。
服务续约
在分布式系统中,服务续约机制是非常重要的,这样能让中心系统(注册中心)知道客户端还存活着。接下来就来看看服务续约的机制。
Eureka Client 定时发送心跳
在初始化 DiscoveryClient 的调度任务时,下面这部分代码就是在创建定时发送心跳的任务,心跳每隔30秒发送一次。发送心跳的接口是 PUT /apps/{appName}/{instanceId}
。
private void initScheduledTasks() {
// 定时刷新本地缓存...
if (clientConfig.shouldRegisterWithEureka()) {
// 续约间隔时间,默认30秒
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
// 心跳调度器的延迟时间扩大倍数,默认10
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// 心跳的定时任务
heartbeatTask = new TimedSupervisorTask(
"heartbeat", scheduler, heartbeatExecutor,
renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound,
new HeartbeatThread()
);
// 30秒后开始调度心跳的任务
scheduler.schedule(heartbeatTask, renewalIntervalInSecs, TimeUnit.SECONDS);
// 服务注册...
}
}
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
// 更新最后心跳时间
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
// 发送心跳的接口:PUT /apps/{appName}/{instanceId}
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
REREGISTER_COUNTER.increment();
long timestamp = instanceInfo.setIsDirtyWithTime();
// 服务端未找到对应的实例,就重新注册
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
// 续约成功
return httpResponse.getStatusCode() == Status.OK.getStatusCode();
} catch (Throwable e) {
return false;
}
}
复制代码
Eureka Server 接收心跳续约
顺着 PUT /apps/{appName}/{instanceId}
找可以发现,服务端接收注册的入口在 InstanceResource
的 renewLease
方法中,它调用了注册表单 renew
方法进行服务续约。
@PUT
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
boolean isFromReplicaNode = "true".equals(isReplication);
// 调用注册表的 renew 进行服务续约
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
// 注册表中没有实例,续约失败,返回 NOT_FOUND,让客户端重新注册,在集群同步时会用到
if (!isSuccess) {
logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
return Response.status(Status.NOT_FOUND).build();
}
//....
return response;
}
复制代码
进去可以看到,调用了父类的 renew
方法续约,然后会判断 isReplication
,如果是复制,说明是来自 eureka-server 集群中其它节点的同步请求,就复制到其它节点。复制到其它集群这块代码在前面已经提到过了,就不再展示。
public boolean renew(final String appName, final String id, final boolean isReplication) {
// 调用父类(AbstractInstanceRegistry)的 renew 续约
if (super.renew(appName, id, isReplication)) {
// 续约完成后同步到集群其它节点
replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
return true;
}
return false;
}
复制代码
接着看父类的 renew
续约方法:
- 首先根据服务名从注册表中取出租约信息
- 然后根据实例ID取出实例的租约信息
- 然后判断是否是覆盖实例状态
- 将最近一分钟续约次数计数器
renewsLastMin +1
- 最后调用实例租约对象的 renew 方法进行续约,其内部只是更新了租约的最后更新时间
lastUpdateTimestamp
,更新为当前时间 + 续约间隔时间
。
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
// 根据服务名从注册表取出租约信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
// 根据实例ID取出实例租约信息
leaseToRenew = gMap.get(id);
}
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);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
// 最近一分钟续约计数器+1
renewsLastMin.increment();
// 续约
leaseToRenew.renew();
return true;
}
}
public void renew() {
// 更新最后更新时间,在当前时间的基础上加了周期时间,默认90秒
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
复制代码