前面我们已经在K8S中部署了Nacos集群,并学习了服务注册和服务发现的基本概念。 本节进行一个实战练习,将两个基于Spring Boot的微服务集成到Nacos的服务注册和服务发现。

这两个Spring Boot微服务分别是订单服务order-svc和库存服务stock-svc,订单服务的下单接口中会调用库存服务的库存查询接口。 因为只是演示Nacos和Spring Cloud,所以这两个服务的逻辑十分简单,只会有类似helloworld的演示代码。

1.创建Spring Boot微服务

首先使用Spring initializr(https://start.spring.io/)分别创建order-svcstock-svc两个Spring Boot 的工程。

spring-boot-initializr.png

在Spring initializr的中定制的配置如下:

  • 选择的是Gradle构建的项目Gradle Project
  • Spring Boot的版本选择的是2.6.4(Spring Boot, Spring Cloud, Spring Cloud Alibaba三者之间有严格的版本关系,这里选择Spring Boot为2.6.4,就基本上决定了后边所能使用的Spring Cloud和Spring Cloud Alibaba的版本)
  • 前面说过这里创建的订单服务和库存服务只是用来演示框架的整合功能,因此只添加了Spring Web的依赖

order-svcstock-svc两个Spring Boot 工程创建成功后,接下来进入微服务代码开发阶段。

1.1 开发库存微服务

我们首先开发库存微服务stock-svc, 创建这个服务的application.yml如下:

1
2
3
4
5
6
server:
  port: 8080

spring:
  application:
    name: stock-svc

在stock-svc中创建一个Spring MVC的StockController,并创建查询库存的API接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.stock;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ThreadLocalRandom;

@RestController
public class StockController {

    @GetMapping("/stock")
    public Integer getStock() {
        return ThreadLocalRandom.current().nextInt(100);
    }
}

库存查询接口比较简单,是一个GET请求,不接收任何参数,直接返回剩余库存数量。具体的逻辑是随机返回一个100以内的随机数。

stock-svc开发完成后,启动这个服务,并使用curl命令测试如下:

1
2
3
4
5
6
7
8
curl 127.0.0.1:8080/stock
43

curl 127.0.0.1:8080/stock
15

curl 127.0.0.1:8080/stock
59

1.2 开发订单微服务

接下来开发order-svc,创建这个服务的application.yml如下:

1
2
3
4
5
6
server:
  port: 8081

spring:
  application:
    name: order-svc

在这个服务里创建一个创建订单的接口,是一个POST请求,这里只是演示,暂时不接收任何参数。在创建订单的逻辑里将调用库存微服务检查库存,并根据剩余库存来决定订单是否创建成功。

为了调用订单服务,这里我们选择使用Sring Webflux中的WebClient来发起http请求,先在项目的build.gradle中添加spring-boot-starter-webflux的依赖。

1
2
3
4
5
6
7
...
dependencies 
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
...

简单起见直接在Spring Boot的Application类里配置WebClient.Builder对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

@SpringBootApplication
public class OrderSvcApplication {


	@Bean
	public WebClient.Builder register() {
		return WebClient.builder();
	}

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

}

接下来创建OrderController,在里面添加创建订单的接口:

 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
package com.example.order;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;

@RestController
public class OrderController {

    @Autowired
    private WebClient.Builder webClientBuilder;

    @PostMapping("/order")
    public String createOrder() {
        // request stock-svc
       Integer stock = webClientBuilder.build().get()
               .uri("http://127.0.0.1:8080/stock")
               .retrieve().bodyToMono(Integer.class)
               .block();

       if (stock == null || stock < 30) {
           return "Not OK";
       }
       return "OK";
    }
}

创建订单的接口逻辑比较简单,使用WebClient调用库存服务,如果返回的库存大于30的话,直接返回订单创建成功的OK消息。

注意在使用WebClient调用库存服务时,库存服务的地址和端口写死为http://127.0.0.1:8080的,后边在将服务注册到Nacos并使用服务发现功能后,这里将不再写死服务的端口。

开发完成后,同时启动这两个服务,并使用curl命令调用创建订单服务测试如下:

1
2
3
4
5
6
curl --request POST 'http://localhost:8081/order' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

OK

2.自动将服务注册到Nacos

到现在为止,我们开发了两个基于Spring Boot的微服务,搭建起了从创建订单时从订单服务到库存服务的调用链路,但在这个调用链路中还没有服务注册中心参与其中。

库存服务作为下游服务需要将自己注册到Nacos,订单服务作为上游服务需要从Nacos中获取所有可供调用的下游库存服务实例,这个获取下游服务实例的过程就是服务发现。

2.1 添加依赖spring-cloud-starter-alibaba-nacos-discovery

为了在订单服务和库存服务这两个Spring Boot的项目中集成Nacos,需要添加spring-cloud-starter-alibaba-nacos-discovery的依赖。 Nacos Discovery是Spring Cloud Alibaba的一个组件,要引入Nacos Discovery依赖,需要先引入Spring Cloud和Spring Cloud Alibaba依赖版本管理的mavenBOM。

前面提到过Spring Boot, Spring Cloud, Spring Cloud Alibaba三者之间有严格的版本关系,在https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明 中给出了三者的兼容性矩阵。

从中我们可以找到:

  • Spring Boot 2.6.3
  • Spring Cloud 2021.0.1
  • Spring Cloud Alibaba 2021.0.1.0

前面在创建订单服务和库存服务时选择的是Spring Boot 2.6.4,小版本是兼容的,因此需要选择Spring Cloud 2021.0.1和Spring Cloud Alibaba 2021.0.1.0。

根据选择的版本,在两个项目的build.gradle加入mavenBOMspring-cloud-starter-alibaba-nacos-discovery:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...
dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:2021.0.1"
		mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:2021.0.1.0"
	}
}

