理论
CAP
- 一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
(强)一致性 Consistency
- 一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,所以,一致性,说的就是数据一致性。分布式的一致性
- 一致性是因为有并发读写才有的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。
- 对于一致性,可以分为从客户端和服务端两个不同的视角。
- 从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
- 从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
三种一致性
- 对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
- CAP中说,不可能同时满足的这个一致性指的是这个强一致性
- 如果能容忍后续的部分或者全部访问不到,则是弱一致性。
- 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。
可用性 Availability
- 可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。
- 对于一个可用性的分布式系统,每一个非故障的节点必须对每一个请求作出响应。所以,一般我们在衡量一个系统的可用性的时候,都是通过停机时间来计算的。

- 通常我们描述一个系统的可用性时,我们说淘宝的系统可用性可以达到5个9,意思就是说他的可用水平是99.999%,即全年停机时间不超过 (1-0.99999)36524*60 = 5.256 min,这是一个极高的要求。
- 好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。一个分布式系统,上下游设计很多系统如负载均衡、WEB服务器、应用代码、数据库服务器等,任何一个节点的不稳定都可以影响可用性。
分区容错性 Partition tolerance
- 分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
- 分区容错性和扩展性紧密相关。在分布式应用中,可能因为一些分布式的原因导致系统无法正常运转。好的分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,或者是机器之间有网络异常,将分布式系统分隔未独立的几个部分,各个部分还能维持分布式系统的运作,这样就具有好的分区容错性。
- 简单点说,就是在网络中断,消息丢失的情况下,系统如果还能正常工作,就是有比较好的分区容错性。
如何取舍
- CA: 优先保证一致性和可用性,放弃分区容错。 这也意味着放弃系统的扩展性,系统不再是分布式的,有违设计的初衷。
- CP: 优先保证一致性和分区容错性,放弃可用性。在数据一致性要求比较高的场合(譬如:zookeeper,Hbase) 是比较常见的做法,一旦发生网络故障或者消息丢失,就会牺牲用户体验,等恢复之后用户才逐渐能访问。
- AP: 优先保证可用性和分区容错性,放弃一致性。NoSQL中的Cassandra 就是这种架构。跟CP一样,放弃一致性不是说一致性就不保证了,而是逐渐的变得一致。
注册中心
eureka、zookeeper、consul、nacos等
注册中心作用:
- 微服务数量众多,要进行远程调用就需要知道服务端的ip地址和端口,注册中心帮助我们管理这些服务的ip和端口。
- 微服务会实时上报自己的状态,注册中心统一管理这些微服务的状态,将存在问题的服务踢出服务列表,客户端获取到可用的服务进行调用。
eureka
服务发现框架
本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。
Spring Cloud Eureka 是对Netflix公司的Eureka(它实现了服务治理的功能)的二次封装,集成在自己的子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。
为什么要用eureka呢,因为分布式开发架构中,任何单点的服务都不能保证不会中断,因此需要服务发现机制,某个节点中断后,服务消费者能及时感知到保证服务高可用。
https://www.cnblogs.com/jing99/p/11576133.html
https://blog.csdn.net/qq_18153015/article/details/108415307
【原理解析】
- Eureka包含两个组件,分为客户端和服务端
- 客户端提供服务注册与注销、服务发现的功能
- Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个**服务节点移除(默认90秒)**。
- Eureka Client分为两个角色,分别是:
- Application Service(Service Provider): 服务提供方,是注册到Eureka Server中的服务。
- Application Client(Service Consumer): 服务消费方,通过Eureka Server发现服务,并消费。
- Application Service和Application Client不是绝对上的定义,因为Provider在提供服务的同时,也可以消费其他Provider提供的服务;Consumer在消费服务的同时,也可以提供对外服务。
- 服务端提供服务治理的功能。
- Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
- Eureka Server本身也是一个服务,默认情况下会自动注册到Eureka注册中心。
- 如果搭建单机版的Eureka Server注册中心,则需要配置取消Eureka Server的自动注册逻辑。毕竟当前服务注册到当前服务代表的注册中心中是一个说不通的逻辑。
- Eureka Server通过Register、Get、Renew等接口提供服务的注册、发现和心跳检测等服务。
1、Eureka架构
a、Eureka Server架构原理:

- Register(服务注册):把自己的IP和端口注册给Eureka。
- Renew(服务续约):发送心跳包,每30秒发送一次。告诉Eureka自己还活着。
- Cancel(服务下线):当provider关闭时会向Eureka发送消息,把自己从服务列表中删除。防止consumer调用到不存在的服务。
- Get Registry(获取服务注册列表):获取其他服务列表。
- Replicate(集群中数据同步):eureka集群中的数据复制与同步。
- Make Remote Call(远程调用):完成服务的远程调用。
b、Eureka Server缓存架构

