尼恩说在前面
Nacos 的是AP还是CP,是怎么实现的? 说说Nacos 的底层原理? Eureka怎么AP?Nacos既CP又AP,怎么实现的? Eureka怎么AP?Nacos既CP又AP,怎么实现的?
特别说明, 本文属于 穿透 SpringCloud 工业级 底座工程(一共包括 15大学习圣经 )的 其中之一,并且,此系列15大学习圣经底座正在录制视频, 帮助大家一举成为技术高手。
本文,就是 Nacos 学习圣经。稍后会录制视频。录完之后,Nacos学习圣经 正式版本会有更新, 最新版本找尼恩获取。
本文目录
- 尼恩说在前面
- 1 简介
- Nacos的三大核心能力:
- 2 安装Nacos
- 2.1 单机模式安装
- 2.1.1 安装步骤
- 2.1.2 单机模式的数据库
- 2.2 集群模式安装
- 2.2.1 准备虚拟机环境
- 2.2.2 Nacos持久化配置
- 2.2.3 Nginx反向代理配置
- 2.2.4 Nacos 2.x集群配置
- 3 注册中心实战
- 3.1 父工程
- 3.2 支付微服务(Provider)
- 3.3 订单微服务(Consumer)
- 4 配置中心实战
- 配置中心的 起源
- 4.1 基本概念
- 4.2 配置中心 基础配置
- 4.3 配置隔离
- 4.4 配置拆分
- 5 Nacos架构和 底层原理
- 5.1 Nacos架构图
- 5.2 配置模型
- 5.2.1 基础模型
- 5.2.2 配置资源模型
- 5.2.3 配置存储模型(ER图)
- 5.3 分布式Nacos 节点之间,数据一致性设计
- 5.3.1 CAP定理和BASE理论
- 5.3.2 Nacos是AP还是CP
- 5.3.3 Nacos一致性协议
-早期的一致性协议
-当前的一致性协议层
-一致性协议下沉
- 5.3.4 Distro协议(AP实现)
-设计思想
-数据初始化
-数据校验
-写操作
-读操作
-Distro 协议 的总结
- 5.3.5 Nacos中CP的实现
- 5.3.6 Raft协议
-基础概念
-领导选举
-日志复制
- 5.3.7 配置一致性模型 的底层原理
-Server 间的一致性协议
-SDK 与 Server 的一致性协议
- 5.4 服务注册 的底层原理
- 1.x版本的服务注册实现
- 2.x版本的服务注册实现
- 总结
- 5.5 心跳机制 的底层原理
- 1.x心跳实现
- 底层原理:2.x 版本心跳实现
- 总结
- 5.6 健康检查 的底层原理
- 5.7 服务发现 的底层原理
- 1.x服务订阅实现
- 2.x服务订阅的实现
- 总结
- 5.8 临时实例和永久实例
- 说在最后:有问题找老架构取经
1 简介
/nɑ:kəʊs/
是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Kubernetes Service gRPC & Dubbo RPC Service Spring Cloud RESTful Service
Nacos的三大核心能力:
2 安装Nacos
2.1 单机模式安装
2.1.1 安装步骤
standanlone:单节点模式 cluster:集群模式
bin/startup.sh -m standalone # linux
bin/startup.cmd -m standalone # windows
set MODE="standalone"
2.1.2 单机模式的数据库
内存 本地数据库
Derby 是 Java 编写的数据库,属于 Apache 的一个开源项目
standalone 的话仅会使用 Derby,即使在 application.properties 里边配置 MySQL 也照样无视; cluster 模式会自动使用 MySQL,这时候如果没有 MySQL 的配置,是会报错的。
注意:不支持 MySQL 8.0 版本
2.2 集群模式安装
2.2.1 准备虚拟机环境
Mysql JDK Nginx
2.2.2 Nacos持久化配置
tar -zxvf nacos-server-1.4.1.tar.gz -C /usr/local
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456
### Connection pool configuration: hikariCP
db.pool.config.connectionTimeout=30000
db.pool.config.validationTimeout=10000
db.pool.config.maximumPoolSize=20
db.pool.config.minimumIdle=2
10.0.2.15:8848
10.0.2.15:8858
10.0.2.15:8868
server.port=8858
2.2.3 Nginx反向代理配置
#gzip on;
upstream nacos_cluster {
server 127.0.0.1:8848;
server 127.0.0.1:8858;
server 127.0.0.1:8868;
}
server {
listen 8838;
server_name localhost;
location / {
proxy_pass http://nacos_cluster;
proxy_set_header Host $host:$server_port;
}
}
spring:
application:
name: order-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: 192.168.56.110:1111
file-extension: yaml
extension-configs[0]:
data-id: common.yaml
refresh: true
#!/bin/bash
echo "starting nacos 8848"
/home/nacos-cluster/nacos-8848/bin/startup.sh
echo "starting nacos 8858"
/home/nacos-cluster/nacos-8858/bin/startup.sh
echo "starting nacos 8868"
/home/nacos-cluster/nacos-8868/bin/startup.sh
#!/bin/bash
echo "starting nacos 8848"
/home/nacos-cluster/nacos-8848/bin/shutdown.sh
echo "starting nacos 8858"
/home/nacos-cluster/nacos-8858/bin/shutdown.sh
echo "starting nacos 8868"
/home/nacos-cluster/nacos-8868/bin/shutdown.sh
2.2.4 Nacos 2.x集群配置
stream {
upstream nacos-server-grpc {
server 127.0.0.1:9848;
server 127.0.0.1:9858;
server 127.0.0.1:9868;
}
server {
listen 9838;
proxy_pass nacos-server-grpc;
}
}
# 端口规则
server.port(默认8848)
raft port: ${server.port} - 1000
grpc port: ${server.port} + 1000
grpc port for server: ${server.port} + 1001
上面端口的介绍, 请参见 尼恩技术团队 《史上最强的 微服务技术基座》 配套视频
3 注册中心实战
Nacos Server:Nacos注册中心。 支付服务:服务提供者。 订单服务:服务消费者。
3.1 父工程
Spring Cloud 2020.0.3 Spring Cloud Alibaba 2021.1 Spring Boot 2.4.9
<?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>com.lxs.demo</groupId>
<artifactId>cloud-alibaba-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>payment</module>
<module>order</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<alibaba-cloud.version>2021.1</alibaba-cloud.version>
<springcloud.version>2020.0.3</springcloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 支付微服务(Provider)
pom.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">
<parent>
<artifactId>cloud-alibaba-demo</artifactId>
<groupId>com.lxs.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: ${port:9001}
spring:
application:
name: payment-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
PaymentApplication
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentApplication
{
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
PaymentController
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/{id}")
public ResponseEntity<String> payment(@PathVariable("id") Long id) {
return ResponseEntity.ok("订单号 = " + id + ",支付成功,server.port" + serverPort);
}
}
3.3 订单微服务(Consumer)
pom.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">
<parent>
<artifactId>cloud-alibaba-demo</artifactId>
<groupId>com.lxs.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 84
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
启动器
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderApplication
{
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
ApplicationContextConfig
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
FeignClient
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public ResponseEntity<String> payment(Long id) {
return new ResponseEntity<String>("feign调用,异常降级方法", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@FeignClient(value = "payment-service", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping("/payment/{id}")
public ResponseEntity<String> payment(@PathVariable("id") Long id);
}
OrderController
@RestController
@RequestMapping("/order")
public class OrderController {
public static final String SERVICE_URL = "http://payment-service";
@Resource
private RestTemplate restTemplate;
@GetMapping("/lb/{id}")
public ResponseEntity<String> consumer_ribbon(@PathVariable("id") Integer id){
String result = restTemplate.getForObject("http://payment-service" + "/payment/" + id, String.class);
return ResponseEntity.ok(result);
}
//OpenFeign
@Resource
private PaymentService paymentService;
@GetMapping(value = "/feign/{id}")
public ResponseEntity<String> consumer_feign(@PathVariable("id") Long id) {
return paymentService.payment(id);
}
}
4 配置中心实战
配置中心的 起源
配置的动态更新:在实际应用会有动态更新位置的需求,比如修改服务连接地址、限流配置等。在传统模式下,需要手动修改配置文件并且重启应用才能生效,这种方式效率太低,重启也会导致服务暂时不可用。 配置集中式管理:在微服务架构中某些核心服务为了保证高性能会部署上百个节点,如果在每个节点中都维护一个配置文件,一旦配置文件中的某个属性需要修改,可想而知,工作量是巨大的。 不同部署环境下配置的管理:前面提到通过profile机制来管理不同环境下的配置,这种方式对于日常维护来说也比较繁琐。
4.1 基本概念
Profile
spring:
application:
name: order-service
profiles:
active: dev
Data ID
Data ID的拼接格式: ${prefix}-${spring.profiles.active}.${file-extension}
prefix 默认为 spring.application.name
的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置spring.profiles.active
取spring.profiles.active
的值,即为当前环境对应的 profile,当spring.profiles.active
为空时,对应的连接符-
也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
file-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持properties
和yaml
类型。
Group
4.2 配置中心 基础配置
引入依赖
<!--nacos config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
配置文件
spring:
application:
name: payment-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
server:
port: ${port:9001}
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
nacos中的配置DataID
Data ID的拼接格式: ${prefix}-${spring.profiles.active}.${file-extension}
prefix 默认为 spring.application.name
的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置spring.profiles.active
取spring.profiles.active
的值,即为当前环境对应的 profile,当spring.profiles.active
为空时,对应的连接符-
也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
file-extension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持properties
和yaml
类型。
业务中读取配置属性
@RestController
@RequestMapping("/payment")
@RefreshScope
public class PaymentController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
测试
config info public default group
4.3 配置隔离
Nacos的服务器 namespace命名空间 group分组,
Nacos的服务器 spring.cloud.nacos.config.server-addr Nacos的命名空间 spring.cloud.nacos.config.namespace,注意,这里使用命名空间的ID不是名称 Nacos的分组 spring.cloud.nacos.config.group
命名空间
DataID
bootstrap.yml
spring:
application:
name: payment-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001
group: MY_GROUP
config info prod public group
service隔离
server:
port: ${port:9001}
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001
4.4 配置拆分
配置拆分策略
DataID配置
配置文件
spring:
application:
name: payment-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
namespace: f521b45b-e0bb-4c4d-b97c-3d0158970001
group: MY_GROUP
extension-configs[0]:
data-id: common.yaml
refresh: true
注:extension-configs配置属性和shared-configs配置属性功能一致,都是读取配置文件,这里我们把重复配置放到common.yaml中,这样在不同的工程中就可以重复使用[n]的值越大,优先级越高。 注: spring.application.name
和spring.profiles.active
配置必须放到bootstrap.yml中,否则影响配置自动刷新功能。
spring:
application:
name: order-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: localhost:8848 #nacos 配置中心的地址
file-extension: yaml
extension-configs[0]:
data-id: common.yaml
refresh: true
5 Nacos架构和 底层原理
临时实例和永久实例是什么?有什么区别? 服务实例是如何注册到服务端的? 服务实例和服务端之间是如何保活的? 服务订阅是如何实现的? 集群间数据是如何同步的?CP还是AP? Nacos的数据模型是什么样的?
5.1 Nacos架构图
用户层主要解决用户使用的易用性问题, 业务层主要解决服务发现和配置管理的功能问题, 内核层解决分布式系统一致性、存储、高可用等核心问题,插件解决扩展性问题。
用户层
OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成 Console:易用控制台,做服务管理、配置管理等操作 SDK:多语言 SDK,目前几乎支持所有主流编程语言 Agent:Sidecar 模式运行,通过标准 DNS 协议与业务解耦 CLI:命令行对产品进行轻量化管理,像 git 一样好用
业务层
服务管理:实现服务 CRUD,域名 CRUD,服务健康状态检查,服务权重管理等功能 配置管理:实现配置管 CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能 元数据管理:提供元数据 CURD 和打标能力,为实现上层流量和服务灰度非常关键
内核层
插件机制:实现三个模块可分可合能力,实现扩展点 SPI 机制,用于扩展自己公司定制 事件机制:实现异步化事件通知,SDK 数据变化异步通知等逻辑,是Nacos高性能的关键部分 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档 回调机制:SDK 通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性 寻址模式:解决 Server IP 直连,域名访问,Nameserver 寻址、广播等多种寻址模式,需要可扩展 推送通道:解决 Server 与存储、Server 间、Server 与 SDK 间高效通信问题 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制 缓存机制:容灾目录,本地缓存,Server 缓存机制,是 Nacos 高可用的关键 启动模式:按照单机模式,配置模式,服务模式,DNS 模式,启动不同的模块 一致性协议:解决不同数据,不同一致性要求情况下(包括AP 协议和CP协议)的诉求 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
插件
Nameserver:解决 Namespace 到 ClusterID 的路由问题,解决用户环境与 Nacos 物理环境映射问题 CMDB:解决元数据存储,与三方 CMDB 系统对接问题,解决应用,人,资源关系 Metrics:暴露标准 Metrics 数据,方便与三方监控系统打通 Trace:暴露标准 Trace,方便 SLA 系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程 用户管理:解决用户管理,登录,SSO 等问题 权限管理:解决身份识别,访问控制,角色管理等问题 审计系统:扩展接口方便与不同公司审计系统打通 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
5.2 配置模型
当应用程序实例比较少的时候还可以维护。
需要支持动态修改配置 需要动态变更实时生效 变更快了之后如何管控控制变更风险,如灰度、回滚等 敏感配置如何做安全配置
5.2.1 基础模型
上图是 Nacos 配置管理的基础模型:
Nacos 提供可视化的控制台,可以对配置进行发布、更新、删除、灰度、版本管理等功能。 SDK 可以提供发布配置、更新配置、监听配置等功能。 SDK 通过 GRPC 长连接监听配置变更,Server 端对比 Client 端配置的 MD5 和本地 MD5 是否相等,不相等推送配置变更。 SDK会保存配置的快照,当服务端出现问题的时候从本地获取。
5.2.2 配置资源模型
从单个租户的角度来看,我们要配置多套环境的配置,可以根据不同的环境来创建 Namespace 。比如开发环境、测试环境、线上环境,我们就创建对应的 Namespace(dev、test、prod),Nacos 会自动生成对应的 Namespace Id 。如果同一个环境内想配置相同的配置,可以通过 Group 来区分。如下图所示:
从多个租户的角度来看,每个租户都可以有自己的命名空间。我们可以为每个用户创建一个命名空间,并给用户分配对应的权限,比如多个租户(zhangsan、lisi、wangwu),每个租户都想有一套自己的多环境配置,也就是每个租户都想配置多套环境。那么可以给每个租户创建一个 Namespace (zhangsan、lisi、wangwu)。同样会生成对应的 Namespace Id。然后使用 Group 来区分不同环境的配置。如下图所示:
5.2.3 配置存储模型(ER图)
Nacos存储配置有几个比较重要的表分别是:
config_info 存储配置信息的主表,里面包含 dataId、groupId、content、tenantId、encryptedDataKey 等数据。 config_info_beta 灰度测试的配置信息表,存储的内容和 config_info 基本相似。有一个 beta_ips 字段用于客户端请求配置时判断是否是灰度的ip。 config_tags_relation 配置的标签表,在发布配置的时候如果指定了标签,那么会把标签和配置的关联信息存储在该表中。 his_config_info 配置的历史信息表,在配置的发布、更新、删除等操作都会记录一条数据,可以做多版本管理和快速回滚。
5.3 分布式Nacos 节点之间,数据一致性设计
5.3.1 CAP定理和BASE理论
CAP定理 BASE理论
C,Consistency单词的缩写,代表一致性,指分布式系统中各个节点的数据保持强一致,也就是每个时刻都必须一样,不一样整个系统就不能对外提供服务 A,Availability单词的缩写,代表可用性,指整个分布式系统保持对外可用,即使从每个节点获取的数据可能都不一样,只要能获取到就行 P,Partition tolerance单词的缩写,代表分区容错性。
基本可用(Basically Available):系统出现故障还是能够对外提供服务,不至于直接无法用了 软状态(Soft State):允许各个节点的数据不一致 最终一致性,(Eventually Consistent):虽然允许各个节点的数据不一致,但是在一定时间之后,各个节点的数据最终需要一致的
5.3.2 Nacos是AP还是CP
从服务注册发现来看
从配置管理来看
Nacos其实目前是同时支持AP和CP的 具体使用AP还是CP得取决于Nacos内部的具体功能,并不是有的文章说的可以通过一个配置自由切换。 服务注册场景, 临时实例,Nacos会优先保证可用性,也就是AP 永久实例,Nacos会优先保证数据的一致性,也就是CP 就配置管理而言是CP
5.3.3 Nacos一致性协议
早期的一致性协议
当前的一致性协议层
一致性协议下沉
一致性协议抽象
public interface ConsistencyProtocol<T extends Config, P extends RequestProcessor> extends CommandOperations {
...
/**
* Obtain data according to the request.
*
* @param request request
* @return data {@link Response}
* @throws Exception {@link Exception}
*/
Response getData(ReadRequest request) throws Exception;
/**
* Data operation, returning submission results synchronously.
*
* @param request {@link com.alibaba.nacos.consistency.entity.WriteRequest}
* @return submit operation result {@link Response}
* @throws Exception {@link Exception}
*/
Response write(WriteRequest request) throws Exception;
...
}
public class ProtocolManager extends MemberChangeListener implements DisposableBean {
...
private void initAPProtocol() {
ApplicationUtils.getBeanIfExist(APProtocol.class, protocol -> {
Class configType = ClassUtils.resolveGenericType(protocol.getClass());
Config config = (Config) ApplicationUtils.getBean(configType);
injectMembers4AP(config);
protocol.init((config));
ProtocolManager.this.apProtocol = protocol;
});
}
private void initCPProtocol() {
ApplicationUtils.getBeanIfExist(CPProtocol.class, protocol -> {
Class configType = ClassUtils.resolveGenericType(protocol.getClass());
Config config = (Config) ApplicationUtils.getBean(configType);
injectMembers4CP(config);
protocol.init((config));
ProtocolManager.this.cpProtocol = protocol;
});
}
...
}
数据存储抽象
public interface KvStorage {
enum KvType {
/**
* Local file storage.
*/
File,
/**
* Local memory storage.
*/
Memory,
/**
* LSMTree storage.
*/
LSMTree,
AP,
CP,
}
// 获取一个数据
byte[] get(byte[] key) throws KvStorageException;
// 存入一个数据
void put(byte[] key, byte[] value) throws KvStorageException;
// 删除一个数据
void delete(byte[] key) throws KvStorageException;
...
}
5.3.4 Distro协议(AP实现)
设计思想
Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。 每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据一致性。 每个节点独立处理读请求,及时从本地发出响应。
数据初始化
数据校验
写操作
整个步骤包括几个部分(图中从上到下顺序):
前置的 Filter 拦截请求,并根据请求中包含的 IP 和 port 信息计算其所属的 Distro 责任节点,并将该请求转发到所属的 Distro 责任节点上。 责任节点上的 Controller 将写请求进行解析。 Distro 协议定期执行 Sync 任务,将本机所负责的所有的实例信息同步到其他节点上。
读操作
Distro 协议 的总结
5.3.5 Nacos中CP的实现
Leader,负责所有的读写请求,一个集群只有一个 Follower,从节点,主要是负责复制Leader的数据,保证数据的一致性 Candidate,候选节点,最终会变成Leader或者Follower
Nacos服务2
节点,之后向Nacos服务2
注册服务Nacos服务2
接收到请求之后,会判断自己是不是Leader节点,发现自己不是Nacos服务2
就会向Leader节点发送请求,Leader节点接收到请求之后,会处理服务注册的过程首先,Leader在处理写请求时,不会直接数据应用到自己的系统,而是先向所有的Follower发送请求,让他们先处理这个请求 当超过半数的Follower成功处理了这个写请求之后,Leader才会写数据,并返回给客户端请求处理成功 如果超过一定时间未收到超过半数处理成功Follower的信号,此时Leader认为这次写数据是失败的,就不会处理写请求,直接返回给客户端请求失败
5.3.6 Raft协议
基础概念
领导者:集群中霸道总裁,一切以我为准,处理写请求、管理日志复制和不断地发送心跳信息。 跟随者:普通成员,处理领导者发来的消息,发现领导者心跳超时,推荐自己成为候选人。 候选人:先给自己投一票,然后请求其他集群节点投票给自己,得票多者成为新的领导者。 任期编号:每任领导者都有任期编号。当领导者心跳超时,跟随者会变成候选人,任期编号 +1,然后发起投票。任期编号小的服从编号大的。 心跳超时:每个跟随者节点都设置了随机心跳超时时间,目的是避免跟随者们同时成为候选人,同时发起投票。 选举超时:每轮选举的结束时间,随机选举超时时间可以避免多个候选人同时结束选举未果,然后同时发起下一轮选举
领导选举
选举规则
领导者周期性地向跟随者发送心跳消息,告诉跟随者我是领导者,阻止跟随者发变成候选人发起新选举; 如果跟随者的随机心跳超时了,那么认为没有领导者了,推荐自己为候选人,发起新的选举; 在一轮选举中,赢得一半以上选票的候选人,即成为新的领导者; 在一轮选举中,每个节点对每个任期编号的选举只能投出一票,先来先服务原则; 日志完整性高(也就是最后一条日志项对应的任期编号值更大,索引号更大)的跟随者A拒绝给完整性低的候选人B投票,即使B的任期编号大;
选举动画
日志复制
5.3.7 配置一致性模型 的底层原理
第一部分是 Server 间一致性协议,一个是 SDK 与 Server 的一致性协议,配置作为分布式系统中非强一致数据,在出现脑裂的时候可用性高于一致性,因此阿里配置中心是采用 AP 一致性协议。 第二部分Server 间采用 Raft 协议保证数据一致性,行业大部分产品才用此模式,Nacos 提供此模式,是方便用户本机运行,降低对存储依赖。
Server 间的一致性协议
SDK 与 Server 的一致性协议
Nacos 1.X
Nacos 2.X
5.4 服务注册 的底层原理
1.x版本的服务注册实现
2.x版本的服务注册实现
通信协议的改变
具体的实现
Redo操作
除了注册之外,比如服务订阅之类的操作也需要Redo操作,当连接重新建立,之前客户端的操作都需要Redo一下
总结:
既然2.x有Redo机制保证客户端与服务端通信正常之后重新注册,那么1.x有类似的这种Redo机制么?
5.5 心跳机制 的底层原理
1.x心跳实现
当最后一次心跳时间超过15s,但没有超过30s,会把这服务实例标记成不健康 当最后一次心跳超过30s,直接把服务从服务注册表中剔除
底层原理:2.x 版本心跳实现
连接本身的心跳机制,断开就直接剔除服务实例 Nacos主动检查机制,服务端会对20s没有发送数据的连接进行检查,出现异常时也会主动断开连接,剔除服务实例
总结
心跳机制仅仅针对临时实例而言 1.x心跳机制是通过客户端和服务端两个定时任务来完成的,客户端定时上报心跳信息,服务端定时检查心跳时间,超过15s标记不健康,超过30s直接剔除 1.x心跳机制还有类似2.x的Redo作用,服务端发现心跳的服务信息不存在会,会将服务信息添加到注册表,相当于重新注册了 2.x是基于gRPC长连接本身的心跳机制和服务端的定时检查机制来的,出现异常直接剔除
5.6 健康检查 的底层原理
TCP HTTP MySQL
5.7 服务发现 的底层原理
主动查询(pull) 服务订阅(push)
NamingService#subscribe
方法来发起订阅的EventListener
,就可以拿到最新的服务实例数据了1.x服务订阅实现
第一步,客户端在启动的时候,会去构建一个叫PushReceiver的类
第二步,调用 **NamingService#subscribe**
发起订阅时,先去服务端查询需要订阅服务的所有实例信息
第三步,服务器会为这次订阅开启一个不定时执行的任务
2.x服务订阅的实现
总结
服务查询1.x是通过Http请求;2.x通过gRPC请求 服务订阅1.x是通过UDP来推送的;2.x就基于gRPC长连接来实现的 1.x和2.x客户端都有服务实例的缓存,也有定时对比机制,只不过1.x会自动开启;2.x提供了一个开关,可以手动选择是否开启,默认不开启
5.8 临时实例和永久实例
临时实例
就比较适合于业务服务,服务下线之后可以不需要在注册中心中查看到 临时实例在注册到注册中心之后仅仅只保存在服务端内部一个缓存中,不会持久化到磁盘 这个服务端内部的缓存在注册中心届一般被称为服务注册表 当服务实例出现异常或者下线之后,就会把这个服务实例从服务注册表中剔除
永久实例
就比较适合需要运维的服务,这种服务几乎是永久存在的,比如说MySQL、Redis等等 永久服务实例不仅仅会存在服务注册表中,同时也会被持久化到磁盘文件中 当服务实例出现异常或者下线,Nacos只会将服务实例的健康状态设置为不健康,并不会对将其从服务注册表中剔除
spring
cloud:
nacos:
discovery:
#ephemeral单词是临时的意思,设置成false,就是永久实例了
ephemeral: false
健康检查机制不同 临时实例,使用心跳机制 永久实例,server端主动发起探测请求,http,tcp,mysql 注册数据一致性方案不同 临时实例,使用自研Distro协议,属于AP 永久实例,使用jRaft协议,属于CP 临时实例存储在内存,永久实例还会持久化到硬盘
说在最后:有问题找老架构取经
被裁之后, 空窗1年/空窗2年, 如何 起死回生 ?
案例1:42岁被裁2年,天快塌了,急救1个月,拿到开发经理offer,起死回生
案例2:35岁被裁6个月, 职业绝望,转架构急救上岸,DDD和3高项目太重要了
案例3:失业15个月,学习40天拿offer, 绝境翻盘,如何实现?
被裁之后,100W 年薪 到手, 如何 人生逆袭?
100W案例,100W年薪的底层逻辑是什么? 如何实现年薪百万? 如何远离 中年危机?
如何 逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