6 网关和配置服务器

2024-01-07 18:18:28


前面的文章:
1、 1 一个测试驱动的Spring Boot应用程序开发
2、 2 使用React构造前端应用
3、 3 试驱动的Spring Boot应用程序开发数据层示例
4、 4 向微服务架构转变
5、 5 转向事件驱动的架构

后续文章:
7、7 集中式日志和分布式跟踪
8、8 容器化微服务

示例代码

通过消息代理可实现事件驱动架构的开发,使用RabbitMQ能够解决消费端处理事件的松耦合,实现消费端的负载均衡,便于扩展系统,提供弹性解决方案,但前端和微服务之间的负载均衡问题还没有解决。

网关模式

网关模式可以解决一些问题:

  • React应用程序需要指向多个后端微服务来与API进行交互。这是不正确的,因为前端应该将后端视为具有多个API的单独服务器,之后,不会公开架构,从而更灵活,便于进行变更。
  • 如果引入了后端服务的多个实例,前端就不知道如何平衡它们之间的负载。万一某个实例不可用,也不知道如何将请求重定向到另外的实例。尽管从技术角度看,可以在Web客户端中实现负载均衡和弹性模式,但也应该放在后端逻辑中,只实现一次,对任何客户端都有效,保持前端逻辑尽可能简单。
  • 当前情况下,如果向系统添加用户身份验证,就需要验证每个后端服务中的安全凭证。将这个放在后端的边缘,在那里验证API调用,然后将简单的请求传递给其他微服务,会更合理。只要保证其余的后端微服务无法从外部访问,就不必担心安全问题。

这些问题可通过网关微服务来解决。网关模式可集中HTTP访问,负责将请求代理到其他服务。网关会根据配置的规则来决定将请求路由到何处,另外,路由服务可以过滤器来修改请求和响应通过时的状态。如图所示:

-发送尝试
-获取用户别名
-获取排行榜
8000
-发送尝试
-获取用户别名
8080
-获取排行榜
8081
Gateway
过滤器
路由
浏览器
Multiplication
Gamification
尝试
(主题交换)
Gamification
队列

有时会将网关称为边缘服务,因为其他系统必须通过网关来访问后端,且网关将外部流量路由到相应的内部微服务,网关的引入通常会限制对其他后端服务的访问。

Spring Cloud网关

Spring Cloud是Spring系列中的一组独立项目,提供技术来快速构建分布式系统(如微服务等)所需的通用模式。这些模式称为云模式,即使在服务器中部署微服务,也适用。它利用 Spring Boot 的开发便利性简化了分布式系统的开发,比如服务发现、服务网关、服务路由、链路追踪等。Spring Cloud不重复造轮子,而是将市面上开发得比较好的模块集成进去,进行封装,从而减少了各模块的开发成本。换句话说:Spring Cloud 提供了构建分布式系统所需的“全家桶”。
网关模式是Spring Cloud提供的一个工具,Spring长期使用Spring Cloud Netflix支持集成,它由Netflix开发人员作为开源软件发行并维护了许多年了,Netflix支持网关模式的组件是Zuul,与Spring集成通过Spring Cloud Netflix Zuul模块实现。
这里不使用Spring Cloud Netflix,原因是Spring将不再使用Netflix工具集成,而是用其他工具替代,甚至是自己实现,可能是Netflix将某些项目置于维护模式,如Hystrix(断路)和Ribbon(负载均衡),不再主动开发,这还会影响Netflix堆栈中的其他工具,如服务发现工具Eureka依靠Ribbon进行负载均衡。
这里将需求一种新的方案实现网关模式,Spring Cloud网关,不依赖于任何外部工具。
Spring Cloud网关项目定义了一些核心概念(如图所示):

  • 谓词(Predicate):要匹配的条件,以确定将请求路由到何处。Spring Cloud网关提供了一系列基于请求路径、请求头、时间、远程主机等的条件构造器,甚至可以组合为表达式,适用于路由,也称路由谓词。
  • 路由(Route):如果请求与指定的谓词匹配,路由就是请求将代理到的URI。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
  • 过滤器(Filter):可选处理,可以连接到路由(路由过滤器)或全局应用于所有请求(全局过滤器)。过滤器允许修改请求(传入过滤器)和响应(传出过滤器)。Spring Cloud网关中由很多内置的过滤器,如,可以在请求中添加或删除头,限制来自给定主机的请求数量,或在被代理服务的响应返回给请求者之前对其进行转换等。
网关
路由1:
-谓词A
-谓词B
路由2:
-谓词C
路由过滤器X
全局过滤器
HTTP
请求/响应
(匹配B)
HTTP
请求/响应
(匹配C)
服务1
服务2

要使用Spring Cloud网关,需要定义相关的配置,主要是谓词、路由和过滤器,可以在属性文件或yaml文件中配置。示例如下:

