Spring Cloud 负载均衡初体验

  • 2019 年 10 月 3 日
  • 筆記

本文首发于我的个人博客,Spring Cloud 负载均衡初体验 ,欢迎访问!

使用 Spring Cloud Netflix 组件 Eureka 和 Ribbon 构建单注册中心的负载均衡服务。

Spring Cloud 是基于 Spring 的微服务技术栈,可以这么概括吧,里面包含了很多例如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等组件,可以通过 Spring Boot 的形式进行集成和使用。

目前,项目中有这么个需求,Spring Boot 做一个 web 服务,然后调用 TensorFlow 模型得到结果。但是由于 TensorFlow GPU 版,不支持多线程多引擎,所以只能采用多进程的方式去进行调度,所以需要做一个负载均衡。负载均衡的话,可以分为客户端和服务端负载均衡。我目前还没能领悟到有什么不同,毕竟整体的架构都是一样的,如下如图。其中客户端均衡负载的代表是 Spring Cloud 的 Ribbon,服务端负载均衡代表是 Nginx。

由于项目的压力并不大,日平均请求约 5000 左右,因此就采用 Spring Cloud 中的组件进行客户端负载均衡。主要用到的就是 Spring Cloud 和 Eureka。很多博客中会也看到 Ribbon 的身影。其实他们都是 Spring Cloud Netflix 中的组件,用来构建微服务。本文所讲的例子,也可以看作是一个微服务,把原来一个的算法服务拆成了若干个小服务。之前讲到的 Spring Cloud 的各种组件也都是为了使得这些独立的服务能够更好的管理和协作。

回到负载均衡,一个使用 Spring Boot 搭建的客户端负载均衡服务,其实只需要 Rureka 这一个组件就够了。

Eureka 是 Spring Cloud Netflix 当中的一个重要的组件,用于服务的注册和发现。Eureka 采用了 C-S 的设计架构。具体如下图,Eureka Server 作为一个注册中心,担任服务中台的角色,余下的其他服务都是 Eureka 的 Client。所有的服务都需要注册到 Eureka Server 当中去,由它进行统一管理和发现。Eureka Server 作为管理中心,自然,除了注册和发现服务外,还有监控等其他辅助管理的功能。

具体从负载均衡的角度来讲:

  1. Eureka Server——提供服务注册和发现
  2. Service Provider——服务提供方,一般有多个服务参与调度
  3. Service Consumer——服务消费方,从 Eureka 获取注册服务列表,从而能够消费服务,也就是请求的直接入口。

服务搭建

下面主要实战一下负载均衡服务搭建。

正如上面所说,一套完整的负载均衡服务,至少需要三个服务。

1.注册中心——Eureka Server

直接通过 IDEA 创建一个包含 Eureka Server 的 Spring Boot 项目,直接引入所需的 dependency。主要是 spring-cloud-dependenciesspring-cloud-starter-netflix-eureka-server

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

在启动类上添加注解 @EnableEurekaServer

@EnableEurekaServer  @SpringBootApplication  public class EurekaServerApplication {      public static void main(String[] args) {          SpringApplication.run(EurekaServerApplication.class, args);      }  }

在 yml 中配置 eureka。

server:    port: 8080  eureka:.      instance:      prefer-ip-address: true    client:      # 表示是否将自己注册到 Eureka Server,默认为 true      register-with-eureka: false      # 表示是否从 Eureka Server 获取注册信息,默认为 true      fetch-registry: false      service-url:        # 默认注册的域,其他服务都往这个 url 上注册        defaultZone: http://localhost:${server.port}/eureka/

由于目前配置的是单节点的注册中心,因此 register-with-eurekafetch-registry 都设为 false,不需要把自己注册到服务中心,不需要获取注册信息。

启动工程后,访问:http://localhost:8080/,就能看到一个图形化的管理界面,目前没有注册任何服务。

2.服务提供方——Service Provider

在 yml 中配置 Eureka。

eureka:    instance:      prefer-ip-address: true    # 以 IP 的形式注册      # 默认是 hostname 开头的,修改成 ip 开头      instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}    client:      serviceUrl:        defaultZone: http://localhost:8080/eureka/    # 注册到之前的服务中心  server:    port: 8084  spring:    application:      name: services-provider    # 应用名称

