第二章-Nacos服务注册中心

第二章 Nacos 服务注册与发现

在上一章中实现了微服务拆分,并且通过HTTP请求(RestTemplate)实现了跨微服务的远程调用,不过这种手动发起HTTP请求的方式存在问题:

@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    //private final IItemService itemService;
    private final RestTemplate restTemplate;

    ...

    private void handleCartItems(List<CartVO> vos) {
        // 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        String itemUrl = "http://localhost:8081/items?ids={ids}";
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                itemUrl,//请求路径
                HttpMethod.GET,//请求方式
                null,//请求实体
                new ParameterizedTypeReference<List<ItemDTO>>() {
                },//响应数据类型
                Map.of("ids", CollUtils.join(itemIds, ","))//请求参数
        );
        List<ItemDTO> items = null;
        if (response.getStatusCode().is2xxSuccessful()) {
            items = response.getBody();
        }
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream()
								        .collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 4.写入vo
        for (CartVO v : vos) {
            ItemDTO item = itemMap.get(v.getItemId());
            if (item == null) {
                continue;
            }
            v.setNewPrice(item.getPrice());
            v.setStatus(item.getStatus());
            v.setStock(item.getStock());
        }
    }
   ...
}

在查询商品的时候,URL地址是写死的:String itemUrl = "http://localhost:8081/items?ids={ids}

如果商品服务是热点接口,为了应对更高的并发/避免单点故障,进行多实例部署:

image-20240105171039026

每一个item-service实例的IP或者端口号是不同的,此时就会出现问题:

  • item-service多实例,服务调用者cart-service如何知道每一个实例的地址?

  • HTTP请求需要指定URL地址,此时服务调用者cart-service到底调用哪一个实例?

  • 如果某一个item-service实例宕机,cart-service仍然在调用怎么办?

  • 并发量过高,item-service临时部署多台,cart-service如何知道最新的实例地址?

为了解决这些问题,必须引入服务注册中心的概念。

注册中心原理

在微服务的远程调用中有两个角色:

  • 服务提供者:item-service
  • 服务调用者:cart-service

在大型微服务项目中,服务提供者的数量会非常多,为了管理这些微服务引入了注册中心。注册中心、服务提供者、服务消费者之间的关系:

image.png | 550

流程如下:

  • 服务启动时将自己的服务信息(服务名称、IP、端口号、版本)写入服务注册中心的服务注册表中,服务注册表保存了所有的服务

任何一个微服务都可能是调用者/提供者

  • 服务调用者需要调用微服务时,首先在注册中心订阅需要的服务,将服务注册表下载到本地

  • 根据调用的微服务名称在服务注册表中获取对应的服务提供者集合

  • 调用者自身对服务提供者负载均衡,根据调用者本地的负载均衡策略挑选一个实例进行调用

服务提供者实例宕机或启动新实例:

  • 服务提供者定期向注册中心发送请求,报告自己的健康状态(心跳请求)

  • 注册中心如果长时间收不到服务提供者的心跳请求,认为该实例宕机,将其从服务注册表中删除

  • 每当服务有新实例启动时,会发送注册请求,信息会被记录在注册中心的服务注册表中。

  • 注册中心的服务注册表变更后,主动通知微服务,更新本地的服务注册表

注册中心产品

目前开源的注册中心有很多,比较常见的有:

  • Eureka:Netflix公司出品,目前集成在Spring Cloud应用中,一般用于Java语言
  • Nacos:Alibaba公司出品,目前被集成在Spring Cloud Alibaba中,一般用于Java语言
  • Consul:HashiCorp公司出品,目前集成在Spring Cloud中,不限制微服务语言

这三种注册中心都遵循Spring Cloud的API规范,在业务开发上没有太大差异,Spring Cloud Alibaba使用的注册中心是Nacos

Nacos简介

Nacos的两个重要功能:服务发现与配置管理

image-20240105171935295 | 650

云原生:微服务 + DevOps + CI/CD + 容器化

Nacos架构:

1561217892717-1418fb9b-7faa-4324-87b9-f1740329f564.jpeg

Nacos client:Java语言编写的客户端

  • sidecar:多语言异构模块,通过这个模块可以让其他语言使用nacos

  • Name Server:命名服务,支持的服务:dns、vip、address-server

  • OpenAPI:客户端(Provider/Consumer)通过API接口就可以访问服务端(Nacos)的 Naming Service(服务发现)和 Config Service(配置管理),客户端和服务端通信协议支持http/dns/udp/tls

  • Consistency Protocol:集群之间的一致性协议,priv-raft(Naming Service)、sync renew、rdbms based(Config Service)