spring:
  cloud:
    gateway:
      routes:
        - id: old-path
          uri: http://oldhost/path-old
          predicates:
            - Before=2023-12-10T09:10:57.013+08:00
            - Path=/path-special/**
        - id: new-path
          uri: http://newhost/path-new
          predicates:
            - Path=/path-special/**
            - After=2023-12-10T09:10:57.013+08:00
          filters:
            - AddResponseHeader=X-New-Conditions-Apply, 2023-Dec

这里定义了两个共享路径路由谓词,该谓词会捕获任何访问/path-special/开头的请求,每个路由的附加条件分别由Before和After谓词定义,决定了在何处代理请求。

  • 如果请求发生在2023-12-10T09:10:57.013+08:00之前,代理到http://oldhost/path-old/。
  • 如果请求发生在2023-12-10T09:10:57.013+08:00之后,代理到http://newhost/path-new/。而且,额外的过滤器将添加响应头:X-New-Conditions-Apply,值为2023-Dec

这里的oldhost和newhost不需要从外部访问,仅对后端的网关和其他内部服务可见。
内置的谓词和过滤器可以满足网关的各种需求,非常方便,在示例中将主要使用路径路由谓词,将外部请求代理到相应的微服务。

Spring Cloud网关微服务

Spring Boot为Spring Cloud Gateway提供了启动器(spring-boot-starter-*),将此启动器依赖添加到Spring Boot应用程序中,即可获得可用的Gateway微服务。实际上,Gateway项目是建立在Spring Boot之上的,只能在Spring Boot应用程序中使用。这里,自动配置类GatewayAutoConfiguration位于Spring Boot Gateway中,而不是Spring Boot的autoconfigure包中,它读取配置信息并构建路由、过滤器、谓词等。
在IDEA中创建新的微服务gateway,添加Gateway依赖项,如图所示:
gateway依赖
打开项目的maven配置文件pom.xml,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.zhangjuli</groupId>
    <artifactId>gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway</name>
    <description>gateway</description>
    <properties>
        <java.version>21</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

下面将application.properties改名为application.yml,添加配置信息,如下所示:

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: multiplication
          uri: http://localhost:8080/
          predicates:
            - Path=/challenges/**,/attempts,/attempts/**,/users/**
        - id: gamification
          uri: http://localhost:8081/
          predicates:
            - Path=/leaders
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "http://localhost:3000"
            allowedHeaders:
              - "*"
            allowedMethods:
              - "GET"
              - "POST"
              - "OPTIONS"

这里配置的路由将使网关按如下方式运行:

  • 对http://localhost:8000/attempts的任何请求将被代理到http://localhost:8080/的微服务Multiplication的attempts,其他情况类似,如challenges、users等。
  • 对http://localhost:8080/leaders的任何请求将转换为对Gamification微服务的请求,http://localhost:8081/leaders。

也可用编写一个更简单的配置,不需要路由到每个微服务的显式端点列表,可用使用网关的另一个功能来完成此操作,该功能允许捕获路径段。如果获得了诸如http://localhost:8000/multiplication/attempts的API调用,就可用将Multiplication提取出来,然后,映射到相应的主机和端口,这种情况,当微服务只包含一个API域时才有效。其他情况下,都会将内部架构公开给客户端。示例中,当客户端发出http://localhost:8000/multiplication/users请求时,应该使用一个http://localhost:8000/users代替,以便隐藏Multiplication微服务中包含User域的事实,提高安全性。

其他项目的变更

引入gateway微服务后,可将用于外部请求的所有配置保留在同一服务内,这意味着不需要向Multiplication和Gamification微服务中添加CORS配置。可在网关中保留该配置,因为其他两个服务位于新的代理服务之后。因此,可以从现有项目Multiplication和Gamification的文件夹configuration中删除WebConfiguration类。
删除了CORS配置类后,还需要修改React应用程序使其指向两个微服务的相同主机/端口。还可以根据偏好来重构GameApiClient和ChallengesApiClient类,单个微服务调用所有端点或每个API上下文(挑战、用户等)调用一个服务,不再需要两个不同的服务器URL,因为前端现在将后端视为具有多个API的单个主机。例如:

class GameApiClient {
    static SERVER_URL = 'http://localhost:8000';
    static GET_LEADERBOARD = '/leaders';

    static leaderBoard(): Promise<Response> {
        return fetch(GameApiClient.SERVER_URL + GameApiClient.GET_LEADERBOARD);
    }
}
export default GameApiClient;
class ChallengesApiClient {
    static SERVER_URL = 'http://localhost:8000';
    static GET_CHALLENGE = '/challenges/random';
    static POST_RESULT = '/attempts';
    static GET_ATTEMPTS_BY_ALIAS = '/attempts?alias=';
    static GET_USERS_BY_IDS = '/users';

    static challenge(): Promise<Response> {
        return fetch(ChallengesApiClient.SERVER_URL + ChallengesApiClient.GET_CHALLENGE);
    }

    static sendGuess(user: string,
                     a: number,
                     b: number,
                     guess: number): Promise<Response> {
        return fetch(ChallengesApiClient.SERVER_URL + ChallengesApiClient.POST_RESULT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                userAlias: user,
                factorA: a,
                factorB: b,
                guess: guess
            })
        });
    }

    static getAttempts(userAlias: string): Promise<Response> {
        console.log('Get attempts for ' + userAlias)
        return fetch(ChallengesApiClient.SERVER_URL + ChallengesApiClient.GET_ATTEMPTS_BY_ALIAS + userAlias);
    }

    static getUsers(userIds: number[]): Promise<Response> {
        return fetch(ChallengesApiClient.SERVER_URL +
            ChallengesApiClient.GET_USERS_BY_IDS +
            '/' + userIds.join(','));
    }
}
export default ChallengesApiClient;

运行和测试

要执行整个系统,需要启动各个微服务,运行各个Spring Boot应用程序:

  • 运行RabbitMQ服务器。
  • 启动Multiplication微服务。
  • 启动Gamification微服务。
  • 启动Gateway微服务。
  • 启动前端React应用程序。

这里不需要按照顺序启动这些微服务,在启动过程中,系统可能不稳定,但最终会准备就绪。
当访问前端时,不会注意到任何改变,排行榜已经加载,可以正常发送尝试。验证请求是否被代理,可以查看浏览器开发人员工具中的网络选项卡,选择一个后端请求,以http://localhost:8000开头;也可以向网关中添加跟踪日志配置,就可以看到正在执行的操作了,在application.yml中进行配置,例如:

# 配置路由跟踪日志
logging:
  level:
    org.springframework.cloud.gateway.handler.predicate: trace

重新启动网关,就会看到每个请求的日志,如下所示:

2023-12-14T12:07:12.333+08:00 TRACE 50552 --- [ctor-http-nio-2] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "[/challenges/**, /attempts, /attempts/**, /users/**]" does not match against value "/leaders"
2023-12-14T12:07:12.334+08:00 TRACE 50552 --- [ctor-http-nio-2] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "/leaders" matches against value "/leaders"
2023-12-14T12:07:12.445+08:00 TRACE 50552 --- [ctor-http-nio-2] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "[/challenges/**, /attempts, /attempts/**, /users/**]" does not match against value "/leaders"
2023-12-14T12:07:12.446+08:00 TRACE 50552 --- [ctor-http-nio-2] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "/leaders" matches against value "/leaders"
2023-12-14T12:07:12.448+08:00 TRACE 50552 --- [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "/users/**" matches against value "/users/1,3"

小结

通过网关微服务,获得了一些优势:

  • 前端不了解后端的结构。
  • 外部请求的通用配置保留在同一位置。示例中的CORS设置,也可能是其他关注点,如用户身份验证、指标等。

下面将引入负载均衡,以便可以在每个服务的所有实例之间分配流量,增加系统的可扩展性和冗余性,这需要一些先决条件:

  • 网关需要知道给定服务的可用实例。前面给出了一个只有一个实例的配置,如果有多个实例呢?不应该在路由配置中包含硬编码列表,因为实例是动态的,应该透明地更新实例。
  • 需要实现后端组件的健康度。只有这样,才能知道实例何时未准备好处理流量,并切换到其他任何运行正常的实例上。

满足第一个条件,需要引入服务发现模式,并使用一个公共注册表,不同的分布式组件可以访问该列表以了解可用服务。
对第二个条件,可使用Spring Boot Actuator将微服务公开,并指示是否正常,以便其他组件知道,这也是服务发现需要的。

运行状况

在生产环境中运行的系统永远无法避免出错,可能由于代码错误导致内存不足,网络连接可能失败,微服务实例可能崩溃等。要想构建一个弹性系统,就需要引入冗余机制(同一微服务存在多个副本)来防范这些错误,以将影响降到最低。
如何知道微服务无法工作呢?如果要公开一个接口(如REST API或RabbitMQ),可使用探针与样本进行交互,看看是否作出反应。但是,使用探针时应该小心,因为想涵盖所有可能使微服务不健康(不正常)的情况。与其通过泄露逻辑来确定服务是否正常工作,不如提供一个标准的、简单的探测接口来告诉调用者服务是否正常。由服务逻辑来决定何时转换到不支持状态,具体取决于服务使用的接口的可用性、服务自身的可用性以及错误的严重性。如果该服务无法提供响应,则调用者认为它不正常。如图所示:

/health
无法到达代理
(如网络问题)
错误(服务不可用)
/health
OK(healthy)
/health
OK(healthy)
/health
(请求超时)
微服务A
(实例1)
微服务A
(实例2)
微服务B
(实例1)
微服务B
(实例2)
消息代理
数据库引擎
访问

许多工具和框架都需要这种简单的接口约定来确定服务的运行状况,例如,负载均衡器可暂时停止将流量转移到不响应运行状况探测或响应为未就绪状态的实例,如果实例不正常,则服务发现工具可能将其从注册表中删除,如果服务在配置时间段内运行不正常,则Kubernetes等容器平台可决定重新启动服务。

Spring Boot Actuator

Spring Boot提供了一种开箱即用的解决方案,Spring Boot Actuator,来报告微服务的运行状况,这不是Actuator的唯一功能,它还可以公开其他端点,以访问有关应用程序的不同数据,如配置的记录器、HTTP跟踪、审核事件等,甚至可以打开一个管理端点,来关闭应用程序。
Actuator端点可以独立启用或禁用,不仅可以通过Web接口使用,还可以通过Java管理扩展(Java Management Extensions,JMX)使用。这里重点介绍用作REST API端点的Web接口,默认配置仅公开两个端点:info和health。info旨在提供应用程序的常规信息,health使用运行状况指示器来输出应用程序的状态,有多个内置的运行状况指示器,可以影响应用程序的总体运行状况。这些指示器有许多是针对某些工具的,只有在应用程序中使用这些工具时,才可用,使用Spring Boot可使用自动配置进行控制。
Spring Boot Actuator工件中包含RabbitHealthIndicator类,使用RabbitTemplate对象,可以访问RabbitMQ服务器,与RabbitMQ服务器进行交互,进行运行状况检查。RabbitHealthIndicator 类代码如下:

public class RabbitHealthIndicator extends AbstractHealthIndicator {
    private final RabbitTemplate rabbitTemplate;

    public RabbitHealthIndicator(RabbitTemplate rabbitTemplate) {
        super("Rabbit health check failed");
        Assert.notNull(rabbitTemplate, "RabbitTemplate must not be null");
        this.rabbitTemplate = rabbitTemplate;
    }

    protected void doHealthCheck(Health.Builder builder) throws Exception {
        builder.up().withDetail("version", this.getVersion());
    }

    private String getVersion() {
        return (String)this.rabbitTemplate.execute((channel) -> {
            return channel.getConnection().getServerProperties().get("version").toString();
        });
    }
}

如果使用RabbitMQ,会在上下文中自动插入该指示器。这有助于改善整体运行状况。包含在工件spring-boot-actuator-autoconfigure中的RabbitHealthContributorAutoConfiguration类负责解决这个问题。这个配置取决于是否存在RabbitTemplate Bean,如果存在则意味着正在使用RabbitMQ模块,这会创建一个HealthContributor Bean,由整体运行状况自动配置进行检测和汇总。RabbitHealthContributorAutoConfiguration类代码如下:

@AutoConfiguration(
    after = {RabbitAutoConfiguration.class}
)
@ConditionalOnClass({RabbitTemplate.class})
@ConditionalOnBean({RabbitTemplate.class})
@ConditionalOnEnabledHealthIndicator("rabbit")
public class RabbitHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration<RabbitHealthIndicator, RabbitTemplate> {
    public RabbitHealthContributorAutoConfiguration() {
        super(RabbitHealthIndicator::new);
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"rabbitHealthIndicator", "rabbitHealthContributor"}
    )
    public HealthContributor rabbitHealthContributor(Map<String, RabbitTemplate> rabbitTemplates) {
        return (HealthContributor)this.createContributor(rabbitTemplates);
    }
}

那么,Actuator到底这么用呢?

在微服务中包含Actuator

在pom.xml文件中添加依赖项spring-boot-starter-actuator,就可以获得Actuator功能,代码如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

需要将该依赖添加到Multiplication、Gamification和Gateway微服务中。默认配置在/actuator上下文中公开health和info端点,这就够了,而且如果需要,可通过属性进行调整。重启应用程序就可以使用该功能了,可通过浏览器或命令行,验证服务器的运行状况。

注意:不是通过网关公开/health端点,因为这不是要对外公开的功能,只是系统内部功能。

测试Gamification的Actuator,可在命令行运行如下命令,结果如下:

> http :8081/actuator/health
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 15 Dec 2023 01:21:22 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "status": "UP"
}

如果系统正常运行,将获得UP值和HTTP状态码200。如果停止RabbitMQ服务器,再尝试相同的请求,由于Actuator包含了一个检查了RabbitMQ服务器的运行状况指示器,该指示器会失败,就导致聚合的运行状况切换为DOWN。在RabbitMQ服务器停止的情况下发出请求,结果确实是这样的,结果如下:

> http :8081/actuator/health
HTTP/1.1 503
Connection: close
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 15 Dec 2023 01:22:21 GMT
Transfer-Encoding: chunked

{
    "status": "DOWN"
}

这时返回的HTTP状态码是503,即服务不可用。调用者不需要解析响应主体,只需要检查HTTP响应状态码是否为200即可确定该应用程序是否正常。还可以在控制台日志中看到RabbitHealthIndicator未能成功,例如:

2023-12-15T09:22:18.897+08:00  INFO 15900 --- [ntContainer#0-2] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2023-12-15T09:22:21.506+08:00  INFO 15900 --- [nio-8081-exec-8] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2023-12-15T09:22:21.510+08:00  WARN 15900 --- [nio-8081-exec-8] o.s.b.a.amqp.RabbitHealthIndicator       : Rabbit health check failed

org.springframework.amqp.AmqpIOException: java.io.IOException
	at org.springframework.amqp.rabbit.support.RabbitExceptionTranslator.convertRabbitAccessException(RabbitExceptionTranslator.java:70) ~[spring-rabbit-3.1.0.jar:3.1.0]
	at 
	... omitted

如果重启RabbitMQ服务器,并再次检查运行状况,就会切换到UP状态。这就是健壮系统需要的行为:微服务有问题时,应该标记,让其他微服务知道;而且,应尝试从错误中恢复并在可能的情况下再次切换到再次状态。

服务发现和负载均衡

已经能知道服务是否可用了,就可以在系统中集成服务发现和负载均衡了。
服务发现包含两个主要概念:

  • 服务注册表:位于中心位置,列出了可用服务、服务所在的地址以及其他一些元数据(如名称)。服务实例将自身服务信息注册到注册中心。这部分服务信息包括服务所在主机IP和提供服务的Port,以及暴露服务自身状态以及访问协议等信息。可能包含不同服务的条目,有可能包含同一服务的多个实例。后一种情况下,访问注册表的客户端可以通过查询服务别名来获得可用实例的列表。
  • 注册服务者:负责注册服务者完成注册服务实例的逻辑。可以是观察微服务状态的外部运行过程,也可以作为库嵌入服务本身。

服务发现就是服务实例请求注册中心获取所依赖服务信息。服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。

服务列表:
-multiplication->host1:8080,host2:9080
-gamification->host1:8081
multiplication=host1:8080
multiplication=host2:9080
gamification=host1:8081
服务注册表
服务注册者
Multiplication
host1:8080
服务注册者
Multiplication
host2:9080
服务注册者
Gamification
host1:8081

从图中可以看到,有三个服务注册。host1在端口8080上部署了一个Multiplication实例,在端口8081上部署了一个Gamification实例,host2在端口9080上部署了一个Multiplication实例,这些实例都知道它们所在的位置,然后,这些注册者将相应的URI发送到服务注册中心,服务注册中心客户端可以使用其名称简单地请求服务的位置,接着,服务注册中心返回实例及其位置的列表。
负载均衡模式与服务发现紧密相关,如果多个服务在注册时使用相同的名称,则意味着有多个副本可用。想要平衡它们之间的流量,就可以增加系统的容量并通过增加的冗余使其在出现错误时更具弹性。
其他服务可从注册表中查询给定服务的名称,检索列表,然后确定要调用的实例,这称为客户端发现,表示客户端知道服务注册表并自行执行负载均衡。这里的客户端,指的是想要对另一个服务执行HTTP调用的应用程序、微服务、浏览器等。如图所示:

客户端
报告
服务列表:
-multiplication->host1:8080,host2:9080
-gamification->host1:8081
1
multiplication=host1:8080
1
multiplication=host2:9080
1
gamification=host1:8081
3
3
2
multiplication?
2
host1:8080,
host2:9080
服务发现
负载均衡
服务注册表
服务注册者
Multiplication
host1:8080
服务注册者
Multiplication
host2:9080
服务注册者
Gamification
host1:8081

另一方面,服务器端发现通过提供一个预先已知的唯一地址,来从客户端抽象所有这些逻辑,调用者可在该地址中找到给定的服务。当发出请求时,负载均衡器会拦截请求,然后将请求代理到其中一个副本。如图所示:

客户端
报告
服务列表:
-multiplication->host1:8080,host2:9080
-gamification->host1:8081
1
multiplication=host1:8080
1
multiplication=host2:9080
1
gamification=host1:8081
3
2
multiplication?
2
host1:8080,
host2:9080
服务发现
负载均衡
服务注册表
服务注册者
Multiplication
host1:8080
服务注册者
Multiplication
host2:9080
服务注册者
Gamification
host1:8081

通常,在微服务架构中,会看到两种方法结合在一起使用,或者仅使用服务器端发现。当API客户端位于系统之外时,客户端发现机制无法正常工作,因为不应该要求外部客户端与服务注册表进行交互并自行进行负载均衡。一般由网关承担此职责,因此,API网关将连接的服务注册表,并包含一个负载均衡器,用于在实例之间分配负载。
对后端内部的其他任何服务到服务通信,可将它们全部都连接到注册表进行客户端发现,也可以使用负载均衡器将每个服务集群抽象为唯一地址。后者是诸如Kubernetes的一些平台中选择的技术,该平台中,每个服务都被分配一个唯一的地址,而不管有多少副本,也不管它们位于哪个节点。
微服务没有相互调用,就可用简单实现客户端发现。Spring Boot具有连接到服务注册表并实现负载均衡的集成。
到后端的任何非内部HTTP通信都将使用服务器端发现方法,这意味着网关不仅会路由流量,还会负载均衡。如图所示。

Gamification
Multiplication
网关
查找服务
-发送尝试
-获取用户别名
-获取排行榜
-发送尝试
-获取用户别名
-获取排行榜
注册表自身
(每个实例)
注册表自身
(每个实例)
查找服务
实例
HTTP请求
(假设)
注册服务者
注册服务者
服务发现
客户端
Spring Cloud
Consul发现
负载均衡器
Spring Cloud
负载均衡器
(如有必要)
服务发现
客户端
Spring Cloud
Consul发现
负载均衡器
Spring Cloud
负载均衡器
路由器
Spring Cloud
网关
服务注册表
注册表
Consul
浏览器
尝试
(主题交换)
Gamification
队列

Consul

Consul、Eureka、Zookeeper等工具实现了服务发现模式,还有一些完整的平台将这种模式作为其功能。在Spring生态系统中,Netflix的Eureka长期以来一直是最受欢迎的选择,但是由于Netflix的原因,不再是好的选择。这里将所有Consul,该工具可以提供服务发现和其他功能,并通过Spring Cloud进行集成,方便使用。
首先安装Consul,这里使用Docker下的Consul,来进行单节点部署。命令如下:

docker run -id --name=consul -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8500:8500 -p 8600:8600 consul agent -server -ui -node=n1 -bootstrap-expect=1 -client=0.0.0.0

这里暴露了几个端口,它们的作用如下:

  • 8300 TCP协议,用于Consul集群中各个节点相互连结通信的端口
  • 8301 TCP或者UDP协议,用于Consul节点之间相互使用Gossip协议健康检查等交互
  • 8302 TCP或者UDP协议,用于单个或多个数据中心之间的服务器节点的信息同步
  • 8500 HTTP协议,用于API接口或者我们上述的网页管理界面访问
  • 8600 TCP或者UDP协议,作为DNS服务器,用于通过节点名查询节点信息

在Spring Cloud模块集成Consul服务发现时,需要配置8500端口。除此之外,还有几个参数:

  • agent 表示启动一个Agent进程
  • server 表示该节点类型为Server节点(下面会讲解集群中的节点类型)
  • ui 开启网页可视化管理界面
  • node 指定该节点名称,注意每个节点的名称必须唯一不能重复!上面指定了第一台服务器节点的名称为n1,那么别的节点就得用其它名称
  • bootstrap-expect 最少集群的Server节点数量,少于这个值则集群失效,这个选项必须指定,由于这里是单机部署,因此设定为1即可
  • advertise 这里要指定本节点外网地址,用于在集群时告诉其它节点自己的地址,如果是在自己电脑上或者是内网搭建单节点/集群则不需要带上这个参数
  • client 指定可以外部连接的地址,0.0.0.0表示外网全部可以连接

安装后,打开控制台日志,可以看到如下信息:

2023-12-15 11:57:36 ==> Starting Consul agent...
2023-12-15 11:57:36            Version: '1.11.1'
2023-12-15 11:57:36            Node ID: 'ecf41197-fa98-7cc6-a2c3-295ad75c7efd'
2023-12-15 11:57:36          Node name: 'n1'
2023-12-15 11:57:36         Datacenter: 'dc1' (Segment: '<all>')
2023-12-15 11:57:36             Server: true (Bootstrap: true)
2023-12-15 11:57:36        Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, gRPC: -1, DNS: 8600)
2023-12-15 11:57:36       Cluster Addr: 172.17.0.4 (LAN: 8301, WAN: 8302)
2023-12-15 11:57:36            Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false
2023-12-15 11:57:36 
2023-12-15 11:57:36 ==> Log data will now stream in as it occurs:
...

日志显示一些有关服务器的信息和启动操作,这里采用本地代理,实际的生产设置是由多个数据中心的集群组成。这些数据中心可运行一个或多个代理,其中每个服务器中只有一个代理可以充当服务器代理。代理之间使用协议进行通信以同步信息并通过共识选举领导者,这些设置确保高可用性。如果数据中心无法访问,代理会知道并选举出新的领导者。
Consul在端口8500上运行HTTP服务,提供RESTful API,用于服务注册和发现以及其他功能,还提供一个用户界面,通过http://localhost:8500来访问,如下图所示:
Consul UIService部分显示已注册服务列表,因为还没有进行任何操作,唯一可用的服务是Consul服务器。其他选项卡可显示可用的节点、键/值、Intention、ACL等功能。
还可用通过REST API来访问可用服务列表,例如HTTPie就可以请求可用服务列表,会输出一个空的响应体,如下所示:

> http -b :8500/v1/agent/services
{}

通过Service API,可列出服务,查询服务信息,了解服务是否正常运行,对服务进行注册和注销。这里不会直接使用这些API,因为Spring Cloud Consul模块会自动完成这些操作。
Consul包括用于验证所有服务状态的功能:运行状况检查功能。它提供了多种可用于确定运行状况的选项:HTTP、TCP、脚本等。这里,计划让Consul通过HTTP接口(具体地说是/actuator/health端点)联系微服务。运行状况检查位置是在服务注册时配置的,Consul会定期触发一次(也可以自定义)。如果服务无法响应或状态为非正常(2XX除外),则Consul会将其标记为不正常。

Spring Cloud Consul

不需要使用Consul API来注册服务、定义运行状况检查或访问注册表来查找服务地址,这些功能由Spring Cloud Consul提供,因此,在Spring Boot应用程序中包含对应的启动器,并进行一些配置即可。
首先,在Gateway项目中添加Spring Cloud Consul 发现依赖项,如下所示:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

接着,对另外两个微服务Multiplication和Gamification依次添加Spring Cloud和spring-cloud-starter-consul-discovery依赖项,如下所示:

    <dependencies>
        <!-- ... -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- ... -->
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Consul自带的Spring Boot默认自动配置符合现在的情况:服务器位于http://localhost:8500。如果要查看这些默认设置,可以查看ConsulProperties的源代码,如下所示:

@ConfigurationProperties(PREFIX)
@Validated
public class ConsulProperties {

	/**
	 * Prefix for configuration properties.
	 */
	public static final String PREFIX = "spring.cloud.consul";

	/** Consul agent hostname. Defaults to 'localhost'. */
	@NotNull
	private String host = "localhost";

	/**
	 * Consul agent scheme (HTTP/HTTPS). If there is no scheme in address - client will
	 * use HTTP.
	 */
	private String scheme;

	/** Consul agent port. Defaults to '8500'. */
	@NotNull
	private int port = 8500;

	/** Is spring cloud consul enabled. */
	private boolean enabled = true;

	/** Configuration for TLS. */
	private TLSConfig tls;

	/** Custom path if consul is under non-root. */
	private String path;
	
  //...
}

