源码分析:Pulsar 的Broker dynamic config 是怎么玩的

有没有听说过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方法:

image.png

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();
            }
        }
    }

复制代码

image.png

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…

扫描二维码关注公众号,回复: 14240106 查看本文章

实现原理

我们设置一个动态配置:

hcb@ubuntu:~/data/code/pulsar/bin$ ./pulsar-admin brokers update-dynamic-config --config  loadBalancerAutoBundleSplitEnabled --value true
复制代码

更改的配置都会在/admin/configuration结点上。

image.png 下面以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级别的配置策略。这个我们下次再进行分析!

猜你喜欢

转载自juejin.im/post/7106075976357904420
今日推荐