Nacos下载与配置

Windows环境

在Nacos官网找到最新稳定版本 ,目前的Spring Cloud Alibaba版本是 2022.0.0.0-RC2,对应的Nacos版本是2.2.1。

image.png

对于2.2.0.1和2.2.1版本,需要修改conf下application.properties文件:

设置其中的nacos.core.auth.plugin.nacos.token.secret.key值(https://nacos.io/docs/v2/guide/user/auth/)

参数名 默认值 启止版本 说明
nacos.core.auth.enabled false 1.2.0 ~ latest 是否开启鉴权功能
nacos.core.auth.system.type nacos 1.2.0 ~ latest 鉴权类型
nacos.core.auth.plugin.nacos.token.secret.key SecretKey012345678901234567890123456789012345678901234567890123456789(2.2.0.1后无默认值) 2.1.0 ~ latest 默认鉴权插件用于生成用户登陆临时accessToken所使用的密钥,使用默认值有安全风险
nacos.core.auth.plugin.nacos.token.expire.seconds 18000 2.1.0 ~ latest 用户登陆临时accessToken的过期时间
nacos.core.auth.enable.userAgentAuthWhite false 1.4.1 ~ latest 是否使用useragent白名单,主要用于适配老版本升级,置为true时有安全风险
nacos.core.auth.server.identity.key serverIdentity(2.2.1后无默认值) 1.4.1 ~ latest 用于替换useragent白名单的身份识别key,使用默认值有安全风险
nacos.core.auth.server.identity.value security(2.2.1后无默认值) 1.4.1 ~ latest 用于替换useragent白名单的身份识别value,使用默认值有安全风险
nacos.core.auth.default.token.secret.key SecretKey012345678901234567890123456789012345678901234567890123456789 1.2.0 ~ 2.0.4 同nacos.core.auth.plugin.nacos.token.secret.key
nacos.core.auth.default.token.expire.seconds 18000

修改配置文件:

image.png

启动Nacos:

startup.cmd -m standalone

默认是集群启动,需要指定为单机启动

生态融合

参照Nacos官网-生态融合 -> 服务发现

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

官网说明的使用方式:

server.port=8081
spring.application.name=nacos-producer
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
management.endpoints.web.exposure.include=*

要求指定服务名称、nacos地址

注册provider

spring:  
  # 微服务名称  
  application:  
    name: depart-provider  
  #nacos注册中心地址  
  cloud:  
    nacos:  
	  discovery:
	      server-addr: 127.0.0.1:8848  
	      username: nacos  
	      password: nacos

在2.2.1版本中,不加username和password会报错:user not found,某些版本加了反而可能会报错

在2.1.0版本中,不需要指定

注册consumer

spring:  
  application:  
    name: depart-consumer  
  cloud:  
    nacos:  
      server-addr: 127.0.0.1:8848  
      username: nacos  
      password: nacos

注册完毕后在nacos控制台 127.0.0.1/nacos 中就可以看到这两个服务

改造案例一:简单consumer、provider

在案例一中,我们进行远程调用使用的是RestTemplate的简单直连方式:

@RestController  
@RequestMapping("/consumer/depart")  
public class DepartController {  
    @Resource  
    private RestTemplate restTemplate;  
  
    public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart";  
  
    @PostMapping  
    public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart){  
  
        ResponseResult<Boolean> responseResult = restTemplate.postForObject(SERVICE_PROVIDER, depart,
			         ResponseResult.class);  
        return responseResult;  
    }  
  
    @GetMapping("/{id}")  
    public ResponseResult<Depart> getHandler(@PathVariable Long id){  
        String url = SERVICE_PROVIDER + "/" + id;  
        ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);  
        return result;  
    }  
    @GetMapping("/list")  
    public ResponseResult<List<Depart>> listHandler(){  
        String url = SERVICE_PROVIDER + "/list";  
        ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);  
        return result;  
    }  
}

在调用时使用的地址:public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart"

使用微服务方式:

@RestController  
@RequestMapping("/consumer/depart")  
public class DepartController {  
    @Resource  
    private RestTemplate restTemplate;  
  
    //直连方式  
    //public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart";  
  
    //微服务方式  
    public static final String SERVICE_PROVIDER = "http://depart-provider/provider/depart";  
  
  
    @PostMapping  
    public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart){  
  
        ResponseResult<Boolean> responseResult = restTemplate.postForObject(SERVICE_PROVIDER, depart, ResponseResult.class);  
        return responseResult;  
    }  
  
    @GetMapping("/{id}")  
    public ResponseResult<Depart> getHandler(@PathVariable Long id){  
        String url = SERVICE_PROVIDER + "/" + id;  
        ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);  
        return result;  
    }  
    @GetMapping("/list")  
    public ResponseResult<List<Depart>> listHandler(){  
        String url = SERVICE_PROVIDER + "/list";  
        ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);  
        return result;  
    }  
}