dependencies {
	...
	implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
	...
}
...

2.2 Spring Cloud Alibaba Nacos服务自动注册

添加依赖完成后,需要配置服务端的自动注册。在早期的Spring Cloud版本中可能需要在Spring Boot的启动类上加上@EnableDiscoveryClient注解以开启服务注册和发现。 现在版本的Spring Cloud已经不再需要这一步。

受益于spring-cloud-starter-alibaba-nacos-discovery,我们只需要在spring boot的配置文件中做好配置,就可以开启Nacos的服务注册功能,都会在服务启动过程中自动装配好。

下面在Spring Boot的配置文件application.yml中添加Nacos的配置项,以库存服务stock-svc项目为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
server:
  port: 8080


spring:
  application:
    name: stock-svc
  cloud:
    nacos:
      discovery:
        server-addr: https://nacos.youcomany.com:443
        service: stock-svc
        heart-beat-interval: 5000
        heart-beat-timeout: 15000
        namespace: dev
        cluster-name: clusert1
        group: b2bGroup
        register-enabled: true

对nacos的各个配置简单介绍一下:

  • spring.cloud.nacos.config.server-addr: Nacos的服务注册地址,可以配置多个,逗号分隔。这里使用Nacos被部署到了K8S集群中,并使用Ingress以域名nacos.youcomany.com形式暴露出来,可以让本地开发机连上。
  • spring.cloud.nacos.discovery.service: 是给当前服务命名,默认值是spring.application.name。如果指定了spring.application.name的话,可以不配置。
  • spring.cloud.nacos.discovery.heart-beat-interval: nacos客户端向服务端发送心跳的时间间隔,单位为毫秒。
  • spring.cloud.nacos.discovery.heart-beat-timeout: 服务端未收到到客户端心跳请求的时间间隔,超过这个时间就会将服务设置为不健康,单位毫秒
  • spring.cloud.nacos.discovery.namespace: Nacos中创建的命名空间ID, 通过不同的命名空间来做不同环境的隔离或租户的隔离。例如这里配置dev表示开发环境的命名空间。
  • spring.cloud.nacos.discovery.cluster-name: 在每个服务实例启动时,可以设置服务实例所属的集群,这样一个服务的多个实例可以分属于不同的集群。在集群这个层面上可以配置健康检查模式、元数据、同步机制。
  • spring.cloud.nacos.discovery.group: Goup是一个命令空间下的分组,不同分组之间的微服务是相互隔离的,无法相互调用的。
  • spring.cloud.nacos.discovery.register-enabled: 是否向Nacos注册中心注册,默认为true

照葫芦画瓢,参考上面库存服务的配置,配置好订单服务的application.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
spring:
  application:
    name: order-svc
  cloud:
    nacos:
      discovery:
        server-addr: https://nacos.youcomany.com:443
        service: order-svc
        heart-beat-interval: 5000
        heart-beat-timeout: 15000
        namespace: dev
        cluster-name: clusert1
        group: b2bGroup
        register-enabled: true

配置完成后,登录到Nacos的Web控制台,创建一个名称为"开发环境",ID为"dev"的命名空间。

nacos-namespaces.png

命名空间创建好,就可以启动订单服务和库存服务了。如果启动正常,就可以在Nacos控制台的服务列表页面看到服务已经注册进去了。

nacos-service-list.png

2.3 通过Nacos服务发现机制调用服务

上面已经完成了服务的注册,接下来,还需要调整一下上游订单服务的代码,为WebClient.Build添加@org.springframework.cloud.client.loadbalancer.LoadBalanced注解。

需要在build.gradle中引入spring-cloud-starter-loadbalancer依赖。

1
2
3
4
5
6
7
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
	implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

spring-cloud-starter-loadbalancer是Spring Cloud全家桶中的Loadbalancer组件,替代了以前Netflix Ribbon组件。

WebClient.Build的Bean配置中加上LoadBalanced注解,将会自动为其构造器注入实现负载均衡功能的Filter。

1
2
3
4
5
	@LoadBalanced
	@Bean
	public WebClient.Builder register() {
		return WebClient.builder();
	}

配置好Webclient后,在创建订单接口使用WebClient调用库存服务的代码中,将写死的服务地址和端口修改为Nacos中注册的服务名称:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    @PostMapping("/order")
    public String createOrder() {
        // request stock-svc
       Integer stock = webClientBuilder.build().get()
               .uri("http://stock-svc/stock")
               .retrieve().bodyToMono(Integer.class)
               .block();

       if (stock == null || stock < 30) {
           return "Not OK";
       }
       return "OK";
    }

修改完成后,重启订单服务。使用curl调用创建订单接口,确认从订单服务到库存服务的调用链路是通的。

1
2
3
4
5
6
curl --request POST 'http://localhost:8081/order' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

OK

关于Nacos的服务发现功能,服务中集成的Nacos Client是通过轮询机制从Nacos Server获取服务实例的注册信息,采用的是Pull的形式。

参考