如果需要改变,使用前缀spring.cloud.consul的相关属性即可。
现在,需要在应用程序中添加一个新的配置:spring.application.name,以指定应用程序的名称。将这个属性添加到所有微服务的配置中,如下所示:

spring.application.name=multiplication

在Multiplication中设置为multiplication,Gamification中设置为gamification,Gateway中设置为gateway。
启动微服务Multiplication和Gamification,来看看它们如何通过运行状况检查进行注册的。注意,要启动RabbitMQ和Consul服务器。查看Gamification运行日志,可以找到如下行:

2023-12-21T17:42:38.706+08:00  INFO 5012 --- [gamification] [           main] o.s.c.c.s.ConsulServiceRegistry          : Registering service with consul: NewService{id='gamification-8081', name='gamification', tags=[], address='host.docker.internal', meta={secure=false}, port=8081, enableTagOverride=null, check=Check{script='null', dockerContainerID='null', shell='null', interval='10s', ttl='null', http='http://host.docker.internal:8081/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null', grpc='null', grpcUseTLS=null}, checks=null}

这行提示表明了应用程序启动时通过Spring Cloud Consul进行了服务注册。可以看到请求的内容:由服务名和端口组成的id、可以对多个实例进行分组的服务名称、本地地址,以及通过HTTP对Spring Boot Actuator公开的服务运行状况端点地址进行配置的状态检查,时间间隔为10秒。
在Consul服务器端,可以看到如下输出:

