服务发现
在传统的系统部署中,服务运行在一个固定的已知的 IP 和端口上,如果一个服务需要调用另外一个服务,可以通过地址直接调用。但是,在虚拟化或容器化的环境中,服务实例的启动和销毁是很频繁的,服务地址在动态的变化,如下图所示:
在基于云的微服务应用中,服务实例的网络地址(IP Address和Port)是动态分配的,并且由于系统的auto-scaling, failures 和 upgrades等因数,一些服务运行的实例数量也是动态变化的。因此,客户端代码需要使用一个非常精细和准确的服务发现机制。
如果需要将请求发送到动态变化的服务实例上,至少需要两个步骤:
- 服务注册 — 存储服务的主机和端口信息
- 服务发现 — 允许其他用户发现服务注册阶段存储的信息
服务发现的主要优点是可以无需了解架构的部署拓扑环境,只通过服务的名字就能够使用服务,提供了一种服务发布与查找的协调机制。服务发现除了提供服务注册、目录和查找三大关键特性,还需要能够提供健康监控、多种查询、实时更新和高可用性等。
有两种主要的服务发现方式:客户端发现(client-side service discovery)和 服务端发现(server-side discovery)
客户端服务发现
在使用客户端发现方式时,客户端通过查询服务注册中心,获取可用的服务的实际网络地址(IP Adress 和 端口号)。然后通过负载均衡算法来选择一个可用的服务实例,并将请求发送至该服务。下图显示了客户端发现方式的结构图:
在服务启动的时候,向服务注册中心注册服务;在服务停止的时候,向服务注册中心注销服务。服务注册的一个典型的实现方式就是通过 heartbeat 机制 定时刷新。Netflix OSS 就是使用客户端发现方式的一个很好的例子。 Netflix Eureka 是一个服务注册中心。它提供了一个管理和查询可用服务的 REST API。 负载均衡功能是通过Netflix Ribbon(是一个IPC客户端)和Eureka一起共同实现的, 后面将深入的介绍Eureka。
客户端发现方式的优缺点。由于客户端知道所有可用的服务的实际网络地址,所以可以非常方便的实现负载均衡功能(比如:一致性哈希)。但是这种方式有一 个非常明显的缺点就是具有非常强的耦合性。针对不同的语言,每个服务的客户端都得实现一套服务发现的功能。
服务端服务发现
另外一种服务发现的方式就是Server-Side Discovery Pattern,下图展示了这种方式的架构示例图
客户端向load balancer 发送请求。load balancer 查询服务注册中心找到可用的服务,然后转发请求到该服务上。和客户端发现一样,服务都要到注册中心进行服务注册和注销。AWS的弹性负载均衡(Elastic Load Balancer–ELB)就是服务端发现的一个例子。ELB通常是用于为外网服务提供负载平衡的。当然你也可以使用ELB为内部虚拟私有云(VPC)提供负载均衡服务。客户端通过使用 DNS名称,发送HTTP或TCP请求到ELB。ELB为EC2或ECS集群提供负载均衡服务。AWS并没有提供单独的服务注册中心。而是通过ELB实现EC2实例和ECS容 器注册的。
NGINX不仅可以作为HTTP反向代理服务器和负载均衡器,也可以用来作为一个服务发现的负载均衡器。
Kubernetes 和 Marathon 是在通过集群中每个节点都运行一个代理来实现服务发现的功能的,代理的角色就是server-side discovery,客户端通过使用主机 的IP Address和Port向Proxy发送请求,Proxy再将请求转发到集群中任何一个可用的服务上。
服务器端发现方式的优点是,服务的发现逻辑对客户端是透明的。客户只需简单的向load balancer发送请求就可以了。这就避免了为每种不同语言的客户端实现一套发现的逻辑。此外,许多软件都内置实现了这种功能。这种方式的一个最大的缺点是,你必须得关心该组件(负载均衡器)的高可用性。
服务注册中心
服务注册中心是服务发现的核心。它保存了各个可用服务实例的网络地址(IP Address和Port)。服务注册中心必须要有高可用性和实时更新功能。
上面提到的 Netflix Eureka 就是一个服务注册中心。它提供了服务注册和查询服务信息的REST API。服务通过使用POST请求注册自己的IP Address和Port。 每30秒发送一个PUT请求刷新注册信息。通过DELETE请求注销服务。客户端通过GET请求获取可用的服务实例信息。
Netflix的高可用(Netflix achieves high availability )是通过在Amazon EC2 运行多个实例来实现的,每一个Eureka服务都有一个弹性IP Address。当 Eureka服务启动时,有DNS服务器动态的分配。Eureka客户端通过查询 DNS来 获取Eureka的网络地址(IP Address和Port)。一般情况下,都是返回和客户端在同一个可用区Eureka服务器地址。
我们已经了解了服务注册中心的概念,接下来我们看看服务是如果注册到注册中心的。有两种不同的方式来处理服务的注册和注销。一种是服务自己主动注册- 自己注册(self-registration)。另一种是通过其他组件来管理服务的注册-第三方注册(third-party registration)。
Self-Registration
使用Self-Registration的方式注册,服务实例必须自己主动的到注册中心注册和注销。比如可以使用heartbeat机制了实现。下图为这种方式的示意图:
Netflix OSS Eureka client就是使用这种方式进行服务注册的。Eureka的客户端 处理了服务注册和注销的所有工作。
Self-Registration方式的优缺点:一个明显的优点就是,非常简单,不需要任何其它辅助组件。而缺点也是比较明显的,使得各个服务和注册中心的耦合度比较高。服务的不同语言的客户端都得实现注册和注销的逻辑。另一种服务注册方式,可以达到解耦的功能,就是third-party registration方式。
Third-Party Registration
使用Third-Party方式进行服务注册时,服务本身不必关心注册和注销功能。而是通过其他组件(service registrarhandles)来实现服务注册功能。可以通过如事件订阅等方式来监控服务的状态,如果发现一个新的服务实例运行,就向注册中心注册该服务,如果监控到某一服务停止了,就向注册中心注销该服务。下图显示了这种方式的结构图示意图:
third-party Registration方式的优点是使服务和注册中心解耦,不用为每种语言实现服务注册的逻辑。这种方式的缺点就是必须得考虑该组件的高可用性。
总结一下
在微服务的应用系统中,服务实例的数量是动态变化。各服务实例动态分配网络 地址(IP Address 和Port)。因此,为了为客户端能够访问到服务,就必须要有一种服务的发现机制。
服务发现的核心是服务注册中心。服务注册中心保存了各个服务可用的实例的相关信息。服务注册中心提供了管理API和查询API。使用管理API进行服务注册、 注销。系统的其他组件可以通过查询API来获得当前可用的服务实例信息。
有两种主要的服务发现方式:客户端发现(client-side service discovery)和 服务端发现( server-side discovery)。在使用客户端服务发现的方式中,客户通过查询服务注册中心,选择一个可用的服务实例。在使用服务器端发现系统中,客户访问 Router/load balancer,通过Router/load balancer查询服务注册中心,并将请求转发到一个可用服务实例上。
服务注册和注销的方式也有两种。一种是服务自己主动的将自己注册到服务注册中心,称为self-registration。另一种是通过其他组件来处理服务的注册和注销,称为third-party registration。
在有些环境中,服务发现功能需要自己通过服务注册中心(比如:Netflix Eureka, etcd, Apache Zookeeper)实现,而有些环境中,服务发现功能是内置的。例如,Kubernetes和Marathon。
Nginx可以作为HTTP反向代理和负载平衡器,也可以用来作为一个服务发现的负载均衡器。通过向nginx推送routing information来修改nginx的配置,比如使用:Consul Template动态修改NGINX的配置. NGINX Plus 也支持动态修改配置功能。
常见的服务发现框架
常见服务发现框架 Consul、 ZooKeeper、Etcd、Eureka、Nacos。
ZooKeeper
ZooKeeper 是这种类型的项目中历史最悠久的之一,它起源于 Hadoop。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。
ZooKeeper的功能有:
- 作为配置信息的存储的中心服务器
- 命名服务
- 分布式同步
- 分组服务
能看出,zookeeper并不只是作为服务发现框架使用的,它非常庞大。 如果只是打算将zookeeper作为服务发现工具,就需要用到其配置存储和分布式同步的功能。前者可以理解成具有一致性的kv存储,后者提供了zookeeper特有的watcher注册于异步通知机制,zookeeper能将节点的状态实时异步通知给 zookeeper客户端。
zookeeper的使用流程如下:
- 确保有所选语言的sdk,理论上github上第三方的库有一些,仔细筛选一下应该可以用。
- 调用zookeeper接口连接zookeeper服务器。
- 注册自身服务
- 通过watcher获取监听服务的状态
- 服务提供者需自行保持与zookeeper服务器的心跳。
总得来说,zookeeper需要胖客户端,每个客户端都需要通过其sdk与 zookeeper服务保活,增加了编写程序的复杂性。此外,还提供api实现服务注册与发现逻辑,需要服务的消费者实现服务提供者存活的检测。
Etcd
etcd是一个采用http协议的分布式键值对存储系统,因其易用,简单。很多系统都采用或支持etcd作为服务发现的一部分,比如kubernetes。但正是因为其只是一个存储系统,如果想要提供完整的服务发现功能,必须搭配一些第三方的工具。
比如配合Etcd(健/值对存储系统)、Registrator(服务注册器)、Confd(轻量级的配置管理工具)组合,就能搭建一个非常简单而强大的服务发现框架。但这种搭建操作就稍微麻烦了点,尤其是相对consul来说。所以etcd大部分场景都是被用来做kv存储,比如kubernetes。
Consul
Consul 是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件。由 HashiCorp 公司用 Go 语言开发,基于 Mozilla Public License 2.0 的 协议进行开源。Consul 支持健康检查,并允许 HTTP 和 DNS 协议调用 API 存储键值对。
一致性协议采用 Raft 算法,用来保证服务的高可用.。使用 GOSSIP 协议管理成员和广播消息,并且支持 ACL 访问控制。
相较于etcd、zookeeper,consul最大的特点就是:它整合了用户服务发现普遍的需求,开箱即用,降低了使用的门槛,并不需要任何第三方的工具。代码实现上也足够简单。
Consul has multiple components, but as a whole, it is a tool for discovering and configuring services in your infrastructure. It provides several key features:
- Service Discovery
- Health Checking
- KV Store
- Multi Datacenter
展开了说,consul的功能有:
- 通过DNS或HTTP,应用能轻易地找到它们依赖的系统
- 提供了多种健康检查方式:http返回码200,内存是否超限,tcp连接是否成功
- kv存储,并提供http api
- 多数据中心,这点是zookeeper所不具备的。
consul使用
相比于zookeeper的服务发现使用,consul并不需要专门的sdk集成到服务中, 因此它不限制任何语言的使用。我们看看consul一般是怎么使用的。
- 每台服务器上都要安装一个consul agent。
- consul agent支持通过配置文件注册服务,或者在服务中通过http接口来注册服务。
- 注册服务后,consul agent通过指定的健康检查方式,定期检查服务是否存活。
- 如果服务想查询其他服务的存活状态,只需要与本机的consul agent发起一次http请求或者dns请求即可。
简单点说,consul的使用不依赖任何sdk,依靠简单的http请求就能满足服务发现的所有逻辑。
不过,服务每次都从consul agent获取其他服务的存活状态,相比于 zookeeper的watcher机制,实时性稍差一点,需考虑如何尽可能提高实时性, 问题不会很大。
Nacos
Nacos 是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos 致力于帮助客户发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
Nacos 有三大主要功能:
- 服务发现和服务健康监测
Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。 - 动态配置管理
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。 - 动态 DNS 服务
服务发现的未来一定是基于标准的 DNS 协议做,而不是像 Eureka 或者像 ZooKeeper 这样的私有 API 或者协议做, 同时在云上,在服务发现场景中,注 册中心更关注的是可用性而不是数据一致性
架构
- 服务 (Service)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。 Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service. - 服务注册中心 (Service Registry)
服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。 - 服务元数据 (Service Metadata)
服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例 权重、路由规则、安全策略等描述服务的数据 - 服务提供方 (Service Provider)
是指提供可复用和可调用服务的应用方 - 服务消费方 (Service Consumer)
是指会发起对某个服务调用的应用方 - 配置 (Configuration)
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。 - 配置管理 (Configuration Management)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管 理、变更审计等所有与配置相关的活动统称为配置管理。 - 名字服务 (Naming Service)
提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。 - 配置服务 (Configuration Service)
在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
Eureka
Eureka 是 Netflix 出品的用于实现服务注册和发现的工具。 Spring Cloud 集成了 Eureka,并提供了开箱即用的支持。其中, Eureka 又可细分为 Eureka Server 和 Eureka Client。
1.基本原理
上图是来自Eureka的官方架构图,这是基于集群配置的Eureka;
- 处于不同节点的Eureka通过Replicate进行数据同步
- Application Service为服务提供者
- Application Client为服务消费者
- Make Remote Call完成一次服务调用
服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server 进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。
当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。
服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。
2.Eureka的自我保护机制
在默认配置中,Eureka Server在默认90s没有得到客户端的心跳,则注销该实例,但是往往因为微服务跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,但是因为网络分区故障时,Eureka Server注销服务实例则会让大部分微服务不可用,这很危险,因为服务明明没有问题。 为了解决这个问题,Eureka 有自我保护机制,通过在Eureka Server配置如下参 数,可启动保护机制
eureka.server.enable‐self‐preservation=true
它的原理是,当Eureka Server节点在短时间内丢失过多的客户端时(可能发生了网络故障),那么这个节点将进入自我保护模式,不再注销任何微服务,当网络故障恢复后,该节点会自动退出自我保护模式。
Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。
自我保护模式的架构哲学是宁可放过一个,决不可错杀一千
3. 作为服务注册中心,Eureka比Zookeeper好在哪里
著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和 P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。
3.1 Zookeeper保证CP
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举 leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去 master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
3.2 Eureka保证AP
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
- Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
- Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
- 当网络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。
4. 总结
Eureka作为单纯的服务注册中心来说要比zookeeper更加“专业”,因为注册服务更重要的是可用性,我们可以接受短期内达不到一致性的状况。不过 Eureka目前1.X版本的实现是基于servlet的Java web应用,它的极限性能肯定会受到影响。期待正在开发之中的2.X版本能够从servlet中独立出来成为单独可部署执行的服务。
对比总结
Eureka实战与原理
Spring Cloud是一系列框架的集合,其基于Spring Boot的开发便利性巧妙地简 化了分布式系统基础设施的开发,构建了服务治理(发现注册)、配置中心、消息总线、负载均衡、断路器、数据监控、分布式会话和集群状态管理等功能,为我们提供一整套企业级分布式云应用的完美解决方案。
Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产 品),比如:Spring Cloud Config、Spring Cloud Netflix、Spring Cloud CloudFoundry、Spring Cloud AWS、Spring Cloud Security、Spring Cloud Commons、Spring Cloud Zookeeper、Spring Cloud CLI等项目。这些项目是Spring将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理, 最终给我们开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud 具有特性,以及适用于哪些场景等包含:
- 基于版本的分布式配置管理
- 服务注册与发现
- 路由
- 服务之间调用(依赖)
- 负载均衡
- 断路器
- 全局锁(分布式锁)
- 选主以及集群状态管理
- 分布式消息服务
Spring Cloud的核心是服务治理。而服务治理主要通过整合Netflix的相关产品来实现这方面的功能,也就是Spring Cloud Netflix,在该项目中包括用于服务注册和发现的Eureka,调用断路器Hystrix,调用端负载均衡Ribbon,Rest客户端Feign,智能服务路由Zuul,用于监控数据收集和展示的Spectator、Servo、 Atlas,用于配置读取的Archaius和提供Controller层Reactive封装的RxJava。 除此之外,针对Feign和RxJava并不是Netiflix的产品,但也被整合到了Spring Cloud Netflix中。
Eureka核心概念
Eureka 作为 Spring Cloud 体系中最核心、默认的注册中心组件,研究它的运行机制,有助于我们在工作中更好地使用它。 看下图服务注册调用示意图,服务提供者和服务的消费者,本质上也是 Eureka Client 角色。整体上可以分为两个主体:Eureka Server 和 Eureka Client。
Eureka Server:注册中心服务端
注册中心服务端主要对外提供了三个功能:
- 服务注册
服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息, Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表 - 提供注册表
服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表 - 同步状态
Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。
Eureka Client:注册中心客户端
Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。 Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。
Register: 服务注册
服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。 当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地 址、端口,运行状况指示符 URL,主页等。
Renew: 服务续约
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注 册表中删除,此时间可配置,一般情况不建议更改。
服务续约的两个重要属性
# 服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease‐renewal‐interval‐in‐seconds=30
# 服务失效的时间,默认为90秒。
eureka.instance.lease‐expiration‐duration‐in‐seconds=90
Eviction:服务剔除
当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。
Cancel:服务下线
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
GetRegisty: 获取注册列表信息
Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同, Eureka Client 自动处理。 如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。
获取服务是服务消费者的基础,所以必有两个重要参数需要注意:
# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch‐registry=true
# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry‐fetch‐interval‐seconds=30
Remote Call: 远程调用
当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求 调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。
Eurka 工作流程
整体梳理一下 Eureka 的工作流程:
- Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息
- Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务
- Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常
- 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例
- 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳, 则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端
- 当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保 护模式
- Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地
- 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存
- Eureka Client 获取到目标服务器信息,发起服务调用
- Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除
这就是Eurka基本工作流程。
Eureka 为了保障注册中心的高可用性,容忍了数据的非强一致性,服务节点间的数据可能不一致, Client-Server 间的数据可能不一致。比较适合跨越多机房、对注册中心服务可用性要求较高的使用场景。
搭建Eureka注册中心
这里我们以创建并运行Eureka注册中心来看看在IDEA中创建并运行SpringCloud应用的正确姿势。
使用IDEA来创建SpringCloud应用
创建一个eureka-server模块,并使用Spring Initializer初始化一个 SpringBoot项目
填写应用信息
选择你需要的SpringCloud组件进行创建
创建完成后会发现pom.xml文件中已经有了eureka-server的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐netflix‐eureka‐server</artifactId>
</dependency>
在启动类上添加@EnableEurekaServer注解来启用Euerka注册中心功能
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
在配置文件application.properties中添加Eureka注册中心的配置
#指定运行端口
server.port=8001
#指定服务名称
spring.application.name=eureka‐server
#指定主机地址
eureka.instance.hostname=localhost
#指定是否要从注册中心获取服务(注册中心不需要开启)
eureka.client.fetch‐registry=false
#指定是否要注册到注册中心(注册中心不需要开启)
eureka.client.register‐with‐eureka=false
#关闭保护模式
eureka.server.enable‐self‐preservation=false
使用IDEA的Run Dashboard来运行SpringCloud应用
此时服务已经创建完成,点击启动类的main方法就可以运行了。但是在微服务项目中我们会启动很多服务,为了便于管理,我们使用IDEA的Run Dashboard来启动。
打开Run Dashboard,默认情况下,当IDEA检查到你的项目中有 SpringBoot应用时,会提示你开启,如果你没开启,可以用以下方法开启。
运行SpringCloud应用
运行完成后访问地址http://localhost:8001/
可以看到Eureka注册中心的界面
搭建Eureka客户端
新建一个eureka-client模块,并在pom.xml中添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId>
</dependency>
在启动类上添加@EnableDiscoveryClient注解表明是一个Eureka客户端
importorg.springframework.boot.SpringApplication;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
importorg.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication{
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
在配置文件application.properties中添加Eureka客户端的配置
#运行端口号
server.port=8101
#服务名称
spring.application.name=eureka‐client
#注册到Eureka的注册中心
eureka.client.register‐with‐eureka=true
#获取注册实例列表
eureka.client.fetch‐registry=true
#配置注册中心地址
eureka.client.service‐url.defaultZone=http://localhost:8001/eureka/
运行eureka-client
查看注册中心http://localhost:8001/
发现Eureka客户端已经成功注册
搭建Eureka注册中心集群
搭建两个注册中心
由于所有服务都会注册到注册中心去,服务之间的调用都是通过从注册中心获取的服务列表来调用,注册中心一旦宕机,所有服务调用都会出现问题。所以我们需要多个注册中心组成集群来提供服务,下面将搭建一个双节点的注册中心集群。
给eureka-sever添加配置文件application-replica1.yml配置第一个注册中心
server:
port: 8002
spring:
application:
name: eureka-server
eureka:
instance:
hostname: replica1
client:
serviceUrl:
defaultZone: http://replica2:8003/eureka/ #注册到另一个Eureka注册中心
fetch‐registry: true
register‐with‐eureka: true
给eureka-sever添加配置文件application-replica2.yml配置第二个注册中心
server:
port: 8003
spring:
application:
name: eureka-server
eureka:
instance:
hostname: replica2
client:
serviceUrl:
defaultZone: http://replica1:8002/eureka/ #注册到另一个Eureka注册中心
fetch‐registry: true
register‐with‐eureka: true
这里我们通过两个注册中心互相注册,搭建了注册中心的双节点集群,由于 defaultZone使用了域名,所以还需在本机的host文件中配置一下。
修改本地host文件
127.0.0.1 replica1
127.0.0.1 replica2
运行Eureka注册中心集群
在IDEA中我们可以通过使用不同的配置文件来启动同一个SpringBoot应用。
- 添加两个配置,分别以application-replica1.yml和application-replica2.yml来启动eureka-server
- 从原启动配置中复制一个出来
- 配置启动的配置文件
启动两个eureka-server,访问其中一个注册中心http://replica1:8002/
发现另一个已经成为其备份
修改Eureka-client,让其连接到集群
添加eureka-client的配置文件application-replica.yml,让其同时注册到两个注册中心。
server:
port: 8102
spring:
application:
name: eureka-client
eureka:
client:
serviceUrl:
defaultZone: http://replica1:8002/eureka/,http://replica2:8003/eureka/ #同时注册到两个注册中心
fetch‐registry: true
register‐with‐eureka: true
以该配置文件启动后访问任意一个注册中心节点都可以看到eureka-client
给Eureka注册中心添加认证
创建一个eureka-security-server模块,在pom.xml中添加以下依赖
需要添加SpringSecurity模块。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐netflix‐eureka‐server</artifactId>
</dependency>
添加application.yml配置文件
主要是配置了登录注册中心的用户名和密码。
server:
port: 8004
spring:
application:
name: eureka‐security‐server
security: #配置SpringSecurity登录用户名和密码
user:
name: zhuawa
password: 123456
eureka:
instance:
hostname: localhost
client:
fetch‐registry: false
register‐with‐eureka: false
添加Java配置WebSecurityConfig
默认情况下添加SpringSecurity依赖的应用每个请求都需要添加CSRF token才能访问,Eureka客户端注册时并不会添加,所以需要配置/eureka/**路径不需要CSRF token。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}
运行eureka-security-server,访问http://localhost:8004
发现需要登录认证
eureka-client注册到有登录认证的注册中心
配置文件中需要修改注册中心地址格式
http://${username}:${password}@${hostname}:${port}/eureka/
添加application-security.yml配置文件,按格式修改用户名和密码
server:
port: 8103
spring:
application:
name: eureka-client
eureka:
client:
serviceUrl:
defaultZone: http://zhuawa:123456@localhost:8004/eureka/
fetch‐registry: true
register‐with‐eureka: true
以application-security.yml配置运行eureka-client,可以在注册中心界面看到eureka-client已经成功注册
附:Eureka的常用配置
eureka:
client: #eureka客户端配置
register‐with‐eureka: true #是否将自己注册到eureka服务端上去
fetch‐registry: true #是否获取eureka服务端上注册的服务列表
service‐url:
defaultZone: http://localhost:8001/eureka/ # 指定注册中心地址
enabled: true # 启用eureka客户端
registry‐fetch‐interval‐seconds: 30 #定义去eureka服务端获取服务列表的时间间隔
instance: #eureka客户端实例配置
lease‐renewal‐interval‐in‐seconds: 30 #定义服务多久去注册中心续约
lease‐expiration‐duration‐in‐seconds: 90 #定义服务多久不去续约认为服务失效
metadata‐map:
zone: jiangsu #所在区域
hostname: localhost #服务主机名称
prefer‐ip‐address: false #是否优先使用ip来作为主机名
server: #eureka服务端配置
enable‐self‐preservation: false #关闭eureka服务端的保护机制
Eureka 源码分析
Eureka Client
基于Spring Cloud的 eureka 的 client 端在启动类上加上 @EnableDiscoveryClient 注解,就可以 用 NetFlix 提供的 Eureka client。下面就以 @EnableDiscoveryClient 为入口,进行Eureka Client的源码分析。 @EnableDiscoveryClient,通过源码可以发现这是一个标记注解:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.discovery;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}
通过注释可以知道 @EnableDiscoveryClient 注解是用来启用 DiscoveryClient 的实现,DiscoveryClient接口代码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.discovery;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
public interface DiscoveryClient {
String description();
List<ServiceInstance> getInstances(String serviceId);
List<String> getServices();
}
接口说明:
- description():实现描述。
- getInstances(String serviceId):获取与特定serviceId关联的所有 ServiceInstance
- getServices():返回所有已知的服务ID
DiscoveryClient 接口的实现结构图:
- EurekaDiscoveryClient:Eureka 的 DiscoveryClient 实现类。
- CompositeDiscoveryClient:用于排序可用客户端的发现客户端的顺序。
- NoopDiscoveryClient:什么都不做的服务发现实现类,已经被废弃。
- SimpleDiscoveryClient:简单的服务发现实现类 SimpleDiscoveryClient,具体的服务实例从 SimpleDiscoveryProperties 配置中获取。
EurekaDiscoveryClient 是 Eureka 对 DiscoveryClient接口的实现,代码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.netflix.eureka;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.PortType;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.util.Assert;
public class EurekaDiscoveryClient implements DiscoveryClient {
public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client";
private final EurekaInstanceConfig config;
private final EurekaClient eurekaClient;
public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
this.config = config;
this.eurekaClient = eurekaClient;
}
public String description() {
return "Spring Cloud Eureka Discovery Client";
}
public List<ServiceInstance> getInstances(String serviceId) {
List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId, false);
List<ServiceInstance> instances = new ArrayList();
Iterator var4 = infos.iterator();
while(var4.hasNext()) {
InstanceInfo info = (InstanceInfo)var4.next();
instances.add(new EurekaDiscoveryClient.EurekaServiceInstance(info));
}
return instances;
}
public List<String> getServices() {
Applications applications = this.eurekaClient.getApplications();
if (applications == null) {
return Collections.emptyList();
} else {
List<Application> registered = applications.getRegisteredApplications();
List<String> names = new ArrayList();
Iterator var4 = registered.iterator();
while(var4.hasNext()) {
Application app = (Application)var4.next();
if (!app.getInstances().isEmpty()) {
names.add(app.getName().toLowerCase());
}
}
return names;
}
}
public static class EurekaServiceInstance implements ServiceInstance {
private InstanceInfo instance;
public EurekaServiceInstance(InstanceInfo instance) {
Assert.notNull(instance, "Service instance required");
this.instance = instance;
}
public InstanceInfo getInstanceInfo() {
return this.instance;
}
public String getServiceId() {
return this.instance.getAppName();
}
public String getHost() {
return this.instance.getHostName();
}
public int getPort() {
return this.isSecure() ? this.instance.getSecurePort() : this.instance.getPort();
}
public boolean isSecure() {
return this.instance.isPortEnabled(PortType.SECURE);
}
public URI getUri() {
return DefaultServiceInstance.getUri(this);
}
public Map<String, String> getMetadata() {
return this.instance.getMetadata();
}
}
}
从代码可以看出 EurekaDiscoveryClient 实现了 DiscoveryClient 定义的规范接口,真正实现发现服务的是 EurekaClient,下面是 EurekaClient 依赖结构图:
EurekaClient 唯一实现类 DiscoveryClient,查看DiscoveryClient 的构造方法可以发现主要做了下面几件事:
- 创建了scheduler定时任务的线程池,heartbeatExecutor心跳检查线程池(服务续约),cacheRefreshExecutor(服务获取)
- 然后initScheduledTasks()开启上面三个线程池,往上面3个线程池分别添加相应任务。然后创建了一个instanceInfoReplicator(Runnable任务),然后调用InstanceInfoReplicator.start方法,把这个任务放进上面 scheduler定时任务线程池(服务注册并更新)。
服务注册(Registry)
上面说了,initScheduledTasks()方法中调用了InstanceInfoReplicator.start() 方法,InstanceInfoReplicator 的 run()方法代码如下:
public void run() {
boolean var6 = false;
ScheduledFuture next;
label53: {
try {
var6 = true;
this.discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
this.discoveryClient.register();
this.instanceInfo.unsetIsDirty(dirtyTimestamp);
var6 = false;
} else {
var6 = false;
}
break label53;
} catch (Throwable var7) {
logger.warn("There was a problem with the instance info replicator", var7);
var6 = false;
} finally {
if (var6) {
ScheduledFuture next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
}
}
next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
return;
}
next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
}
发现 InstanceInfoReplicator的run方法,run方法中会调用DiscoveryClient的 register方法。DiscoveryClient 的 register方法代码如下:
boolean register() throws Throwable {
logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
} catch (Exception var3) {
logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
throw var3;
}
if (logger.isInfoEnabled()) {
logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
最终又经过一系列调用,最终会调用到AbstractJerseyEurekaHttpClient的 register方法,代码如下:
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
EurekaHttpResponse var5;
try {
Builder resourceBuilder = this.jerseyClient.resource(this.serviceUrl).path(urlPath).getRequestBuilder();
this.addExtraHeaders(resourceBuilder);
response = (ClientResponse)((Builder)((Builder)((Builder)resourceBuilder.header("Accept-Encoding", "gzip")).type(MediaType.APPLICATION_JSON_TYPE)).accept(new String[]{"application/json"})).post(ClientResponse.class, info);
var5 = EurekaHttpResponse.anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", new Object[]{this.serviceUrl, urlPath, info.getId(), response == null ? "N/A" : response.getStatus()});
}
if (response != null) {
response.close();
}
}
return var5;
}
可以看到最终通过http rest请求eureka server端,把应用自身的InstanceInfo 实例注册给server端,我们再来完整梳理一下服务注册流程:
Renew服务续约
服务续约和服务注册非常类似,HeartbeatThread 代码如下:
// DiscoveryClient 内部类
private class HeartbeatThread implements Runnable {
private HeartbeatThread() {
}
// 更新最后一次心跳的时间
public void run() {
if (DiscoveryClient.this.renew()) {
DiscoveryClient.this.lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
// 续约的主方法
boolean renew() {
try {
EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
logger.debug("DiscoveryClient_{} - Heartbeat status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == 404) {
this.REREGISTER_COUNTER.increment();
logger.info("DiscoveryClient_{} - Re-registering apps/{}", this.appPathIdentifier, this.instanceInfo.getAppName());
long timestamp = this.instanceInfo.setIsDirtyWithTime();
boolean success = this.register();
if (success) {
this.instanceInfo.unsetIsDirty(timestamp);
}
return success;
} else {
return httpResponse.getStatusCode() == 200;
}
} catch (Throwable var5) {
logger.error("DiscoveryClient_{} - was unable to send heartbeat!", this.appPathIdentifier, var5);
return false;
}
}
发送心跳 ,请求 eureka server 端 ,如果接口返回值为404,就是说服务不存在,那么重新走注册流程。
服务续约流程如下图:
服务下线cancel
在服务shutdown的时候,需要及时通知服务端把自己剔除,以避免客户端调用 已经下线的服务,shutdown()方法代码如下:
@PreDestroy
public synchronized void shutdown() {
if (this.isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (this.statusChangeListener != null && this.applicationInfoManager != null) {
this.applicationInfoManager.unregisterStatusChangeListener(this.statusChangeListener.getId());
}
// 关闭各种定时任务
// 关闭刷新实例信息/注册的定时任务
// 关闭续约(心跳)的定时任务
// 关闭获取注册信息的定时任务
this.cancelScheduledTasks();
if (this.applicationInfoManager != null && this.clientConfig.shouldRegisterWithEureka() && this.clientConfig.shouldUnregisterOnShutdown()) {
// 更改实例状态,使实例不再接受流量
this.applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
// 向EurekaServer端发送下线请求
this.unregister();
}
if (this.eurekaTransport != null) {
this.eurekaTransport.shutdown();
}
this.heartbeatStalenessMonitor.shutdown();
this.registryStalenessMonitor.shutdown();
logger.info("Completed shut down of DiscoveryClient");
}
}
private void cancelScheduledTasks() {
if (this.instanceInfoReplicator != null) {
this.instanceInfoReplicator.stop();
}
if (this.heartbeatExecutor != null) {
this.heartbeatExecutor.shutdownNow();
}
if (this.cacheRefreshExecutor != null) {
this.cacheRefreshExecutor.shutdownNow();
}
if (this.scheduler != null) {
this.scheduler.shutdownNow();
}
}
void unregister() {
if (this.eurekaTransport != null && this.eurekaTransport.registrationClient != null) {
try {
logger.info("Unregistering ...");
EurekaHttpResponse<Void> httpResponse = this.eurekaTransport.registrationClient.cancel(this.instanceInfo.getAppName(), this.instanceInfo.getId());
logger.info("DiscoveryClient_{} - deregister status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
} catch (Exception var2) {
logger.error("DiscoveryClient_{} - de-registration failed{}", new Object[]{this.appPathIdentifier, var2.getMessage(), var2});
}
}
}
先关闭各种定时任务,然后向eureka server 发送服务下线通知。服务下线流程如下图:
Eureka Server
从 @EnableEurekaServer 注解为入口分析,通过源码可以看出他是一个标记注解:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.netflix.eureka.server;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}