微服务架构的核心部分是服务治理,而服务治理最基础的组件就是注册中心。随着微服务架构的发展,出现了很多微服务架构的解决方案,包括大家熟知的Dubbo和SpringCloud。关于registry方案,dubbo支持Zookeeper、Redis、Multicast和Simple,官方推荐使用Zookeeper。SpringCloud支持Zookeeper、Consul和Eureka,官方推荐使用Eureka。两者之所以推荐不同的实现方式,是因为组件的特性和适用场景不同。简单来说:ZK的设计原则是CP,即强一致性和分区容错。他保证了数据的强一致性,但是放弃了可用性。如果网络出现问题,可能会影响ZK的选举,导致ZK注册中心不可用。Eureka的设计原则是AP,即可用性和分区容错性。他保证了注册中心的可用性,但是放弃了数据一致性,每个节点上的数据可能不一致(最终会一致)。Eureka是用纯Java实现的。除了实现注册中心的基本服务注册和发现外,极大的满足了注册中心的可用性。即使只有一项服务可用,也可以保证注册中心的可用性。本文将重点介绍Eureka的内部实现原理,首先从微服务架构的部署图介绍Eureka的整体架构,然后分析服务信息的存储结构,最后探讨服务注册机制、服务更新机制、服务注销机制、服务移除机制、服务获取机制、服务同步机制。Eureka的整体架构下面是Eureka注册中心部署在多个机房的架构图,这是它高可用的优势(Zookeeper一定不能这样部署)。从组件功能来看:黄色的登记中心集群分别部署在北京、天津、青岛机房;红色服务商分别部署在北京和青岛机房;浅绿色服务消费者分别部署在北京和天津机房;从机房分布来看:注册中心、服务提供者、服务消费者部署在北京机房;注册中心和服务消费者部署在天津机房;注册中心和服务商部署在青岛机房;发起注册请求,注册服务在运行过程中会周期性的向注册中心发送更新心跳,证明“我还活着”。停止服务提供者,向注册中心发起注销请求,清除当前服务注册信息。服务消费者启动后,从注册中心拉取服务注册信息。在运行过程中,服务注册信息会定期更新。服务消费者发起远程调用:a>服务消费者(北京)从服务注册信息中选择同一机房的服务提供者(北京)发起远程调用。只有同机房的服务商挂了,才会选择其他机房(青岛)的服务商。b>由于同一机房没有服务提供者,服务消费者(天津)会根据负载均衡算法选择北京或青岛的服务提供者发起远程调用。注册中心启动后,从其他节点拉取服务注册信息。在运行过程中,定期运行evict任务,剔除没有按时更新的服务(包括异常停止和网络故障的服务)。运行过程中,接收到的注册、更新、取消请求会同步到其他注册节点。本文将详细介绍上图中registry、register、renew、cancel、getRegistry、evict的内部机制。由于数据存储结构是一个服务注册中心,所以需要存储服务信息。我们知道ZK在树节点上存储服务信息。下面是Eureka的数据存储结构:Eureka的数据存储分为两层:数据存储层和缓存层。EurekaClient拉取服务信息时,首先从缓存层(相当于Redis)获取。如果获取不到,则先将数据存储层的数据加载到缓存中(相当于Mysql),再从缓存中获取。值得注意的是,数据存储层的数据结构是服务信息,存储在缓存中的数据结构经过处理后可以直接传给EurekaClient。Eureka的数据结构设计将内部数据存储结构与外部数据结构隔离开来,就像我们平时设计接口一样,外部数据结构和数据库中的数据结构往往是不同的。.为什么这里说的数据存储层是存储层而不是持久层呢?因为rigistry本质上是一个两层的ConcurrentHashMap存储在内存中。第一层的key是spring.application.name,value是第二层的ConcurrentHashMap;第二层ConcurrentHashMap的key是服务的InstanceId,value是Lease对象;Lease对象包含与服务治理相关的服务详细信息和属性。二级缓存层Eureka实现了二级缓存来存储对外传输的服务信息,数据结构完全一样。一级缓存:ConcurrentHashMapreadOnlyCacheMap,本质上是一个HashMap,没有过期时间,存储服务信息对外输出的数据结构。二级缓存:LoadingreadWriteCacheMap,本质上是Guava的缓存,包含失效机制,存储服务信息对外输出的数据结构。既然是缓存,就必须有更新机制来保证数据的一致性。下面是缓存的更新机制:更新机制包括删除和加载两部分。上图中黑色箭头表示删除缓存的动作,绿色表示加载或触发加载的动作。删除二级缓存:EurekaClient发送注册、更新、取消请求并更新registry注册表后,删除二级缓存;EurekaServer自身的EvictTask淘汰服务后,删除二级缓存;二级缓存本身设置了guava失效机制,一段时间后会自动失效;加载二级缓存:EurekaClient发送getRegistry请求后,如果没有二级缓存,会触发guava的加载,即从注册中心获取原始服务信息,进行处理,然后加载到二级缓存。EurekaServer更新一级缓存时,如果二级缓存没有数据,也会触发guava的加载。更新一级缓存:EurekaServer内置了一个TimerTask,定时将二级缓存中的数据同步到一级缓存中(这个动作包括删除和加载)。缓存的实现参考ResponseCacheImpl服务注册机制。服务提供者、服务消费者和服务注册中心本身都会在启动后向注册中心注册服务(如果配置了注册的话)。下图展示了如何完成服务注册:注册服务收到注册请求后:保存服务信息,将服务信息保存在注册中心;更新队列,将此事件添加到更新队列中,供EurekaClient增量同步服务信息使用。清除二级缓存即readWriteCacheMap,保证数据一致性。更新剔除服务使用的阈值。同步服务信息,将此事件同步到其他EurekaServer节点。服务续订机制服务注册后,需要定时(默认30S,可自行配置)向注册中心发送续订请求,告诉注册中心“我还活着”。注册中心收到续约请求后:更新服务对象的最新续约时间,即Lease对象的lastUpdateTimestamp;同步服务信息,将此事件同步到其他EurekaServer节点。在排除该服务之前,它会先判断该服务是否已经过期。判断服务是否过期的条件之一是续费时间与当前时间的差值是否大于阈值。服务注销机制在服务正常停止之前,它会向注册中心发送注销请求,告诉注册中心“我要下线了”。注册中心服务收到取消请求后:删除服务信息,从注册中心删除该服务信息;更新队列,将此事件添加到EurekaClient增量同步服务信息的更新队列中。清除二级缓存即readWriteCacheMap,保证数据一致性。更新剔除服务使用的阈值。同步服务信息,将此事件同步到其他EurekaServer节点。只有在服务正常停止时才会发送取消。如果异常停止,则不会发送。该服务被EurekaServer主动拒绝。服务移除机制EurekaServer提供了服务移除机制,用于移除不正常下线的服务。删除服务包括三个步骤。首先判断是否满足服务移除的条件,然后找出过期的服务,最后执行移除。判断是否满足服务移除条件有两种情况可以满足服务移除条件:关闭自我保护如果开启自我保护,则需要进一步判断是EurekaServer还是Eureka有问题客户。如果是EurekaClient有问题,就会被淘汰。这里的核心条件是自我保护机制。Eureka自我保护机制是为防止误杀服务而提供的一种机制。Eureka的自我保护机制是“谦虚”的,认为如果有大量的服务续约失败,就认为是自己有问题(比如断网),不会被淘汰;否则就是EurekaClient的问题,需要排查。消除。自我保护阈值是区分EurekaClient和EurekaServer的临界值。如果超过阈值,则表示有大量服务可用,少量服务不可用。确定是EurekaClient有问题。如果在没有超过阈值的情况下大量服务不可用,则确定EurekaServer有问题。如果在第一种情况下关闭了自我保护,则认为是EurekaClient有问题,将所有不按时更新的服务移除(这里移除有最大限制)。这里比较难理解的是阈值的计算:自我保护阈值=服务总数*每分钟续订次数*自我保护阈值因子。每分钟更新次数=(60S/客户端更新间隔)最终自保阈值的计算公式为:自保护阈值=服务总数*(60S/客户端更新间隔)*自保护阈值系数。示例:如果有100个服务,更新间隔为30S,自我保护阈值为0.85。自保阈值=100*60/30*0.85=170。如果最后一分钟续费次数=180>170,说明有大量服务可用,属于服务问题,进入淘汰过程;如果最后一分钟续费次数=150<170,说明大量服务不可用,是注册中心自己的问题,进入自我保护模式,不进入淘汰流程。找出过期服务遍历所有服务,判断上次续订时间距离当前时间大于阈值,标记为过期。并将这些过期的服务保存到一个集合中。剔除服务在剔除服务前先计算剔除次数,然后遍历过期服务,使用混洗算法保证每次要剔除的任务都被公平选择,最后剔除。剔除服务后:删除服务信息,从注册表中删除服务。更新队列,将当前剔除事件保存到更新队列中。清除二级缓存以保证数据的一致性。实现过程参考AbstractInstanceRegistry.evict()方法。服务获取机制EurekaClient获取服务有两种方式,全量同步和增量同步。获取过程基于EurekaServer的多层数据结构:无论是全量同步还是增量同步,都是先从缓存中获取。如果没有缓存,则先加载到缓存中,再从缓存中获取。(注册中心只保存数据结构,就绪服务信息保存在缓存中。)先从一级缓存中获取a>先判断是否启用一级缓存b>如果启用,则获取从一级缓存中获取,如果存在则返回,如果不存在则从二级缓存中获取d>如果不开启则跳过一级缓存,从二级缓存中获取然后获取>来自二级缓存,如果二级缓存中存在,则直接返回;b>如果二级缓存中不存在,则先将数据加载到二级缓存中,再从二级缓存中获取。注意加载的时候需要判断是增量同步还是全量同步。增量同步从recentlyChangedQueue加载,完全同步从registry加载。服务同步机制服务同步机制用于在EurekaServer节点之间同步服务信息。包括EurekaServer启动时的同步,以及运行过程中的同步。启动时同步EurekaServer启动后遍历eurekaClient.getApplications获取服务信息,将服务信息注册到自己的注册中心。注意这里有两层循环。第一层循环保证服务信息已经拉取,第二层循环遍历拉取的服务信息。运行时同步当EurekaServer节点有注册、更新、取消请求时,会将请求封装成一个TaskHolder放入acceptorQueue队列中,经过一系列处理后放入batchWorkQueue中。TaskExecutor.BatchWorkerRunnable是一个线程池,不断从batchWorkQueue队列中轮询TaskHolder,然后向其他EurekaServer节点发送同步请求。这里省略了两部分:一是acceptorQueue转换为batchWorkQueue时,省略了中间的processingOrder和pendingTasks过程。另一种是当同步失败时,失败的TaskHolder会保存在reprocessQueue中,重试处理。写在最后,微服务解决方案Dubbo和SpringCloud有很多对比。这里简单对比一下注册中心。ZookeeperEureka设计原理CPAP优点强数据一致性服务高可用性缺点网络分区会影响Leader选举。超过阈值后,集群将不可用。服务节点之间的数据可能不一致;Client和Server之间的数据可能不一致;适用于单机房集群,对数据一致性要求高。云机房集群跨多个机房部署;对注册中心服务的可用性有很高的要求。