2023-12-21 17:41:24 2023-12-21T09:41:24.834Z [INFO]  agent: Synced service: service=multiplication
2023-12-21 17:41:32 2023-12-21T09:41:32.373Z [INFO]  agent: Synced check: check=service:multiplication
2023-12-21 17:42:38 2023-12-21T09:42:38.725Z [INFO]  agent: Synced service: service=gamification-8081
2023-12-21 17:42:40 2023-12-21T09:42:40.883Z [INFO]  agent: Synced check: check=service:gamification-8081

使用Consul浏览器界面访问,结果如下图所示:
UI
在Services列表中,单击gamification,显示其详细信息,可以看到该服务的运行状况,如图所示:
gamification
单击服务的各种检查项,可以看到更详细的检查结果,例如点击“All service checks passing”,如图所示:
check
还可以启动第二个服务实例,以查看注册表如何对其进行处理,命令行如下所示:

> ./mvnw spring-boot:run -D"spring-boot.run.arguments"="--server.port=9080" 

在Consul注册表中,仍然只有一个multiplication服务,但显示有2个实例,如图所示:
2
当点击multiplication服务时,导航到Instances选项卡,就可以看到有2个实例了,如图所示:
Instances
每个实例都有对应的运行状况检查。
从Consul获取服务列表的API请求检索这些服务,响应如下所示:

