从零实现一个注册中心 - 服务端
什么是注册中心?
注册中心, 也称命名服务(Naming servive), 它的核心功能与DNS服务类似, 无非就是通过一个特定的名字来查找相关的实例集合, 但是它们也有很多不同点
1. DNS中的配置是静态的一个ip或多个ip, 而注册中心中是动态变化的实例列表
2. DNS无法为ip添加元信息, 而注册中心可以根据需求为实例添加元信息
3. DNS无法保证解析到的IP是可用的, 而注册中心可以利用健康检查机制来保证实例是可用的
注册中心的适用场景
当我们需要访问一个集群服务, 但是这个服务集群中的实例地址不是固定的, 又或者说各个实例的可用状态是不固定的, 这种情况下就需要用到注册中心了
主要场景: 微服务 (SpringCloud / Dubbo 等)
常见的注册中心
Zookeeper / Eureka / Consul / Nacos / Etcd ...
如何实现一个注册中心?
设计实现一个产品, 首先我们得知道它的核心功能是什么, 其次是它的衍生需求有哪些
核心功能
1. 注册 - 可以将自己的访问地址注册到注册中心
2. 发现 - 可以从注册中心获取指定服务的访问地址
衍生需求
1. 健康检测 - 自动剔除不可用的实例
2. 元信息 - 用于服务分组/状态管理/自定义打标等
3. 数据管理 - 了解当前注册中心中所有实例的状态
4. 异常告警 - 当服务实例出现突发异常时能及时感知
5. ...
服务端设计
通讯模块
这个模块定义了客户端与服务端之间的通讯协议, 以完成注册/发现过程中的数据交换, 并维护客户端的会话
注册中心的通讯协议有几种选择:
1. HTTP (Rest)协议
2. WebSocket 长链接协议
3. TCP 自定义私有协议 (如: Dubbo)
4. 多种协议混合·
这里我们希望实例的变更能实时的推送到客户端,所以选择了基于Netty实现的WebSocket来作为服务端与客户端之间的通讯协议,
客户端在运行期间会始终与服务端保持长链接
这有一个好处就是服务端可以主动给客户端推送数据变更的消息, 并且客户端异常断开连接时服务端可以及时感知,
但也有一定的弊端, 如果没有做良好的优化, 服务端短时间内频繁向客户端发送变更消息会使得客户端的性能有一定影响
请求处理模块
这个模块服务处理客户端的消息, 这里包含两个过程, 1: 将请求路由给对应的Action; 2: 由Action完成对客户端请求的逻辑处理
关于请求路由: 我们知道SpringMvc有一个DispatcherServlet和一套RequestMapping注解来自动将HTTP请求路由到具体的逻辑执行器上, 我们可以借鉴它的原理自己实现一套将Websocket消息转发到不同执行器上的功能组件, 这个组件已经开源在github上 - 传送
有了分发组件的支持, 我们只需要专心编写我们的处理逻辑就可以了, 像下面这个样子 (是不是跟SpringMvc的注解很像呢~~)
客户端数据同步模块
这个模块负责处理所有向客户端同步数据的行为, 如: 推送全量订阅数据 / 推送增量变更数据 等
它作为客户端消息推送的收口, 可以最大程度的保障推送的有序性, 同时可以在这里做一系列的优化(如: 延迟推送机制) 来减少推送对客户端性能造成的影响
一致性保障线程: 用来监控每一条推送消息, 确认客户端已经收到消息并处理完成, 如果在一定时间内没有收到客户端的反馈, 那么意味着本次推送已经丢失, 此时如果重推本条消息有可能会因为顺序问题而导致客户端数据不一致, 所以在这种情况下, 一致性保障线程会触发一次向此客户端推送全量数据的事件, 来将此客户端的数据进行订正
数据存储模块
数据存储模块中保存了所有注册上来的实例以及它们的元信息, 它相当于是服务端的数据中心, 所有的读取与写入都是基于这个数据中心
我们将注册数据存储在内存的ConcurrentHashMap中, 其中key为实例id, value则为实例的完整信息
同时当数据发生变更时, 会触发相应的DataCenterListener, 来执行相对应的监听逻辑, 比如有一个新的实例加入进来了, 那么对应的监听逻辑就是把这个实例同步给所有订阅这个实例所属服务的客户端
加入注册中心服务端为集群模式部署, 那将会通过DatasourcePublisher将增量变更同步至外部数据源, 由服务端数据同步模块来将数据变更同步至其他注册中心节点, 这一块我们下面会继续展开说明
服务端数据同步模块
此模块专门解决注册中心在集群部署时节点间的数据同步问题
如果是单机模块部署的注册中心, 其实上面几个模块已经完全够用了, 并且不需要额外的数据源, 毕竟注册中心的数据是没有持久化的必要的, 除非数据量大到一定级别时才会使用落盘或落库等操作
当然, 对于注册中心这种核心服务来说, 集群部署那是必须的, 一方面是规避单点宕机问题, 另一方面是解决单机性能瓶颈, 所以我们需要设计一套数据同步机制来将集群中的节点并联起来
跟着我来走一遍这个流程, 首先其中一个注册中心节点的数据发生变更, 并同步到了外部数据源Zk, 此时其它节点中的ZkWatcher监听到了数据变更的事件, 将事件丢入有序队列(EventQueue), 随后队列中的事件被事件处理器(EventProcessor)处理, 将事件消息分类整理后调用数据源变更监听器(DatasourceListener), 更改本地数据中心中的数据, 至此就完成了节点间的数据同步
注意:
- ZkWatcher在监听到数据变更时需要检查此事件是否由自身节点所产生, 如果是, 则忽略即可
- 其他节点将变更同步到本地数据中心时, 由于是写操作, 如果不做区分, 会将变更再次同步到外部数据源 (参考数据存储模块逻辑), 这样就造成了死循环, 所以对本地数据中心的写操作需要告知是否需要同步到外部数据源
健康检测
这个模块主要负责管理所有当前注册实例的状态, 明确哪些实例是可用的, 哪些实例是不可用的
健康检测可以分为客户端上报 和 服务端探测 两种模式, 客户端在注册时可以自己决定使用哪一种检测模式, 两种模式之间各有优劣, 我们直接来看两种模式的实现逻辑
客户端上报: 优点是实现简单, 缺点是检测结果不够靠谱, 假设由于网络配置原因, 其他客户端请求无法进入此实例的情况下, 实例自身是检测不出来的
服务端探测: 优点是检测结果接近实际场景, 比较可靠, 缺点是对注册中心的性能会造成影响, 其次, 该模式要求注册中心与客户端实例网络互通
异常告警
这个模块的目标是让 开发/运维 人员第一时间知道业务服务发生的异常情况
在服务注册发现的过程中, 其实有很多值得记录和回溯的事件, 如: 注册 / 注销 / 禁用 / 不健康 / 恢复健康 / 长链接中断 等等, 其中不乏一些意料之外的事件, 如: 不健康 / 长链接中断 等, 当这些事件发生的时候, 我们需要第一时间知道, 以减小故障时间和范围
从上图可以看到, 服务端在特定的场景下都进行了日志记录, 随后日志文件被 日志收集系统(如: ELK) 收集并转储特定数据库, 随后, 我们可以在 定时任务调度平台 创建一个任务, 定期去扫描数据库中的异常事件, 如果发现新的异常事件数据, 则调用用户触达平台相关服务进行告警发送(如: 短信/钉钉/飞书 等)
数据管理
数据管理是方便我们对注册中心的数据进行查阅和管理
当有了数据以后, 我们希望能有一个途径来对数据进行查看
比如: 我想知道现在有多少个服务? 每个服务有多少个实例? 有多少不健康的实例?
除了查看, 在特定情况下我们需要对数据进行管理
比如: 我想禁用一个实例来对他进行dump
对于数据管理的需求, 还是比较简单粗暴的, 我们创建一个Web前端服务 和 一个给其提供接口的 Protal服务, 注册中心则为Portal提供底层数据操作接口, 就这样数据管理的流程也就通了