SpringCloud Alibaba 基于 Nacos 实现服务注册 与 服务发现

  • 二、Nacos 实现服务注册与发现

2.1 Nacos 产生背景
  • 2.1.1 传统 RPC 远程调用存在哪些问题?
  • 2.1.2 传统的服务注册中心实现方式
  • 2.1.3 注册中心功能
  • 2.2 服务注册实现原理
  • 2.2.1 服务注册相关名词
  • 2.2.2 服务注册原理实现
  • 2.3 Nacos 的基本介绍
  • 2.3.1 配置 nacos
  • 2.3.2 编写 nacos 服务注册
  • 2.3.3 使用 DiscoverClient 实现 RPC 远程调用
  • 2.3.4 使用 rest 实现 RPC 远程调用
  • 2.4 手写负载均衡轮训算法
  • 2.5 实现服务动态下线
  • 2.6 Nacos 相关面试问题

Author: Gorit

Date:2020年12月

Refer:阿里云大学

2021年发表博文: 7/50

二、Nacos 实现服务注册与发现

2.1 Nacos 产生背景

Nacos 分布式注册与发现功能 | 分布式配置中心

RPC 远程调用框架:HttpClient、groc、dubbo、rest、openfeign

2.1.1 传统 RPC 远程调用存在哪些问题?

  1. 超时问题
  2. 安全的问题
  3. 服务与服务之间 URL 地址管理

在我们的微服务架构通讯,服务之间依赖关系非常大,如果通过传统的方式管理我们服务的 URL 地址的情况下,一旦地址发生变化的情况下,还需要人工修改 rpc 远程调用地址。

在我们的微服务架构通讯,服务之间依赖关系非常大,每个服务的urL管理地址非常复杂,所在这时候我们采用服务 URL 治理技术,治理的技术可以实现对我们的整个实现动态服务着注册与发现、本地负载均衡、容错等。

2.1.2 传统的服务注册中心实现方式

RPC 远程调用中,地址中 域名和端口号 / 调用方法的名称

  • 域名和端口号 / 调用方法名称
  • 数据库存储,服务名称,服务地址,服务端口等等,并没有完全绝对的智能

2.1.3 注册中心功能

微服务的核心就是 注册中心

注册中心:存放我们服务的地址信息,能够实现动态的感知。

常见注册中心:

  1. Zookeeper
  2. Eureka
  3. Consuler
  4. Nacos
  5. Redis
  6. 数据库

2.2 服务注册实现原理

2.2.1 服务注册相关名词

服务注册:提供服务接口地址信息存放

生产者:提供我们接口被其他服务调用

消费者:调用接口实现消费

2.2.2 服务注册原理实现

  1. 生产者启动时会将自己的 key=服务名称 value=ip 和 端口号 注册到我们微服务注册中心上
  2. 注册中心存放地址列表类型:key 唯一,列表是 list 集合Map<Key,List>
  3. 我们的消费者会从注册中心上根据服务名称查询服务地址列表(集合)
  4. 消费者获取到集群列表之后,采用负载均衡选择一个 rpc 远程调用

2.3 Nacos 的基本介绍

Nacos 实现

  • 注册中心
  • 分布式配置中心

2.3.1 配置 nacos

Nacos 官方文档

下载 Nacos(1.3.2 版本)

初始化数据库

运行 Nacos:startup.cmd -m standalone

账号密码均是:nacos

输入:127.0.0.1:8848/nacos 可以看到内容

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_远程调用

2.3.2 编写 nacos 服务注册

  1. 配置 maven 项目,创建一个名为 gorit-member-8080 的 module,并创建 application.yml 并编写配置
spring:
application:
name: gorit-member
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 8080
  1. 导入父依赖 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.gorit</groupId>
<artifactId>AlibabaNacos</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>gorit-member-8080</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
</dependencies>
</project>
  1. 编写 消费类
package cn.gorit.service;

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

/**
* @Classname MemberService
* @Description TODO
* @Date 2020/12/11 1:42
* @Created by CodingGorit
* @Version 1.0
*/
@RestController
public class MemberService {

// 这个借口在下面的 rpc 远程调用会使用到
@GetMapping("/user")
public String getUser() {
return "user";
}

@GetMapping("/user/{id}")
public String getUser(@PathVariable("userId") Integer userId) {
return "xxx";
}
}
  1. 编写主类 (前提保证 nacos 必须运行)
