有没有听说过Pulsasr的动态配置?namespace 策略 ,以及topic策略,今天我们先来看看动态配置的玩法~
带着疑问来看源码:
- broker的配置是怎么加载的?
- broker的动态配置应该怎么配置? 支持哪些动态配置?有什么需要注意的?
- 动态配置和静态配置有啥区别?
- broker的动态配置是怎么实现的?
好了,正文开始,哦,不,先打个广告: 本文已参与「开源摘星计划」,欢迎正在阅读的你加入。活动链接:github.com/weopenproje…
好了,正文真的开始了!
broker的配置加载
我们启动时的配置在broker.conf文件中。 初始化PulsarBrokerStarter对象的时候会在加载配置:
brokerConfig = loadConfig(starterArguments.brokerConfigFile);
private static ServiceConfiguration loadConfig(String configFile) throws Exception {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
try (InputStream inputStream = new FileInputStream(configFile)) {// 创建文件输入流
// create 负责读入数据流进行load配置
ServiceConfiguration config = create(inputStream, ServiceConfiguration.class);
// it validates provided configuration is completed
isComplete(config);
// 返回ServiceConfiguration,
return config;
}
}
复制代码
配置会转换成ServiceConfiguration对象,这个对象有一堆的set,get方法:
public static <T extends PulsarConfiguration> T create(InputStream inStream,
Class<? extends PulsarConfiguration> clazz) throws IOException, IllegalArgumentException {
try {
checkNotNull(inStream);
Properties properties = new Properties();
// 将输入流转换成KeyValue形式的属性Properties
properties.load(inStream);
return (create(properties, clazz)); // 即将创建配置对象
} finally {
if (inStream != null) {
inStream.close();
}
}
}
复制代码
public static <T extends PulsarConfiguration> T create(Properties properties,
Class<? extends PulsarConfiguration> clazz) throws IOException, IllegalArgumentException {
checkNotNull(properties);
T configuration = null;
try {
configuration = (T) clazz.getDeclaredConstructor().newInstance(); // 通过反射创建配置对象
configuration.setProperties(properties);
update((Map) properties, configuration); // 遍历每个属性Properties,给对象属性赋值,具体就不展开分析了。
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
throw new IllegalArgumentException("Failed to instantiate " + clazz.getName(), e);
}
return configuration;
}
复制代码
broker动态配置
broker.conf 配置是静态的,如果需要更改,必须重启,而且是所有broker结点的配置都需要更改,都要重启,虽然说broker是无状态的,但是这也设计bundle的迁移,客户端需要重新走Lookup流程,这对客户端来说,是有感知的!
动态配置能实现不重启达到配置更改的效果。修改动态配置方法如下:
// cli
设置动态配置
$ pulsar-admin brokers update-dynamic-config --config 配置名--value 配置值
查看所有可动态配置的参数
pulsar-admin brokers list-dynamic-config
查看所有已经动态配置的参数
pulsar-admin brokers get-all-dynamic-config
复制代码
其他restfulapi 和 java client方式请看pulsar.apache.org/docs/next/a…
实现原理
我们设置一个动态配置:
hcb@ubuntu:~/data/code/pulsar/bin$ ./pulsar-admin brokers update-dynamic-config --config loadBalancerAutoBundleSplitEnabled --value true
复制代码
更改的配置都会在/admin/configuration结点上。
下面以loadBalancerAutoBundleSplitEnabled 这个动态配置为例,来分析代码上的实现:
在ServiceConfiguration对象中,loadBalancerAutoBundleSplitEnabled 属性有一个注解,这个注解中显示了dynamic=true 什么?! 你还不知道loadBalancerAutoBundleSplitEnabled ,推荐你看一下Pulsar 负载管理 & Topic归属 Lookup机制
@FieldContext(
dynamic = true,
category = CATEGORY_LOAD_BALANCER,
doc = "enable/disable automatic namespace bundle split"
)
private boolean loadBalancerAutoBundleSplitEnabled = true;
复制代码
在BrokerService中有一个dynamicConfigurationMap 属性,这个属性初始化的时候,BrokerService会将所有broker.conf中能动态更新的配置都缓存一份到dynamicConfigurationMap 中。
private static final ConcurrentOpenHashMap<String, ConfigField> dynamicConfigurationMap =
prepareDynamicConfigurationMap();
private static ConcurrentOpenHashMap<String, ConfigField> prepareDynamicConfigurationMap() {
ConcurrentOpenHashMap<String, ConfigField> dynamicConfigurationMap = new ConcurrentOpenHashMap<>();
for (Field field : ServiceConfiguration.class.getDeclaredFields()) {
if (field != null && field.isAnnotationPresent(FieldContext.class)) { //
field.setAccessible(true);
if (field.getAnnotation(FieldContext.class).dynamic()) { // 如果dynamic属性是true,则会将这个值添加到map中,将所有broker.conf中能动态更新的配置都缓存一份到broker中
dynamicConfigurationMap.put(field.getName(), new ConfigField(field));
}
}
}
return dynamicConfigurationMap;
}
复制代码
另外,我们发现,这个/admin/configuration节点并非是一个临时结点!!!
这样就代表着,即使zk,broker重启,这个配置依旧存在,那么如果zk和broker中都有,最终读取的是哪个配置呢?
答案是优先读取zk的数据
! 所以如果我们改动了broker.conf的配置,但是发现并没有生效,那么你就需要看下zk的配置了!
最后当配置发生改变时怎么办呢? 肯定是使用监听机制对zk的/admin/configuration结点进行监听,一旦发现有配置变更就会更新这个map。我们来看下具体的实现:
// update dynamic configuration and register-listener
updateConfigurationAndRegisterListeners();
private void updateConfigurationAndRegisterListeners() {
// (1) Dynamic-config value validation: add validator if updated value required strict check before considering
// validate configured load-manager classname present into classpath
addDynamicConfigValidator("loadManagerClassName", (className) -> {
try {
Class.forName(className);
} catch (ClassNotFoundException | NoClassDefFoundError e) {
log.warn("Configured load-manager class {} not found {}", className, e.getMessage());
return false;
}
return true;
});
// (2) update ServiceConfiguration value by reading zk-configuration-map ,从这里函数里,我们可以看到其实会去读zk的数据,也就是说,zk的数据会覆盖broker的配置!
updateDynamicServiceConfiguration();
// (3) Listener Registration
// add listener on "maxConcurrentLookupRequest" value change
registerConfigurationListener("maxConcurrentLookupRequest",
(maxConcurrentLookupRequest) -> lookupRequestSemaphore.set(
new Semaphore((int) maxConcurrentLookupRequest, false)));
// add listener on "maxConcurrentTopicLoadRequest" value change
registerConfigurationListener("maxConcurrentTopicLoadRequest",
(maxConcurrentTopicLoadRequest) -> topicLoadRequestSemaphore.set(
new Semaphore((int) maxConcurrentTopicLoadRequest, false)));
复制代码
从上面可以看出,我们为每个值的改变都单独设置了监听回调函数,从这点来看,pulsar做得更为细致,并不是单单监听change事件就一顿遍历所有动态配置,设计十分得到位。
public <T> void registerConfigurationListener(String configKey, Consumer<T> listener) {
validateConfigKey(configKey);
configRegisteredListeners.put(configKey, listener);
}
复制代码
以负载类为例,当loadManagerClassName发生改变之后,会触发创建一个新的LoadManager。其实我认为这里如果可以先判断一下是否和原有配置相等,不相等才会进行更新,这样效果应该是更好的!
registerConfigurationListener("loadManagerClassName", className -> {
pulsar.getExecutor().execute(() -> {
try {
final LoadManager newLoadManager = LoadManager.create(pulsar);
log.info("Created load manager: {}", className);
pulsar.getLoadManager().get().stop();
newLoadManager.start();
pulsar.getLoadManager().set(newLoadManager); // 设置新的负载管理对象!
} catch (Exception ex) {
log.warn("Failed to change load manager", ex);
}
});
});
复制代码
最后我们分析一下updateDynamicServiceConfiguration()从zk读取数据以及覆盖配置的地方 pulsar().getPulsarResources().getDynamicConfigResources() ,另外我们注意到,配置读进来是字符串的,怎么转换成数字和其他类型的呢?
private void updateDynamicServiceConfiguration() {
Optional<Map<String, String>> configCache = Optional.empty();
try {
configCache =
pulsar().getPulsarResources().getDynamicConfigResources().getDynamicConfiguration();
// create dynamic-config if not exist. // 先读取的是zk的数据
if (!configCache.isPresent()) {
pulsar().getPulsarResources().getDynamicConfigResources()
.setDynamicConfigurationWithCreate(n -> Maps.newHashMap());
}
} catch (Exception e) {
log.warn("Failed to read dynamic broker configuration", e);
}
configCache.ifPresent(stringStringMap -> stringStringMap.forEach((key, value) -> {
// validate field
if (dynamicConfigurationMap.containsKey(key) && dynamicConfigurationMap.get(key).validator != null) {
if (!dynamicConfigurationMap.get(key).validator.test(value)) {
log.error("Failed to validate dynamic config {} with value {}", key, value);
throw new IllegalArgumentException(
String.format("Failed to validate dynamic-config %s/%s", key, value));
}
}
// update field value
try {
Field field = ServiceConfiguration.class.getDeclaredField(key);
if (field != null && field.isAnnotationPresent(FieldContext.class)) {
field.setAccessible(true);
field.set(pulsar().getConfiguration(), FieldParser.value(value, field)); // FiedParser.value解析字符串转换成其他类型
log.info("Successfully updated {}/{}", key, value);
}
} catch (Exception e) {
log.warn("Failed to update service configuration {}/{}, {}", key, value, e.getMessage());
}
}));
}
public static Object value(String strValue, Field field) {
checkNotNull(field);
// if field is not primitive type
Type fieldType = field.getGenericType();
if (fieldType instanceof ParameterizedType) {
Class<?> clazz = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
if (field.getType().equals(List.class)) {
// convert to list
return stringToList(strValue, clazz);
} else if (field.getType().equals(Set.class)) {
// covert to set
return stringToSet(strValue, clazz);
} else if (field.getType().equals(Map.class)) {
Class<?> valueClass =
(Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[1];
return stringToMap(strValue, clazz, valueClass);
} else if (field.getType().equals(Optional.class)) {
Type typeClazz = ((ParameterizedType) fieldType).getActualTypeArguments()[0];
if (typeClazz instanceof ParameterizedType) {
throw new IllegalArgumentException(format("unsupported non-primitive Optional<%s> for %s",
typeClazz.getClass(), field.getName()));
}
return Optional.ofNullable(convert(strValue, (Class) typeClazz));
} else {
throw new IllegalArgumentException(
format("unsupported field-type %s for %s", field.getType(), field.getName()));
}
} else {
return convert(strValue, field.getType());
}
}
复制代码
小结
-
broker的配置是怎么加载的?
- 答: 通过解析broker.conf文件里的配置生成配置对象,再将这个配置传递给每个服务模块
-
broker的动态配置应该怎么配置? 支持哪些动态配置?有什么需要注意的?
- 可通过admin cli, restful , lanuage api 配置
- 查看支持配置的动态:pulsar-admin brokers list-dynamic-config
- 注意动态配置高于broker.conf的配置
-
动态配置和静态配置有啥区别?
- 静态配置只对一个broker生效
- 动态配置对所有的broker生效
-
broker的动态配置是怎么实现的?
- 读取元数据的配置进行动态配置的,当发现有数据改变会触发对应的函数。
-
注:除了broker的配置,其实我们还可以设置namespace的配置策略,或者topic级别的配置策略。这个我们下次再进行分析!