1.架构图
架构图.png
-
Euraka支持多副本机制
-
所有的Replicate互相保持数据(已注册的服务列表)同步
-
客户端可以保持与Replicate通信
2.基本流程
-
服务提供者启动服务,构建InstanceInfo对象,将此对象信息注册至服务中心
-
注册完成后会从注册中心拉取所有的服务列表缓存至本地
-
注册中心每隔30秒向服务提供者发送心跳,判断服务提供者是否存活, 同时进行续约服务
-
如果服务治理中心在90s内没有收到一个服务的续约,就会认为服务已经挂了,会把服务注册信息删掉。
-
服务停止前,服务会主动发送一个停止请求,服务治理中心会删除这个服务的信息。
-
如果Eureka Server收到的心跳包不足正常值的85%(可配置)就会进入自我保护模式,在这种模式下,Eureka Server不会删除任何服务信息。
3.自我保护机制
在默认配置中,Eureka Server在默认90s没有得到客户端的心跳,则注销该实例,但是往往因为微服务跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,但是因为网络分区故障时,Eureka Server注销服务实例则会让大部分微服务不可用,这很危险,因为服务明明没有问题。
为了解决这个问题,Eureka 有自我保护机制,通过在Eureka Server配置如下参数,可启动保护机制
eureka.server.enable-self-preservation=true
当Eureka Server节点在短时间内丢失过多的客户端时(可能发送了网络故障),那么这个节点将进入自我保护模式,不再注销任何微服务,当网络故障回复后,该节点会自动退出自我保护模式。
服务过期时间配置:
eureka.instance.lease-expiration-duration-in-seconds
服务刷新时间配置:
eureka.instance.lease-renewal-interval-in-seconds
自我保护模式的架构哲学是宁可放过一个,决不可错杀一千
4. 服务端
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
-
服务注册时,会把服务的信息写到这个registry中
-
服务从治理中心拉取服务列表信息时,不会从这个registry中拉取,而是从一个ResponseCache中拉取,这样读写分离的原因应该是为了支持高并发。
而ResponseCache又分为了两个部分,一个是ReadWriteMap,一个是ReadOnlyMap。 -
ReadWriteMap的数据是从registry中来的,可以认为是registry的缓存,当服务注册时,除了把信息写到registry中外,还会让ReadWriteMap主动过期,使得会去从registry重新拉取数据。
-
ReadOnlyMap的数据是从ReadWriteMap来的,可以认为是ReadWriteMap的缓存(所以它是registry缓存的缓存,双层缓存了),当服务需要获取服务列表是,会直接取这个ReadOnlyMap的数据,当这个数据不存在时,才会从ReadWriteMap中更新。
-
ReadWriteMap与registry的数据是实时一致的(因为有注册后让ReadWriteMap失效的机制),但是ReadWriteMap与ReadOnlyMap不是实时一致的。
-
有定时任务会定时从ReadWriteMap同步到ReadOnlyMap,这个时间配置是:
eureka.server.responseCacheUpdateInvervalMs
- EurekaServer内部有定时任务,每隔检查过期实例时间,扫描Registry里面过期的实例并删除,并且使对应的ReadWriteMap缓存失效,这个时间是
eureka.server.eviction-interval-timer-in-ms
5.客户端
服务在启动完成后,会启动一个线程,去执行注册信息,具体代码在InstanceInfoReplicator中,这个类实现了Runnable:
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
- 在run方法中,会先去检测服务信息是否发生了变更,如果发生了,会调用 discoveryClient.register();方法重新注册,第一次的时候也会认为是发生了变更,从而发起第一次注册。
- 在finally块中,schedule了this,说明会在指定时间后再次执行这个方法
服务端对应处理过程
- 首先会处理上面所说的registry以及它的缓存对象。
- 其次会把这次的注册信息添加到一个“最近变更队列中”
private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue =
new ConcurrentLinkedQueue<RecentlyChangedItem>();
-
之所以要维护一个这样的队列,是为了方便服务增量更新服务列表的信息。
-
然后会把注册的信息发送到其他的Eureka节点,发送的方式就是调用其他节点的register方法。
6.续约
在服务启动时,会创建一个后台心跳任务,定时去续约服务信息:
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
HeartbeatThread就是心跳线程,这里把它包装成了一个TimedSupervisorTask,
注意:scheduler.schedule方法只会在延时一定时间后执行一次提交的任务,所以这里会延时执行,同时,虽然schedule只会执行一次,但是TimedSupervisorTask里封装了循环调用的信息,所以其实是定时调用了。
而HeartbeatThread非常简单:
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
if语句中的renew()方法就是服务续约的方法,里面的逻辑就是向服务治理中心发送了一个http请求。
如果http请求返回的状态码是404,则会认为这个服务没有注册或者注册了但是已经失效,因此会直接调用register()方法进行注册。
服务取消的细节
服务停止前会向服务治理中心发一个服务取消的请求,用于注销服务。收到服务注销的请求之后,服务治理中心会做以下操作:
- 从registry中删除对应的服务信息
- 使ReadWriteMap缓存失效
- 将服务取消的信息加入到最近变更队列中
7.与zookeeper异同
著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。
8.1 Zookeeper保证CP
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
8.2 Eureka保证AP
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
- Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
- Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
- 当网络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。
9. 代码
源码地址:https://github.com/spring-cloud/spring-cloud-netflix
作者:life_zl
链接:https://www.jianshu.com/p/6a3db6939fb0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。