服务发现、负载均衡

服务发现

Service Discovery,本质也是一个服务(与配置服务类似)

好处

  • 快速水平伸缩,而不是垂直伸缩。不影响客户端

    水平伸缩:服务实例数动态增加或减少

  • 提高应用程序的弹性

    一个服务(发生故障)挂了,服务中心会将其删除,同时也会通知客户端

Eureka服务发现引擎

  • 服务端向服务中心注册自己的信息:服务名、IP地址、端口号;

  • 客户端向服务中心提供需要访问的服务名,服务中心向其返回实例有哪些、端口以及IP

image-20220417183413162

Ribbon,客户端负载均衡

  • 主要提供客户侧的软件负载均衡算法

  • 服务端启动后会向服务中心注册

  • 客户端访问服务时,Ribbon按照一定策略返回服务的实例

    • 轮询策略(按照顺序)
    • 随机策略

    访问时获取的服务信息也有缓存

image-20220417183804696

服务调用关系

image-20220417184709504

注册服务

eurekasvr

  1. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    <!-- 服务端 -->
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
    </dependencies>
  2. 启动类加上@EnableEurekaServer注解

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaServerApplication.class, args);
    }
    }
  3. application.yml配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server:
    port: 8761

    eureka:
    client:
    registerWithEureka: false # 当前服务是否要注册到服务中心去
    fetchRegistry: false # 是否要(定时?)更新服务信息
    server:
    waitTimeInMsWhenSyncEmpty: 5 # 服务注册后等待多久才对外提供服务信息(单位:毫秒)
    serviceUrl: # 当前服务的url
    defaultZone: http://localhost:8761

confsvr

application.yml中添加:

1
spring.cloud.config.discovery.enabled=true

organizationservice

  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
  2. 启动类添加@EnableEurekaClient注解

    意味着当前服务启动时,需要与EurekaSever进行交互,将当前服务的信息登记到EurekaServer中去

  3. application.yml中配置需要登记的信息

    1
    2
    3
    4
    5
    6
    7
    8
    eureka:
    instance:
    preferIpAddress: true # 记住ip地址而不是机器的机器名
    client:
    registerWithEureka: true # 服务信息需要注册到服务中心去
    fetchRegistry: true # 定期刷新
    serviceUrl: # 服务中心的Url
    defaultZone: http://localhost:8761/eureka/

查找和调用服务(客户端如何调用服务端的接口)

  • 第三方库:Ribbon,本地缓存,本地负载均衡
  • 三种方式
    1. Spring DiscoveryClient(不建议)
    2. 使用支持Ribbon的RestTemplate
    3. 使用Netflix Feign

licensingservice

  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
  2. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    eureka:
    instance:
    preferIpAddress: true
    client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
    defaultZone: http://localhost:8761/eureka/

注:以下代码都是在licensingservice中进行编写

Spring DiscoveryClient(比较原始,不结合Ribbon)

  1. 启动类添加注解

    • @EnableDiscoveryClient 不指定实现工具
    • @EnableEurekaClient 指定实现工具为Eureka
    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    // @EnableEurekaClient // 二选一
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }
  2. 创建类OrganizationDiscoveryClient可以在client包下)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.cloud.client.ServiceInstance;

    import org.springframework.web.client.RestTemplate; 、// springweb里的,和服务发现没关系

    @Component
    public class OrganizationDiscoveryClient {

    // 注入对象
    @Autowired
    private DiscoveryClient discoveryClient;

    // 获取实例
    public Organization getOrganization(String organizationId) {
    //
    RestTemplate restTemplate = new RestTemplate();
    // 指定服务名,获取实例(可能有多个)
    List<ServiceInstance> instances = discoveryClient.getInstances("organizationservice");

    if (instances.size() == 0) return null;
    // 组装出实例的uri
    String serviceUri = String.format("%s/v1/organizations/%s", instances.get(0).getUri().toString(), organizationId);
    System.out.println("!!!! SERVICE URI: " + serviceUri);

    // 给organization发送一个get请求(url见上一行代码)
    ResponseEntity<Organization> restExchange =
    restTemplate.exchange(
    serviceUri,
    HttpMethod.GET,
    null, Organization.class, organizationId);

    return restExchange.getBody();
    }
    }

