Spring Cloud 简明教程
Spring Cloud - Introduction
在我们关注 Spring Cloud 之前,让我们简要概述微服务架构以及 Spring Boot 在创建微服务中的作用。
Microservice Architecture
微服务架构是一种应用程序开发风格,其中应用程序被分解成小型服务,并且这些服务彼此之间松散耦合。以下是使用微服务架构的主要优点:
-
Easy to maintain − 微服务体积小,只需要处理单一业务任务。因此,它们易于开发和维护。
-
Independent Scaling & Deployment − 微服务有其独立的部署模式和节奏。因此,每个服务可以根据该服务需要满足的负载进行扩展。每个服务都可以根据其计划进行部署。
-
Independent Technology Usage − 微服务将其代码库与部署环境分离,因此,微服务需要使用的语言和技术可以根据用例决定。所有微服务都无需使用通用堆栈。
有关微服务架构的更多详细信息,请访问 Microservice Architecture
Spring Boot
Spring Boot 是一个基于 Java 的框架,用于创建微服务架构中使用的微服务。它进一步缩短了开发 Spring 应用程序所需的时间。以下是它提供的主要好处:
-
理解并开发 Spring 应用程序非常容易
-
Increases productivity
-
Reduces the development time
有关 Spring Boot 的更多信息,请访问 Spring Boot
Spring Cloud
Spring Cloud 提供了一组组件,这些组件在云中构建分布式应用程序时很有用。我们可以自己开发这些组件,但是这会浪费开发和维护此样板代码的时间。
这就是 Spring Cloud 发挥作用的地方。它为分布式环境中观察到的常见问题提供了开箱即用的云模式。它试图解决的一些模式是:
-
Distributed Messaging
-
Load Balancing
-
Circuit Breakers
-
Routing
-
Distributed Logging
-
Service Registration
-
Distributed Lock
-
Centralized Configuration
这就是为什么它成为开发需要高可伸缩性、性能和可用性的应用程序时非常有用的框架。
在本教程中,我们将介绍 Spring Cloud 的以上组件。
Benefits of Using Spring Cloud
-
Developers focus on Business Logic − Spring Cloud 提供了所有样板代码来实现云的常见设计模式。因此,开发人员可以专注于业务逻辑,而无需开发和维护此样板代码。
-
Quick Development Time − 由于开发人员可以免费获得样板,因此他们可以在维护代码质量的同时快速交付所需的项目。
-
Easy to use − Spring Cloud 项目可以轻松地与现有的 Spring 项目集成。
-
Active Project − Spring Cloud 由 Spring 背后的公司 Pivotal 积极维护。因此,我们只需升级 Spring Cloud 版本即可免费获得所有新功能和错误修复。
微服务架构有多个优点;但是,它最关键的缺点之一是在分布式环境中部署它。对于分布式系统,我们有一些常见问题经常出现,例如:
-
服务 A 如何知道在哪里联系服务 B,即服务 B 的地址?
-
多项服务如何相互通信,即使用什么协议?
-
如何监测我们环境中的各种服务?
-
我们如何使用服务实例分发服务的配置?
-
出于调试目的,我们如何关联跨服务的调用?
-
and so on…
这些是 Spring Cloud 尝试解决并提供通用解决方案的一系列问题。
虽然 Spring Boot 用于快速应用程序开发,但将它与 Spring Cloud 一起使用可以减少我们开发和部署在分布式环境中的微服务的集成时间。
Spring Cloud Components
现在让我们来看看 Spring Cloud 提供的各种组件以及这些组件解决的问题
Problem |
Components |
Distributed Cloud Configuration |
Spring Cloud 配置、Spring Cloud Zookeeper、Spring Consul 配置 |
Distributed Messaging |
Spring Stream 与 Kafka、Spring Stream 与 RabbitMQ |
Service Discovery |
Spring Cloud Eureka、Spring Cloud Consul、Spring Cloud Zookeeper |
Logging |
Spring Cloud Zipkin、Spring Cloud Sleuth |
Spring Service Communication |
Spring Hystrix、Spring Ribbon、Spring Feign、Spring Zuul |
我们将在接下来的章节中研究其中的几个组件。
Spring Cloud - Dependency Management
在本教程中,我们将使用 Spring Cloud 构建我们的第一个应用程序。让我们在使用 Spring Boot 作为基本框架时来了解 Spring Cloud 应用程序的项目结构和依赖设置。
Core Dependency
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')"
}
}
Project Architecture and Structure
本教程中,我们将使用餐馆示例 -
-
Restaurant Service Discovery - 用于注册服务地址。
-
Restaurant Customer Service - 向客户端和其他服务提供客户信息。
-
Restaurant Service - 向客户端提供餐馆信息。使用客户服务获取客户的城市信息。
-
Restaurant Gateway - 应用程序的入口点。但出于简化考虑,本教程中仅使用一次。
下图展示了项目架构 -
我们还将拥有以下项目结构。注意,我们将在后续章节查看文件。
Project 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>
Points to note -
-
POM 依赖项管理部分几乎包含我们所需的所有项目。当我们需要时,我们将添加依赖项部分。
-
我们将使用 Spring Boot 作为应用程序开发的基础框架,这就是为什么您看到它在依赖项中列出的原因。
Spring Cloud - Service Discovery Using Eureka
Introduction
当应用程序作为云中的微服务部署时,服务发现是其中最关键的部分之一。这是因为,对于任何使用操作,微服务架构中的应用程序可能需要访问多个服务,并进行相互通信。
服务发现有助于跟踪服务地址以及可以联系到服务实例的端口。这里有三个组件在发挥作用 -
-
Service Instances - 负责处理服务传入的请求并响应这些请求。
-
Service Registry - 跟踪服务实例的地址。服务实例应该向服务注册中心注册其地址。
-
Service Client - 想要访问或想要放置请求并从服务实例获得响应的客户端。服务客户端联系服务注册中心以获取实例的地址。
Apache Zookeeper、Eureka 和 Consul 是一些用于服务发现的众所周知的组件。在本教程中,我们将使用 Eureka
Setting up Eureka Server/Registry
要设置 Eureka 服务器,我们需要更新 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);
}
}
如果我们想要配置注册表并更改其默认值,还需要一个 properties file 。以下是我们将做出的一些更改:
-
将端口更新为 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 上。
Setting up Eureka Client for Instance
现在,我们将设置将注册到 Eureka 服务器的服务实例。为了设置 Eureka 客户端,我们将使用一个单独的 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);
}
}
如果我们想要配置客户端并更改其默认值,还需要一个 properties file 。以下是我们将做出的一些更改:
-
我们将在执行时通过 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 服务器仪表板。正如我们所见,Eureka 服务器了解到有正在运行的两个“CUSTOMER-SERVICE”实例:
Eureka Client Consumer Example
我们的 Eureka 服务器已获得已注册的“Customer-Service”设置的客户端实例。现在,我们可以设置消费者,它可以向 Eureka 服务器询问“Customer-Service”节点的地址。
为此,让我们添加一个可以从 Eureka 注册表获取信息的控制器。此控制器将添加到我们之前的 Eureka 客户端本身,即“Customer Service”。让我们为客户端创建以下控制器。
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 上启动后,我们现在进入控制器中创建的 [role="bare"] [role="bare"]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"
}
]
Eureka Server API
Eureka 服务器为客户端实例或服务相互通信提供各种 API。这些 API 中有很多是抽象的,可以直接与我们之前定义和使用的 @DiscoveryClient 一起使用。需要注意的是,它们也有 HTTP 对应项,可用于 Eureka 的非 Spring 框架用法。
实际上,我们之前使用的 API,即获取有关运行“Customer_Service”的客户端的信息,也可以通过浏览器使用 [role="bare"] https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html 调用,如下所示:
<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 包括:
Action |
API |
Register a new service |
POST /eureka/apps/{appIdentifier} |
Deregister the service |
DELTE /eureka/apps/{appIdentifier} |
Information about the service |
GET /eureka/apps/{appIdentifier} |
服务实例信息 |
GET /eureka/apps/{appIdentifier}/ {instanceId} |
有关编程 API 的更多详细信息可以在此处找到 https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html
Eureka – High Availability
我们一直在以独立模式使用 Eureka 服务器。然而,在生产环境中,理想情况下,我们应该运行多个 Eureka 服务器实例。这确保了即使一台机器宕机,另一台 Eureka 服务器的机器仍继续运行。
让我们尝试以高可用性模式设置 Eureka 服务器。对于我们的示例,我们将使用两个实例。为此,我们将使用以下 application-ha.yml 来启动 Eureka 服务器。
Points to note -
-
我们对端口进行参数化,以便我们可以使用相同的配置文件启动多个实例。
-
我们添加了地址,再次进行参数化,以传递 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 – Zone Awareness
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 客户端项目。为了执行,我们将运行两个服务实例。为此,让我们打开两个外壳,然后在一个外壳上执行以下命令 −
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 服务器是否注册了服务区域。在下图中看到,我们有两个可用区,而不是迄今为止所见的 1 个。
现在,任何客户端都可以查看它所处的区域。比如,如果客户端在美国,它将选择美国的实例服务。它可以从 Eureka 服务器获取区域信息。
Spring Cloud - Synchronous Communication with Feign
Introduction
在分布式环境中,服务需要彼此通信。通信可以同步或异步发生。在本节中,我们将了解服务如何通过同步 API 调用进行通信。
虽然这听起来很简单,但作为 API 调用的一部分,我们需要处理以下问题 −
-
Finding address of the callee − 调用者服务需要知道要调用的服务地址。
-
Load balancing − 调用者服务可以执行一些智能负载均衡,以将负载分配到被调用服务上。
-
Zone awareness − 调用者服务最好调用位于同一区域中的服务,以便快速响应。
Netflix Feign 和 Spring RestTemplate (连同 Ribbon )是用于进行同步 API 调用的两个众所周知的 HTTP 客户端。在本教程中,我们将使用 Feign Client 。
Feign – Dependency Setting
让我们使用我们在前面章节中使用的 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>
然后,使用正确的注释(即 @EnableDiscoveryClient 和 @EnableFeignCLient)注释我们的 Spring 应用程序类
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);
}
}
Points to note in the above code −
-
@ EnableDiscoveryClient − 这是我们用于从 Eureka 服务器读/写的相同注释。
-
@EnableFeignCLient − 此注释扫描我们的程序包以查找启用的 feign 客户端,并相应地初始化它。
完成后,现在让我们简要了解一下我们定义 feign 客户端所需的 Feign 接口。
Using Feign Interfaces for API calls
只需在接口中定义 API 调用,Feign 就可以轻松设置 feign 客户端,以便用于构造调用 API 所需的样板代码。例如,考虑我们有两个服务 −
-
Service A − 使用 Feign 客户端的调用者服务。
-
Service B - 被上述 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);
}
Points to note -
-
@FeignClient 注释将由 Spring Feign 初始化的接口,并且可以被其余代码使用。
-
请注意,FeignClient 注释需要包含服务名称,这用于发现服务地址,即从 Eureka 或其他发现平台发现服务 B 的地址。
-
然后,我们可以定义我们计划从服务 A 调用的所有 API 函数名称。这可以是一般的 HTTP 调用,其中包含 GET、POST、PUT 等动词。
完成后,服务 A 可以简单地使用以下代码来调用服务 B 的 API -
@Autowired
ServiceBInterface serviceB
.
.
.
ObjectOfServiceB object = serviceB. getObjectById(5);
我们来看一个示例,以了解实际操作。
Example – Feign Client with Eureka
假设我们要查找与客户所在城市相同的城市的餐厅。我们将使用以下服务 -
-
Customer Service - 拥有所有客户信息。我们之前在 Eureka 客户端部分中定义了此信息。
-
Eureka Discovery Server - 拥有上述服务的相关信息。我们之前在 Eureka 服务器部分中定义了此信息。
-
Restaurant Service - 我们将定义的新服务,其中包含所有餐厅信息。
我们首先向我们的客户服务添加一个基本控制器 -
@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
Note - 一旦启动 Eureka 服务器和此服务,我们应该能够看到在 Eureka 中注册的此服务的一个实例。
若要查看我们的 API 是否正常工作,让我们点击 [role="bare"] [role="bare"]http://localhost:8081/customer/1
我们将获得以下输出 -
{
"id": 1,
"name": "Jane",
"city": "DC"
}
这证明我们的服务运行良好。
现在,让我们开始定义 Restaurant 服务将用来获取客户所在城市的 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 的关键所在。
让我们也定义 Restaurant 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
总而言之,我们有以下各项运行:
-
Standalone Eureka server
-
Customer service
-
Restaurant service
我们可以从 [role="bare"] [role="bare"]http://localhost:8900/ 上的仪表板上确认上述各项是否正常工作。
现在,让我们尝试找到能够为 Jane 服务的所有餐厅,Jane 居住在华盛顿特区。
为此,首先让我们访问对应的客户服务: [role="bare"] [role="bare"]http://localhost:8080/customer/1
{
"id": 1,
"name": "Jane",
"city": "DC"
}
然后,对 Restaurant 服务进行一次调用: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
正如我们所见,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 Client – Zone Awareness
Feign 客户端还支持区域感知。假设我们收到一个针对服务的传入请求,我们需要选择应该为该请求服务的服务器。与其在位于远处的服务器上发送和处理该请求,不如选择同一区域中的服务器会更有成效。
现在,让我们尝试设置一个区域感知的 Feign 客户端。为此,我们将使用上一个示例中的案例。我们将遵循以下步骤:
-
A standalone Eureka server
-
两个区域感知的客户服务实例(代码与上述代码保持一致,我们只使用“Eureka 区域感知”中提到的属性文件)
-
两个分区感知餐厅服务的实例。
现在,让我们首先启动分区感知的客户服务。重新回顾一下,以下为 application property 文件。
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
现在,我们已经以分区感知模式设置两个餐厅和客户服务的实例。
现在,让我们访问 [role="bare"] [role="bare"]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 - Load Balancer
Introduction
在分布式环境中,服务需要相互通信。通信可以同步或异步进行。服务同步通信时,最好让这些服务在工作人员之间负载均衡请求,这样就不会让某个工作人员不胜负荷。有两种方法可以对请求进行负载均衡
-
Server-side LB − 软件在工作人员的前面,并将传入的请求分配给工作人员。
-
Client-side LB − 调用者服务自身将请求分配给工作人员。客户端负载均衡的优点是我们不需要以负载均衡器的形式来拥有单独的组件。我们不需要负载均衡器的高可用性等。此外,我们不需要从客户端到负载均衡器到工作人员的额外中转就可以完成请求。因此,我们可以节省延迟、基础设施和维护成本。
Spring Cloud 负载均衡器 ( SLB ) 和 Netflix Ribbon 两个著名的客户端负载均衡器,用于处理此类情况。在本教程中,我们将使用 Spring Cloud 负载均衡器。
Load Balancer Dependency Setting
让我们使用我们在前几章中已经使用过的餐厅案例。让我们重新使用拥有餐厅所有信息的餐厅服务。请注意,我们会将 Feign 客户端与我们的负载均衡器结合使用。
首先,让我们用以下依赖项更新服务的 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);
}
}
Using Spring Load Balancer with 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());
}
}
现在我们已经完成了设置,让我们尝试一下。这里有一点背景知识,我们要执行以下操作——
-
Start the Eureka Server.
-
启动两个客户服务实例。
-
启动一个餐厅服务,它在内部调用客户服务并使用 Spring Cloud 负载均衡器
-
对餐厅服务执行四次 API 调用。理想情况下,每个客户服务将服务两个请求。
假设我们已启动 Eureka 服务器和客户服务实例,现在让我们编译餐厅服务代码并使用以下命令执行:
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
现在,让我们通过访问以下 API 来查找位于 DC 的 Jane 的餐厅 [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1 ,并让我们再次访问相同的 API 三次。你会从客户服务的日志中注意到两个实例都服务了 2 个请求。每个客户服务 shell 都会打印以下内容:
Querying customer for id with: 1
Querying customer for id with: 1
这实际上意味着请求是循环轮播的。
Configuring Spring Load Balancer
我们可以配置负载均衡器来更改算法类型,或者我们还可以提供自定义算法。让我们看看如何调整我们的负载均衡器以优先考虑相同客户端的请求。
为此,让我们更新 Feign Client 以包含负载均衡器定义。
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);
}
}
现在,如你所见,我们已设置客户端负载均衡,以每次都优先考虑同一个实例。现在我们已经完成了设置,让我们尝试一下。这里有一个背景知识,我们将执行以下操作:
-
Start the Eureka Server.
-
启动两个客户服务实例。
-
启动一个餐厅服务,它在内部调用客户服务并使用 Spring Cloud 负载均衡器
-
对餐厅服务执行 4 次 API 调用。理想情况下,所有四个请求都将由相同的客户服务处理。
假设我们已启动 Eureka 服务器和客户服务实例,现在让我们编译餐厅服务代码并使用以下命令执行:
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
现在,让我们通过访问以下 API 来查找位于 DC 的 Jane 的餐厅 [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1 ,并让我们再次访问相同的 API 三次。你会从客户服务的日志中注意到一个实例处理了所有 4 个请求:
Querying customer for id with: 1
Querying customer for id with: 1
Querying customer for id with: 1
Querying customer for id with: 1
这实际上意味着请求优先考虑相同的客户服务代理。
在类似的情况下,我们可以使用各种其他负载均衡算法来使用粘滞会话、基于提示的负载均衡、区域偏好负载均衡等。
Spring Cloud - Circuit Breaker using Hystrix
Introduction
在分布式环境中,服务需要相互通信。通信可以同步或异步发生。当服务同步通信时,可能会出现多种造成问题的原因。例如:
-
Callee service unavailable - 调用正在进行中的服务由于某种原因而关闭,例如错误、部署等。
-
Callee service taking time to respond - 由于高负载或资源消耗,要调用的服务可能很慢,或者它正处于初始化服务的过程中。
在任何一种情况下,对于调用方来说,都浪费了时间和网络资源而等待被调用者做出响应。对于服务而言,更有意义的做法是退避并根据需要在一段时间后对被调用服务发出调用或共享默认响应。
Netflix Hystrix, Resilince4j 是两个众所周知且用于处理这种情况的断路器。在本教程中,我们将使用 Hystrix。
Hystrix – Dependency Setting
让我们使用我们之前用过的 Restaurant 的案例。让我们将 hystrix dependency 添加到我们的 Restaurant 服务中,这些服务会调用 Customer 服务。首先,让我们使用以下依赖关系更新服务的 pom.xml :
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.7.0.RELEASE</version>
</dependency>
然后,使用正确的注释(即,@EnableHystrix)为我们的 Spring 应用程序类添加注释
package com.tutorialspoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@EnableHystrix
public class RestaurantService{
public static void main(String[] args) {
SpringApplication.run(RestaurantService.class, args);
}
}
Points to Note
-
@ EnableDiscoveryClient 和 @EnableFeignCLient ——我们在上一章已经查看了这些注释。
-
@EnableHystrix ——此注释扫描我们的软件包,查找正在使用 @HystrixCommand 注释的方法。
Hystrix Command Annotation
完成之后,我们将重复使用之前在 Restaurant 服务中为客户服务类定义的 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);
}
现在,让我们在此处定义 service implementation 类,该类将使用 Feign 客户端。这将是 feign 客户端的一个简单包装。
package com.tutorialspoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class CustomerServiceImpl implements CustomerService {
@Autowired
CustomerService customerService;
@HystrixCommand(fallbackMethod="defaultCustomerWithNYCity")
public Customer getCustomerById(Long id) {
return customerService.getCustomerById(id);
}
// assume customer resides in NY city
public Customer defaultCustomerWithNYCity(Long id) {
return new Customer(id, null, "NY");
}
}
现在,让我们了解一下上面代码中的一些要点——
-
HystrixCommand annotation ——此命令负责封装函数调用,即 getCustomerById ,并在其周围提供代理。然后,该代理提供各种挂钩,通过这些挂钩我们可以控制对客户服务的调用。例如,对请求进行超时设置,对请求进行池化,提供回退方法等。
-
Fallback method ——当 Hystrix 确定调用者出了问题时,我们可以指定要调用的方法。此方法需要具有与带注释的方法相同的签名。在我们的例子中,我们决定将数据提供给控制器,作为纽约市的数据。
此注释提供了几个有用的选项——
-
Error threshold percent ——在中断跳闸之前允许失败的请求的百分比,即调用回退方法。这可以通过使用 cicutiBreaker.errorThresholdPercentage 来控制
-
Giving up on the network request after timeout ——如果被调用服务(在我们的例子中为客户服务)很慢,我们可以设置超时时间,超时后我们将放弃请求并转到回退方法。这通过设置 execution.isolation.thread.timeoutInMilliseconds 来控制
最后,这里是我们称为 CustomerServiceImpl 的控制器
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
CustomerServiceImpl 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(3L, 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());
}
}
Circuit Tripping/Opening
现在我们已经完成了设置,让我们尝试一下。这里有一点背景知识,我们要执行以下操作——
-
Start the Eureka Server
-
Start the Customer Service
-
启动 Restaurant 服务,该服务将在内部调用客户服务。
-
对 Restaurant 服务进行 API 调用
-
关闭客户服务
-
对 Restaurant 服务进行 API 调用。鉴于客户服务已关闭,它会导致故障,最终将调用回退方法。
现在,让我们编译 Restaurant 服务代码,并使用以下命令执行
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
另外,启动客户服务和 Eureka 服务器。请注意,这些服务没有发生任何变化,它们与我们在前几章中看到的情况相同。
现在,让我们尝试为在华盛顿特区的简寻找餐馆。
{
"id": 1,
"name": "Jane",
"city": "DC"
}
为此,我们将点击以下 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。
现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
{
"id": 4,
"name": "Pizzeria",
"city": "NY"
}
从输出中可以看到,我们拿到了位于纽约的餐厅,尽管我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了一个位于纽约的虚拟顾客。虽然没有用,但上面的示例显示后备按预期进行了调用。
Integrating Caching with Hystrix
为了让上面这个方法更有用,我们可以在使用 Hystrix 时整合缓存。当底层服务不可用时,这会是一个提供更好答案的有用模式。
首先,让我们创建该服务的缓存版本。
package com.tutorialspoint;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service
public class CustomerServiceCachedFallback implements CustomerService {
Map<Long, Customer> cachedCustomer = new HashMap<>();
@Autowired
CustomerService customerService;
@HystrixCommand(fallbackMethod="defaultToCachedData")
public Customer getCustomerById(Long id) {
Customer customer = customerService.getCustomerById(id);
// cache value for future reference
cachedCustomer.put(customer.getId(), customer);
return customer;
}
// get customer data from local cache
public Customer defaultToCachedData(Long id) {
return cachedCustomer.get(id);
}
}
我们使用 hashMap 作为存储来缓存数据。这是出于开发目的。在生产环境中,我们可能希望使用更好的缓存解决方案,例如 Redis、Hazelcast 等。
现在,我们只需要更新控制器中的一行来使用上面的服务 −
@RestController
class RestaurantController {
@Autowired
CustomerServiceCachedFallback customerService;
static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
…
}
我们将遵循与上面相同步骤 −
-
Start the Eureka Server.
-
Start the Customer Service.
-
启动 Restaurant Service,该服务在内部调用 Customer Service。
-
对 Restaurant Service 进行一次 API 调用。
-
关闭 Customer Service。
-
对 Restaurant Service 进行一次 API 调用。假设 Customer Service 已关闭,但数据已被缓存,我们将得到一组有效数据。
现在,让我们进行同样的过程,直到步骤 3。
现在点击 URL: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
因此,这里没有什么新鲜事,我们拿到了位于华盛顿特区的餐厅。现在,让我们转到关闭客户服务这个有趣的环节。你可以通过按 Ctrl+C 或者直接终止 shell 来实现。
现在,让我们再次点击相同的 URL − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
从输出中可以看到,我们拿到了位于华盛顿特区的餐厅,这正是我们预期的,因为我们的顾客来自华盛顿特区。这是因为我们的后备方法返回了缓存的顾客数据。
Integrating Feign with Hystrix
我们发现了如何使用 @HystrixCommand 注解来触发断路并提供后备服务。但是,我们不得不另外定义一个 Service 类来封装我们的 Hystrix 客户端。但是,我们也可以通过简单的传递正确参数给 Feign 客户端来实现相同的目标。让我们尝试这么做。为此,首先通过添加注解 fallback class 来更新我们的 CustomerService 的 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", fallback = FallBackHystrix.class)
public interface CustomerService {
@RequestMapping("/customer/{id}")
public Customer getCustomerById(@PathVariable("id") Long id);
}
现在,让我们添加 Feign 客户端的后备类,当 Hystrix 断路触发时将调用该类。
package com.tutorialspoint;
import org.springframework.stereotype.Component;
@Component
public class FallBackHystrix implements CustomerService{
@Override
public Customer getCustomerById(Long id) {
System.out.println("Fallback called....");
return new Customer(0, "Temp", "NY");
}
}
最后,我们还需要创建 application-circuit.yml 以启用 Hystrix。
spring:
application:
name: restaurant-service
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
feign:
circuitbreaker:
enabled: true
现在我们已经准备好设置,让我们来测试一下。我们将按以下步骤进行:
-
Start the Eureka Server.
-
我们不启动客户服务。
-
启动 Restaurant 服务,该服务将在内部调用客户服务。
-
向餐馆服务进行 API 调用。鉴于客户服务已关闭,我们将注意到后备。
假设第一步已经完成,让我们继续进行第三步。让我们编译代码并执行以下命令:
java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar --
spring.config.location=classpath:application-circuit.yml
现在我们尝试点击 − [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
由于我们尚未启动客户服务,因此将调用后备,并且后备将 NY 作为城市发送过来,这就是为什么我们在以下输出中看到 NY 餐馆的原因。
{
"id": 4,
"name": "Pizzeria",
"city": "NY"
}
此外,为了确认,在日志中,我们会看到:
….
2021-03-13 16:27:02.887 WARN 21228 --- [reakerFactory-1]
.s.c.o.l.FeignBlockingLoadBalancerClient : Load balancer does not contain an
instance for the service customer-service
Fallback called....
2021-03-13 16:27:03.802 INFO 21228 --- [ main]
o.s.cloud.commons.util.InetUtils : Cannot determine local hostname
…..
Spring Cloud - Gateway
Introduction
在分布式环境中,服务需要相互通信。但是,这是服务间通信。我们也有这样的用例:我们域外的客户希望点击我们的服务以获取 API。因此,我们可以公开所有可以在客户端调用的微服务的地址,也可以创建一个服务网关,将请求路由到各个微服务,并响应客户端。
创建网关在本文档中是一个更好的方法。有以下两个主要优点:
-
无需维护各个服务的安全性。
-
而且,跨领域问题,例如添加元信息,可以在一个地方进行处理。
Netflix Zuul 和 Spring Cloud Gateway 是两个众所周知的云网关,用于处理此类情况。在本教程中,我们将使用 Spring Cloud Gateway。
Spring Cloud Gateway – Dependency Setting
让我们使用我们一直在使用的餐馆的案例。让我们在我们两个服务的前面添加一个新服务(网关),即餐馆服务和客户服务。首先,让我们使用以下依赖关系更新服务的 pom.xml :
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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 RestaurantGatewayService{
public static void main(String[] args) {
SpringApplication.run(RestaurantGatewayService.class, args);
}
}
我们正在使用 @EnableDiscoveryClient 进行注释,因为我们希望使用 Eureka 服务发现来获取托管特定用例的主机列表
Dynamic Routing with Gateway
Spring Cloud Gateway 有三个重要的部分。它们是:
-
Route − 这些是网关的构建块,其中包含要将请求转发到的 URL 以及应用于传入请求的谓词和过滤器。
-
Predicate − 这是一个标准集,该标准集应与要转发到内部微服务的传入请求匹配。例如,路径谓词仅在传入 URL 包含该路径时才会转发请求。
-
Filters − 它们充当了位置,你可以在向内部微服务发送请求或向客户端进行回复之前在此修改传入请求。
我们针对餐厅和客户服务编写一个用于网关的简单配置。
spring:
application:
name: restaurant-gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: customers
uri: lb://customer-service
predicates:
- Path=/customer/**
- id: restaurants
uri: lb://restaurant-service
predicates:
- Path=/restaurant/**
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
Points to note about the above configuration −
-
我们启用了 discovery.locator 以确保网关能够从 Eureka 服务器读取。
-
我们在此处使用了基于路径的谓词来路由请求。这意味着以 / customer 开头的任何请求都将路由到客户服务,而对于 / restaurant ,我们将该请求转发给餐厅服务。
现在让我们设置网关服务之前需要的其他服务——
-
Start the Eureka Server
-
Start the Customer Service
-
Start the Restaurant Service
现在,让我们编译并执行网关项目。我们将为此使用以下命令——
java -Dapp_port=8084 -jar .\target\spring-cloud-gateway-1.0.jar
完成后,我们的网关便已准备好好在端口 8084 上进行测试。让我们首先访问 [role="bare"] [role="bare"]http://localhost:8084/customer/1 ,我们看到请求正确路由到客户服务,并且我们获得以下输出——
{
"id": 1,
"name": "Jane",
"city": "DC"
}
现在,访问我们的餐厅 API,即 [role="bare"] [role="bare"]http://localhost:8084/restaurant/customer/1 ,我们获得以下输出——
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
这意味着两个调用都正确路由到各个服务。
Predicates & Filters Request
我们在上述示例中使用了路径谓词。以下是其他几个重要谓词——
Predicate |
Description |
Cookie 谓词(输入:名称和正则表达式) |
将 Cookie 中的“名称”与“正则表达式”进行比较 |
Header 谓词(输入:名称和正则表达式) |
将 Header 中的“名称”与“正则表达式”进行比较 |
Host 谓词(输入:名称和正则表达式) |
将传入请求的“名称”与“正则表达式”进行比较 |
权重谓词(输入:组名称和权重) |
权重谓词(输入:组名称和权重) |
Filters 用于在向下游服务发送数据或向客户端发送响应之前,向请求添加/删除数据。
以下是添加元数据的一些重要过滤器。
Filter |
Description |
添加请求标头过滤器(输入:标头和值) |
在将请求向下游转发之前添加一个“标头”和“值”。 |
添加响应标头过滤器(输入:标头和值) |
在将请求向上游(即客户端)转发之前添加一个“标头”和“值”。 |
重定向过滤器(输入:状态和 URL) |
在传递到下游主机之前,添加一个重定向标头以及 URL。 |
ReWritePath(输入:regexp 和替换) |
此过滤器负责通过使用输入替换替换与“regexp”匹配的字符串来重写路径。 |
Monitoring
为监视网关或访问各种路由、谓词等,我们可以在项目中启用执行器。为此,让我们首先更新 pom.xml,将执行器作为依赖项包含在内。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
对于监控,我们将使用一个单独的应用程序属性文件,其中将包含用于启用执行器的标志。因此,以下是它的样子 −
spring:
application:
name: restaurant-gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: customers
uri: lb://customer-service
predicates:
- Path=/customer/**
- id: restaurants
uri: lb://restaurant-service
predicates:
- Path=/restaurant/**
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
management:
endpoint:
gateway:
enabled: true
endpoints:
web:
exposure:
include: gateway
现在,要列出所有路由,我们可以点击: [role="bare"] [role="bare"]http://localhost:8084/actuator/gateway/routes
[
{
"predicate": "Paths: [/customer/**], match trailing slash: true",
"route_id": "customers",
"filters": [],
"uri": "lb://customer-service",
"order": 0
},
{
"predicate": "Paths: [/restaurant/**], match trailing slash: true",
"route_id": "restaurants",
"filters": [],
"uri": "lb://restaurant-service",
"order": 0
}
]
用于监控的其他重要 API −
API |
Description |
GET /actuator/gateway/routes/{id} |
获取有关特定路由的信息 |
POST /gateway/routes/{id_to_be assigned} |
将新路由添加到网关 |
DELETE /gateway/routes/{id} |
从网关中移除路由 |
POST /gateway/refresh |
移除所有缓存条目 |
Spring Cloud - Streams with Apache Kafka
Introduction
在分布式环境中,服务需要互相通信。通信可以同步或异步发生。在本节中,我们将研究服务如何使用 message brokers 异步通信。
执行异步通信的两个主要好处:
-
Producer and Consumer speed can differ - 如果数据的消费者速度较慢或较快,它不会影响生产者处理,反之亦然。两者都可以以自己独立的速度工作,而不会相互影响。
-
Producer does not need to handle requests from various consumers - 可能有多个消费者想要从生产者读取相同的数据集。有了中间的消息代理,生产者就不必处理这些消费者产生的负载。此外,生产者级别的任何中断都不会阻止消费者读取较旧的生产者数据,因为这些数据在消息代理中可用。
Apache Kafka 和 RabbitMQ 是用于实现异步通信的两个著名的消息中间件。本教程中,我们将使用 Apache Kafka。
Kafka – Dependency Setting
让我们使用之前一直使用过的“Restaurant”示例。因此,让我们假设将客户服务和餐厅服务通过异步通信进行通信。为此,我们将使用 Apache Kafka。并且我们需要在两个服务中使用它,即客户服务和餐厅服务。
为了使用 Apache Kafka,我们将更新两个服务的 POM,并添加以下依赖项。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
我们还需要运行 Kafka 实例。可以通过多种方式实现,但我们更愿意使用 Docker 容器启动 Kafka。以下是我们可能考虑使用的几个映像 −
无论使用哪个映像,这里需要注意的重要一点是,一旦映像启动并运行,请确保能够在 localhost:9092 访问 Kafka 集群。
既然我们在映像中运行了 Kafka 集群,让我们进入核心示例。
Binding & Binders
在 Spring Cloud 流中,有三个重要的概念 −
-
External Messaging System − 这是由外部管理的组件,负责存储应用程序产生的事件/消息,这些事件/消息可由其订阅者/使用者读取。请注意,这不在应用程序/Spring 中管理。一些示例包括 Apache Kafka、RabbitMQ
-
Binders − 这是提供与消息传递系统集成的组件,例如,包括消息传递系统的 IP 地址、身份验证等。
-
Bindings − 组件使用 Binder 向消息传递系统发送消息或从特定主题/队列使用该消息。
所有上述属性都在 application properties file 中定义。
Example
让我们使用之前一直使用过的“Restaurant”示例。因此,让我们假设每当向客户服务添加新服务时,我们都希望将客户信息通知到附近的餐厅。
为此,首先让我们更新我们的客户服务,以包含和使用 Kafka。请注意,我们将使用客户服务作为数据的生产者。也就是说,当我们通过 API 添加客户时,它也会添加到 Kafka 中。
spring:
application:
name: customer-service
cloud:
stream:
source: customerBinding-out-0
kafka:
binder:
brokers: localhost:9092
replicationFactor: 1
bindings:
customerBinding-out-0:
destination: customer
producer:
partitionCount: 3
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
Points to note -
-
我们已经使用本地 Kafka 实例的地址定义了一个绑定。
-
我们还定义了绑定“customerBinding-out-0”,它使用“customer”主题来输出消息。
-
我们还已经在 stream.source 中提到了我们的绑定,以便我们可以在代码中强制使用它。
完成后,让我们通过添加一个新的“addCustomer”方法来更新我们的控制器,该方法负责提供 POST 请求。然后,从 post 请求,我们将数据发送到 Kafka Broker。
package com.tutorialspoint;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RestaurantCustomerInstancesController {
@Autowired
private StreamBridge streamBridge;
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) {
System.out.println("Querying customer for id with: " + id);
return mockCustomerData.get(id);
}
@RequestMapping(path = "/customer/{id}", method = RequestMethod.POST)
public Customer addCustomer(@PathVariable("id") Long id) {
// add default name
Customer defaultCustomer = new Customer(id, "Dwayne", "NY");
streamBridge.send("customerBinding-out-0", defaultCustomer);
return defaultCustomer;
}
}
Points to note
-
我们正在自动连接 StreamBridge,这将是我们用于发送消息的内容。
-
我们用于
send
方法的参数同时指定我们要用于将数据发送到的绑定。
现在,让我们更新我们的餐厅服务以包含和订阅“客户”主题。请注意,我们将使用餐厅服务作为数据的消费者。也就是说,每当我们通过 API 添加客户时,餐厅服务都将通过 Kafka 得知此情况。
首先,让我们更新 application properties 文件。
spring:
application:
name: restaurant-service
cloud:
function:
definition: customerBinding
stream:
kafka:
binder:
brokers: localhost:9092
replicationFactor: 1
bindings:
customerBinding-in-0:
destination: customer
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
完成后,让我们通过添加一个新的 customerBinding
方法来更新我们的控制器,该方法负责获取请求并提供一个将打印请求及其元数据详细信息的函数。
package com.tutorialspoint;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
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;
@Autowired
private StreamBridge streamBridge;
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());
}
@RequestMapping("/restaurant/cust/{id}")
public void getRestaurantForCust(@PathVariable("id") Long id) {
streamBridge.send("ordersBinding-out-0", id);
}
@Bean
public Consumer<Message<Customer>> customerBinding() {
return msg -> {
System.out.println(msg);
};
}
}
Points to note -
-
我们使用
customerBinding
,它应传递在为该绑定收到消息时被调用的函数。 -
我们在创建捆绑和指定主题时,也需要在 YAML 文件中使用我们为此函数/bean 使用的名称。
现在,让我们像往常一样执行上述代码,启动 Eureka 服务器。请注意,这不是硬性要求,只是为了完整性而存在的。
然后,让我们使用以下命令编译并开始更新客户服务 -
mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient-
1.0.jar --spring.config.location=classpath:application-kafka.yml
然后,让我们使用以下命令编译并开始更新餐厅服务 -
mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-
1.0.jar --spring.config.location=classpath:application-kafka.yml
我们已经设置好了,现在让我们通过点击 API 来测试我们的代码块 -
curl -X POST http://localhost:8083/customer/1
这是我们将为该 API 获得的输出 -
{
"id": 1,
"name": "Dwayne",
"city": "NY"
}
现在,让我们检查餐厅服务的日志 -
GenericMessage [payload=Customer [id=1, name=Dwayne, city=NY],
headers={kafka_offset=1,...
因此,实际上,您看到使用 Kafka Broker,餐厅服务已收到有关新添加的客户的通知。
Partitions & Consumer Groups
分区和消费者组是您在使用 Spring Cloud Stream 时应该了解的两个重要概念。
Partitions - 它们用于对数据进行分区,以便我们可以在多个使用者之间划分工作。
让我们看看如何使用 Spring Cloud 对数据进行分区。比如说,我们想根据客户 ID 对数据进行分区。因此,让我们更新我们的客户服务以使其相同。为此,我们需要说明
让我们更新我们的客户服务应用程序属性,以指定我们数据的键。
spring:
application:
name: customer-service
cloud:
function:
definition: ordersBinding
stream:
source: customerBinding-out-0
kafka:
binder:
brokers: localhost:9092
replicationFactor: 1
bindings:
customerBinding-out-0:
destination: customer
producer:
partitionKeyExpression: 'getPayload().getId()'
partitionCount: 3
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
对于指定键,即“partitionKeyExpression”,我们提供 Spring Expression Language。该表达式将类型假定为 GenericMessage<Customer>,因为我们正在消息中发送 Customer 数据。请注意,GenericMessage 是 Spring Framework 类,用于在一个对象中包装有效负载和头信息。因此,我们从该消息中获取有效负载,该有效负载的类型为 Customer,然后我们在客户上调用 getId() 方法。
现在,让我们还更新我们的消费者(即餐厅服务)以在使用请求时记录更多信息。
现在,我们按惯例执行上面的代码,启动 Eureka 服务端。注意,这不是一项硬性要求,在此出现是为了完整性。
然后,让我们使用以下命令编译并开始更新客户服务 -
mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient-
1.0.jar --spring.config.location=classpath:application-kafka.yml
然后,让我们使用以下命令编译并开始更新餐厅服务 -
mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-
1.0.jar --spring.config.location=classpath:application-kafka.yml
我们已做好设置,现在开始测试我们的代码部分。下面是我们在测试中将要执行的操作:
-
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/1
-
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/1
-
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/5
-
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/3
-
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/1
我们不太关心 API 的输出。相反,我们更关心将数据发送到的分区。由于我们使用顾客 ID 作为关键信息,我们希望具有相同 ID 的顾客将最终进入相同的分区。
现在,让我们检查餐厅服务的日志 -
Consumer: org.apache.kafka.clients.consumer.KafkaConsumer@7d6d8400
Consumer Group: anonymous.9108d02a-b1ee-4a7a-8707-7760581fa323
Partition Id: 1
Customer: Customer [id=1, name=Dwayne, city=NY]
Consumer: org.apache.kafka.clients.consumer.KafkaConsumer@7d6d8400
Consumer Group: anonymous.9108d02a-b1ee-4a7a-8707-7760581fa323
Partition Id: 1
Customer: Customer [id=1, name=Dwayne, city=NY]
Consumer: org.apache.kafka.clients.consumer.KafkaConsumer@7d6d8400
Consumer Group: anonymous.9108d02a-b1ee-4a7a-8707-7760581fa323
Partition Id: 2
Customer: Customer [id=5, name=Dwayne, city=NY]
Consumer: org.apache.kafka.clients.consumer.KafkaConsumer@7d6d8400
Consumer Group: anonymous.9108d02a-b1ee-4a7a-8707-7760581fa323
Partition Id: 0
Customer: Customer [id=3, name=Dwayne, city=NY]
Consumer Group: anonymous.9108d02a-b1ee-4a7a-8707-7760581fa323
Partition Id: 1
Customer: Customer [id=1, name=Dwayne, city=NY]
因此,正如我们所见,具有 Id 1 的顾客每次都最终进入相同的分区,即第 1 分区。
Consumer Group − 消费群组是为相同目的读取相同话题的消费者的逻辑分组。话题中的数据在消费群组中的消费者之间进行分区,因此,特定消费群组中只有一个消费者可以读取话题的一个分区。
要定义一个消费群组,我们只需要在使用 Kafka 话题名称的绑定中定义一个群组。例如,让我们在用于控制器的应用程序文件中定义消费群组名称。
spring:
application:
name: restaurant-service
cloud:
function:
definition: customerBinding
stream:
kafka:
binder:
brokers: localhost:9092
replicationFactor: 1
bindings:
customerBinding-in-0:
destination: customer
group: restController
server:
port: ${app_port}
eureka:
client:
serviceURL:
defaultZone: http://localhost:8900/eureka
让我们重新编译并启动 Restaurant 服务。现在,让我们通过对 Customer 服务上的 POST API 执行操作来生成事件:
使用 Id 1 插入一名顾客:curl -X POST [role="bare"] [role="bare"]http://localhost:8083/customer/1
现在,如果我们检查 Restaurant 服务的日志,我们会看到以下内容:
Consumer: org.apache.kafka.clients.consumer.KafkaConsumer@7d6d8400
Consumer Group: restContoller
Partition Id: 1
Customer: Customer [id=1, name=Dwayne, city=NY]
因此,从输出中可以看到,我们创建了一个名为“rest-contoller”的消费群组,其消费者负责读取这些话题。在上述情况下,我们仅仅运行了该服务的单个实例,因此“customer”话题的所有分区都分配给了同一个实例。但是,如果我们有多个分区,那么分区将分布在工作进程之间。
Distributed Logging using ELK and Sleuth
Introduction
在分布式环境或单体环境中,当出现问题时,应用程序的日志对于调试至关重要。在本部分,我们将了解如何有效地记录日志并提高可追溯性,以便我们能够轻松查看日志。
记录模式对日志记录至关重要的两个主要原因:
-
Inter-service calls − 在微服务架构中,我们有服务之间的异步和同步调用。将这些请求关联起来非常重要,因为单个请求可能会嵌套多个级别。
-
Intra-service calls − 单个服务会收到多个请求,而针对这些请求的日志可能会容易混淆。这就是为什么让请求关联特定 ID 对于筛选请求的所有日志变得至关重要的原因。
Sleuth 是一个用于在应用程序中记录日志的知名工具,而 ELK 则用于简化跨系统观察。
Dependency Setting
让我们使用我们在每一章中都一直在使用的 Restaurant 案例。因此,假设我们的 Customer 服务和 Restaurant 服务通过 API(即同步通信)进行通信。我们希望 Sleuth 用于追踪请求,而 ELK 堆栈用于集中可视化。
为此,首先设置 ELK 堆栈。要做到这一点,首先,我们将设置 ELK 堆栈。我们将使用 Docker 容器启动 ELK 堆栈。以下是可以考虑的镜像:
配置 ELK 后,通过调用以下 API 确保它按预期工作:
-
Elasticsearch − localhost:9200
-
Kibana − localhost:5601
我们在本节结尾处查看 logstash 配置文件。
然后,我们向我们的客户服务和餐馆服务添加以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
现在,我们在设置好依赖项并运行 ELK,让我们进入核心示例。
Request Tracing inside Service
在非常基本的层面上,以下是 Sleuth 添加的元数据:
-
Service name - 当前处理请求的服务。
-
Trace Id - 向日志添加的元数据 ID,该 ID 在服务之间发送,以处理输入请求。这对于对所有内部请求分组以处理一个输入请求的跨服务通信非常有用。
-
Span Id - 向日志添加的元数据 ID,它在服务为处理请求而记录的所有日志记录中都是相同的。它对服务内日志非常有用。请注意,父服务的 Span ID = Trace Id 。
让我们实际看一看。为此,我们更新客户服务代码以包含日志行。这是我们将使用的控制器代码。
package com.tutorialspoint;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RestaurantCustomerInstancesController {
Logger logger =
LoggerFactory.getLogger(RestaurantCustomerInstancesController.class);
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) {
logger.info("Querying customer with id: " + id);
Customer customer = mockCustomerData.get(id);
if(customer != null) {
logger.info("Found Customer: " + customer);
}
return customer;
}
}
现在,让我们执行代码,像往常一样,启动 Eureka 服务器。请注意,这不是一项硬性要求,此处的存在是为了完整性。
然后,让我们使用以下命令编译并开始更新客户服务 -
mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient-
1.0.jar
我们已经设置好了,现在让我们通过点击 API 来测试我们的代码块 -
curl -X GET http://localhost:8083/customer/1
这是我们将为该 API 获得的输出 -
{
"id": 1,
"name": "Jane",
"city": "DC"
}
现在,让我们检查客户服务的日志:
2021-03-23 13:46:59.604 INFO [customerservice,
b63d4d0c733cc675,b63d4d0c733cc675] 11860 --- [nio-8083-exec-7]
.t.RestaurantCustomerInstancesController : Querying customer with id: 1
2021-03-23 13:46:59.605 INFO [customerservice,
b63d4d0c733cc675,b63d4d0c733cc675] 11860 --- [nio-8083-exec-7]
.t.RestaurantCustomerInstancesController : Found Customer: Customer [id=1,
name=Jane, city=DC]
…..
因此,实际上,正如我们所见,日志记录中添加了服务名称、跟踪 ID 和 span ID。
Request Tracing across Service
让我们看看我们如何在服务之间进行日志记录和跟踪。所以,例如,我们将要做的是使用餐馆服务,它在内部调用客户服务。
为此,我们更新我们的餐馆服务代码以包含日志行。这是我们将使用的控制器代码。
package com.tutorialspoint;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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;
Logger logger = LoggerFactory.getLogger(RestaurantController.class);
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) {
logger.info("Get Customer from Customer Service with customer id: " + id);
Customer customer = customerService.getCustomerById(id);
logger.info("Found following customer: " + customer);
String customerCity = customer.getCity();
return mockRestaurantData.entrySet().stream().filter(
entry -> entry.getValue().getCity().equals(customerCity))
.map(entry -> entry.getValue())
.collect(Collectors.toList());
}
}
让我们编译并使用以下命令启动更新后的餐馆服务:
mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client-1.0.jar
确保正在运行 Eureka 服务器和客户服务。我们已经准备就绪,现在让我们通过调用 API 来测试我们的代码片断:
curl -X GET http://localhost:8082/restaurant/customer/2
这是我们将为该 API 获得的输出 -
[
{
"id": 2,
"name": "Indies",
"city": "SFO"
}
]
现在,让我们检查餐馆服务的日志:
2021-03-23 14:44:29.381 INFO [restaurantservice,
6e0c5b2a4fc533f8,6e0c5b2a4fc533f8] 19600 --- [nio-8082-exec-6]
com.tutorialspoint.RestaurantController : Get Customer from Customer Service
with customer id: 2
2021-03-23 14:44:29.400 INFO [restaurantservice,
6e0c5b2a4fc533f8,6e0c5b2a4fc533f8] 19600 --- [nio-8082-exec-6]
com.tutorialspoint.RestaurantController : Found following customer: Customer
[id=2, name=John, city=SFO]
然后,让我们检查客户服务的日志:
2021-03-23 14:44:29.392 INFO [customerservice,
6e0c5b2a4fc533f8,f2806826ac76d816] 11860 --- [io-8083-exec-10]
.t.RestaurantCustomerInstancesController : Querying customer with id: 2
2021-03-23 14:44:29.392 INFO [customerservice,
6e0c5b2a4fc533f8,f2806826ac76d816] 11860 --- [io-8083-exec-10]
.t.RestaurantCustomerInstancesController : Found Customer: Customer [id=2,
name=John, city=SFO]…..
因此,实际上,正如我们所见,日志记录中添加了服务名称、跟踪 ID 和 span ID。此外,我们看到跟踪 ID,即 6e0c5b2a4fc533f8 在客户服务和餐馆服务中重复。
Centralized Logging with ELK
到目前为止,我们已经看到了一种通过 Sleuth 改进日志记录和跟踪功能的方法。然而,在微服务架构中,我们有多个正在运行的服务和每个服务都有多个实例。查看每个实例的日志以识别请求流并不实用。这就是 ELK 对我们有所帮助的地方。
我们以与 Sleuth 相同的跨服务通信案例为例。我们要更新我们的餐厅和顾客,以便为 ELK 栈添加 logback appenders 。
在继续之前,请确保已设置 ELK 栈并且可以通过 localhost:5601 访问 Kibana。另外,使用以下设置配置 Lostash 配置 −
input {
tcp {
port => 8089
codec => json
}
}
output {
elasticsearch {
index => "restaurant"
hosts => ["http://localhost:9200"]
}
}
完成此操作后,我们需要执行两步才能在 Spring 应用中使用 logstash。我们将针对我们服务的这两步执行以下步骤。首先,添加 logback 的依赖关系,以便使用 logstash 的附加组件。
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version>
</dependency>
其次,为 logback 添加一个附加组件,以便 logback 可以使用此附加组件将数据发送到 Logstash
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="logStash"
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>10.24.220.239:8089</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -
%msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="logStash" />
<appender-ref ref="console" />
</root>
</configuration>
上述 appender 将记录到控制台,并将日志发送到 logstash。现在,完成此操作后,我们就可以开始进行测试。
现在,让我们像往常一样执行上述代码、启动 Eureka Server。
然后,让我们使用以下命令编译并开始更新客户服务 -
mvn clean install ; java -Dapp_port=8083 -jar .\target\spring-cloud-eurekaclient- 1.0.jar
然后,让我们使用以下命令编译并开始更新餐厅服务 -
mvn clean install; java -Dapp_port=8082 -jar .\target\spring-cloud-feign-client- 1.0.jar
我们已经设置好了,现在让我们通过点击 API 来测试我们的代码块 -
curl -X GET http://localhost:8082/restaurant/customer/2
这是我们将为该 API 获得的输出 -
[
{
"id": 2,
"name": "Indies",
"city": "SFO"
}
]
但更重要的是,日志语句也会在 Kibana 上可用。
因此,正如我们所看到的,我们可以筛选出特定 traceId 并查看为满足请求而记录的跨服务的所有日志语句。