微服务学习之路
微服务学习之路,本系列文章是博主学习微服务的路程,特意分享自己的学习笔记,也为了以后能回溯查看。
服务发现
前言
本章介绍服务发现的实现原理。
一、服务发现是什么
在任何分布式架构中,都需要找到机器所在的物理位置。这个概念自分布式计算开始出现就已经存在,并且被正式成为服务发现。
服务发现对于微服务和基于云的应用程序至关重要,主要有两个原因:
- 它为应用团队提供了一种能力,可以快速地对在环境中运行的服务实例数量进行水平伸缩。
- 通过服务发现,服务消费者能够将服务的物理位置抽象出来。由于服务消费者不知道实际服务实例的物理位置,因此可以从可用服务池中添加或移除服务实例。
二、云中的服务发现
基于云的微服务环境的解决方案是使用服务发现机制,这一机制具有以下特点:
- 高可用。服务发现需要能够支持“热”集群环境,在服务发现集群中可以跨多个节点共享服务查找。如果一个节点变得不可用,集群中的其他节点应该能够接管工作。
- 点对点。服务发现集群中的每个节点共享服务实例的状态。
- 负载均衡。服务发现需要在所有服务实例之间动态地对请求进行负载均衡,以确保服务调用分布在由它管理的所有服务实例上。
- 有弹性。服务发现的客户端应该在本地“缓存”服务信息。本地缓存允许服务发现功能逐步降级,这样,如果服务发现服务变得不可用时,应用程序仍然可以基于本地缓存中维护的信息来运行和定位服务。
- 容错。服务发现需要检测出服务实例什么时候是不健康的,并从可以接收客户端请求的可用服务列表中移除该实例。
- 了解基于云的服务发现代理的工作方式的概念架构。
- 展示即使服务发现代理不可用时,客户端缓存和负载均衡如何使服务能够继续发挥作用。
- 了解如何使用Spring Cloud和Netfix的Eureka服务发现代理实现服务发现功能。
2.1. 服务发现架构
要了解服务发现架构,先熟悉一下4个概念:
- 服务注册。服务如何使用服务发现代理进行注册。
- 服务地址的客户端查找。服务客户端查找服务信息的方法是什么?
- 信息共享。如何跨节点共享服务信息?
- 健康监测。服务如何将它的健康信息传回服务发现代理?
当服务实例启动时,它们将通过一个或多个服务发现实例来注册它们可以访问的物理位置路径和端口。虽然每个服务实例都具有唯一的IP地址和端口,但是每个服务实例都将以相同的服务ID进行注册。服务ID是唯一标识一组相同服务实例的键。
服务通常只在一个服务发现中注册。大多数服务发现的实现使用数据传播的点对点模型,每个服务实例的数据都被传递到服务发现集群中的所有其他节点。
最后,每个服务实例将通过服务发现服务去推送服务实例的状态,或者服务发现服务从服务实例拉取状态。任何未能返回良好的健康检查信息的服务都将从可用服务实例池中删除。
服务在向服务发现代理进行注册后,这个服务就可以被需要用到该服务的其他应用程序使用。在每次调用服务时,客户端可以只依赖于服务发现引擎来解析服务位置。使用这种方法,每次调用服务时,服务发现引擎就会被调用,但是这种方法很脆弱,因为服务客户端完全依赖于服务发现引擎来查找和调用服务。
一种更健壮的方法是使用所谓的客户端负载均衡。
在客户端负载均衡模型中:
- 它将联系服务发现代理,获取它请求的所有服务实例,然后在服务消费者的机器上本地缓存数据。
- 每当客户端需要调用该服务时,服务消费者将从缓存中查找该服务的位置信息。通常客户端缓存将使用简单的负载均衡算法,如“轮询”负载均衡算法,以确保服务调用分布式多个服务实例之间。
- 然后,客户端将定期与服务发现代理进行联系,并刷新服务实例的缓存。客户端缓存最终是一致的,但是始终存在这样的风险,在客户端联系服务发现实例以进行刷新和调用时,调用可能会被定向到不健康的服务实例上。
2.2. 使用Spring和Netflix Eureka进行服务发现实战
本文将使用Spring Cloud和Neflix的Eureka服务发现引擎来实现服务发现模式。对于客户端负载均衡,本书使用Spring Cloud和Neflix的Ribbon库。
以下示例是准备使用服务发现注册两个服务实例,然后使用客户端负载均衡来查找服务,并在每个服务实例中缓存注册表。
- 随着服务的启动,服务A和服务B将通过Eureka服务进行注册。这个注册过程将告诉Eureka每个服务实例的物理位置和端口号,以及正在启动的服务的服务ID。
- 当服务A调用服务B时,服务A将使用Neflix RIbbon库来提供客户端负载均衡。RIbbon将联系Eureka服务去检索服务位置信息,然后在本地进行缓存。
- Netflix Ribbon库将定期对Eureka服务进行ping操作,并刷新服务位置的本地缓存。
三、构建Spring Eureka服务
构建Spring Eureka服务的步骤如下:
- 在pom文件引入spring-cloud-starter-eureka-server依赖。
- 在application.yml文件中,创建Eureka的配置信息。
- 在Application.java引导类中添加Eureka的启动注解
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jrol</groupId>
<artifactId>eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
// application.yml
server:
port: 8761 // 用于设置Eureka服务的默认端口
spring:
application:
name: eureka_service
eureka:
instance:
hostname: localhost
client:
// 告知服务,在Spring Boot Eureka应用程序启动时不要通过Eureka服务注册,因为它本身就是Eureka服务
register-with-eureka: false
fetch-registry: false // 该属性表示是否尝试在本地缓存注册表信息
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
四、通过Spring Eureka注册服务
第三节已经创建了一个服务发现代理,本节就开始创建一个新的服务,并通过服务发现代理进行注册。
创建一个新的服务的步骤如下:
- 在pom文件添加spring-cloud-starter-eureka依赖。
- 在application.yml文件中增加Eureka配置项。
- 在启动类中添加Eureka的启动注解。
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.2.RELEASE</version>
</dependency>
...
spring:
application:
// 将使用Eureka注册的服务逻辑名称
name: serverA
profiles:
active: default
cloud:
config:
enabled: true
eureka:
instance:
// 注册服务的IP,而不是服务器名称
prefer-ip-address: true
client:
// 向Eureka注册服务
register-with-eureka: true
// 拉取注册表的本地副本
fetch-registry: true
service-url:
// Eureka服务的位置
defaultZone: http://localhost:8761/eureka/
@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
注册成功后,可以登录http://localhost:8761/查看。
五、使用服务发现查找服务
本节创建一个新的服务B,通过服务发现代理来查找服务A的实际位置。
为了达成目的,我将从3个不同的Spring/Netflix客户端库,服务消费者可以使用它们来和Ribbon进行交互。从最低级别到最高级别,这些库包含了不同的与Ribbon进行交互的抽象层次。这里要讨论的库包括:
- Spring DiscoveryClient;
- 启用了RestTemplate的Spring DiscoveryClient;
- NetFlix Feign客户端。
先构造一个服务B,并创建一个接口,这个接口包括一个参数,用来区别使用哪种方式查找服务实例IP。
@GetMapping("/config/{clientType}")
public String getConfig(@PathVariable String clientType) {
String str = "" ;
if (StringUtils.hasText(clientType)) {
switch (clientType) {
case "A":
System.out.println("使用springDiscoveryClient方式");
str = useSpringDiscoveryClient();
case "B":
System.out.println("使用springTemplateWithRibbon");
str = useSpringTemplateWithRibbon();
case "C":
System.out.println("使用netflixFeign");
str = useNetflixFeign();
default:
System.out.println("使用默认方式,springDiscoveryClient方式");
str = useSpringDiscoveryClient();
}
}
return str;
}
5.1. 使用Spring DiscoveryClient查找服务实例
Spring DiscoveryCLient提供了对Ribbon和Ribbon中缓存的注册服务的最低层次访问。使用DiscoveryClient,可以查询通过Ribbon注册的所有服务以及这些服务对应得URI。
实现步骤如下:
@SpringBootApplication
// 1、激活Spring DiscoveryClient
@EnableDiscoveryClient
public class ConfigClient {
public static void main(String[] args) {
SpringApplication.run(ConfigClient.class, args);
}
}
@Component
public class ServerADiscoveryClient {
// 2、使用自动注入discoveryClient对应的bean, 注意其完整路径为org.springframework.cloud.client.discovery.DiscoveryClient
@Resource
private DiscoveryClient discoveryClient;
public String getUserInfo(String userId) {
RestTemplate restTemplate = new RestTemplate();
// 3、获取服务对应的所有本地缓存实例信息
List<ServiceInstance> instances = discoveryClient.getInstances("config-server");
if (instances.isEmpty()) {
return null;
}
// 4、自定义负载均衡策略,取其中一个服务实例进行调用
String IP = instances.get(0).getUri().toString();
String uri = String.format("%s/userInfo/%s", IP, userId);
ResponseEntity<String> restExchange = restTemplate.exchange(
uri,
HttpMethod.GET,
null, String.class, userId);
return restExchange.getBody();
}
}
5.2. 使用带有Ribbon功能的RestTemplate调用服务
要使用带有Ribbon的RestTemplate类,需要使用Spring Cloud注解@LoadBalanced来定义RestTemplate Bean的构造方法。
除了在定义目标服务的URL上有一点小小的差异,使用支持Ribbon的RestTemplate类几乎和使用标准的RestTemplate类一样。我们将使用要调用的服务的Eureka服务ID来构建目标URL,而不是在RestTemplate调用中使用服务的物理位置。
实现步骤如下:
@SpringBootApplication
public class ConfigClient {
public static void main(String[] args) {
SpringApplication.run(ConfigClient.class, args);
}
// 1、该注解告诉Spring Cloud创建一个支持Ribbon的RestTemplate
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
@Component
public class ServerARestTemplateClient {
// 2、自动装配restTemplate类bean
@Autowired
private RestTemplate restTemplate;
public String getUserInfo(String userId) {
// 3、http://{serverId}/userInfo,注意这里使用serverId发起请求,带有Ribbon功能的RestTemplate帮我们自动实现了负载均衡策略
ResponseEntity<String> restExchange = restTemplate.exchange(
"http://config-server/userInfo/{userId}",
HttpMethod.GET,
null, String.class, userId);
return restExchange.getBody();
}
}
5.3. 使用Neflix Feign客户端调用服务
Neflix的Feign客户端库是Spring启用RIbbon的RestTemplate类的替代方案。Feign库采用不同的方法来调用REST服务,方法是让开发人员首先定义一个Java接口,然后使用Spring Cloud注解来标注接口,以映射RIbbon将要调用的基于Eureka的服务。Spring Cloud框架将动态生成一个代理类,用于调用目标REST服务,除了编写接口定义,开发人员不需要编写其他调用服务的代码。
实现步骤如下:
@SpringBootApplication
// 1、激活FeignClient
@EnableFeignClients
public class ConfigClient {
public static void main(String[] args) {
SpringApplication.run(ConfigClient.class, args);
}
}
// 2、该注解标识将要使用哪个服务
@FeignClient("config-server")
public interface ServerAFeignClient {
// 3、该注解映射HTTP动词和将在服务中公开的端点
@RequestMapping(method = RequestMethod.GET, value = "/userInfo/{userId}", consumes = "application/json")
String getUserInfo(@PathVariable("userId") String userId);
}
总结
再来总结一下服务发现的一些知识点。
1、服务发现模式用于抽象服务的物理位置。
2、诸如Eureka这样的服务发现引擎可以在不影响服务客户端的情况下,无缝地向环境中添加和移除服务实例。
3、通过在进行服务调用的客户端中缓存服务的物理位置,客户端负责均衡可以提供额外的性能和弹性。
4、Eureka是Neflix项目,在于Spring Cloud一起使用时,很容易对Eureka进行建立和配置。