默认,Eureka 是通过 hostname 来注册到 Eureka Server 上的,由于后面可能涉及到多节点的配置,hostname 可能不如 ip 方便管理,所以将 prefer-ip-address 设为 true,通过 ip 注册,并修改 instance-id 格式为:${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}。注意 ip-address 为短线连接。

然后写一个简单的 web 服务做测试。

@RestController  public class ServicesController {      @RequestMapping("/")      public String home() {          return "Hello world";      }  }

然后运行工程。

3.服务消费方——Service Consumer

新建项目同 2,然后配置 Eureka:

eureka:    instance:      prefer-ip-address: true      instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}    client:      serviceUrl:        defaultZone: http://localhost:8080/eureka/  server:    port: 8085  spring:    application:      name: services-consumer

和之前一样,注意区分端口号和 name 就可以了。

再在启动类,配置 RestTemplate 用于负载均衡的服务分发。这是服务消费的一种基本方式,下面还会介绍第二种 Feign。

@SpringBootApplication  public class ServicesConsumerApplication {      @Bean      @LoadBalanced      public RestTemplate restTemplate() {          return new RestTemplate();      }      public static void main(String[] args) {          SpringApplication.run(ServicesConsumerApplication.class, args);      }  }

在 Controller 中的调用

@RestController  public class ConsumerController {      // services-provider 为服务 provider 的 application.name,负载均衡要求多个服务的 name 相同      private final String servicesUrl = "http://services-provider/";      private final RestTemplate restTemplate;        public ConsumerController(RestTemplate restTemplate) {          this.restTemplate = restTemplate;      }        @RequestMapping("/")      public String home() {          return restTemplate.getForObject(servicesUrl,String.class);      }  }

然后运行工程,可以发现两个服务都已经成功注册到注册中心。

有的博客中可能提到需要添加,@EnableDiscoveryClient 或 @EnableEurekaClient,而实际上,官方文档已经明确,只要 spring-cloud-starter-netflix-eureka-client 在 classpath 中,应用会自动注册到 Eureka Server 中去。

By having spring-cloud-starter-netflix-eureka-clienton the classpath, your application automatically registers with the Eureka Server. Configuration is required to locate the Eureka server.
https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.M1/

直接调用 localhost:8085,就显示 hello world,说明成功!如果不相信的话,可以起两个 provider 服务,然后分别输出不同的字符串来验证负载均衡是否成功。

2019-08-13 15:08:18.689  INFO 14100 --- [nio-8085-exec-4] c.n.l.DynamicServerListLoadBalancer: DynamicServerListLoadBalancer for client services-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=services-provider,current list of Servers=[192.168.140.1:8084]}

简化后的日志如上,可以看到,注解 @LoadBalanced 的负载均衡功能是通过 DynamicServerListLoadBalancer 来实现,这是一种默认的负载均衡策略。获取到服务列表后,通过 Round Robin 策略(服务按序轮询从 1 开始,直到 N)进行服务分发。

至此,本地的负载均衡服务就搭建完成了。

服务消费 Feign 与断路器 Hystrix

如果是初体验的话可以忽略这一节。添加这一节的目的是为了服务的完整性。在实际开发中,Feign 可能用到的更多,并且多会配合 Hystrix。

Feign 也是一种服务消费的方式,采用的是申明式的形式,使用起来更像是本地调用,它也是 Spring Cloud Netflix 中的一员。

Hystrix,简而言之就是一种断路器,负载均衡中如果有服务出现问题不可达后,通过配置 Hystrix,可以实现服务降级和断路的作用,这个功能 Eureka 默认是不提供的。因此,之前说了,为了负载均衡的完整性,需要添上它,否则,Ribbon 依然会将请求分发到有问题的服务上去。

那么要使用他们,首先需要添加依赖

<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>

在启动类上加上 @EnableFeignClients

调用的时候需要实现接口。

@FeignClient(value = "services-provider")  public interface IRestRemote {      @GetMapping("/")      String home();  }

最后在 Controller 中注入接口,直接调用就可以了。