并且要指明RestTemplate以负载均衡方式调用:

@Configuration  
public class RestTemplateConfig {  
  
    //以负载均衡的方式调用服务  
    @LoadBalanced  
    @Bean    
    public RestTemplate restTemplate(){  
        return new RestTemplate();  
    }  
}

但是这样启动会报错:

java.net.UnknownHostException: depart-provider

因为新版的Spring Cloud不再支持Ribbon了,老版本Spring Cloud默认使用Ribbon做负载均衡,需要增加依赖:

<!--负载均衡-->  
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>  
</dependency>

获取服务注册表信息

通过DiscoveryClient获取到服务注册表信息

@Autowired  
private DiscoveryClient discoveryClient;  
  
@GetMapping("/discovery")  
public ResponseResult<List<String>> discoveryHandler(){  
    //获取注册中心所有服务名称  
    List<String> services = discoveryClient.getServices();  
    for (String service : services) {  
        //获取指定服务名称对应的所有服务实例  
        List<ServiceInstance> instances = discoveryClient.getInstances(service);  
        for (ServiceInstance instance : instances) {  
            //获取实例对应的信息  
            HashMap<String, Object> hashMap = new HashMap<>() {{  
                put("serviceName",service);  
                put("serviceID",instance.getServiceId());  
                put("serviceHost",instance.getHost());  
                put("servicePort",instance.getPort());  
                put("serviceURI",instance.getUri());  
            }};  
  
            System.out.println(hashMap);  
        }  
    }  
    return ResponseResult.success(services);  
}

请求得到的服务名称列表:

{

    "code": 200,

    "data": [

        "depart-consumer",

        "depart-provider"

    ],

    "message": null

}

服务对应的实例信息:

{
	serviceHost=192.168.11.1, 
	serviceURI= http://192.168.11.1:8081 , 
	servicePort=8081, 
	serviceName=depart-provider, 
	serviceID=depart-provider
}

改造案例二的RestTemplate调用方式

在案例二中,发起远程调用:

    //微服务方式  
    public static final String SERVICE_PROVIDER = "http://depart-provider/provider/depart";  

这种方式是通过RestTemplate自己的负载均衡策略调用,常用的负载均衡算法有:

  • 随机
  • 轮询
  • IP的hash
  • LRU

我们可以根据需要调用服务的名称通过DiscoveryClient获取到对应的服务实例列表,自定义负载均衡策略,获取到URL进行访问

@Autowired  
private DiscoveryClient discoveryClient;  
  
private String getLoadBalancedServerAddress() {  
    List<ServiceInstance> instances = discoveryClient.getInstances("depart-provider");  
    Collections.shuffle(instances);  
    String URL = instances.get(0).getUri().toString() + "/provider/depart";  
    System.out.println(URL);  
    return URL;  
}  
  
  
@PostMapping  
public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart) {  
  
    ResponseResult<Boolean> responseResult = 
			restTemplate.postForObject(getLoadBalancedServerAddress(), depart, ResponseResult.class);  
    return responseResult;  
}  
  
  
@GetMapping("/{id}")  
public ResponseResult<Depart> getHandler(@PathVariable Long id) {  
    String url = getLoadBalancedServerAddress() + "/" + id;  
    ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);  
    return result;  
}  
  
@GetMapping("/list")  
public ResponseResult<List<Depart>> listHandler() {  
    String url = getLoadBalancedServerAddress() + "/list";  
    ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);  
    return result;  
}

注册表缓存

如果此时停止Nacos,consumer还是可以正常访问provider的。

服务启动后,发生调用时会自动从Nacos注册中心下载并缓存注册表到本地。所以,即使Nacos宕机,consumer仍然可以调用provider,只是此时不能有新服务进行注册了,服务缓存中的注册表信息无法更新。

但是如果服务启动后,没有发生调用Nacos就宕机,consumer就获取不到服务注册表信息,自然也无法调用provider

临时实例与持久实例

