本章主要内容
为什么服务发现对基于云的应用程序环境很重要
与传统的负载均衡方法作对比,了解服务发现的优缺点
建立一个Spring Netflix Eureka服务器
通过Eureka注册一个基于Spring Boot的微服务
使用Spring Cloud和Netflix的Ribbon库来完成客户端负载均衡
在任何分布式架构中,都需要找到机器所在的物理地址。这个概念自分布式计算开始出现就已经存在,并且被正式称为服务发现。服务发现可以非常简单,只需要维护一个属性文件,这个属性文件包含应用程序使用的所有远程服务的地址,也可以像通用描述、发现与集成服务(Universal Description, Discovery, and Integration,UUDI)存储库一样正式(和复杂)。
服务发现对于微服务和基于云的应用程序至关重要,主要原因有两个。首先,它为应用团队提供了一种能力,可以快速地对在环境中运行的服务实例数量进行水平伸缩。通过服务发现,服务消费者能够将服务的物理位置抽象出来。由于服务消费者不知道实际服务实例的物理位置,因此可以从可用服务池中添加或移除服务实例。
这种在不影响服务消费者的情况下快速伸缩服务的能力是一个非常强大的概念,因为它驱使习惯于构建单一整体、单一租户(如一个客户)的应用程序的开发团队,远离仅考虑通过增加更大型、更好的硬件(垂直伸缩)的方法来扩大服务,而是通过更强大的方法——添加更多服务器(水平伸缩)来实现扩大。
单体架构通常会驱使开发团队在过度购买处理能力的道路上越走越远。处理能力的增长以跳跃式和峰值的形式体现出来,很少按照平稳路径的形式增长。微服务允许开发人员对服务实例进行伸缩。服务发现有助于抽象出这些服务部署,使它们远离服务消费者。
服务发现的第二个好处是,它有助于提高应用程序的弹性。当微服务实例变得不健康或不可用时,大多数服务发现引擎将从内部可用服务列表中移除该实例。由于服务发现引擎会在路由服务时绕过不可用服务,因此能够使不可用服务造成的损害最小。
我们已经了解了服务发现的好处,但是它有什么大不了的呢?难道我们就不能使用诸如域名服务(Domain Name Service,DNS)或负载均衡器等可靠的方法来帮助实现服务发现吗?接下来让我们就来讨论一下,为什么这些方法不适用于基于微服务的应用程序,特别是在云中运行的应用程序。
4.1 我的服务在哪里
每当应用程序调用分布在多个服务器上的资源时,这个应用程序就需要定位这些资源的物理位置。在非云的世界中,这种服务位置解析通常由DNS和网络负载均衡器的组合来解决。图4-1展示了这个模型。
图4-1 使用DNS和负载均衡器的传统服务位置解析模型
应用程序需要调用位于组织其他部分的服务。它尝试通过使用通用DNS名称以及唯一表示需要调用的服务的路径来调用该服务。DNS名称会被解析到一个商用负载均衡器(如流行的F5负载均衡器)或开源负载均衡器(如HAProxy)。
负载均衡器在接收到来自服务消费者的请求时,会根据服务消费者尝试访问的路径,在路由表中定位物理地址条目。此路由表条目包含托管该服务的一个或多个服务器的列表。接着,负载均衡器选择列表中的一个服务器,并将请求转发到该服务器上。
服务的每个实例被部署到一个或多个应用服务器。这些应用程序服务器的数量往往是静态的(例如,托管服务的应用程序服务器的数量并没有增加和减少)和持久的(例如,如果运行应用程序的服务器崩溃,它将恢复到崩溃时的状态,并将具有与之前相同的IP和配置)。
为了实现高可用性,辅助负载均衡器会处于空闲状态,并ping主负载均衡器以查看它是否处于存活(alive)状态。如果主负载均衡器未处于存活状态,那么辅助负载均衡器将变为存活状态,接管主负载均衡器的IP地址并开始提供请求。
这种模型适用于在企业数据中心内部运行的应用程序,以及在一组静态服务器上运行少量服务的情况,但对基于云的微服务应用程序来说,这种模型并不适用。原因有以下几个。
单点故障——虽然负载均衡器可以实现高可用,但这是整个基础设施的单点故障。如果负载均衡器出现故障,那么依赖它的每个应用程序都会出现故障。尽管可以使负载平衡器高度可用,但负载均衡器往往是应用程序基础设施中的集中式阻塞点。
有限的水平可伸缩性——在服务集中到单个负载均衡器集群的情况下,跨多个服务器水平伸缩负载均衡基础设施的能力有限。许多商业负载均衡器受两件事情的限制:冗余模型和许可证成本。第一,大多数商业负载均衡器使用热插拔模型实现冗余,因此只能使用单个服务器来处理负载,而辅助负载均衡器仅在主负载均衡器中断的情况下,才能进行故障切换。这种架构本质上受到硬件的限制。第二,商业负载均衡器具有有限数量的许可证,它面向固定容量模型而不是更可变的模型。
静态管理——大多数传统的负载均衡器不是为快速注册和注销服务设计的。它们使用集中式数据库来存储规则的路由,添加新路由的唯一方法通常是通过供应商的专有API(Application Programming Interface,应用程序编程接口)来进行添加。
复杂——由于负载均衡器充当服务的代理,它必须将服务消费者的请求映射到物理服务。这个翻译层通常会为服务基础设施增加一层复杂度,因为开发人员必须手动定义和部署服务的映射规则。在传统的负载均衡器方案中,新服务实例的注册是手动完成的,而不是在新服务实例启动时完成的。
这4个原因并不是对负载均衡器的刻意指摘。负载均衡器在企业级环境中工作良好,在这种环境中,大多数应用程序的大小和规模可以通过集中式网络基础设施来处理。此外,负载均衡器仍然可以在集中化 SSL 终端和管理服务端口安全性方面发挥作用。负载均衡器可以锁定位于它后面的所有服务器的入站(入口)端口和出站(出口)端口访问。在需要满足行业标准的认证要求,如PCI(Payment Card Industry,支付卡行业)合规时,这种最小网络访问概念经常是关键组成部分。
然而,在需要处理大量事务和冗余的云环境中,集中的网络基础设施并不能最终发挥作用,因为它不能有效地伸缩,并且成本效益也不高。现在我们来看一下,如何为基于云的应用程序实现一个健壮的服务发现机制。
4.2 云中的服务发现
基于云的微服务环境的解决方案是使用服务发现机制,这一机制具有以下特点。
高可用——服务发现需要能够支持“热”集群环境,在服务发现集群中可以跨多个节点共享服务查找。如果一个节点变得不可用,集群中的其他节点应该能够接管工作。
点对点——服务发现集群中的每个节点共享服务实例的状态。
负载均衡——服务发现需要在所有服务实例之间动态地对请求进行负载均衡,以确保服务调用分布在由它管理的所有服务实例上。在许多方面,服务发现取代了许多早期Web应用程序实现中使用的更静态的、手动管理的负载均衡器。
有弹性——服务发现的客户端应该在本地“缓存”服务信息。本地缓存允许服务发现功能逐步降级,这样,如果服务发现服务变得不可用,应用程序仍然可以基于本地缓存中维护的信息来运行和定位服务。
容错——服务发现需要检测出服务实例什么时候是不健康的,并从可以接收客户端请求的可用服务列表中移除该实例。服务发现应该在没有人为干预的情况下,对这些故障进行检测,并采取行动。
在接下来的几节中,我们将:
了解基于云的服务发现代理的工作方式的概念架构;
展示即使在服务发现代理不可用时,客户端缓存和负载均衡如何使服务能够继续发挥作用;
了解如何使用Spring Cloud和Netflix的Eureka服务发现代理实现服务发现功能。
4.2.1 服务发现架构
为了开始讨论服务发现架构,我们需要了解4个概念。这些一般概念在所有服务发现实现中是共通的。
服务注册——服务如何使用服务发现代理进行注册?
服务地址的客户端查找——服务客户端查找服务信息的方法是什么?
信息共享——如何跨节点共享服务信息?
健康监测——服务如何将它的健康信息传回给服务发现代理?
图4-2展示了这4个概念的流程,以及在服务发现模式实现中通常发生的情况。
图4-2 随着服务实例的添加与删除,它们将更新服务发现代理,并可用于处理用户请求
在图4-2中,启动了一个或多个服务发现节点。这些服务发现实例通常是独立的,在它们之前一般不会有负载均衡器。
当服务实例启动时,它们将通过一个或多个服务发现实例来注册它们可以访问的物理位置、路径和端口。虽然每个服务实例都具有唯一的IP地址和端口,但是每个服务实例都将以相同的服务ID进行注册。服务ID是唯一标识一组相同服务实例的键。
服务通常只在一个服务发现实例中进行注册。大多数服务发现的实现使用数据传播的点对点模型,每个服务实例的数据都被传递到服务发现集群中的所有其他节点。
根据服务发现实现机制的不同,传播机制可能会使用硬编码的服务列表来进行传播,也可能会使用像“gossip”或“infection-style”协议这样的多点广播协议,以允许其他节点在集群中“发现”变更。
最后,每个服务实例将通过服务发现服务去推送服务实例的状态,或者服务发现服务从服务实例拉取状态。任何未能返回良好的健康检查信息的服务都将从可用服务实例池中删除。
服务在向服务发现服务进行注册之后,这个服务就可以被需要使用这项服务功能的应用程序或其他服务使用。客户端可以使用不同的模型来“发现”服务。在每次调用服务时,客户端可以只依赖于服务发现引擎来解析服务位置。使用这种方法,每次调用注册的微服务实例时,服务发现引擎就会被调用。但是,这种方法很脆弱,因为服务客户端完全依赖于服务发现引擎来查找和调用服务。
一种更健壮的方法是使用所谓的客户端负载均衡。图4-3阐示了这种方法。
图4-3 客户端负载均衡缓存服务的位置,以便服务客户端不必在每次调用时联系服务发现
在这个模型中,当服务消费者需要调用一个服务时:
(1)它将联系服务发现服务,获取它请求的所有服务实例,然后在服务消费者的机器上本地缓存数据。
(2)每当客户端需要调用该服务时,服务消费者将从缓存中查找该服务的位置信息。通常,客户端缓存将使用简单的负载均衡算法,如“轮询”负载均衡算法,以确保服务调用分布在多个服务实例之间。
(3)然后,客户端将定期与服务发现服务进行联系,并刷新服务实例的缓存。客户端缓存最终是一致的,但是始终存在这样的风险:在客户端联系服务发现实例以进行刷新和调用时,调用可能会被定向到不健康的服务实例上。
如果在调用服务的过程中,服务调用失败,那么本地的服务发现缓存失效,服务发现客户端将尝试从服务发现代理刷新数据。
现在,让我们使用通用服务发现模式,并将它应用到EagleEye问题域。
4.2.2 使用Spring和Netflix Eureka进行服务发现实战
现在,我们将通过创建一个服务发现代理来实现服务发现,然后通过代理注册两个服务。接着,通过使用服务发现检索到的信息,让一个服务调用另一个服务。Spring Cloud提供了多种从服务发现代理查找信息的方法。本书将介绍每种方法的优点和缺点。
Spring Cloud项目再一次让这种创建变得极其简单。本书将使用Spring Cloud和Netflix的Eureka服务发现引擎来实现服务发现模式。对于客户端负载均衡,本书使用Spring Cloud和Netflix的Ribbon库。
在前两章中,我们尽可能让许可证服务保持简单,并将组织名称和许可证数据包含在许可证中。在本章中,我们将把组织信息分解到它自己的服务中。
当许可证服务被调用时,它将调用组织服务以检索与指定的组织ID相关联的组织信息。组织服务的位置的实际解析存储在服务发现注册表中。本例将使用服务发现注册表注册两个组织服务实例,然后使用客户端负载均衡来查找服务,并在每个服务实例中缓存注册表。图4-4展示了这个过程。
图4-4 通过许可证服务和组织服务实现客户端缓存和Eureka,可以减轻Eureka服务器上的负载, 并提高Eureka不可用时的客户端稳定性
(1)随着服务的启动,许可证和组织服务将通过Eureka服务进行注册。这个注册过程将告诉Eureka每个服务实例的物理位置和端口号,以及正在启动的服务的服务ID。
(2)当许可证服务调用组织服务时,许可证服务将使用Netflix Ribbon库来提供客户端负载均衡。Ribbon将联系Eureka服务去检索服务位置信息,然后在本地进行缓存。
(3)Netflix Ribbon库将定期对Eureka服务进行ping操作,并刷新服务位置的本地缓存。
任何新的组织服务实例现在都将在本地对许可证服务可见,而任何不健康实例都将从本地缓存中移除。
接下来,我们将通过建立Spring Cloud Eureka服务来实现这个设计。
4.3 构建Spring Eureka服务
在本节中,我们将通过Spring Boot建立Eureka服务。
与Spring Cloud配置服务一样,我们将从构建新的Spring Boot项目开始,并应用注解和配置来建立Spring Cloud Eureka服务。
首先从Maven的pom.xml 开始。代码清单4-1展示了正在建立的Spring Boot项目所需的Eureka服务依赖项。
代码清单4-1 添加依赖项到pom.xml
<?xml version="1.0" encoding="UTF-8 ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.thoughtmechanix</groupId>
<artifactId>eurekasvr</artifactId>
<version>0.0.1.SNAPSHOT</version>
<packaging>jar</packaging>
<name>Eureka Server</name>
<description>Eureka Server demo project</description>
<!--没有显示使用Spring Cloud Parent的Maven定义-->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId> ⇽-- 告诉Maven构建包含Eureka库(其中包括Ribbon)
</dependency>
</dependencies>
为了简洁,省略了pom.xml的其余部分.
...
</project>
接着,需要创建src/main/resources/application.yml文件,在这里需要添加以独立模式(例如,集群中没有其他节点)运行Eureka服务所需的配置,如代码清单4-2所示。
代码清单4-2 在application.yml文件中创建Eureka配置
server:
port: 8761 ⇽--- Eureka服务器将要监听的端口
eureka:
client:
registerWithEureka: false ⇽不要使用Eureka服务进行注册
fetchRegistry: false ⇽-不要在本地缓存注册表信息
server:waitTimeInMsWhenSyncEmpty: 5 ⇽--- 在服务器接收请求之前等待的初始时间
要设置的关键属性是server.port属性,它用于设置Eureka服务的默认端口。eureka.client.registerWithEureka属性会告知服务,
在Spring Boot Eureka应用程序启动时不要通过Eureka服务注册,因为它本身就是Eureka服务。
eureka.client.fetchRegistry属性设置为false,以便Eureka服务启动时,它不会尝试在本地缓存注册表信息。
在运行Eureka客户端时,为了缓存通过Eureka注册的Spring Boot服务,我们需要更改eureka.client.fetchRegistry的值。
读者会注意到,最后一个属性eureka.server.waitTimeInMsWhenSyncEmpty被注释掉了。在本地测试服务时,读者应该取消注释此行,因为Eureka不会马上通告任何通过它注册的服务,默认情况下它会等待5 min,让所有的服务都有机会在通告它们之前通过它来注册。进行本地测试时取消注释此行,将有助于加快Eureka服务启动和显示通过它注册服务所需的时间。( 百度搜索该选项的含义为:在Eureka服务器获取不到集群里对等服务器上的实例时,需要等待的时间,单位为毫秒, 默认为1000 * 60 * 5)
每次服务注册需要30 s的时间才能显示在Eureka服务中,因为Eureka需要从服务接收3次连续心跳包ping,每次心跳包ping间隔10 s,然后才能使用这个服务。在部署和测试服务时,要牢记这一点。
在建立Eureka服务时,需要进行的最后一项工作就是在启动Eureka服务的应用程序引导类中添加注解。
对于Eureka服务,应用程序引导类可以在src/main/java/com/thoughtmechanix/eurekasvr/ EurekaServerApplication.java中找到。
代码清单4-3展示了添加注解的位置。
代码清单4-3 标注引导类以启用Eureka服务器
package com.thoughtmechanix.eurekasvr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer ⇽--- 在Spring服务中启用Eureka服务器
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
只需要使用一个新的注解@EnableEurekaServer,就可以让我们的服务成为一个Eureka服务。此时,可以通过运行mvn spring-boot:run或运行docker-compose(参见附录A)来启动服务。一旦运行这个命令,Eureka服务就会运行,此时没有任何服务注册在这个Eureka服务中。接下来,我们将构建组织服务,并通过这个Eureka服务注册。