@RestController  public class ConsumerController {      private final IRestRemote remote;        public ConsumerController(IRestRemote remote) {          this.remote = remote;      }        @RequestMapping("/")      public String home() {          return remote.home();      }

相对于 RestTemplate,Fegin 使用起来更简单,不需要去构造参数请求了,并且 Feign 底层集成了 Ribbon,也不需要显示添加 @LoadBalanced 注解了。同时 Feign 也可以直接使用下面要讲的 Hystrix。

Hystrix 的功能很多,远不止断路器一种,需要详细了解可看别的博客。这里就讲一下 Feign 如何集成 Hystrix 工作的。

下面先描述一下具体场景。假如现在有两个负载均衡服务,其中有一个挂了或出现异常了,如果没有断路器,进行服务分发的时候,仍然会分配到。如果开启 Hystrix,首先会进行服务降级(FallBack),也就是出现问题,执行默认的方法,并在满足一定的条件下,熔断该服务。

在原来 @FeignClient 的基础上添加 fallback 参数, 并实现降级服务。

@FeignClient(value = "services-provider",fallback = RestRemoteHystrix.class)  public interface IRestRemote {      @GetMapping("/")      String home();  }
@Component  public class RestRemoteHystrix implements IRestRemote {      @override      public String home() {          return "default";      }  }

最后,在配置文件中开启 Feign 的 Hystrix 开关。

feign:    hystrix:      enabled: true

下面就可以测试了,沿着之前的例子,分别开启两个服务,并输出不同的文本,当关闭一个服务后,再请求会发现,分配到关闭的那个服务时,会显示 “default”,请求多次后发现,该服务不可用,说明断路器配置成功!欲知更多,可以阅读参考文献 5-6。

特别注意

1.多节点的配置

如果不是单机,则需要修改部分字段,具体如下注释:

eureka:    instance:      prefer-ip-address: true     # 以 IP 的形式注册      # 主机的 ip 地址(同样考虑集群环境,一个节点可能会配置多个网段 ip,这里可指定具体的)      ip-address:      # 主机的 http 外网通信端口,该端口和 server.post 不同,比如外网需要访问某个集群节点,直接是无法访问 server.post 的,而是需要映射到另一端口。      # 这个字段就是配置映射到外网的端口号      non-secure-port:       # 默认是 hostname 开头的,修改成 ip 开头      instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}    client:      serviceUrl:        # 这里不是 localhost 了,而是注册中心的 ip:port        defaultZone: http://ip:8080/eureka/  server:    port: 8084  spring:    application:      name: services-provider    # 应用名称

2.高可用注册中心

本文上述搭建的是单个注册中心的例子,基本已经满足我目前的项目需求。但是在阅读别的博客中,发现真正的微服务架构,为了实现高可用,一般会有多个注册中心,并且相互注册形成。这里先简单做个记录。

3.更复杂,自定义的负载均衡规则。

目前,其实只是引入了 spring cloud 和 Eureka 的依赖就实现了简单的负载均衡。但仔细看 DynamicServerListLoadBalancer 类的位置,是在 Ribbon 下的。虽然没有显式去添加 Ribbon 的依赖包,但是实际上已经被包含进去了。那么 Ribbon 是什么?

Ribbon is a client-side load balancer that gives you a lot of control over the behavior of HTTP and TCP clients. Feign already uses Ribbon, so, if you use @FeignClient, this section also applies.

Ribbon 是 Spring Cloud 中的一个组件,主要是做客户端负载均衡的。不关注底层细节,可能上文搭建的服务它无关,实际上还是用到了。那么,如果需要自定义复杂均衡的规则,则需要通过配置 Ribbon 来实现。

Summary

本文主要是”拿来主义“的思想,直接用了 Spring Cloud 中的几个组件来组建一个负载均衡服务。为了能够更好地进行服务治理,还需按部就班先学习基本的原理概念,例如CAP理论等,在逐步学习 Cloud 中的组件,有一定的全局观。

注册中心 Eureka 本身满足的就是 AP,在生产环境中,为了保证服务的高可用,势必要有至少两个的注册中心。

Reference

  1. springcloud(二):注册中心 Eureka
  2. springcloud(三):服务提供与调用
  3. Spring Cloud Netflix
  4. SpringCloud – 负载均衡器 Ribbon
  5. Spring Cloud(四):服务容错保护 Hystrix【Finchley 版】
  6. Setup a Circuit Breaker with Hystrix, Feign Client and Spring Boot

Source Code