package cn.gorit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @Classname App
* @Description TODO
* @Date 2020/12/11 1:52
* @Created by CodingGorit
* @Version 1.0
*/
// 服务注册不需要自己加进去
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class);
}
}

可以看到服务自动被注册了

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_负载均衡_02

2.3.3 使用 DiscoverClient 实现 RPC 远程调用

编写第二个 module

  1. 编写配置
spring:
application:
name: gorit-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 8081
  1. 编写服务层
package cn.gorit.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* @Classname OrderService
* @Description 消费者调用生产者的服务
* @Date 2020/12/11 2:26
* @Created by CodingGorit
* @Version 1.0
*/
@RestController
public class OrderService {

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/order")
public Object getOrderToUser() {
// 1. 根据服务名称从 注册中心获取集群列表地址,得到的是集群列表
List<ServiceInstance> instances = discoveryClient.getInstances("gorit-member");
// 2. 列表任意选择一个实现 rpc 调用,远程 rpc 调用 gorit-member 的服务
return instances.get(0);
}
}
  1. 运行类和上面一致,区别一下名称

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_远程调用_03

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_spring_04

2.3.4 使用 rest 实现 RPC 远程调用

RestTemplate 不是 Spring Cloud Alibaba写的,本身 Spring 支持 Http 调用,实现 Http 调用

RPC 远程调用设计到本地的负载均衡算法【后期会在第七章专门介绍负载均衡算法】

  1. 从中心获取服务集群的列表
  2. 从列表中选择一个 负载均衡的算法:
  1. 一致性 hash 计算
  2. 轮训、权重
  3. 随机

采用 设计模式。策略模式

  1. 将 RestTemplate 类使用 Bean 注解,注入到 Spring 中
  2. 在 orderService 引入它。
package cn.gorit.service;

import cn.gorit.localbalance.LoadBalancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
* @Classname OrderService
* @Description 消费者调用生产者的服务
* @Date 2020/12/11 2:26
* @Created by CodingGorit
* @Version 1.0
*/
@RestController
public class OrderService {

@Autowired
private DiscoveryClient discoveryClient;

@Autowired
private RestTemplate restTemplate;

@GetMapping("/order")
public Object getOrderToUser() {
// 1. 根据服务名称从 注册中心获取集群列表地址,得到的是集群列表
List<ServiceInstance> instances = discoveryClient.getInstances("gorit-member");
// 2. 列表任意选择一个实现 rpc 调用,这里拿到地址,本地实现远程 rpc 调用 gorit-member 的服务
ServiceInstance serviceInstance = instances.get(0); // 一个问题,这里写死了,集群就没办法应用到负载均衡了。
// ip + 端口 + 调用加上接口名称
String result = restTemplate.getForObject(rpcMemberUrl.getUri()+"/user", String.class);
return "订单调用会员返回结果:"+result;
}
}

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_负载均衡_05

2.4 手写负载均衡轮训算法

在上面代码中,我们获取集群列表地址的时候,默认获取的是第一个,如果是在集群环境下,负载均衡就会失效,这里我们手写实现一个负载均衡实现的策略 —— 轮询

我们启动两个相同的 member 服务,来测试负载均衡。

在 gorit-member 的app 设置

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_spring_06

并在 application.yml 中设置启动端口为 8082

启动项目

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_spring_07

在 nacos 中,我们就可以看到 member 启动了两个,也就是说我们可以使用集群了

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_负载均衡_08

编写负载均衡轮询算法

  1. 创建 LoadBalancer 接口d
package cn.gorit.localbalance;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

/**
* @Classname LoadBalancer
* @Description TODO
* @Date 2021/1/11 17:42
* @Created by CodingGorit
* @Version 1.0
*/
public interface LoadBalancer {
/**
* 从注册中心集群列表获得单个地址
* @param serviceInstanceList
* @return
*/
ServiceInstance getSingleAddress(List<ServiceInstance> serviceInstanceList);
}
  1. 创建轮询实现类
package cn.gorit.localbalance;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @Classname RotationLoadBalancer
* @Description TODO
* @Date 2021/1/11 17:46
* @Created by CodingGorit
* @Version 1.0
*/
@Component
public class RotationLoadBalancer implements LoadBalancer {
// 从 0 开始去计数
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 从注册中心集群列表获得单个地址
* 轮询算法:
* 1%2 = 1
* 2%2 = 0
* 3%2 = 1
* 4%2 = 0
* 1%1 = 0
* @param serviceInstanceList
* @return
*/
@Override
public ServiceInstance getSingleAddress(List<ServiceInstance> serviceInstanceList) {
int index = atomicInteger.incrementAndGet()%serviceInstanceList.size();
return serviceInstanceList.get(index);
}
}
  1. 修改 memberService 的接口为如下,以及 yml 配置,分别启动 8080 和 8082 端口