> http -b :8500/v1/agent/services
{
    "gamification-8081": {
        "Address": "host.docker.internal",
        "Datacenter": "dc1",
        "EnableTagOverride": false,
        "ID": "gamification-8081",
        "Meta": {
            "secure": "false"
        },
        "Port": 8081,
        "Service": "gamification",
        "Tags": [],
        "Weights": {
            "Passing": 1,
            "Warning": 1
        }
    },
    "multiplication": {
        "Address": "host.docker.internal",
        "Datacenter": "dc1",
        "EnableTagOverride": false,
        "ID": "multiplication",
        "Meta": {
            "secure": "false"
        },
        "Port": 8080,
        "Service": "multiplication",
        "Tags": [],
        "Weights": {
            "Passing": 1,
            "Warning": 1
        }
    },
    "multiplication-9080": {
        "Address": "host.docker.internal",
        "Datacenter": "dc1",
        "EnableTagOverride": false,
        "ID": "multiplication-9080",
        "Meta": {
            "secure": "false"
        },
        "Port": 9080,
        "Service": "multiplication",
        "Tags": [],
        "Weights": {
            "Passing": 1,
            "Warning": 1
        }
    }
}

如果没有Spring抽象,会如何使用Consul服务呢?首先,所有服务只有知道HTTP主机和端口才能找到注册表;然后,如果要与Gamification API进行交互,要所有Consul的Service API来获取可用的实例列表,遵循客户端发现方法,会使用负载均衡从列表中选择一个正常的实例;然后,客户端服务知道了地址和请求的端口后,便可用执行请求。过程很繁琐,Spring Cloud Consul已经自动完成了,大大简化了操作。
网关是系统中调用其他服务的服务,已经使用Consul实现了服务发现,那么,如何实现负载均衡呢?

Spring Cloud负载均衡器

这里,实现一种客户端发现方法,其中后端服务查询注册表并确定如果有多个可用实例的话,应该调用哪个实例。Spring Cloud负载均衡器是Spring Cloud Commons的组件,与Consul和Eureka集成在一起,提供简单的负载均衡器实现,默认情况下,会自动配置能够迭代遍历所有实例的循环负载均衡器。
Netflix的Ribbon曾经是实现负载均衡器的首选,因为其处于维护模式,所以放弃该选项,选择Spring的负载均衡器实现。Spring Cloud负载均衡器包含在Spring Cloud Consul启动器依赖中。
要在两个应用程序之间进行负载均衡的调用,可在创建RestTemplate对象时只使用@LoadBalanced注解,然后,在执行对该服务的请求时,将服务名称用作URL中的主机名,Spring Cloud Consul和负载均衡器将完成其余工作,查询注册表并按顺序选择一个实例。下面的代码表明了如何在客户端Multiplication微服务中集成服务发现和负载均衡:

