8 启动器 DubboBootstrap
在说启动器之前先将关注点拉回到第一章《1-DubboV3.3 从一个服务提供者的 Demo 说起》的 main 方法,贴下 DubboBootstrap 相关的代码:
private static void startWithBootstrap() {
ServiceConfig<DemoServiceImpl> service = new ServiceConfig<>();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
// 这一个篇章主要说这里:
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap
.application(new ApplicationConfig("dubbo-demo-api-provider"))
.registry(new RegistryConfig(REGISTRY_URL))
.protocol(new ProtocolConfig(CommonConstants.DUBBO, -1))
.service(service)
.start()
.await();
}
Dubbo3 往云原生的方向走,自然要针对云原生应用进行应用启动、运行、发布等信息做一些建模,而 DubboBootstrap 就是用于启动 Dubbo 服务的,类似于 Netty Bootstrap 类型和 ServerBootstrap 启动器
本章内容会分为几部分介绍,分别为:
- DubboBootstrap Instance Create
- DubboBootstrap 构造函数实例化
- ApplicationConfig 配置加载
- RegistryConfig 配置加载
- ProtocolConfig 配置加载
8.1 DLQ Create Instance
Dubbo 的 Bootstrap 类型为啥要用单例模式:
通过调用其静态方法 getInstance 获取单例实例,之所以设计为单例模式,主要是因为 Dubbo 中一些类(如 ExtensionLoader)只为每个进程设计一个实例
DubboBootstrap#getInstance 获取对象方法:
public static DubboBootstrap getInstance() {
// 双重校验锁第一次判断空
if (instance == null) {
// 为空时都进入排队
synchronized (DubboBootstrap.class) {
// 双重校验锁第二次判断空
if (instance == null) {
// 调用重载方法获取对象
instance = DubboBootstrap.getInstance(ApplicationModel.defaultModel());
}
}
}
return instance;
}
获取 DubboBootstrap 对象重载方法:DubboBootstrap#getInstance(ApplicationModel applicationModel)
computeIfAbsent 方法对 hashMap 中指定 Key 值进行重新计算,若不存在该 Key,则添加到 HashMap 中
instanceMap 设计为 Map<ApplicationModel>, DubboBootstrap>
类型,意味着可以为多个应用程序模型创建不同的启动器,启动多个服务
public static DubboBootstrap getInstance(ApplicationModel applicationModel) {
return ConcurrentHashMapUtils.computeIfAbsent(
instanceMap, applicationModel, _k -> new DubboBootstrap(applicationModel));
}
8.2 DubboBootstrap 构造器
构造器是逻辑比较复杂的地方,详细先来看下代码:
private DubboBootstrap(ApplicationModel applicationModel) {
// 存储应用程序启动模型
this.applicationModel = applicationModel;
// 获取配置管理器:ConfigManager -> 配置管理器的扩展类型 ApplicationExt,扩展名字 config
configManager = applicationModel.getApplicationConfigManager();
// 获取环境信息 Environment:环境信息的扩展类型为 ApplicationExt,扩展名字 environment
environment = applicationModel.modelEnvironment();
// 执行器存储仓库(线程池)ExecutorRepository:扩展类型为 ExecutorRepository,扩展名字 executor
executorRepository = ExecutorRepository.getInstance(applicationModel);
// 初始化并启动应用程序实例 ApplicationDeployer,DefaultApplicationDeployer 类型
applicationDeployer = applicationModel.getDeployer();
// listen deploy events
// 为发布器 设置生命周期回调
applicationDeployer.addDeployListener(new DeployListenerAdapter<ApplicationModel>() {
@Override
public void onStarted(ApplicationModel scopeModel) {
notifyStarted(applicationModel);
}
@Override
public void onStopped(ApplicationModel scopeModel) {
notifyStopped(applicationModel);
}
@Override
public void onFailure(ApplicationModel scopeModel, Throwable cause) {
notifyStopped(applicationModel);
}
});
// register DubboBootstrap bean
// 将启动器对象注册到应用程序模型 applicationModel Bean 工厂中
applicationModel.getBeanFactory().registerBean(this);
}
8.3 ApplicationConfig 加载应用程序配置
在介绍 DubboBootstrap 启动器时,创建好实例对象后,会去加载应用程序配置,如下代码:
bootstrap.application(new ApplicationConfig("dubbo-demo-api-provider"))
ApplicationConfig 构造器比较简单,就是单纯为成员变量 name 属性赋值来标识应用程序的名字
8.3.1 应用程序配置
下面可以直接参考下官网 Dubbo 3.3 版本的配置解释,如下:
属性 | 类型 | 描述 |
---|---|---|
name | string | 当前应用名称,用于注册中心计算应用间依赖关系 注意:消费者和提供者应用名不要一样,此参数不是匹配条件,当前项目叫什么名字就填什么,与提供者消费者角色无关 比如:kylin 应用调用了 morgan 应用的服务,则 kylin 项目配成 kylin,morgan 项目配成 morgan,可能 kylin 也提供其它服务给别人使用,但 kylin 项目永远配成 kylin,这样注册中心将显示 kylin 依赖于 morgan |
version | string | 当前应用的版本 |
owner | string | 应用负责人,用于服务治理,请填写负责人公司邮箱前缀 |
organization | string | 组织名称(BU或部门),用于注册中心区分服务来源 此配置项建议不要使用 autoconfig,直接写死在配置中 比如 china、intl、itu、crm、asc、dw、aliexpress 等 |
architecture | string | 用于服务分层对应的架构 如:intl、china。不同的架构使用不同的分层 |
environment | string | 应用环境,如:develop/test/product 不同环境使用不同的缺省值 |
compiler | string | JAVA 字节码编译器,用于动态类生成 可选:jdk 或 javassist |
logger | string | 日志输出方式,可选:slf4j、jcl、log4j、log4j2、jdk |
registries | List | 应用级注册中心列表 |
registry-ids | string | 服务将注册以逗号分隔的注册表ID列表 |
monitor | MonitorConfig | 应用级监控配置 |
dump-directory | string | 存储线程堆栈信息的文件目录 |
dump-enable | boolean | 是否启用线程堆栈存储 |
qosEnable | boolean | 是否启动 Qos |
qosHost | string | 启动 Qos 绑定的主机 |
qosPort | boolean | 启动 Qos 绑定的端口 |
qosAcceptForeignIp | boolean | 是否允许远程访问 |
qosAnonymousAccessPermissionLevel | string | 支持的匿名访问的权限级别 默认值 NONE |
qosAnonymousAllowCommands | string | 匿名(任何外部ip)允许命令,默认为空,无法访问任何 cmd |
metadataType | String | metadata 传递方式,是以 Provider 视角而言的,Consumer 侧配置无效,可选值有: 1、remote - Provider 把 metadata 放到远端注册中心,Consumer 从注册中心获取 2、local - Provider 把 metadata 放在本地,Consumer 从 Provider 处直接获取 |
registerConsumer | boolean | 是否注册使用者实例,默认为 false |
registerMode | string | 将 interface/instance/all 地址注册到注册中心,默认为 all |
enableEmptyProtection | string | 在空地址通知上启用空保护,默认为 true |
protocol | string | 此应用程序的首选协议(名称) |
shutwait | string | 应用程序关闭时间 |
hostname | string | 主机名 |
enableFileCache | string | 是否开启本地文件缓存 |
metadataServiceProtocol | string | 用于点对点的元数据传输的协议 |
metadataServicePort | Integer | 元数据服务端口号,用于服务发现 |
livenessProbe | string | Liveness 存活探针 用于设置 qos 中探测器的扩展 |
readinessProbe | string | Readiness 就绪探针 |
startupProbe | string | Startup 启动探针 |
官网的配置很详细了,上面有一些属性是值得注意的,比如:name、compiler、logger、metadata-type
等
我们可能要多看下默认值是什么,方便我们在使用过程中遇到问题,方便进行排查
8.3.2 应用程序配置加载过程
DubboBootstrap#application 方法可设置一个应用程序配置 ApplicationConfig 对象
public DubboBootstrap application(ApplicationConfig applicationConfig) {
// 将启动器构造函数中初始化的默认应用程序模型对象传递给配置对象
applicationConfig.setScopeModel(applicationModel);
// 将配置信息添加到配置管理器中
configManager.setApplication(applicationConfig);
return this;
}
直接来看 ConfigManager#setApplication 方法是如何实现的
public void setApplication(ApplicationConfig application) {
addConfig(application);
}
执行 AbstractConfigManager#addConfig 配置管理器方法,如下:
public final <T extends AbstractConfig> T addConfig(AbstractConfig config) {
if (config == null) {
return null;
}
// ignore MethodConfig
// 检查配置管理器可管理的配置对象
// 目前支持的配置:ApplicationConfig、MonitorConfig、MetricsConfig、SslConfig
// ProtocolConfig、RegistryConfig、ConfigCenterConfig、MetadataReportConfig
if (!isSupportConfigType(config.getClass())) {
throw new IllegalArgumentException("Unsupported config type: " + config);
}
if (config.getScopeModel() != scopeModel) {
config.setScopeModel(scopeModel);
}
// 缓存中是否存在
Map<String, AbstractConfig> configsMap = configsCache.computeIfAbsent(getTagName(config.getClass()), type -> new ConcurrentHashMap<>());
// fast check duplicated equivalent config before write lock
// 不是服务级配置则直接从缓存中读取到配置后返回
if (!(config instanceof ReferenceConfigBase || config instanceof ServiceConfigBase)) {
for (AbstractConfig value : configsMap.values()) {
if (value.equals(config)) {
return (T) value;
}
}
}
// lock by config type
// 添加配置
synchronized (configsMap) {
return (T) addIfAbsent(config, configsMap);
}
}
添加配置调用的是 AbstractConfigManager#addIfAbsent 方法,如下:
private <C extends AbstractConfig> C addIfAbsent(C config, Map<String, C> configsMap) throws IllegalStateException {
// 配置信息为空直接返回
if (config == null || configsMap == null) {
return config;
}
// find by value
// 根据配置规则判断,配置存在则返回
Optional<C> prevConfig = findDuplicatedConfig(configsMap, config);
if (prevConfig.isPresent()) {
return prevConfig.get();
}
// 生成配置 key
String key = config.getId();
if (key == null) {
do {
// generate key if id is not set
key = generateConfigId(config);
} while (configsMap.containsKey(key));
}
// 不相同的配置 key 重复则输出告警日志
C existedConfig = configsMap.get(key);
if (existedConfig != null && !isEquals(existedConfig, config)) {
String type = config.getClass().getSimpleName();
logger.warn(
COMMON_UNEXPECTED_EXCEPTION,
"",
"",
String.format(
"Duplicate %s found, there already has one default %s or more than two %ss have the same id, "
+ "you can try to give each %s a different id, override previous config with later config. id: %s, prev: %s, later: %s",
type, type, type, type, key, existedConfig, config));
}
// override existed config if any
// 将配置对象存入 configsMap 对象中,configsMap 来源于 configsCache
configsMap.put(key, config);
return config;
}
8.4 RegistryConfig 加载注册中心配置
在介绍 DubboBootstrap 启动器时,创建好实例对象后,会去加载注册中心配置,如下代码:
// DubboBootstrap
bootstrap.registry(new RegistryConfig("nacos://127.0.0.1:8848"))
8.4.1 注册中心配置
下面可以直接参考下官网 Dubbo 3.3 版本的配置解释,如下:
属性 | 类型 | 描述 |
---|---|---|
id | string | 注册中心引用 BeanId 可在 <dubbo:service registry=""> 或 <dubbo:reference registry=""> 中引用此 ID |
address | string | 注册中心服务器地址,若地址没有端口缺省为 9090 同一集群内的多个地址用逗号分隔,如:ip:port,ip:port 不同集群的注册中心,配置多个 dubbo:registry 标签 |
protocol | string | 注册中心地址协议,支持dubbo , multicast , zookeeper , redis , consul(2.7.1) , sofa(2.7.2) , etcd(2.7.2) , nacos(2.7.2) 等协议 |
port | int | 注册中心缺省端口,当 address 没有带端口时使用此端口做为缺省值 |
username | string | 登录注册中心用户名,如果注册中心不需要验证可不填 |
password | string | 登录注册中心密码,如果注册中心不需要验证可不填 |
transport | string | 网络传输方式,可选 mina,netty |
timeout | int | 注册中心请求超时时间(毫秒) |
session | int | 注册中心会话超时时间(毫秒),用于检测提供者非正常断线后的脏数据,比如用心跳检测的实现,此时间就是心跳间隔,不同注册中心实现不一样 |
file | string | 使用文件缓存注册中心地址列表及服务提供者列表,应用重启时将基于此文件恢复 注意:两个注册中心不能使用同一文件存储 |
wait | int | 停止时等待通知完成时间(毫秒) |
check | boolean | 注册中心不存在时,是否报错 |
register | boolean | 是否向此注册中心注册服务 如果设为 false,将只订阅,不注册 |
subscribe | boolean | 是否向此注册中心订阅服务 如果设为 false,将只注册,不订阅 |
dynamic | boolean | 服务是否动态注册 如果设为 false,注册后将显示为 disable 状态,需人工启用 并且服务提供者停止时,也不会自动取消注册,需人工禁用 |
group | string | 服务注册分组,跨组的服务不会相互影响,也无法相互调用,适用于环境隔离。 |
simplified | boolean | 注册到注册中心的 URL 是否采用精简模式的(与低版本兼容) |
extra-keys | string | 在 simplified=true 时,extraKeys 允许在默认参数外将额外的 key 放到 URL 中 格式:“interface,key1,key2” |
server | string | |
client | string | |
cluster | string | 影响流量在注册中心之间的分布,在订阅多个注册中心时很有用,可用选项:1 区域感知,特定类型的流量总是根据流量的来源进入一个注册表 |
zone | string | 注册表所属的区域,通常用于隔离流量 |
parameters | Map<String, String> | 自定义参数 |
useAsConfigCenter | boolean | 该地址是否用作配置中心 |
useAsMetadataCenter | boolean | 该地址是否用作远程元数据中心 |
accepts | string | 注册表可接受的 rpc 协议列表,例如:dubbo、rest |
preferred | boolean | 设置为 true,则始终首先使用此注册表,这在订阅多个注册表时非常有用 |
weight | Integer | 影响注册中心之间的流量分布 当订阅多个注册中心仅在未指定首选注册中心时才生效时,此功能非常有用 |
registerMode | string | 注册模式:实例级、接口级、所有 |
enableEmptyProtection | boolean | 收到的空 url 地址列表和空保护被禁用,将清除当前可用地址 |
8.4.2 注册中心加载过程
首先看的是 RegistryConfig 类型的构造器
public RegistryConfig(String address) {
setAddress(address);
}
其次是调用 RegistryConfig#setAddress 方法,如下:
public void setAddress(String address) {
// 保存地址
this.address = address;
// 下面是支持将参数拼接在 URL 地址后面,比如:用户名、密码、协议、端口
// 这几个参数提前做解析并放入成员变量中
if (address != null) {
try {
// 地址转 Dubbo URL 对象,该 URL 会自行实现 URL 封装信息的类型
URL url = URL.valueOf(address);
// Refactor since 2.7.8
// 值不存在时更新属性,非常巧妙的代码设计,通过 泛型+Consumer 重构了多个 if 判断
// 第一个参数值不存在则调用第二个方法,第二个方法的参数为第三个方法
updatePropertyIfAbsent(this::getUsername, this::setUsername, url.getUsername());
updatePropertyIfAbsent(this::getPassword, this::setPassword, url.getPassword());
updatePropertyIfAbsent(this::getProtocol, this::setProtocol, url.getProtocol());
updatePropertyIfAbsent(this::getPort, this::setPort, url.getPort());
// 移除掉 url 中的 backup 自定义参数 (备份的注册中心地址)
Map<String, String> params = url.getParameters();
if (CollectionUtils.isNotEmptyMap(params)) {
params.remove(BACKUP_KEY);
}
// 将自定义参数存储到成员变量中
updateParameters(params);
} catch (Exception ignored) {
}
}
}
再回过头来看 DubboBootstrap#registry 方法,如下:
public DubboBootstrap registry(RegistryConfig registryConfig) {
// 将 ApplicationModel 对象设置给注册中心配置对象
registryConfig.setScopeModel(applicationModel);
// 将注册中心配置对象添加到配置管理器中
configManager.addRegistry(registryConfig);
return this;
}
直接来看 ConfigManager#addRegistry 方法是如何实现的
public void addRegistry(RegistryConfig registryConfig) {
addConfig(registryConfig);
}
后面的执行流程其实就是和 【8.3.2 应用程序配置加载过程】 —> AbstractConfigManager#addConfig 方法一摸一样的
8.5 ProtocolConfig 加载协议配置
在介绍 DubboBootstrap 启动器时,创建好实例对象后,会去加载协议配置,如下代码:
// DubboBootstrap
bootstrap.protocol(new ProtocolConfig(/*dubbo*/CommonConstants.DUBBO, -1))
8.4.1 协议配置
下面可以直接参考下官网 Dubbo 3.3 版本的配置解释,如下:
属性 | 类型 | 描述 |
---|---|---|
id | string | 协议BeanId,可以在 <dubbo:service protocol=""> 中引用此 ID 如果 ID 不填,缺省和 name 属性值一样,重复则在 name 后加序号 |
name | string | 协议名称 |
port | int | 服务端口 |
host | string | 服务主机名,多网卡选择或指定 VIP 及域名时使用,为空则自动查找本机 IP 建议不要配置,让 Dubbo 自动获取本机IP |
threadpool | string | 线程池类型,可选:fixed/cached |
threadname | string | 线程池名称 |
corethreads | Integer | 线程池核心线程大小 |
alive | Integer | 线程池 keepAliveTime 默认时间单位:毫秒 |
exchanger | string | 交换器配置信息如何交换 |
prompt | string | 命令行提示符 |
status | string | 状态检查 |
sslEnabled | Boolean | SSL 是否启用 |
threads | int | 服务线程池大小(固定大小) |
iothreads | int | IO 线程池大小(固定大小) |
accepts | int | 服务提供方最大可接受连接数 |
payload | int | 请求及响应数据包大小限制,单位:字节 |
codec | string | 协议编码方式 |
serialization | string | 协议序列化方式,当协议支持多种序列化方式时使用 比如:dubbo 协议 dubbo、hessian2、java、compactedjava 以及 http 协议的 json 等 |
accesslog | string/boolean | 设为 true,将向 logger 中输出访问日志 也可填写访问日志文件路径,直接把访问日志输出到指定文件 |
path | string | 提供者上下文路径,为服务 path 前缀 |
transporter | string | 协议的服务端和客户端实现类型 比如:dubbo 协议的 mina、netty 等,可拆分为 server、client 配置 |
server | string | 协议的服务器端实现类型 比如:dubbo 协议的 mina、netty 等,http 协议的 jetty、servlet 等 |
client | string | 协议的客户端实现类型 比如:dubbo 协议的 mina、netty 等 |
dispatcher | string | 协议的消息派发方式,用于指定线程模型 比如:dubbo 协议的 all、direct、message、execution、connection 等 |
queues | int | 线程池队列大小 当线程池满时,排队等待执行的队列大小,建议不要设置 当线程池满时应立即失败,重试其它服务提供机器,而不是排队,除非有特殊需求 |
charset | string | 序列化编码 |
buffer | int | 网络读写缓冲区大小 |
heartbeat | int | 心跳间隔,对于长连接,当物理层断开时 比如拔网线,TCP FIN 消息来不及发送,对方收不到断开事件,此时需要心跳来帮助检查连接是否已断开 |
telnet | string | 所支持的 telnet 命令,多个命令用逗号分隔 |
register | boolean | 该协议的服务是否注册到注册中心 |
contextpath | String |
8.4.2 注册中心加载过程
一开始配置了协议类型为 dubbo,端口为 -1,服务会分配一个没有被占用的端口
下面先来看 DubboBoostrap#protocol 方法是如何运行的
public DubboBootstrap protocol(ProtocolConfig protocolConfig) {
// 配置信息转 List
return protocols(singletonList(protocolConfig));
}
继续看 DubboBoostrap#protocols 方法
public DubboBootstrap protocols(List<ProtocolConfig> protocolConfigs) {
if (CollectionUtils.isEmpty(protocolConfigs)) {
return this;
}
for (ProtocolConfig protocolConfig : protocolConfigs) {
// 将 ApplicationModel 对象设置给注册中心配置对象
protocolConfig.setScopeModel(applicationModel);
// 将协议配置对象添加到配置管理器中
configManager.addProtocol(protocolConfig);
}
return this;
}
向配置管理器中添加协议配置对象的逻辑,其实和 【8.3.2 应用程序配置加载过程】【8.4.2 注册中心加载过程】 —> AbstractConfigManager#addConfig 方法一摸一样的
8.5 DubboBootstrap 启动器构建及配置加载
参考文献
https://www.ktyhub.com/zh/chapter_dubbo/8-dubbobootstrap/
https://www.ktyhub.com/zh/chapter_dubbo/9-application-config/