Spring Cloud - 快速指南
Spring Cloud - 简介
在介绍 Spring Cloud 之前,我们先简要概述一下微服务架构以及 Spring Boot 在创建微服务中的作用。
微服务架构
微服务架构是一种应用程序开发风格,其中应用程序被分解为小型服务,并且这些服务之间具有松散耦合。以下是使用微服务架构的主要优点 -
易于维护- 微服务规模较小,应该只处理单一业务任务。因此,它们的开发和维护都很简单。
独立的扩展和部署- 微服务有其各自的部署模式和节奏。因此,每个服务都可以根据该服务应满足的负载进行扩展。每个服务都可以根据其时间表进行部署。
独立的技术使用- 微服务的代码库与部署环境分离,因此可以根据用例决定微服务需要使用的语言和技术。无需在所有微服务中使用通用堆栈。
有关微服务架构的更多详细信息,请参阅微服务架构
春季启动
Spring Boot 是一个基于 Java 的框架,用于创建微服务架构中使用的微服务。它进一步缩短了开发 Spring 应用程序所需的时间。以下是它提供的主要好处 -
易于理解和开发 Spring 应用程序
提高生产力
减少开发时间
有关 Spring Boot 的更多信息可以在以下位置找到:Spring Boot
春云
Spring Cloud 提供了一组可用于在云中构建分布式应用程序的组件。我们可以自己开发这些组件,但这会浪费时间来开发和维护这个样板代码。
这就是 Spring Cloud 发挥作用的地方。它为分布式环境中观察到的常见问题提供了即用型云模式。它试图解决的一些模式是 -
分布式消息传递
负载均衡
断路器
路由
分布式日志记录
服务注册
分布式锁
集中配置
这就是为什么它成为开发需要高可扩展性、性能和可用性的应用程序的非常有用的框架。
在本教程中,我们将介绍上面列出的 Spring Cloud 组件。
使用 Spring Cloud 的好处
开发人员专注于业务逻辑- Spring Cloud 提供了所有样板代码来实现云的常见设计模式。因此,开发人员可以专注于业务逻辑,而无需开发和维护此样板代码。
快速开发时间- 由于开发人员免费获得样板,他们可以快速交付所需的项目,同时保持代码质量。
易于使用- Spring Cloud 项目可以轻松地与现有 Spring 项目集成。
活跃项目- Spring Cloud 由 Spring 背后的公司 Pivotal 积极维护。因此,我们只需升级 Spring Cloud 版本即可免费获得所有新功能和错误修复。
微服务架构具有多重优势;然而,它最关键的缺点之一是它在分布式环境中的部署。对于分布式系统,我们有一些经常出现的常见问题,例如 -
服务A如何知道在哪里联系服务B,即服务B的地址?
多个服务如何相互通信,即使用什么协议?
我们如何监控环境中的各种服务?
我们如何将服务的配置分配给服务实例?
我们如何链接跨服务传输的调用以进行调试?
等等…
这些是 Spring Cloud 试图解决并提供通用解决方案的一系列问题。
虽然Spring Boot用于快速应用程序开发,但将其与 Spring Cloud 一起使用可以减少集成我们在分布式环境中开发和部署的微服务的时间。
Spring 云组件
现在让我们看一下Spring Cloud提供的各种组件以及这些组件解决的问题
问题 | 成分 |
---|---|
分布式云配置 | Spring Cloud 配置、Spring Cloud Zookeeper、Spring Consul 配置 |
分布式消息传递 | Spring Stream 与 Kafka、Spring Stream 与 RabbitMQ |
服务发现 | Spring Cloud Eureka、Spring Cloud Consul、Spring Cloud Zookeeper |
记录 | Spring Cloud Zipkin、Spring Cloud Sleuth |
Spring服务通讯 | Spring Hystrix、Spring Ribbon、Spring Feign、Spring Zuul |
我们将在接下来的章节中介绍其中的一些组件。
Spring Cloud 和 Spring Boot 的区别
这是在开始使用 Spring Cloud 时出现的一个非常常见的问题。其实,这里没有可比性。Spring Cloud 和 Spring Boot 都用于实现不同的目标。
Spring Boot 是一个用于更快的应用程序开发的 Java 框架,专门用于微服务架构。
Spring Cloud用于集成这些微服务,使它们可以轻松地在分布式环境中协同工作并相互通信
事实上,为了获得最大的好处,例如减少开发时间,建议将 Spring Boot 与 Spring Cloud 一起使用。
Spring Cloud - 依赖管理
在本章中,我们将使用 Spring Cloud 构建我们的第一个应用程序。让我们回顾一下 Spring Cloud 应用程序的项目结构和依赖关系设置,同时使用 Spring Boot 作为基础框架。
核心依赖
Spring Cloud 组有多个列为依赖项的包。在本教程中,我们将使用 Spring Cloud 组中的多个包。为了避免这些包之间的任何兼容性问题,让我们使用 Spring Cloud 依赖管理 POM,如下所示 -
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Gradle 用户可以通过使用以下命令来实现相同的目的 -
buildscript { dependencies { classpath "io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE" } } apply plugin: "io.spring.dependency-management" dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies: 'Hoxton.SR8')" } }
项目架构与结构
在本教程中,我们将使用餐厅的案例 -
餐厅服务发现- 用于注册服务地址。
餐厅客户服务- 向客户提供客户信息和其他服务。
餐厅服务- 向客户提供餐厅信息。使用客户服务获取客户的城市信息。
Restaurant Gateway - 我们应用程序的入口点。但是,为了简单起见,我们将在本教程中仅使用一次。
在较高的层面上,这是项目架构 -
我们将有以下项目结构。请注意,我们将在接下来的章节中查看这些文件。
项目POM
为了简单起见,我们将使用基于 Maven 的构建。下面是我们将在本教程中使用的基本 POM 文件。
<?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-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.tutorials.point</groupId> <artifactId>spring-cloud-eureka-client</artifactId> <version>1.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2020.0.1</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.4.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
注意事项-
POM依赖管理部分几乎包含了我们需要的所有项目。我们将在需要时添加依赖部分。
我们将使用 Spring Boot 作为开发应用程序的基础框架,这就是为什么您会看到它被列为依赖项。
Spring Cloud - 使用 Eureka 进行服务发现
介绍
当应用程序作为微服务部署在云中时,服务发现是最关键的部分之一。这是因为对于任何使用操作,微服务架构中的应用程序可能需要访问多个服务以及它们之间的通信。
服务发现有助于跟踪服务地址和可以联系服务实例的端口。这里有三个组成部分 -
服务实例- 负责处理传入的服务请求并响应这些请求。
服务注册表- 跟踪服务实例的地址。服务实例应该向服务注册表注册其地址。
服务客户端- 想要访问或想要发出请求并从服务实例获取响应的客户端。服务客户端联系服务注册表以获取实例的地址。
Apache Zookeeper、Eureka 和 Consul 是一些用于服务发现的著名组件。在本教程中,我们将使用 Eureka
设置 Eureka 服务器/注册表
为了设置 Eureka Server,我们需要更新 POM 文件以包含以下依赖项 -
<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-web</artifactId> </dependency> </dependencies>
然后,使用正确的注释来注释我们的 Spring 应用程序类,即@EnableEurekaServer。
package com.tutorialspoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class RestaurantServiceRegistry{ public static void main(String[] args) { SpringApplication.run(RestaurantServiceRegistry.class, args); } }
如果我们想要配置注册表并更改其默认值,我们还需要一个属性文件。以下是我们将做出的改变 -
将端口更新为 8900 而不是默认的 8080
在生产中,为了实现高可用性,将有多个节点用于注册。这就是我们需要注册管理机构之间进行点对点通信的地方。当我们在独立模式下执行此操作时,我们可以简单地将客户端属性设置为false以避免任何错误。
所以,这就是我们的application.yml文件的样子 -
server: port: 8900 eureka: client: register-with-eureka: false fetch-registry: false
就是这样,现在让我们使用以下命令编译项目并运行程序 -
java -jar .\target\spring-cloud-eureka-server-1.0.jar
现在我们可以在控制台中看到日志 -
... 2021-03-07 13:33:10.156 INFO 17660 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8900 (http) 2021-03-07 13:33:10.172 INFO 17660 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] ... 2021-03-07 13:33:16.483 INFO 17660 --- [ main] DiscoveryClientOptionalArgsConfiguration : Eureka HTTP Client uses Jersey ... 2021-03-07 13:33:16.632 INFO 17660 --- [ main] o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING 2021-03-07 13:33:16.675 INFO 17660 --- [ main] com.netflix.discovery.DiscoveryClient : Initializing Eureka in region useast- 1 2021-03-07 13:33:16.675 INFO 17660 --- [ main] com.netflix.discovery.DiscoveryClient : Client configured to neither register nor query for data. 2021-03-07 13:33:16.686 INFO 17660 --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1615104196685 with initial instances count: 0 ... 2021-03-07 13:33:16.873 INFO 17660 --- [ Thread-10] e.s.EurekaServerInitializerConfiguration : Started Eureka Server 2021-03-07 13:33:18.609 INFO 17660 --- [ main] c.t.RestaurantServiceRegistry : Started RestaurantServiceRegistry in 15.219 seconds (JVM running for 16.068)
从上面的日志中我们可以看到,Eureka 注册表已经设置完毕。我们还获得了 Eureka 的仪表板(参见下图),该仪表板托管在服务器 URL 上。
设置 Eureka 客户端实例
现在,我们将设置将注册到 Eureka 服务器的服务实例。为了设置 Eureka Client,我们将使用单独的 Maven 项目并更新 POM 文件以包含以下依赖项 -
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
然后,使用正确的注释来注释我们的 Spring 应用程序类,即@EnableDiscoveryClient
package com.tutorialspoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class RestaurantCustomerService{ public static void main(String[] args) { SpringApplication.run(RestaurantCustomerService.class, args); } }
如果我们想要配置客户端并更改其默认值,我们还需要一个属性文件。以下是我们将做出的改变 -
我们将在运行时提供端口,而在执行时提供 jar。
我们将指定 Eureka 服务器运行的 URL。
所以,这就是我们的 application.yml 文件的样子
spring: application: name: customer-service server: port: ${app_port} eureka: client: serviceURL: defaultZone: http://localhost:8900/eureka
为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -
java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar
现在我们可以在控制台中看到日志 -
... 2021-03-07 15:22:22.474 INFO 16920 --- [ main] com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30 2021-03-07 15:22:22.482 INFO 16920 --- [ main] c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4 2021-03-07 15:22:22.490 INFO 16920 --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1615110742488 with initial instances count: 0 2021-03-07 15:22:22.492 INFO 16920 --- [ main] o.s.c.n.e.s.EurekaServiceRegistry : Registering application CUSTOMERSERVICE with eureka with status UP 2021-03-07 15:22:22.494 INFO 16920 --- [ main] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1615110742494, current=UP, previous=STARTING] 2021-03-07 15:22:22.500 INFO 16920 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/ localhost:customer-service:8081: registering service... 2021-03-07 15:22:22.588 INFO 16920 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path '' 2021-03-07 15:22:22.591 INFO 16920 --- [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8081 2021-03-07 15:22:22.705 INFO 16920 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/ localhost:customer-service:8081 - registration status: 204 ...
正如我们从上面的日志中看到的,客户端实例已经设置完毕。我们还可以查看之前看到的 Eureka Server 仪表板。正如我们所看到的,Eureka 服务器知道有两个正在运行的“客户服务”实例 -
Eureka客户端消费者示例
我们的尤里卡服务器已经获得了“客户服务”设置的注册客户端实例。我们现在可以设置消费者,它可以向尤里卡服务器询问“客户服务”节点的地址。
为此,我们添加一个可以从 Eureka 注册表获取信息的控制器。该控制器将被添加到我们之前的 Eureka 客户端本身,即“客户服务”。让我们为客户端创建以下控制器。
package com.tutorialspoint; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController class RestaurantCustomerInstancesController { @Autowired private DiscoveryClient eurekaConsumer; @RequestMapping("/customer_service_instances")
请注意注释 @DiscoveryClient,这是 Spring 框架提供的用于与注册表通信的注释。
现在让我们重新编译我们的 Eureka 客户端。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -
java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar
两个 shell 上的客户端启动后,现在让我们点击我们在控制器中创建的 http://localhost:8081/customer_service_instances。此 URL 显示有关两个实例的完整信息。
[ { "scheme": "http", "host": "localhost", "port": 8081, "metadata": { "management.port": "8081" }, "secure": false, "instanceInfo": { "instanceId": "localhost:customer-service:8081", "app": "CUSTOMER-SERVICE", "appGroupName": null, "ipAddr": "10.0.75.1", "sid": "na", "homePageUrl": "http://localhost:8081/", "statusPageUrl": "http://localhost:8081/actuator/info", "healthCheckUrl": "http://localhost:8081/actuator/health", "secureHealthCheckUrl": null, "vipAddress": "customer-service", "secureVipAddress": "customer-service", "countryId": 1, "dataCenterInfo": { "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", "name": "MyOwn" }, "hostName": "localhost", "status": "UP", "overriddenStatus": "UNKNOWN", "leaseInfo": { "renewalIntervalInSecs": 30, "durationInSecs": 90, "registrationTimestamp": 1616667914313, "lastRenewalTimestamp": 1616667914313, "evictionTimestamp": 0, "serviceUpTimestamp": 1616667914313 }, "isCoordinatingDiscoveryServer": false, "metadata": { "management.port": "8081" }, "lastUpdatedTimestamp": 1616667914313, "lastDirtyTimestamp": 1616667914162, "actionType": "ADDED", "asgName": null }, "instanceId": "localhost:customer-service:8081", "serviceId": "CUSTOMER-SERVICE", "uri": "http://localhost:8081" }, { "scheme": "http", "host": "localhost", "port": 8082, "metadata": { "management.port": "8082" }, "secure": false, "instanceInfo": { "instanceId": "localhost:customer-service:8082", "app": "CUSTOMER-SERVICE", "appGroupName": null, "ipAddr": "10.0.75.1", "sid": "na", "homePageUrl": "http://localhost:8082/", "statusPageUrl": "http://localhost:8082/actuator/info", "healthCheckUrl": "http://localhost:8082/actuator/health", "secureHealthCheckUrl": null, "vipAddress": "customer-service", "secureVipAddress": "customer-service", "countryId": 1, "dataCenterInfo": { "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", "name": "MyOwn" }, "hostName": "localhost", "status": "UP", "overriddenStatus": "UNKNOWN", "leaseInfo": { "renewalIntervalInSecs": 30, "durationInSecs": 90, "registrationTimestamp": 1616667913690, "lastRenewalTimestamp": 1616667913690, "evictionTimestamp": 0, "serviceUpTimestamp": 1616667913690 }, "isCoordinatingDiscoveryServer": false, "metadata": { "management.port": "8082" }, "lastUpdatedTimestamp": 1616667913690, "lastDirtyTimestamp": 1616667913505, "actionType": "ADDED", "asgName": null }, "instanceId": "localhost:customer-service:8082", "serviceId": "CUSTOMER-SERVICE", "uri": "http://localhost:8082" } ]
尤里卡服务器API
Eureka Server 为客户端实例或服务提供各种 API 进行通信。其中很多 API 都是抽象的,可以直接与我们之前定义和使用的 @DiscoveryClient 一起使用。需要注意的是,它们的 HTTP 对应项也存在,并且对于 Eureka 的非 Spring 框架使用非常有用。
事实上,我们之前使用的API,即获取有关运行“Customer_Service”的客户端的信息,也可以通过浏览器使用http://localhost:8900/eureka/apps/customer-service来调用,如图所示这里 -
<application slick-uniqueid="3"> <div> <a id="slick_uniqueid"/> </div> <name>CUSTOMER-SERVICE</name> <instance> <instanceId>localhost:customer-service:8082</instanceId> <hostName>localhost</hostName> <app>CUSTOMER-SERVICE</app> <ipAddr>10.0.75.1</ipAddr> <status>UP</status> <overriddenstatus>UNKNOWN</overriddenstatus> <port enabled="true">8082</port> <securePort enabled="false">443</securePort> <countryId>1</countryId> <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"> <name>MyOwn</name> </dataCenterInfo> <leaseInfo> <renewalIntervalInSecs>30</renewalIntervalInSecs> <durationInSecs>90</durationInSecs> <registrationTimestamp>1616667913690</registrationTimestamp> <lastRenewalTimestamp>1616668273546</lastRenewalTimestamp> <evictionTimestamp>0</evictionTimestamp> <serviceUpTimestamp>1616667913690</serviceUpTimestamp> </leaseInfo> <metadata> <management.port>8082</management.port> </metadata> <homePageUrl>http://localhost:8082/</homePageUrl> <statusPageUrl>http://localhost:8082/actuator/info</statusPageUrl> <healthCheckUrl>http://localhost:8082/actuator/health</healthCheckUrl> <vipAddress>customer-service</vipAddress> <secureVipAddress>customer-service</secureVipAddress> <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer> <lastUpdatedTimestamp>1616667913690</lastUpdatedTimestamp> <lastDirtyTimestamp>1616667913505</lastDirtyTimestamp> <actionType>ADDED</actionType> </instance> <instance> <instanceId>localhost:customer-service:8081</instanceId> <hostName>localhost</hostName> <app>CUSTOMER-SERVICE</app> <ipAddr>10.0.75.1</ipAddr> <status>UP</status> <overriddenstatus>UNKNOWN</overriddenstatus> <port enabled="true">8081</port> <securePort enabled="false">443</securePort> <countryId>1</countryId> <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"> <name>MyOwn</name> </dataCenterInfo> <leaseInfo> <renewalIntervalInSecs>30</renewalIntervalInSecs> <durationInSecs>90</durationInSecs> <registrationTimestamp>1616667914313</registrationTimestamp> <lastRenewalTimestamp>1616668274227</lastRenewalTimestamp> <evictionTimestamp>0</evictionTimestamp> <serviceUpTimestamp>1616667914313</serviceUpTimestamp> </leaseInfo> <metadata> <management.port>8081</management.port> </metadata> <homePageUrl>http://localhost:8081/</homePageUrl> <statusPageUrl>http://localhost:8081/actuator/info</statusPageUrl> <healthCheckUrl>http://localhost:8081/actuator/health</healthCheckUrl> <vipAddress>customer-service</vipAddress> <secureVipAddress>customer-service</secureVipAddress> <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer> <lastUpdatedTimestamp>1616667914313</lastUpdatedTimestamp> <lastDirtyTimestamp>1616667914162</lastDirtyTimestamp> <actionType>ADDED</actionType> </instance> </application>
其他有用的 API 很少有:
行动 | 应用程序编程接口 |
---|---|
注册新服务 | POST /eureka/apps/{appIdentifier} |
注销服务 | 删除 /eureka/apps/{appIdentifier} |
有关服务的信息 | GET /eureka/apps/{appIdentifier} |
有关服务实例的信息 | GET /eureka/apps/{appIdentifier}/ {instanceId} |
有关编程 API 的更多详细信息,请访问https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html
尤里卡 – 高可用性
我们一直在独立模式下使用 Eureka 服务器。然而,在生产环境中,理想情况下我们应该运行多个 Eureka 服务器实例。这保证了即使一台机器宕机了,另一台Eureka服务器的机器仍能继续运行。
让我们尝试以高可用性模式设置 Eureka 服务器。对于我们的示例,我们将使用两个实例。为此,我们将使用以下application-ha.yml来启动 Eureka 服务器。
注意事项-
我们已经参数化了端口,以便我们可以使用相同的配置文件启动多个实例。
我们添加了地址,再次参数化,以传递 Eureka 服务器地址。
我们将该应用程序命名为“Eureka-Server”。
spring: application: name: eureka-server server: port: ${app_port} eureka: client: serviceURL: defaultZone: ${eureka_other_server_url}
现在让我们重新编译我们的 Eureka 服务器项目。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -
java -Dapp_port=8900 '-Deureka_other_server_url=http://localhost:8901/eureka' - jar .\target\spring-cloud-eureka-server-1.0.jar -- spring.config.location=classpath:application-ha.yml
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8901 '-Deureka_other_server_url=http://localhost:8900/eureka' - jar .\target\spring-cloud-eureka-server-1.0.jar -- spring.config.location=classpath:application-ha.yml
我们可以通过查看仪表板来验证服务器是否已启动并在高可用性模式下运行。例如,这是 Eureka 服务器 1 上的仪表板 -
这是 Eureka 服务器 2 的仪表板 -
因此,正如我们所看到的,我们有两个正在运行且同步的 Eureka 服务器。即使一台服务器出现故障,另一台服务器也会继续运行。
我们还可以通过使用逗号分隔的服务器地址来更新服务实例应用程序,以拥有两个 Eureka 服务器的地址。
spring: application: name: customer-service server: port: ${app_port} eureka: client: serviceURL: defaultZone: http://localhost:8900/eureka, http://localhost:8901/eureka
尤里卡 – 区域意识
Eureka 还支持区域意识的概念。当我们拥有跨不同地理位置的集群时,区域意识作为一个概念非常有用。比如说,我们收到一个传入的服务请求,我们需要选择应该为该请求提供服务的服务器。与其在距离较远的服务器上发送和处理该请求,不如选择位于同一区域的服务器更为有效。这是因为,网络瓶颈在分布式应用程序中很常见,因此我们应该避免它。
现在让我们尝试设置 Eureka 客户端并让它们感知区域。为此,我们添加application-za.yml
spring: application: name: customer-service server: port: ${app_port} eureka: instance: metadataMap: zone: ${zoneName} client: serviceURL: defaultZone: http://localhost:8900/eureka
现在让我们重新编译我们的 Eureka 客户端项目。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -
java -Dapp_port=8080 -Dzone_name=USA -jar .\target\spring-cloud-eureka-client- 1.0.jar --spring.config.location=classpath:application-za.yml
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8081 -Dzone_name=EU -jar .\target\spring-cloud-eureka-client- 1.0.jar --spring.config.location=classpath:application-za.yml
我们可以返回仪表板来验证 Eureka Server 是否注册了服务的区域。如下图所示,我们有两个可用区,而不是到目前为止我们看到的 1 个可用区。
现在,任何客户端都可以查看它所在的区域。假设客户端位于美国,它会更喜欢美国的服务实例。并且它可以从Eureka Server获取区域信息。
Spring Cloud - 与Feign同步通信
介绍
在分布式环境中,服务需要相互通信。通信可以同步或异步发生。在本节中,我们将了解服务如何通过同步 API 调用进行通信。
虽然这听起来很简单,但作为 API 调用的一部分,我们需要注意以下事项 -
查找被调用者的地址- 调用者服务需要知道它想要调用的服务的地址。
负载平衡- 调用者服务可以执行一些智能负载平衡,以将负载分散到被调用者服务之间。
区域感知- 呼叫者服务最好应该呼叫位于同一区域的服务,以便快速响应。
Netflix Feign和Spring RestTemplate(以及Ribbon)是两个著名的 HTTP 客户端,用于进行同步 API 调用。在本教程中,我们将使用Feign Client。
Feign – 依赖设置
让我们使用前面章节中使用过的Restaurant案例。让我们开发一个包含有关餐厅的所有信息的餐厅服务。
首先,让我们使用以下依赖项更新服务的pom.xml -
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
然后,使用正确的注释来注释我们的 Spring 应用程序类,即 @EnableDiscoveryClient 和 @EnableFeignCLient
package com.tutorialspoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class RestaurantService{ public static void main(String[] args) { SpringApplication.run(RestaurantService.class, args); } }
上面代码中的注意点 -
@EnableDiscoveryClient - 这与我们用于读/写 Eureka 服务器的注释相同。
@EnableFeignCLient - 此注释扫描我们的包以在代码中启用 feign 客户端并相应地对其进行初始化。
完成后,现在让我们简单地看一下定义 Feign 客户端所需的 Feign 接口。
使用 Feign 接口进行 API 调用
Feign 客户端可以通过在接口中定义 API 调用来简单设置,该接口可在 Feign 中用于构建调用 API 所需的样板代码。例如,考虑我们有两项服务 -
服务 A - 使用 Feign 客户端的调用者服务。
服务 B - 被调用者服务,其 API 将由上述 Feign 客户端调用
调用者服务(即本例中的服务 A)需要为其要调用的 API(即服务 B)创建一个接口。
package com.tutorialspoint; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name = "service-B") public interface ServiceBInterface { @RequestMapping("/objects/{id}", method=GET) public ObjectOfServiceB getObjectById(@PathVariable("id") Long id); @RequestMapping("/objects/", method=POST) public void postInfo(ObjectOfServiceB b); @RequestMapping("/objects/{id}", method=PUT) public void postInfo((@PathVariable("id") Long id, ObjectOfBServiceB b); }
注意事项-
@FeignClient 注释了将由 Spring Feign 初始化并可供其余代码使用的接口。
请注意,FeignClient 注解需要包含服务的名称,这用于发现服务地址,即来自 Eureka 或其他发现平台的服务 B。
然后,我们可以定义我们计划从服务 A 调用的所有 API 函数名称。这可以是带有 GET、POST、PUT 等动词的一般 HTTP 调用。
完成此操作后,服务 A 可以简单地使用以下代码来调用服务 B 的 API -
@Autowired ServiceBInterface serviceB . . . ObjectOfServiceB object = serviceB. getObjectById(5);
让我们看一个例子,看看它的实际效果。
示例 – 使用 Eureka 假装客户端
假设我们想找到与客户位于同一城市的餐馆。我们将使用以下服务 -
客户服务- 拥有所有客户信息。我们之前在 Eureka Client 部分中定义了这一点。
Eureka Discovery Server - 包含有关上述服务的信息。我们之前已经在 Eureka Server 部分定义了这一点。
餐厅服务- 我们将定义包含所有餐厅信息的新服务。
让我们首先向我们的客户服务添加一个基本控制器 -
@RestController class RestaurantCustomerInstancesController { static HashMap<Long, Customer> mockCustomerData = new HashMap(); static{ mockCustomerData.put(1L, new Customer(1, "Jane", "DC")); mockCustomerData.put(2L, new Customer(2, "John", "SFO")); mockCustomerData.put(3L, new Customer(3, "Kate", "NY")); } @RequestMapping("/customer/{id}") public Customer getCustomerInfo(@PathVariable("id") Long id) { return mockCustomerData.get(id); } }
我们还将为上述控制器定义一个Customer.java POJO 。
package com.tutorialspoint; public class Customer { private long id; private String name; private String city; public Customer() {} public Customer(long id, String name, String city) { super(); this.id = id; this.name = name; this.city = city; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } }
因此,添加后,让我们重新编译我们的项目并执行以下查询来开始 -
java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar
注意- 一旦 Eureka 服务器和此服务启动,我们应该能够看到在 Eureka 中注册的此服务的实例。
要查看我们的 API 是否有效,请点击 http://localhost:8081/customer/1
我们将得到以下输出 -
{ "id": 1, "name": "Jane", "city": "DC" }
这证明我们的服务运行良好。
现在让我们开始定义 Feign 客户端,餐厅服务将使用它来获取客户城市。
package com.tutorialspoint; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name = "customer-service") public interface CustomerService { @RequestMapping("/customer/{id}") public Customer getCustomerById(@PathVariable("id") Long id); }
Feign 客户端包含服务名称以及我们计划在 Restaurant 服务中使用的 API 调用。
最后,让我们在 Restaurant 服务中定义一个使用上述接口的控制器。
package com.tutorialspoint; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController class RestaurantController { @Autowired CustomerService customerService; static HashMap<Long, Restaurant> mockRestaurantData = new HashMap(); static{ mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC")); mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO")); mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC")); } @RequestMapping("/restaurant/customer/{id}") public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long id) { String customerCity = customerService.getCustomerById(id).getCity(); return mockRestaurantData.entrySet().stream().filter( entry -> entry.getValue().getCity().equals(customerCity)) .map(entry -> entry.getValue()) .collect(Collectors.toList()); } }
这里最重要的一行是:
customerService.getCustomerById(id)
这就是我们之前定义的 Feign 客户端调用 API 的神奇之处。
我们还定义POJO 餐厅-
package com.tutorialspoint; public class Restaurant { private long id; private String name; private String city; public Restaurant(long id, String name, String city) { super(); this.id = id; this.name = name; this.city = city; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } }
定义后,让我们使用以下application.properties文件创建一个简单的 JAR 文件 -
spring: application: name: restaurant-service server: port: ${app_port} eureka: client: serviceURL: defaultZone: http://localhost:8900/eureka
现在让我们编译我们的项目并使用以下命令来执行它 -
java -Dapp_port=8083 -jar .\target\spring-cloud-feign-client-1.0.jar
总之,我们有以下项目正在运行 -
独立尤里卡服务器
客户服务
餐厅服务
我们可以通过 http://localhost:8900/ 上的仪表板确认上述内容正常工作
现在,让我们尝试找到所有可以为位于 DC 的 Jane 提供服务的餐厅。
为此,首先让我们联系客户服务:http://localhost:8080/customer/1
{ "id": 1, "name": "Jane", "city": "DC" }
然后,致电餐厅服务:http://localhost:8082/restaurant/customer/1
[ { "id": 1, "name": "Pandas", "city": "DC" }, { "id": 3, "name": "Little Italy", "city": "DC" } ]
正如我们所见,华盛顿地区有 2 家餐厅可以为 Jane 提供服务。
另外,从客户服务的日志中,我们可以看到 -
2021-03-11 11:52:45.745 INFO 7644 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms Querying customer for id with: 1
总之,正如我们所见,无需编写任何样板代码,甚至无需指定服务的地址,我们就可以对服务进行 HTTP 调用。
假冒客户 – 区域意识
Feign 客户端还支持区域感知。比如说,我们收到一个传入的服务请求,我们需要选择应该服务该请求的服务器。与其在距离较远的服务器上发送和处理该请求,不如选择位于同一区域的服务器更为有效。
现在让我们尝试设置一个区域感知的 Feign 客户端。为此,我们将使用与上一个示例相同的情况。我们将有以下内容 -
独立的尤里卡服务器
区域感知客户服务的两个实例(代码与上面相同,我们将仅使用“尤里卡区域感知”中提到的属性文件
区域感知餐厅服务的两个实例。
现在,让我们首先启动区域感知的客户服务。回顾一下,这里是应用程序属性文件。
spring: application: name: customer-service server: port: ${app_port} eureka: instance: metadataMap: zone: ${zoneName} client: serviceURL: defaultZone: http://localhost:8900/eureka
为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -
java -Dapp_port=8080 -Dzone_name=USA -jar .\target\spring-cloud-eureka-client- 1.0.jar --spring.config.location=classpath:application-za.yml
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8081 -Dzone_name=EU -jar .\target\spring-cloud-eureka-client- 1.0.jar --spring.config.location=classpath:application-za.yml
现在让我们创建区域感知的餐厅服务。为此,我们将使用以下application-za.yml
spring: application: name: restaurant-service server: port: ${app_port} eureka: instance: metadataMap: zone: ${zoneName} client: serviceURL: defaultZone: http://localhost:8900/eureka
为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令:
java -Dapp_port=8082 -Dzone_name=USA -jar .\target\spring-cloud-feign-client- 1.0.jar --spring.config.location=classpath:application-za.yml
并在另一个 shell 上执行以下命令 -
java -Dapp_port=8083 -Dzone_name=EU -jar .\target\spring-cloud-feign-client- 1.0.jar --spring.config.location=classpath:application-za.yml
现在,我们在区域感知模式下分别设置了餐厅和客户服务的两个实例。
现在,让我们通过点击 http://localhost:8082/restaurant/customer/1 来测试一下,我们正在点击美国区域。
[ { "id": 1, "name": "Pandas", "city": "DC" }, { "id": 3, "name": "Little Italy", "city": "DC" } ]
但这里要注意的更重要的一点是,该请求是由美国区域的客户服务提供的,而不是欧盟区域的服务。例如,如果我们点击相同的 API 5 次,我们将看到在美国区域运行的客户服务将在日志语句中包含以下内容 -
2021-03-11 12:25:19.036 INFO 6500 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration Got request for customer with id: 1 Got request for customer with id: 1 Got request for customer with id: 1 Got request for customer with id: 1 Got request for customer with id: 1
而欧盟区的客服不处理任何请求。
Spring Cloud - 负载均衡器
介绍
在分布式环境中,服务需要相互通信。通信可以同步或异步发生。现在,当服务同步通信时,这些服务最好在工作人员之间对请求进行负载平衡,以便单个工作人员不会不堪重负。有两种方法可以对请求进行负载均衡
服务器端 LB - 工作人员前面有一个软件,该软件在工作人员之间分发传入请求。
客户端 LB - 调用者服务本身在工作人员之间分配请求。客户端负载平衡的好处是我们不需要负载平衡器形式的单独组件。我们不需要负载均衡器等的高可用性。此外,我们避免了从客户端到负载均衡再到工作人员的额外跃点来满足请求。因此,我们节省了延迟、基础设施和维护成本。
Spring Cloud 负载均衡器(SLB)和 Netflix Ribbon 是两个著名的客户端负载均衡器,用于处理这种情况。在本教程中,我们将使用 Spring Cloud 负载均衡器。
负载均衡器依赖关系设置
让我们使用前面章节中使用过的餐厅的案例。让我们重用包含有关餐厅的所有信息的餐厅服务。请注意,我们将使用 Feign Client 和负载均衡器。
首先,让我们使用以下依赖项更新服务的pom.xml -
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
我们的负载均衡器将使用 Eureka 作为发现客户端来获取有关工作实例的信息。为此,我们必须使用 @EnableDiscoveryClient 注释。
package com.tutorialspoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class RestaurantService{ public static void main(String[] args) { SpringApplication.run(RestaurantService.class, args); } }
将弹簧负载均衡器与 Feign 结合使用
我们在 Feign 中使用的 @FeignClient 注释实际上打包在负载均衡器客户端的默认设置中,该客户端循环我们的请求。让我们测试一下。这是我们之前 Feign 部分中的同一个 Feign 客户端。
package com.tutorialspoint; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name = "customer-service") public interface CustomerService { @RequestMapping("/customer/{id}") public Customer getCustomerById(@PathVariable("id") Long id); }
这是我们将使用的控制器。同样,这一点没有改变。
package com.tutorialspoint; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController class RestaurantController { @Autowired CustomerService customerService; static HashMap<Long, Restaurant> mockRestaurantData = new HashMap(); static{ mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC")); mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO")); mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC")); mockRestaurantData.put(4L, new Restaurant(4, "Pizeeria", "NY")); } @RequestMapping("/restaurant/customer/{id}") public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long id) { System.out.println("Got request for customer with id: " + id); String customerCity = customerService.getCustomerById(id).getCity(); return mockRestaurantData.entrySet().stream().filter( entry -> entry.getValue().getCity().equals(customerCity)) .map(entry -> entry.getValue()) .collect(Collectors.toList()); } }
现在我们已经完成了设置,让我们尝试一下。这里只是一些背景知识,我们要做的如下 -
启动尤里卡服务器。
启动客户服务的两个实例。
启动一个餐厅服务,该服务在内部调用客户服务并使用 Spring Cloud 负载均衡器
对餐厅服务进行四次 API 调用。理想情况下,每个客户服务将满足两个请求。
假设我们已经启动了 Eureka 服务器和客户服务实例,现在让我们编译餐厅服务代码并使用以下命令执行 -
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
现在,让我们通过访问以下 API http://localhost:8082/restaurant/customer/1 来查找位于华盛顿的 Jane 的餐馆,并再次访问相同的 API 3 次。您会从客户服务的日志中注意到,两个实例都服务于 2 个请求。每个客户服务 shell 都会打印以下内容 -
Querying customer for id with: 1 Querying customer for id with: 1
这实际上意味着请求是循环的。
配置Spring负载均衡器
我们可以配置负载均衡器来更改算法类型,或者我们也可以提供定制算法。让我们看看如何调整负载均衡器以优先使用同一客户端来处理请求。
为此,让我们更新 Feign 客户端以包含负载均衡器定义。
package com.tutorialspoint; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(name = "customer-service") @LoadBalancerClient(name = "customer-service", configuration=LoadBalancerConfiguration.class) public interface CustomerService { @RequestMapping("/customer/{id}") public Customer getCustomerById(@PathVariable("id") Long id); }
如果您注意到,我们添加了 @LoadBalancerClient 注释,它指定用于此 Feign 客户端的负载均衡器的类型。我们可以为负载均衡器创建一个配置类,并将该类传递给注释本身。现在让我们定义LoadBalancerConfiguratio.java
package com.tutorialspoint; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class LoadBalancerConfiguration { @Bean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { System.out.println("Configuring Load balancer to prefer same instance"); return ServiceInstanceListSupplier.builder() .withBlockingDiscoveryClient() .withSameInstancePreference() .build(context); } }
现在,如您所见,我们已经将客户端负载平衡设置为每次都首选相同的实例。现在我们已经完成了设置,让我们尝试一下。这里只是一些背景知识,我们要做的如下 -
启动尤里卡服务器。
启动客户服务的两个实例。
启动一个餐厅服务,该服务在内部调用客户服务并使用 Spring Cloud 负载均衡器
对餐厅服务进行 4 次 API 调用。理想情况下,所有四个请求将由同一客户服务提供服务。
假设我们已经启动了 Eureka 服务器和客户服务实例,现在让我们c