@Configuration
public class RestConfiguration {
	@LoadBalanced
	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
}
@Slf4j
@Service
public class GamificationServiceClient {
	private final RestTemplate restTemplate;
	
	public GamificationServiceClient(final RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}
	
	public boolean sendAttempt(final ChallengeAttempt attempt) {
		try {
			ChallengeSolvedDTO dto = new ChallengeSolvedDTO(attempt.getId(),
				attempt.isCorrect(), attempt.getFactorA(),
				attempt.getFactorB(), attempt.getUser().getId(),
				attempt.getUser().getAlias());
			ResponseEntity<String> r = restTemplate.postForEntity(
				"http://gamification/attempts", dto, String.class);
			log.info("Gamification service response: {}", r.getStatusCode());
			return r.getStatusCode().is2xxSuccessful();
		} catch (Exception e) {
			log.error("There was a problem sending the attempt.", e);
			return false;
		}
	}
}

因为已经去除了微服务之间的HTTP调用,这种方法就不适用了,但是,如果想要了解服务间HTTP交互情况,还是不错的。使用服务发现和负载均衡,可以减少失败的风险,因为这增加了至少有一个实例用于处理同步请求的机会。
这里,计划在网关中集成服务发现和负载均衡,如图所示:

Gamification
服务注册表
网关
查找服务
实例
-发送尝试
-获取用户别名
-获取排行榜
-发送尝试
-获取用户别名
-获取排行榜
注册表自身
(每个实例)
注册服务者
Spring Cloud
Consul发现
Multiplication
注册服务者
Spring Cloud
Consul发现
注册表
Consul
服务发现
客户端
Spring Cloud
Consul发现
负载均衡器
Spring Cloud
负载均衡器
路由器
Spring Cloud
网关
浏览器
尝试(主题交换)
Gamification队列

网关中的服务发现和负载均衡

在应用程序中包含Spring Cloud Consul启动器后,它们会联系注册表用于发布信息,但是,仍然可以显式地使用地址/端口来代理请求的网关。
首先在Gateway项目依赖中添加Spring Cloud Consul发现启动器,如下所示:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

下面,就想要添加配置信息,可以分成三组:

  • 全局设置:为应用程序命名,确保使用Spring Cloud负载均衡器实现,另外,要添加一个配置参数,来指示服务发现客户端仅检索运行正常的服务。
  • 路由配置:不使用显式的主机和端口,而是使用URL模式切换到服务名称,该模式可实现负载均衡。
  • 弹性:为了防止网关无法将请求代理到服务的情况,可设置尝试次数等。

关于网关的配置如下所示:

server:
  port: 8000