临时实例和持久实例的存储位置和健康检测机制是不同的:

  • 临时实例:默认情况下,仅会注册在Nacos内存,不会持久化到Nacos磁盘,其健康检测机制为Client模式,即Client主动向Server上报健康状态。默认心跳间隔为5秒,在15秒内Server未收到Client心跳,就会将其标记为 不健康 状态;30秒内收到了Client心跳,则重新恢复到 健康 状态,否则将Client从Server端内存清除。

  • 持久实例:服务实例不仅会注册到Nacos内存,也会持久化到Nacos磁盘。健康检测机制为Server模式,Server主动检测Client的健康状态,默认20s检测一次,检测失败后标记为 不健康 状态,但不会被清除,因为这是持久化到磁盘的。

大多数情况下都是临时实例,因为互联网项目存在突发流量暴增的情况,Alibaba是云原生的,可以充分利用云端的弹性,流量下降就清除临时实例。持久实例销毁是很麻烦的。

创建持久实例

默认的实例是临时实例(ephemeral为true),设置为true后尝试重启:

spring:  
  # 微服务名称  
  application:  
    name: depart-provider  
  #nacos注册中心地址  
  cloud:  
    nacos:  
      discovery:  
        server-addr: 127.0.0.1:8848  
        username: nacos  
        password: nacos  
        # 注册为持久实例
        ephemeral: false

重启:

Caused by: com.alibaba.nacos.api.exception.NacosException: 
failed to req API:/nacos/v1/ns/instance after all servers([127.0.0.1:8848]) tried: caused: errCode: 400, errMsg: Current service DEFAULT_GROUP@@depart-provider is ephemeral service, can't register persistent instance. ;

临时实例depart-provider不能被注册为持久实例,因为实例一旦被注册过为临时实例,就不能再次注册为持久实例了。

注册为持久实例:更改spring.application.name

spring:  
  # 微服务名称  
  application:  
    name: depart-provider-persistent  
  #nacos注册中心地址  
  cloud:  
    nacos:  
      discovery:  
        server-addr: 127.0.0.1:8848  
        username: nacos  
        password: nacos  
        ephemeral: false

image.png

此时depart-provider-persistent就被注册为持久实例

注销持久实例

此时是无法删除临时实例和持久实例的:

image.png

停机后:

image.png

临时实例下线,持久实例还是不能删除。

删除持久实例需要参照Nacos官网-OpenAPI指南

请求方式:

DELETE

Content-Type:application/x-www-form-urlencoded

请求地址:

/nacos/v2/ns/instance

请求Body:

参数名 参数类型 是否必填 描述说明
namespaceId String 否 命名空间Id,默认为public
groupName String 否 分组名,默认为DEFAULT_GROUP
serviceName String 是 服务名
ip String 是 IP地址
port int 是 端口号
clusterName String 否 集群名称,默认为DEFAULT
healthy boolean 否 是否只查找健康实例,默认为true
weight double 否 实例权重,默认为1.0
enabled boolean 否 是否可用,默认为true
metadata JSON格式String 否 实例元数据
ephemeral boolean 否 是否为临时实例

返回值

参数名 参数类型 描述
data boolean 是否执行成功

请求实例

curl -d 'serviceName=test_service' \
  -d 'ip=127.0.0.1' \
  -d 'port=8090' \
  -d 'weight=0.9' \
  -d 'ephemeral=true' \
  -X DELETE 'http://127.0.0.1:8848/nacos/v2/ns/instance'

IP和端口号在Nacos控制台可以看到,ephemeral为false:

curl -d 'serviceName=depart-provider-persistent' \
  -d 'ip=192.168.11.1' \
  -d 'port=8081' \
  -d 'ephemeral=false' \
  -d 'username=nacos' \
  -d 'password=nacos' \
  -X DELETE 'http://127.0.0.1:8848/nacos/v2/ns/instance'
http://localhost:8848/nacos/v2/ns/instance?serviceName=serviceName=depart-provider-persistent&ip=192.168.11.1&port=8081&weight=1&groupName=DEFAULT_GROUP&namespaceId=public&ephemeral=false&clusterName=DEFAULT&username=nacos&password=nacos

外置数据持久化

在启动Nacos时,控制台输出:

Nacos started successfully in stand alone mode. use embedded storage

使用了内嵌的数据库,在nacos/data/derby-data下可以看到。

Nacos官网-单机模式支持mysql中说明,在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:

  • 1.安装数据库,版本要求:5.6.5+

  • 2.初始化mysql数据库,数据库初始化文件:mysql-schema.sql

  • 3.修改nacos/conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。

spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow

再次以单机模式启动nacos,nacos将所有写入derby的数据都写入了mysql

在Nacos架构图中:

1561217892717-1418fb9b-7faa-4324-87b9-f1740329f564.jpeg

mysql-schema.sql

