本文涉及源码地址:点击跳转至码云的@link笔记
1. 环境准备
拉取我配置好的源码,切换到tjs-study-fetch-master分支即可
1)代码结构
2)如何启动服务端
- 执行sql脚本
- 更改配置
- 增加jvm参数
-Dnacos.standalone=false -Dserver.port=8848 -Dnacos.home=xxx/cluster.conf
- 右键启动多个节点
启动类:com.alibaba.nacos.Nacos
- 随便找个节点,创建命名空间
3)如何启动客户端
如果要自己搭建,nacos有很多版本,处理不好容易遇到与spring cloud整合时版本冲突的问题,建议使用阿里云脚手架初始化start.aliyun.com/
2. 制定本次阅读的目的
源码内容过多,一定要有清晰地目的,不然容易陷进去,本次主要梳理注册中心的核心流程
- [客户端A如何注册到服务端]
- [服务端集群内部如何同步数据]
- [客户端A如何请求客户端B]
- [客户端A如何下线,客户端B如何感知]
- [节点C1下线,客户端A如何切换到节点C2]
- [节点C3宕机一段时间后,重新启动时如何找回数据]
- [心跳机制]
3. 如何寻找切入点
1. 官方文档
简单阅读下官方文档,对官方文档大概有什么东西有个印象,方便解决后面可能遇到的问题
例子: nacos文档提供了很多接口文档,可以根据接口名去寻找其在源码中的位置,并打上断点,观察调用链
2. 日志法
更改日志级别,大部分源码都会在关键节点打印日志。在关键日志所在的代码行,打上断点,观察调用链
例子: 查看日志可知,注册成功后会在NacosServiceRegistry打印register finished
registerService方法调用的api正和官网的注册服务api一致,所以此处必然就是注册时调用的地方,在此处打上断点,观察调用链即可分析出客户端注册逻辑
3. 终点法
尝试抛开具体实现,以终点作为切入点去debug,观察调用链后自下而上,逆推完整的调用逻辑
例子: 集群间的心跳同步,最终肯定是要重置心跳时间的。那么我们在HealthCheckInstancePublishInfo#setLastHeartBeatTime打上断点即可
PS: 常见的点有get/set方法、构造函数、List就找add/remove、Map就找put/computeIfAbsent、抽象接口等处打断点
4. 跳出思维,多角度尝试
不要一种方法死磕到底,抛开具体细节,反问自己,如果一切都成立,那么会有哪些现象或结论?
例子: 如果服务端注册处理成功,肯定会有个变量缓存已经注册的微服务实例信息,那么在这个变量变更的方法打上断点,分析相关调用链,即可快速分析服务端注册实现,那么如何找到这个变量呢?
- 注册切入
查看文档,注册接口为/nacos/v1/ns/instance。在服务端源码中,找到客户端调用的的controller层实现类,发现这里代码并不好看懂
- 查询切入
换个思路,注册代码不好看懂,尝试从查询切入,找到查询方法最终是查的哪个变量R,然后再看这个这个变量R是在哪个方法注入的,即可找到切入点
查看官方文档可知,查询是调用/nacos/v1/ns/instance/list这个接口,找到其实现类
4. 核心流程
核心流程的代码梳理如下,也可以点击跳转至码云的@link笔记查看 鼠标右键@link即可跳转至目标方法,方便后面快速找回记忆
4.1 客户端A如何注册到服务端
4.2 服务端集群内部如何同步数据
4.3 客户端A如何请求客户端B
4.4 客户端A如何下线,客户端B如何感知
4.5 节点C1下线,客户端A如何切换到节点C2
4.6 节点C3宕机一段时间后,重新启动时如何找回数据
4.7 心跳机制
5. 调试经验
1)idea同时启动多个服务,方便集群调试
2)更改日志级别
指定日志目录:-Dnacos.logs.path=/xxx/nacos/console/src/main/resources/home/8848/logs
配置文件:/xxx/nacos/core/src/main/resources/META-INF/logback/nacos.xml
3)naocs源码用了很多事件机制
- 在哪里发布事件:在事件构造方法打上断点
- 在哪里消费事件:
4)延迟任务机制的task实现了Runnable,关注AbstractExecuteTask#run的具体实现即可
5)集群间的同步,关注com.alibaba.nacos.consistency.DataOperation枚举的调用处作为切入点
6)客户端与服务端、服务端与服务端的grpc请求都是在com.alibaba.nacos.core.remote.RequestHandler#handle的实现类处理的
6. 新旧版本对比
旧版本主要疼点:注册中心绝大部分数据变化不频繁,旧版本很多请求都是无效的
新版本核心优势:grpc空耗少,高吞吐
- 注册、心跳等重定向至目标节点
- 旧版本:重定向至目标节点后,再执行注册
- 新版本:首次启动随机绑定一个节点,无需重定向,直接执行注册
- 业务让步:旧版的重定向,目的是为了使得每个节点的处理数量尽可能均衡,但是代价却是可能每次请求都多转发了一次。新版使用随机数,虽然可能导致处理数量相对旧版没有那么均衡(最坏的情况,所有节点首次随机数都为同一个值),但是相对旧版每次转发,性能更高
PS:哪个节点执行的注册,哪个节点就要负责那一个客户端的版本检查与同步。如果所有注册请求都打到了同一个节点,那单节点压力可能会过大
- 维持注册信息(续命)
- 旧版本:客户端定时5秒调用/beat接口+服务端定时5秒清理非活跃实例
- 新版本:依赖grpc自身的keeplive机制,实现grpc的失联回调接口,近实时监听下线
- 优点:极大的减少了客户端与服务端的请求数,降低了资源的占用,提高了tps
- 客户端实例缓存
- 旧版本:服务端处理完变更后使用UDP同步给其他客户端+客户端定时30秒更新一次
- 新版本:服务端处理完变更后grpc同步给其他客户端
- 优点:grpc推送比UDP更可靠
7. 吸收源码
7.1 贴合业务,定制的同步协议Distro
nacos没有照搬redis/zookeeper等复杂的主从同步、raft协议等,而是紧贴业务,去中心化,自己实现的Distro同步协议(AP)。
告诉我们,做事情不可一昧照搬,应该
- 分析核心疼点
疼点,实际上客户端数据变化少,但是http心跳包/beat接口,却占据了大部分资源 -> 改用grpc监听回调,完美解决
- 权衡业务容忍性与程序准确性
如果客户端A发起注册后,为了保证强一致性,那么就要等到服务端所有节点都同步完成,才返回成功,期间可能服务端还不可用,那注册/查询tps将大打折扣。
但是注册中心这个场景下,获取客户端A列表时,即使这次没有查到客户端A1,等个几秒,只要下次能查到A1,那么对业务并没有任何影响。
所以业务让步,牺牲程序部分准确性。放弃服务端同步的实时一致性,只要保证最终一致性即可
遇到业务难点、数据量大、并发高的时候,贴合业务,尝试站在客户/业务的角度思考下
a) 这么大的数据量,客户/业务真正的关心的数据是哪些?真正有用/变化的数据是哪些?
b) 是否真的有必要达到'完美',客户/业务能够接受哪些让步?
7.2 兜底意识
真实场景下的程序运行中,可能存在各种各样的意外,所以要学会考虑给程序上一份保险-兜底
比如:客户端A注册到服务C1后,除了调用grpc接口近实时同步给C2外。还会有一个定时器,间隔5秒向所有其他服务节点确认客户端A的缓存版本号,如果不一致,则发起兜底同步,拉齐数据
7.3 copy-on-write机制
旧版本截图:
写时加锁,直接替换引用,保证数据最终一致性。读时完全无锁,但可能有一定的延迟
核心原理:读、写至少有一个用新的引用
疑问:为什么消费队列时,没有volatile修饰,也没有synchronized对象锁,也可以保证可见性?
原因:阻塞队列消费线程只有1个线程,不存在冲突。读取时只需要最终一致性即可
但是在获取注册信息的时候,需要复制一份,否则循环的时候会触发fast-fail或删除导致的数组越界
验证示例:
7.4 事件机制
- 优点
1)方便插件式扩展
例子:触发A的时候,需要通知B;后面版本需要新增通知C
直接调用:A和C都需改动。C需要提供方法M,方法名可能还不一样;A需要调用C
事件机制:C改动即可。C新增消费A的方法即可
附加优点:松耦合、模块化
- 缺点
1)不方便新人调试和跟踪
例子:A调用B,B调用C,C调用D
直接调用:在D处打上断点即可获得完整的调用链
事件机制:先看C在哪里发布,然后再找到C是由于消费B触发的,再看B在哪里发布...
解决:层级较深的事件,写好某个业务的完整事件链路的注释;规划好模块,同一类事件放到一个类中消费/发布
7.5 写法
1)单一职责原则。1个监听器监听多个同业务事件
2)一个业务多个类公用日志常量
7.6 http改成grpc
- 相对于http-json传输的优点
- 数据传输效率高
数据类型、结构依赖于预定义的proto文件、压缩算法等。所以
序列化数据体积小(是json的20%-80%)、反序列化快,数据传输效率高
- 取消业务自定义心跳,各种监听回调
grpc基于http2的长连接协议,内部实现也是通过定时ping帧(请求数据量极少)来keepalive,极大的减少了 http 请求频繁的连接创建和销毁过程,能大幅度提升性能,节约资源
其可以感知对端是否下线,下线后执行回调。比如nacos客户端异常下线后,服务端能马上感知到,而旧版需要依赖ncaos自己的心跳包http接口/beat来实现
- 多路复用,双端推送
同一个连接通道,客户端、服务端可以并行相互推送多个消息。
多路复用:公用一个连接,无需每次请求创建TCP连接;并行同时发送请求A、B,不需等待A响应成功再发送B;主要原理,一个请求持有一个id,双端根据请求id找到归属的请求,去发送/接收消息
- 相对于json传输的缺点
- proto不可读
- 浏览器不可以直接访问
- 用的人不多,学习成本高
- 相对于netty
netty为gRPC的底层通信框架。grpc重新实现了neety的序列化等功能