spring:
  cloud:
    consul:
      discovery:
        enabled: true
        # 获取通过健康检查的服务
        query-passing: true
    gateway:
      routes:
        - id: multiplication
          uri: lb://multiplication/
          predicates:
            - Path=/challenges/**,/attempts,/attempts/**,/users/**
        - id: gamification
          uri: lb://gamification/
          predicates:
            - Path=/leaders
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "http://localhost:3000"
            allowedHeaders:
              - "*"
            allowedMethods:
              - "GET"
              - "POST"
              - "OPTIONS"
      default-filters: 
        - name: Retry
          args:
            retries: 3
            methods: GET, POST
  application:
    name: gateway

将query-passing设置为true,Spring将只检索那些通过运行状况检查的服务,因为只想将请求代理到正常的实例。在服务不经常轮询更新服务列表的情况下,false才可能有意义,这种情况下,需要获取整个列表,而且有处理不正常实例的机制。
最大的改变是关于URL的修改,现在使用诸如 lb://multiplication/ 的URL,由于添加了Consul客户端,应用程序可以使用Service API将服务名称解析为可用实例,lb告诉Spring应该使用负载均衡。
这里还添加了适用于所有请求的网关过滤器:RetryGatewayFilter,它位于 default-filters 节点下。该过滤器拦截错误响应,透明地重试该请求,与负载均衡器结合使用,意味着该请求将被代理到下一个实例,可以方便地获取不错的弹性(重试)。这里配置重试3次,可以覆盖大多数失败情况,如果所有重试都失败,网关会向客户端返回错误响应(服务不可用)。
那么,为什么要使用重试呢?Consul的工作方式是每10秒轮询一次(默认值,看更改),当服务还没准备好处理时,注册表不会知道,可能的情况是,Consul成功地检查到了实例,然后该实例立即停止运行,注册表会保持该实例正常运行状况几秒钟,直到下一次轮询,可以使用重试来处理这种情况。更新注册表后,网关将不会在服务列表中获取不正常的实例,就不需要重试。

注意,减少两次检查的间隔时间可以减少错误数量,但会增加网络流量。

应用新配置后,Gateway微服务会连接到Consul,以查找其他微服务的可用实例及网络位置,然后,基于Spring Cloud负载均衡器中包含的简单循环算法来均衡负载。
现在的配置,所有外部HTTP通信通过localhost:8000使用网关微服务,实际生产环境中,通常使用端口80的HTTP访问,并使用DNS地址指向服务器所在的IP,但是,公共访问将只有一个入口,这成为系统的关键,必须尽可能提高可用性,如果宕机,系统就会宕机。
为了降低风险,可引入DNS负载均衡(指向多个IP地址的主机名)向网关添加冗余。但是,当其中一台主机没有响应时,它依靠客户端(如浏览器)来管理IP地址列表并处理故障转移。可将其视为网关顶部的一个额外层,以增加客户端发现、负载均衡和容错能力。这不是典型做法。
Amazon、Microsoft、Google等云提供商使用路由和负载均衡来保证托管服务的高可用性,这也是确保网关始终保持运行状态的替代方法,另外,Kubernetes允许在网关上创建负载均衡器,也可以在该层添加冗余。

使用服务发现和负载均衡

下面就来看看服务发现和负载均衡。
在运行之前,在Multiplication微服务的UserController和Gamification微服务的LeaderBoardController中添加一行日志输出,便于在日志中查看与API的交互情况。如下所示:

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserRepository userRepository;

    @GetMapping("/{idList}")
    public List<User> getUsersByIdList(@PathVariable final List<Long> idList) {
        log.info("解析用户别名:{}", idList);
        return userRepository.findAllByIdIn(idList);
    }
}
@Slf4j
@RestController
@RequestMapping("/leaders")
@RequiredArgsConstructor
public class LeaderBoardController {
    private final LeaderBoardService leaderBoardService;

    @GetMapping
    public List<LeaderBoardPosition> getLeaderBoard() {
        log.info("查询排行榜");
        return leaderBoardService.getCurrentLeaderBoard();
    }
}

启动整个系统,包括RabbitMQ、Consul代理、Multiplication微服务、Gamification微服务、Gateway微服务、前端React等。另外给Multiplication和Gamification添加一个额外的实例,命令行如下:

multiplication> ./mvnw spring-boot:run -D"spring-boot.run.arguments"="--server.port=9080"
...
gamification> ./mvnw spring-boot:run -D"spring-boot.run.arguments"="--server.port=9081"
...

启动后,可以在Consul的界面中查看服务信息,如下图所示:
services
验证网关的负载均衡器是否正常很简单:检查两个Gamification服务实例的日志,通过前面添加的日志输出信息,可以方便地查看到排行榜更新的交替请求。如下所示:

2023-12-22T11:37:30.131+08:00  INFO 55672 --- [multiplication] [nio-8080-exec-7] c.z.m.challenge.ChallengeServiceImpl     : attempt: ChallengeAttempt(id=154, user=User(id=3, alias=noise), factorA=50, factorB=60, resultAttempt=3000, correct=true)
2023-12-22T11:37:33.494+08:00  INFO 55672 --- [multiplication] [nio-8080-exec-8] c.z.multiplication.user.UserController   : 解析用户别名:[3, 1]

大家可以多尝试一下,以观察在各个实例上的输出。
现在,系统得到了扩展,一切按预期进行。与RabbitMQ负载均衡类似,HTTP流量在所有实例之间均衡分配,将系统的容量增加了一倍。
实际上,可以启动任意数量的实例,并且负载将透明地分布在所有微服务中,通过网关,使API客户端不了解内部服务,很容易实现这些关注点,如用户身份验证或监视等。
还应该检查是否实现了其他非功能性需求:弹性、高可用性和容错性。
意外总会发生,模拟一下服务不可用的情况。现在模拟服务突然终止的情况,人为停止某个实例,查看是否能够正常。
手动终止Gamification实例2(9081端口),持续发送请求,发现所有的请求都转发到8080端口的实例上了,如图所示:

-发送尝试
-获取用户别名
-获取排行榜
查找服务实例
-发送尝试
-获取用户别名
-获取排行榜(1)
-获取排行榜(2)
(Retry)
-获取排行榜(2)
X
运行状况检查
(每隔10秒钟)
浏览器
网关
服务注册表
(Consul)
Gamification
Gamification
Multiplication
尝试(主题交换)
Gamification队列

可以暂时关闭网关中的重试过滤器配置,来了解它们是如何协作的,注释调Gateway配置中的重试过滤器配置,重新启动,不使用重试模式时如图所示:

-发送尝试
-获取用户别名
-获取排行榜
查找服务实例
-发送尝试
-获取用户别名
-获取排行榜(1)
-获取排行榜(2)
X
运行状况检查
(每隔10秒钟)
浏览器
网关
服务注册表
(Consul)
Gamification
Gamification
Multiplication
尝试(主题交换)
Gamification队列

模拟前面的测试,整个系统在停止一个实例的时候,仍然能够运行。在Consul界面下,导航到Service实例下,可以看到列出来的所有可用实例,如图所示:
service实例
现在,在微服务中实现了可扩展性,通过服务发现注册表的负载均衡器实现了适当的容错性,注册表了解系统中不同组件的运行状况。

环境配置

Spring Boot的优点之一是能够通过配置文件自定义配置,配置文件可以根据需要启用一组配置属性。例如,本地测试期间,可配置使用本地RabbitMQ服务器;生产环境中,可切换到实际的RabbitMQ服务器。
要引入新的rabbitprod配置文件,可以创建一个名为application-rabbitprod.properties文件。Spring Boot使用application-{profile}的命名约定(对应properties和yaml格式),从而可以在单独的文件中定义配置文件。
定义了单独的配置文件,启动应用程序时,要确保启用此配置文件,这需要设置spring.profiles.active属性。可使用maven命令为微服务Multiplication启用指定的配置文件:

> ./mvnw spring-boot:run -D"spring-boot.run.arguments"="--spring.profiles.active=rabbitprod"

可以想象,所有微服务在每种环境中都可能具有许多通用的配置,不仅RabbitMQ的连接详细信息可能是相同的,而且可以添加额外的值,例如exchange名称(amqp.exchange.attempts)。可以将值保存在每个微服务、每种环境和每个工具的单独文件中,例如:

  • application-rabbitprod.properties
  • application-databaseprod.properties
  • application-rabbitstaging.properties
  • application-databasestaging.properties

然后,在需要的时候进行复制,方便重用这些值。但是,保留许多副本带来大量的维护工作,如果需要更改公共配置中的某个值,必须替换每个项目中的相应文件。
更好的方法是将配置放在系统中的通用位置,使应用程序在启动之前对其内容进行同步,然后对每种环境进行集中配置,这样只相应调整一次值。如图所示:

生产环境
Multiplication
Gamification
Get config:prod
Get config:prod
配置服务器
staging config
prod config
配置服务器
客户端
enabled profile:prod
配置服务器
客户端
enabled profile:prod
分段环境
Multiplication
Gamification
Get config:prod
Get config:prod
配置服务器
staging config
prod config
配置服务器
客户端
enabled profile:prod
配置服务器
客户端
enabled profile:prod

在寻找Spring的配置服务器模式下,解决方案是使用Spring Cloud Config Server,它可以保留一组配置文件,这些配置文件分布在文件夹中并通过REST API公开。在客户端,使用此依赖的项目根据其活动配置文件来访问配置服务器并请求相应的配置资源。缺点是,需要创建另外一个微服务以充当配置服务器并公开集中式文件。
替代方案是使用Consul KV,这是Consul软件包中包含的功能,Spring Cloud进行集成以实现集中式配置服务器。可以通过这种方法重用组件,结合Consul的服务发现、运行状况检查和集中式配置来简化系统。

使用Consul进行配置

Consul KV是随Consul安装的键/值存储,可通过REST API和用户界面访问。将Consul设置为集群时,还可以从复制中受益,这样,由于服务无法获得其配置而导致数据丢失或停机的风险较小。
在Consul用户界面中,导航到Key/Value选项卡,如图所示:
key/value
单击Create,就可以创建新的键/值对,如图所示:
create
可使用切换按钮在代码和普通编辑器之间切换。代码编辑器支持几种符号的语法分色,包括json、hcl和yaml等。
Consul KV REST API允许通过HTTP调用创建键/值对和文件夹,并使用键名进行检索,和服务发现一样,不需要使用REST API进行交互,Spring Cloud Consul Config进行了抽象,方便使用。

Spring Cloud Consul Config

使用Consul KV实现集中式配置需要Spring Cloud Consul Config支持,要向项目中添加spring-cloud-starter-consul-config依赖,它包括自动配置类,这些类将在应用程序早期阶段尝试查找Consul代理并读取相应的KV值,之所以使用此阶段,是希望Spring Boot在其余的初始化过程中应用集中式配置值(如,连接到RabbitMQ)。
Spring Cloud Consul Config希望每个配置文件都映射到KV存储中的给定键,其值是一组Spring Boot配置值,格式为YAML或纯格式(.properties)。
可以配置一些值,以帮助应用程序在服务器中找到相应的键,下面是最相关的设置:

  • 前缀:Consul KV中存储所有配置文件的根文件夹,默认为config。
  • 格式:指定值采用YAML还是properties语法。
  • 默认上下文:所有应用程序用作公用属性的文件夹名称。
  • 配置文件分隔符:键可以组合多个配置文件。这种情况下,可指定要用作分隔符的字符(例如,用逗号分隔的prod,extra-logging)。
  • 数据键:保存属性或YAML内容的键名。

对于每个应用程序,必须将与配置服务器相关的所有配置值都放在一个单独的文件中,文件名为bootstrap.yml或bootstrap.properties。如图所示:

Multiplication
1
2
2
从服务器检索配置
配置服务器
客户端
配置服务器
config/
defaults/
application.yml
key1:
subkey: value
defaults,multiplication/
application.yml
bootstrap.properties
prefix = config
default-context = defaults
data-key = application.yml
format = yaml
application.properties
...

注意,连接到配置服务器的应用程序配置(bootstrap)和将本地属性(application)与那些从配置服务器下载的属性合并所产生的应用程序配置之间存在差异。第一种配置是元配置,无法从服务器下载,必须将这些值复制到项目中相应的引导程序配置文件中。下面通过实例来看看它们是如何工作的。

实现集中式配置

首先,需要在微服务项目中添加启动器:spring-cloud-starter-consul-config。代码如下:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-config</artifactId>
        </dependency>

这样,应用程序会尝试在引导阶段连接到Consul,并使用KV自动配置中提供的默认值从KV存储中获取配置文件属性。这里会改变某些设置,以便了解其中的原理。
在Multiplication和Gamification项目在,使用的是properties格式,为保持一致,在同一文件夹中创建一个单独的文件bootstrap.properties,这两个项目中使用相同的设置,代码如下:

spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.format=yaml
spring.cloud.consul.config.default-context=defaults
spring.cloud.consul.config.data-key=application.yml

这里,选择yaml作为远程配置的格式,本地文件为.properties格式,这没问题,Spring Cloud Consul Config可合并远程application.yml键中包含的值和不同格式本地存储的值。
在Gateway项目中,创建等效的bootstrap.yml文件,使用yaml进行配置,如下所示:

spring:
  cloud:
    consul:
      config:
        prefix: config
        format: yaml
        default-context: defaults
        data-key: application.yml

使用这些设置,目的是将所有配置存储在Consul KV中名为config的根文件夹中。在内部有一个defaults文件夹,其中可能包含名为application.yml的键,其配置适用于所有微服务。可为每个应用程序或要使用的应用程序和配置文件组合设置额外的文件夹,且每个文件夹都可能包含application.yml键已经应添加或覆盖的属性。为避免在配置服务器中混淆格式,应统一使用yaml格式。现在,将bootstrap文件添加到Multiplication、Gamification和Gateway项目中,以便它们可以连接到配置服务器并查找外部化的配置(如果有),而且添加了Spring Cloud Consul Config启动器依赖。
下面,可创建如下所示的层次结构作为Consul KV中的文件夹和键,例如:

+- config
| +- defaults
|    \- application.yml
| +- defaults,production
|    \- application.yml
| +- defaults,rabbitmq-production
|    \- application.yml
| +- defaults,database-production
|    \- application.yml
| +- multiplication,production
|    \- application.yml
| +- gamification,production
|    \- application.yml

在Consul界面中,进入Key/Value选项卡,单击Create,如图所示:
create
然后,输入config/以创建与设置中的名称相同的根文件夹,如图所示:
config
单击新创建的项目,导航到config文件夹,如图所示:
导航
config
在config文件夹下创建defaults文件夹,如图所示:
defaults
导航到config/defaults文件夹,内容为空,在此文件夹下创建一个名为application.yml的键,默认情况下,将要应用于所有应用程序的值放在这里。
application.yml
那么,如果运行Multiplication应用程序时使用的活动配置文件列表等于production、rabbitmq-production、database-production,处理顺序如下(按优先级从低到高排列):

  1. 基线值是访问配置服务器的项目的本地application.properties中包含的值,这里是Multiplication。
  2. 然后,Spring Boot合并和覆盖defaults文件夹内application.yml键中包含的远程值,因为它适用于所有服务。
  3. 接着是合并所有活动配置文件的默认值。活动配置文件指所有与defaults,{profile}模式匹配的文件:defaults,production、defaults,rabbitmq-production、defaults,database-production。注意,如果指定了多个配置文件,则优先处理最后一个配置文件的值。
  4. 最后,会按模式{application},{profile}为相应的应用程序名称和活动配置文件查找更具体的设置。这里,multiplication,production键匹配该模式,将合并其配置值。优先顺序与之前相同,优先处理枚举中的最后一个配置文件。

结构化配置值的方法如下:

  • 当要为所有环境的全部应用程序添加全局配置时,使用defaults。
  • 使用defaults,{profile},其配置文件名称代表{tool}-{environment}对,为每种环境设置给定工具的通用值,如rabbitmq-production。
  • 使用配置文件名称为{environment}的{application},{profile}为给定环境中的应用程序设置特定设置,如multiplication,production。

集中配置实践

前面已经在项目中添加了启动器依赖:spring-cloud-starter-consul-config,而且配置了bootstrap属性文件(Spring Cloud Consul 4.1.0不需要了,这些配置可以放在application属性文件中),用于覆盖某些Consul Config默认配置。要启动Multiplication,需要在application.properties中添加下列配置:

spring.config.import=optional:consul:
spring.cloud.consul.config.prefix=config
spring.cloud.consul.config.format=yaml
spring.cloud.consul.config.default-context=defaults
spring.cloud.consul.config.data-key=application.yml

在Consul KV中创建配置:config/defaults/application.yml,值为:

logging:
  level:
    org.springframework.core.env: DEBUG
    org.springframework.amqp.rabbit.core.RabbitAdmin: DEBUG

重启Multiplication应用程序,可以在日志中看到对应的配置在起作用:

2023-12-23T15:17:47.110+08:00 DEBUG 39172 --- [multiplication] [           main] o.s.c.e.PropertySourcesPropertyResolver  : Found key 'spring.application.name' in PropertySource 'environmentProperties' with value of type String
...
2023-12-23T15:17:47.481+08:00 DEBUG 39172 --- [multiplication] [)-192.168.1.234] o.s.amqp.rabbit.core.RabbitAdmin         : Initializing declarations
2023-12-23T15:17:47.494+08:00 DEBUG 39172 --- [multiplication] [)-192.168.1.234] o.s.amqp.rabbit.core.RabbitAdmin         : declaring Exchange 'attempts.topic'
2023-12-23T15:17:47.497+08:00 DEBUG 39172 --- [multiplication] [)-192.168.1.234] o.s.amqp.rabbit.core.RabbitAdmin         : Declarations finished
2

配置config/multiplication/application.yml,设置值为:

server:
  port: 10080

重启Multiplication应用程序,可以发现其服务器端口已变为10080了,配置发生作用了。可以使用下列命令使应用程序启用某些配置文件:

> mvn spring-boot:run -D"spring-boot.run.arguments"="--spring.profiles.active=production,rabbitmq-production"

现在,完成了集中式配置,使用Consul KV功能,如下图所示:

-发送尝试
-获取用户别名
-获取排行榜
-发送尝试
-获取用户别名
-获取排行榜(1)
-获取排行榜(2)
X
查找服务实例
运行状况检查
(每隔10秒钟)
服务注册表
配置服务器
Consul Discovery
& Consul KV
Srv. Registry
Centralized
Configuration
浏览器
网关
Gamification
Gamification
Multiplication
尝试(主题交换)
Gamification队列

请注意,应用程序在启动时与配置服务器存在依赖关系,为避免单点故障,可将Consul配置为生产环境中高度可用。

小结

为了使系统具有可扩展性和弹性,使用Spring Cloud Consul集成,Spring Boot的自动配置能快速建立与Consul的连接,能够提供服务发现和集中式配置功能,方便动态集成,易于实现。而且,从前面的示例可以看出,不需要编写代码,即可获得开箱即用的功能,当然,如果需要更高级的功能,需要深入学习。

文章来源:https://blog.csdn.net/ZhangCurie/article/details/134965124
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。