- 为避免读写冲突,Eureka采用多层缓存的架构。
- 服务注册时,Eureka服务端会将服务实例更新到注册实例列表缓存(register)和读写缓存(readWriteCacheMap)中,然后Eureka服务端每隔30秒会将读写缓存(readWriteCacheMap)的数据更新到只读缓存(readOnlyCacheMap)中。
- 消费者从Eureka服务端获取实例列表时,是直接从只读缓存(readOnlyCacheMap)中获取。
c、Eureka健康检查,自我保护机制
- 为防止因为Eureka服务器网络问题,导致大部分服务实例被踢除,Eureka在进行服务定时更新时,会进行健康检查。
- 健康检查流程:
- 当过期的服务数量比例超过阈值(可配置,默认为0.85)时,Eureka会启动自我保护机制,不会进行服务踢除操作
d、Eureka分区
为避免跨机房调用的网络消耗,Eureka支持通过配置实现优先使用本机房服务实例,当本机房实例不可用时,再使用其它机房的服务实例。

- Eureka的分区概念分为区域(region)和机房(zone)。
- 如上图,区域(region)是北京,机房分为zone-1和zone-2。消费者(Consumer-1)调用服务时,会优先调用zone-1里面的服务提供者(Service-1),只有zone-1里面的服务提供者(Service-1)不可用,才会去调用zone-2里面的服务提供者(Service-2)。
2、功能原理
a、服务发现原理
- eureka server可以集群部署,多个节点之间会进行(异步方式)数据同步,保证数据最终一致性,
- Eureka Server作为一个开箱即用的服务注册中心,提供的功能包括:服务注册、接收服务心跳、服务剔除、服务下线等。
- 需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。
1 | /* eureka server端通过appName和instanceInfoId来唯一区分一个服务实例,服务实例信息是保存在哪里呢?其实就是一个Map中:*/ |
b、服务注册
Service Provider启动时会将服务信息(InstanceInfo)发送给eureka server,eureka server接收到之后会写入registry中,服务注册默认过期时间DEFAULT_DURATION_IN_SECS = 90秒。InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法:
com.netflix.eureka.registry.peerawareinstanceregistryimpl#replicateToPeers
c、写入本地registry
服务信息(InstanceInfo)保存在Lease中,写入本地registry对应方法:
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#registerLease统一保存在内存的ConcurrentHashMap中,在服务注册过程中,首先加个读锁,然后从registry中判断该Lease是否已经存在,如果存在则比较lastDirtyTimestamp时间戳,取二者最大的服务信息,避免发生数据覆盖。使用InstanceInfo创建一个新的InstanceInfo
1 | if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) { |
d、同步给其他peer节点
InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法:
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers如果当前节点接收到的InstanceInfo本身就是另一个节点同步来的,则不会继续同步给其他节点,避免形成“广播效应”;InstanceInfo同步时会排除当前节点。
InstanceInfo的状态有依以下几种:
Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride,默认情况下同步操作是批量异步执行的,同步请求首先缓存到Map中,key为requestType+appName+id,然后由发送线程将请求发送到peer节点。
Peer之间的状态是采用异步的方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。结合服务发现的场景,实际上也并不需要节点间的状态强一致。在一段时间内(比如30秒),节点A比节点B多一个服务实例或少一个服务实例,在业务上也是完全可以接受的(Service Consumer侧一般也会实现错误重试和负载均衡机制)。所以按照CAP理论,Eureka的选择就是放弃C,选择AP。
如果同步过程中,出现了异常怎么办呢,这时会根据异常信息做对应的处理,如果是读取超时或者网络连接异常,则稍后重试;如果其他异常则打印错误日志不再后续处理。
e、服务续约
- Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。主要是用来告诉Eureka Server Service Provider还活着,避免服务被剔除掉。
- renew接口实现方式和register基本一致:首先更新自身状态,再同步到其它Peer,服务续约也就是把过期时间设置为当前时间加上duration的值。
- 注意:服务注册如果InstanceInfo不存在则加入,存在则更新;而服务预约只是进行更新,如果InstanceInfo不存在直接返回false。
f、服务下线
Cancel(服务下线)一般在Service Provider shutdown的时候调用,用来把自身的服务从Eureka Server中删除,以防客户端调用不存在的服务,eureka从本地”删除“(设置为删除状态)之后会同步给其他peer,对应方法:
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel
g、服务失效剔除
Eureka Server中有一个EvictionTask,用于检查服务是否失效。
Eviction(失效服务剔除)用来定期(默认为每60秒)在Eureka Server检测失效的服务,检测标准就是超过一定时间没有Renew的服务。默认失效时间为90秒,也就是如果有服务超过90秒没有向Eureka Server发起Renew请求的话,就会被当做失效服务剔除掉。
- 失效时间可以通过
eureka.instance.leaseExpirationDurationInSeconds进行配置, - 定期扫描时间可以通过
eureka.server.evictionIntervalTimerInMs进行配置。
- 失效时间可以通过
服务剔除#evict方法中有很多限制,都是为了保证Eureka Server的可用性:
比如自我保护时期不能进行服务剔除操作、过期操作是分批进行、服务剔除是随机逐个剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,以致大量剔除发生时,在未进行自我保护前促使了程序的崩溃
3、Eureka Server/Client流程
a、服务信息拉取
- Eureka consumer服务信息的拉取分为全量式拉取和增量式拉取,eureka consumer启动时进行全量拉取,运行过程中由定时任务进行增量式拉取,如果网络出现异常,可能导致先拉取的数据被旧数据覆盖(比如上一次拉取线程获取结果较慢,数据已更新情况下使用返回结果再次更新,导致数据版本落后),产生脏数据。对此,eureka通过类型AtomicLong的fetchRegistryGeneration对数据版本进行跟踪,版本不一致则表示此次拉取到的数据已过期。
- fetchRegistryGeneration过程是在拉取数据之前,执行fetchRegistryGeneration.get获取当前版本号,获取到数据之后, 通过
fetchRegistryGeneration.compareAndSet来判断当前版本号是否已更新 - 注意:如果增量式更新出现意外,会再次进行一次全量拉取更新。
b、Eureka Server的伸缩容
Eureka Server是怎么知道有多少Peer的呢?
- Eureka Server在启动后会调用
EurekaClientConfig.getEurekaServerServiceUrls来获取所有的Peer节点,并且会定期更新。 - 定期更新频率可以通过
eureka.server.peerEurekaNodesUpdateIntervalMs配置。
- Eureka Server在启动后会调用
这个方法的默认实现是从配置文件读取,所以如果Eureka Server节点相对固定的话,可以通过在配置文件中配置来实现。
如果希望能更灵活的控制Eureka Server节点,比如动态扩容/缩容,那么可以override
getEurekaServerServiceUrls方法,提供自己的实现,比如我们的项目中会通过数据库读取Eureka Server列表。eureka server启动时把自己当做是Service Consumer从其它Peer Eureka获取所有服务的注册信息。然后对每个服务信息,在自己这里执行
Register,isReplication=true从而完成初始化。
c、Service Provider
- Service Provider启动时首先时注册到Eureka Service上,这样其他消费者才能进行服务调用,除了在启动时之外,只要实例状态信息有变化,也会注册到Eureka Service。
- 需要注意的是,需要确保配置
eureka.client.registerWithEureka=true。register逻辑在方法AbstractJerseyEurekaHttpClient.register中,Service Provider会依次注册到配置的Eureka Server Url上,如果注册出现异常,则会继续注册其他的url。
- 需要注意的是,需要确保配置
- Renew操作会在Service Provider端定期发起,用来通知Eureka Server自己还活着。
- 这里
instance.leaseRenewalIntervalInSeconds属性表示Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。这部分逻辑在HeartbeatThread类中。 - 在Service Provider服务shutdown的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务,逻辑本身比较简单,通过对方法标记
@PreDestroy,从而在服务shutdown的时候会被触发。
- 这里
d、Service Consumer
- Service Consumer这块的实现相对就简单一些,因为它只涉及到从Eureka Server获取服务列表和更新服务列表。
- Service Consumer在启动时会从Eureka Server获取所有服务列表,并在本地缓存。需要注意的是,需要确保配置
eureka.client.shouldFetchRegistry=true。由于在本地有一份Service Registries缓存,所以需要定期更新,定期更新频率可以通过eureka.client.registryFetchIntervalSeconds配置。
4、属性值
a、客户端 eureka.client
1 | 1、RegistryFetchIntervalSeconds |
b、服务端 eureka.server
1 | 1、AWSAccessId |
c、实例微服务端配置
1 | 1、InstanceId |
FLP impossibility
FLP不可能原理:在网络可靠,存在节点失效(即便只有一个)的最小化异步模型系统中,不存在一个可以解决一致性问题的确定性算法。
- 1985年 FLP 原理实际上说明对于允许节点失效情况下,纯粹异步系统无法确保一致性在有限时间内完成。
- 科学告诉你什么是不可能的;工程则告诉你,付出一些代价,我可以把它变成可能。
- Post title:分布式系统
- Post author:Wei Jieyang
- Create time:2021-03-04 14:11:00
- Post link:https://jieyang-wei.github.io/2021/03/04/分布式系统/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.