支持Ribbon的RestTemplate

  1. 启动类无需添加注解,但需要注入bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import org.springframework.cloud.client.loadbalancer.LoadBalanced;

    @SpringBootApplication
    public class Application {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
    return new RestTemplate();
    }

    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }

    @Bean
    public IRule ribbonRule() {
    return new RandomRule();
    }
    }
  2. 创建client类

    • 可以发现这里没有获取实例(也就是没有IP地址和端口号),而是直接使用了服务名,原理就是在启动类中对restTemplate进行了拦截(基于Ribbon的注解 @LoadBalanced 实现的效果)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Component
    public class OrganizationRestTemplateClient {
    @Autowired
    RestTemplate restTemplate;

    public Organization getOrganization(String organizationId){
    ResponseEntity<Organization> restExchange =
    restTemplate.exchange(
    "http://organizationservice/v1/organizations/{organizationId}",
    HttpMethod.GET,
    null, Organization.class, organizationId);

    return restExchange.getBody();
    }
    }

Netflix Feign(最简便)

  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
  2. 启动类添加注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.springframework.cloud.netflix.feign.EnableFeignClients;

    @SpringBootApplication
    @EnableFeignClients
    public class Application {

    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }

    }
  3. 创建client接口并添加注解@FeignClient("organizationservice")

    1
    2
    3
    4
    5
    6
    7
    8
    @FeignClient("organizationservice")
    public interface OrganizationFeignClient {
    @RequestMapping(
    method= RequestMethod.GET,
    value="/v1/organizations/{organizationId}",
    consumes="application/json") // 需要返回的数据格式
    Organization getOrganization(@PathVariable("organizationId") String organizationId);
    }

    使用时直接将接口的对象注入@Autowired

部署脚本

docker-compose的作用是启动所有的服务,所以在此之前要将所有的服务进行打包镜像

=> 在每个子项目的根目录下运行mvn clean package docker:build

位置:总项目根目录/docker/default/docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
version: '2'
services:
eurekaserver:
image: johncarnell/tmx-eurekasvr:section13
ports:
- "8761:8761"
configserver:
image: johncarnell/tmx-confsvr:section13
ports:
- "8888:8888"
environment:
EUREKASERVER_URI: "http://eurekaserver:8761/eureka/"
EUREKASERVER_PORT: "8761"
ENCRYPT_KEY: "IMSYMMETRIC"
database:
image: postgres:9.5
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=p0stgr@s
- POSTGRES_DB=eagle_eye_local
licensingservice:
image: johncarnell/tmx-licensing-service:section13
ports:
- "8080:8080"
environment:
PROFILE: "default"
SERVER_PORT: "8080"
CONFIGSERVER_URI: "http://configserver:8888"
EUREKASERVER_URI: "http://eurekaserver:8761/eureka/"
EUREKASERVER_PORT: "8761" # 这三个端口号都用于启动前确认
CONFIGSERVER_PORT: "8888"
DATABASESERVER_PORT: "5432"
ENCRYPT_KEY: "IMSYMMETRIC"
organizationservice:
image: johncarnell/tmx-organization-service:section13
# ports:
# - "8085:8085"
environment:
PROFILE: "default"
SERVER_PORT: "8085"
CONFIGSERVER_URI: "http://configserver:8888"
EUREKASERVER_URI: "http://eurekaserver:8761/eureka/"
EUREKASERVER_PORT: "8761"
CONFIGSERVER_PORT: "8888"
DATABASESERVER_PORT: "5432"
ENCRYPT_KEY: "IMSYMMETRIC"

Eureka使用

  1. GET请求http://localhost:8761/eureka/apps/服务名
    • 未在HEADERS中指定返回格式,则默认是HTML格式
    • 若指定格式为application/json,则为JSON格式
  2. 也可以直接在浏览器中打开http://localhost:8761

负载均衡

  1. 在通过docker-compose启动实例时,改成如下命令:

    1
    docker-compose up --scale organizationservice=3 # 表示启动三个organizationservice实例
  2. 策略自定义:对RestTemplate和Feign都有用

    organizationservice的启动类中,添加代码:

    1
    2
    3
    4
    @Bean
    public IRule ribbonRule() {
    return new RandomRule(); // 随机策略,现有最好的策略
    }

策略参考

image-20220417215440312

  1. RoundRobinRule 轮询
  2. RandomRule 随机