该sql脚本在nacos\conf\mysql-schema.sql位置。其中只有建表语句,没有创建数据库语句

其中建议数据库名为 nacos_config

运行脚本,创建了12张表。

修改application.properties

修改nacos/conf/application.properties:

image.png

启动nacos

控制台提示:

Nacos started successfully in stand alone mode. use external storage

Nacos集群

Nacos单节点宕机会导致系统故障,创建Nacos集群。

配置Nacos集群

复制nacos文件夹到nacos-cluster,在该文件夹下以集群方式启动nacos。

nacos8849的配置

修改conf/cluster.conf:

#it is ip
#example
192.168.11.1:8849
192.168.11.1:8851
192.168.11.1:8853

注意:此处必须填写真实ip地址,并且端口号必须间隔一个

同时修改conf/application.properties的端口号:

server.port=8849

nacos8851的配置

复制nacos8849文件夹,改名为nacos8851,只需要修改application.properties的端口号

server.port=8851

nacos8853的配置

和上文相同

启动nacos集群

在上文我们的启动是通过命令:startup.cmd -m standalone 因为我们是单机启动,而nacos默认以集群方式启动。

此时我们配置了nacos集群,直接双击启动即可。

Nacos started successfully in cluster mode. use external storage

image.png

连接Nacos集群

修改provider/consumer的application.yml配置文件:

# 微服务名称  
application:  
  name: depart-provider  
#nacos注册中心地址  
cloud:  
  nacos:  
    discovery:  
      server-addr: 127.0.0.1:8851,127.0.0.1:8853  
      username: nacos  
      password: nacos

此时在8851和8853控制台都能看到相同的内容:

image.png

但是这样做实际上是不安全的,nacos节点的地址都暴露给了客户端,可以使用nginx进行反向代理。

Nacos服务隔离

数据模型

Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。

1561217857314-95ab332c-acfb-40b2-957a-aae26c2b5d71.jpeg | 450

Nacos中的服务是由三元组唯一确定的:namespace、group、serviceName。

namespace和group的作用是相同的,用于划分不同的区域范围,隔离服务。不同的是,namespace的范围更大,不同的namespace可以包含相同的group,不同的group可以包含相同的service。

namespace默认是public(空字符串),group默认是DEFAULT_GROUP

服务隔离

服务隔离:微服务只能在同一个三元组之间互相调用

启动三个 02-provider-8081实例,不同的是namespace、group、port:

  • public + DEFAULT_GROUP + 8081
  • public + MY_GROUP + 8082
  • hello + MY_GROUP + 8083

自定义hello命名空间

image.png

配置服务注册的namespace、group

spring:  
  # 微服务名称  
  application:  
    name: depart-provider  
  #nacos注册中心地址  
  cloud:  
    nacos:  
      discovery:  
        server-addr: 127.0.0.1:8848  
        username: nacos  
        password: nacos  
        # 可以在此处配置namespace和group
        namespace:  
        group:

使用行内参数的方式启动3个服务:

  1. 默认启动的就是 public DEFAULT_GROUP 8081

  2. 启动public + MY_GROUP + 8083

-Dserver.port=8083 -Dspring.cloud.nacos.discovery.group=MY_GROUP

 public registering service depart-provider,nacos registry, MY_GROUP depart-provider 192.168.11.1:8083 register finished
  1. 启动hello + MY_GROUP + 8085
-Dserver.port=8085 -Dspring.cloud.nacos.discovery.group=MY_GROUP -Dspring.cloud.nacos.discovery.namespace=hello

hello registering service depart-provider,nacos registry, MY_GROUP depart-provider 192.168.11.1:8085 register finished

虽然不会报错,但是在nacos控制台是看不到这个服务的,需要指定为namespace的ID:

-Dserver.port=8085  
-Dspring.cloud.nacos.discovery.group=MY_GROUP  
-Dspring.cloud.nacos.discovery.namespace=679c3195-e967-47f7-9925-3c1aa6165a03

679c3195-e967-47f7-9925-3c1aa6165a03 registering service depart-provider
nacos registry, MY_GROUP depart-provider 192.168.11.1:8085 register finished

打开控制台就能看到三个provider实例,启动consumer,consumer当前的配置:

# consumer的配置文件
cloud:  
  nacos:  
    server-addr: 127.0.0.1:8848  
    username: nacos  
    password: nacos

consumer进行接口调用,当前的consumer在public DEFAULT_GROUP下,只能调用相同三元组中的服务,也就是8081

posted @ 2024-03-14 20:34  EUNEIR  阅读(279)  评论(0)    收藏  举报