概述
本篇,着重于描述 dubbo-registry 模块。
多注册中心机制
dubbo 允许配置多个注册中心,服务引用、服务注册时,则会向多个注册中心拉取 provider 信息或者注册自身信息。
需要注意的是,如果填写了多个注册中心的地址,那么在服务有变更时,会收到多个注册中心的推送(nacos 是这么处理的)
代码入口:PushReceiver#run()
nacos 会针对每个注册地址,创建一个单独的 PushReceiver。
以下为配置 demo
dubbo:
registries:
r1:
address: nacos://127.0.0.1:8848
username: nacos
password: nacos
r2:
address: nacos://127.0.0.1:8846
username: nacos
password: nacos
r3:
address: nacos://127.0.0.1:8847
username: nacos
password: nacos
backup
当注册中心是集群,或多注册中心时,不建议配置多个注册中心,建议将集群地址作为 backup 注册,这样就能避免当 provider 变更时,收到多个注册中心的回调。
配置如下: dubbo.registry.address=nacos://localhost:8848?backup=localshot:8846,localshot:8847
变更推送
当 provider 有变更时, 注册中心会推送 全量的 provider 至客户端,而非变更的 provider.
采用全量,而非增量,在笔者看来的原因是:当只推送增量时,有可能会推送失败,客户端会丢失此次增量变更,client 的信息会与 server 不一致。如果每次都推送全量,则保证了 client 的信息与 server 的信息一致。
nacos 代码入口:PushReceiver#run()
zookeeper 代码入口:ZookeeperRegistry#doSubscribe(URL, NotifyListener)
nacos
nacos 使用 udp 的方式将变更实时推送至客户端。
client 接收 server 消息推送代码入口: PushReceiver#run()
实际业务处理代码入口: RegistryDirectory#notify(List<URL>)
重试机制
代码入口: FailbackRegistry(URL)
在 client 注册、订阅失败后,client 默认至多重试 2 次,每次重试间隔 5s。
client 缓存机制
通用性 failback 缓存
默认情况下,client 侧在连接注册中心前,会优先从本地加载所需要的 provider 缓存。
该行为,可通过参数 file.cache
来控制。
该缓存作用: 当执行订阅失败后,会使用缓存的 provider,来做订阅操作。
代码入口: FailbackRegistry#subscribe(URL, NotifyListener)
file.cache
代码入口:AbstractRegistry(URL)
默认从 ${user.home}/.dubbo/dubbo-registry-${applicationName}-${注册中心IP-注册中心端口}.cache
加载缓存。
文件是以 application 为单位。文件中,存储的是每个 application 所需要的 provider 的缓存信息。
例如:
file.cache 写入
代码入口: AbstractRegistry#saveProperties(URL)
触发文件写入的几个时机
- consumer 在执行 refer 时,会从注册中心获取 provider 信息,之后则将信息写入文件
- provider 有变更时, 会重新将所有 provider 信息写入文件
需要注意的是:该缓存文件默认为异步写入。
nacos 中的缓存机制
需要注意:zookeeper 中,没有使用额外的缓存机制。
nacos 在启动后,会从本地路径 ${user.home}/nacos/naming/public
加载所有 provider 的缓存信息。
代码入口: DiskCache#read(String)
consumer 在做 refer 时,则会优先使用这部分缓存信息。因此,如果本地和 nacos-server 信息不一致,则启动很容易失败
nacos 的缓存文件是以 provider 为单位的。每个文件存储了,该 provider 所有的信息。
{
"hosts":[
{
"ip":"192.168.31.100",
"port":12435,
"valid":true,
"healthy":true,
"marked":false,
"instanceId":"192.168.31.100#12435#DEFAULT#DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"metadata":{
"side":"provider",
"methods":"hello",
"release":"2.7.8",
"deprecated":"false",
"dubbo":"2.0.2",
"weight":"122",
"pid":"99343",
"interface":"org.csp.learn.dubbo.nacos.provider.api.service.HelloService",
"actives":"100",
"generic":"false",
"timeout":"4000",
"path":"org.csp.learn.dubbo.nacos.provider.api.service.HelloService",
"protocol":"dubbo",
"delay":"5000",
"metadata-type":"remote",
"application":"provider-service",
"dynamic":"true",
"category":"providers",
"anyhost":"true",
"timestamp":"1687396394033"
},
"enabled":true,
"weight":1,
"clusterName":"DEFAULT",
"serviceName":"DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"ephemeral":true
},
{
"ip":"192.168.31.100",
"port":12436,
"valid":true,
"healthy":true,
"marked":false,
"instanceId":"192.168.31.100#12436#DEFAULT#DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"metadata":{
"side":"provider",
"methods":"hello",
"release":"2.7.8",
"deprecated":"false",
"dubbo":"2.0.2",
"weight":"122",
"pid":"3328",
"interface":"org.csp.learn.dubbo.nacos.provider.api.service.HelloService",
"actives":"100",
"generic":"false",
"timeout":"4000",
"path":"org.csp.learn.dubbo.nacos.provider.api.service.HelloService",
"protocol":"dubbo",
"delay":"5000",
"metadata-type":"remote",
"application":"provider-service",
"dynamic":"true",
"category":"providers",
"anyhost":"true",
"timestamp":"1687398868099"
},
"enabled":true,
"weight":1,
"clusterName":"DEFAULT",
"serviceName":"DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"ephemeral":true
}
],
"dom":"DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"name":"DEFAULT_GROUP@@providers:org.csp.learn.dubbo.nacos.provider.api.service.HelloService::",
"cacheMillis":10000,
"lastRefTime":1687398869399,
"checksum":"8ec1bc3e45da8f0c4c0a128aea4bec84",
"useSpecifiedURL":false,
"clusters":"",
"env":"",
"metadata":{
}
}
nacos 缓存写入
代码入口: DiskCache#write(ServiceInfo, String)
每次写入全量的 provider 信息至缓存。
触发写入时机: provider 有变更
nacos 缓存机制带来的弊端
要聊弊端,需要来看下 consumer 在 refer 时的流程。
- 应用启动时,默认读取的是本地缓存信息。
- consumer 引用 provider 时,会向注册中心拉取 provider 的信息
- 如果缓存中有该 provider 信息,则不会向注册中心拉取信息
- 接下来,consumer 将会检查 provider 是否有效。
- 如果 provider 无法连通,则应用程序启动失败
在笔者公司的开发环境中,存在过因为缓存机制而导致的服务启动失败。
我们的测试环境部署在 k8s 上。有时候我们会本地启动 debug。因为测试环境部署在 k8s 上,所以 IP 经常变动。当服务 ip 变更后,本地存储的信息还是旧的,启动就会失败。
因此,建议在测试环境下,关闭 nacos 的缓存机制。
代码设计
先看下 dubbo-registry 模块的代码设计类图。以下类图,基本包含了 dubbo-registry 包含的几个核心功能。
- RegistryFactory 提供创建注册中心 server
- RegistryService 提供订阅、注册功能
- FailbackRegistry 提供重试功能
- HashedWheelTimer 提供时间轮调度算法
- TimerTask 实现任务重新逻辑
- RegistryDirectory 提供注册变更回调处理
从类图上,我们不难看出。框架作者在写代码时,遵循了以下规则。
- 抽象逻辑,定义接口具备功能
- 抽象类实现接口,定义通用的逻辑,并暴露抽象的保护方法,由子类自定义实现
- 子类继承抽象类,实现更具体的逻辑。
在日常开发中,如果我们的业务具有多个不同实现,我们也可以参考以上代码结构。