@GetMapping("/user")
public String getUser() {
return "user+ 8082";
}

@GetMapping("/user")
public String getUser() {
return "user+ 8080";
}
  1. 重写 order 的服务,并重启
package cn.gorit.service;

import cn.gorit.localbalance.LoadBalancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/**
* @Classname OrderService
* @Description 消费者调用生产者的服务
* @Date 2020/12/11 2:26
* @Created by CodingGorit
* @Version 1.0
*/
@RestController
public class OrderService {

@Autowired
private DiscoveryClient discoveryClient;

@Autowired
private RestTemplate restTemplate;

// 使用负载均衡器
@Autowired
private LoadBalancer loadBalancer;

@GetMapping("/order")
public Object getOrderToUser() {
// 1. 根据服务名称从 注册中心获取集群列表地址,得到的是集群列表
List<ServiceInstance> instances = discoveryClient.getInstances("gorit-member");
// 2. 列表任意选择一个实现 rpc 调用,这里拿到地址,本地实现远程 rpc 调用 gorit-member 的服务
// ServiceInstance serviceInstance = instances.get(0); // 一个问题,这里写死了,集群就没办法应用到负载均衡了。
ServiceInstance rpcMemberUrl = loadBalancer.getSingleAddress(instances);
// ip + 端口 + 调用加上接口名称
String result = restTemplate.getForObject(rpcMemberUrl.getUri()+"/user", String.class);
return "订单调用会员返回结果:"+result;
}
}
  1. 访问 localhost:8081/order 并刷新,我们写的负载均衡算法起效了。

SpringCloud Alibaba学习笔记 ——(二、基于 Nacos 实现服务注册 与 服务发现)_远程调用_09

2.5 实现服务动态下线

我们在 nacos 手动使服务下线,可以看到 http://localhost:8081/order 就会自动切换为另一个集群的服务所在的端口

2.6 Nacos 相关面试问题

Nacos 与 Eureka 的区别?

最大区别: Nacos 支持两种模式 CP/AP 模式,从 Nacos 1.0版本开始,模式就是 ap + cp 混合模式 Eureka 采用 ap 模式行驶实现注册中心

Eureka 与 Nacos 底层实现集群协议哪些区别

  1. 去中心化对等
  2. Raft 协议实现集群产生领导角色。

Eureka 与 Zookeeper 实现注册中心的区别

  • 相同点:都可以实现分布式服务注册中心
  • 不同点:
    • Zookeeper 采用 CP 保证数据一致性的问题,原理采用 Zab原子广播协议,当我们的zk 领导因为某种原因宕机情况下,会自动重新选一个新的领导角色,
    • 保证数据一致性的问题,在选举的过程中整个 zk 环境是不可用的,可以短暂的无法使用 zk,意味着微服务采用该模式,无法实现通信(本地缓存除外)
    • 注意:可运行的节点必须满足过半机制,整个 zk 采用使用
    • Eureka采用 ap的设计理念架构注册中心,完全去中心化思想。没有主从之分。每个节点都是均等的,采用相互注册原理,你中有我,我中有你。只要最后一个 eureka 节点 保证存在,就可以保证整个微服务可以实现通信
    • 我们使用注册中心,可以保证读取数据暂时不一致性,但至少要保证注册中心可用性
    • 中心化:必须围绕一个领导角色作为核心。选取领导和追随者。
    • 去中心化:每个角色都是均等

有哪些注册中心?

Eureka、nacos、consul、Zookeeper

微服务注册中心:管理我们的服务接口地址,从而能够实现动态调用

CAP 概念

一致性(C):在分布式系统中,如果服务器是集群下,每个节点同一时刻查询的数据必须保持一致性问题。

可用性(A):在集群节点中,部分的节点出现了故障仍然可以继续使用

分区容错性 §:在分布式系统中网络存在脑裂问题,部分 Server 与整个集群失去联系无法形成一个群体。

取舍:CP/AP,找到平衡点

采用

  1. CP情况下,服务不能用,但必须保证数据一致性
  2. AP 情况下,可以短暂保证数据不一致性,但是最终可以一致性,不管如何,要保证我们的服务可用

大多数注册中心都是 ap

本节内容看完了,下一节内容是 基于 Nacos 实现 分布式配置中心