Hippo4j和DynamicTp动态线程池介绍和使用中遇到的坑

1.前言


1.1Dromara致力于微服务云原生解决方案的组织

      在介绍Hippo4j和DynamicTp动态线程池之前有必要介绍下一个咱们国人的一个开源组织:Dromara致力于微服务云原生解决方案的组织,组织的地址如下:

https://dromara.org/zh/

       这个口号:为往圣继绝学,一个人或许能走的更快,但一群人会走的更远愿景:让每一位开源爱好者,体会到开源的快乐是我比较喜欢的,至于开放里面的兼容性,这个我就不太喜欢了,在使用Hippo4j和DynamicTp两款动态线程池的框架的时候就发现这个问题是在是让人头大,开源的东西一般都是这种兼容性不是那么好,发现框架bug要么不用要么给官方提issues要么自己上手调试改源码。

Dromara开源社区的一些优秀项目如下:

点击官方首页的快速开始,里面还有好多优秀的开源的项目。


1.2 动态线程池的思路

        动态线程池的思路基本都是来自美团的动态线程池的设计思想,链接如下:

https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

        Java线程池可以动态的参数就只有一下几个,无非就是搞一个界面可视化配置动态刷新、监控,告警、适配第三方框架使用的线程池、JDK原生的线程池等等,基本都是这种玩的:


1.3Hippo4j和DynamicTp动态线程池解决什么痛点

       简单来讲就是要达到一个可视化、可动态、可观测和可实时监控告警的效果,为业务测使用了线程池保驾护航,让线上业务平稳运行,不至于由于各种线程池的参数设置不恰当在大流量的冲击下而无法轻松应对,需要根据业务请求的体量来调整线程池各种参数原先基本都需要改代码然后需要部署重启项目,这个可以说是不方便也不快捷和高效,有了动态线程接入即可轻松可视化配置管理各种线程池,动态修改实时感知刷新都不需要重启和修改业务代码等操作,这两个动态线程池的实现可以说是大同小异,各有千秋-----道都是相同的,只不过是术不一样而已。


2.介绍

2.1Hippo4j的官网如下

https://hippo4j.cn/


2.2DynamicTp的官网如下

https://dynamictp.cn/


2.3Hippo4j的架构


2.3.1架构

Hippo4j 从部署的角度上分为两种角色:Server 端和 Client 端。

Server 端是 Hippo4j 项目打包出的 Java 进程,功能包括用户权限、线程池监控以及执行持久化的动作。

Client 端指的是我们 SpringBoot 应用,通过引入 Hippo4j Starter Jar 包负责与 Server 端进行交互。


2.3.2运行模式

2.3.3server控制台

控制台登录页面


2.4DynamicTp的架构

2.5Hippo4j的工程目录结构

     工程目录结构官网都有相应的说明,看工程模块的名字也大致可以猜测是啥功能的:

       这个是Hippo4j的开发分支的代码,如果你下载的是gitHub上的Hippo4j1.4.3的源码包会跑不起来,因为官方在移除common-lang3的依赖的时候出问题了,hipp4j-common的模块中缺少依赖,之前我下载Hippo4j1.4.3的源码包运行也是缺少包,后面在作者的一个群里问那个作者,他说让我拉取开发分支,然后我拉取了也是一样的问题,所以解决办法就是在hipp4j-common的模块中加入入下的依赖:

<dependency>            <groupId>org.apache.commonsgroupId>            <artifactId>commons-lang3artifactId>        dependency>        <dependency>            <groupId>commons-iogroupId>            <artifactId>commons-ioartifactId>            <version>2.3version>        dependency>                <dependency>            <groupId>org.objenesisgroupId>            <artifactId>objenesisartifactId>            <version>3.0.1version>        dependency>                <dependency>            <groupId>commons-beanutilsgroupId>            <artifactId>commons-beanutilsartifactId>            <version>1.9.4version>        dependency>                <dependency>            <groupId>commons-collectionsgroupId>            <artifactId>commons-collectionsartifactId>            <version>3.2.1version>        dependency>                <dependency>            <groupId>commons-logginggroupId>            <artifactId>commons-loggingartifactId>            <version>1.2version>        dependency>



2.6DynamicTp的工程目录结构

     工程目录结构官网都有相应的说明,看工程模块的名字也大致可以猜测是啥功能的:

编译需要注意的问题:


2.7编译禁用测试


2.8二者的异同

相同点自然不用说了,都是实现动态线程池,拒绝策略的选择都是用的是JDK的接口动态代理,大致有如下不同,还有其它的不同有待大家去探索发现:

  1. DynamicTp集成了阿里开源的TTL:TransmittableThreadLocal 是Alibaba开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递ThreadLocal” 问题的 InheritableThreadLocal 扩展。

  2. Hippo4j有自己的server控制台可以替代nacos等开源配置中心

  3. 二者适配第三方线程池有所不同

  4. 二者的拒绝策略除了JDK原生的拒绝策略之外,还有各自的扩展实现

  5. Hippo4j日志可以接入ES6.x

  6. 二者依赖的第三方的配置中心支持有所不同


3.使用中遇到的坑


3.1 本地环境搭建

参看我之前写的博客:

https://mp.weixin.qq.com/s/PQ8PQ3GfW_mLqmzUmxAHSQ


3.2 demo实例

      由于我只写了Hippo4j的集成的demo的实例,但是在这个demo的版本下Hippo4j和DynamicTp都会遇到相同的关于nacos的动态刷新的问题,为此我还给nacos和springCloudAlibaba都提了一个issue,本来之前是只给nacos官方提了一个nacos的,后面nacos官方开发回复让我第一个问题转springCloudAlibaba提issue,所以我又去springCloudAlibaba提了一个issue:


3.2.1nacos提的isues地址

https://github.com/alibaba/nacos/issues/10139


3.2.2springCloudAlibaba提issue地址如下

https://github.com/alibaba/spring-cloud-alibaba/issues/3217


3.2.3demo分享地址

链接:https://pan.baidu.com/s/1-xkNUK-80w1Vf9DE9wcRog 
提取码:fji5

分享内容说明:


3.3demo版本

springBoot版本:2.3.12.RELEASEspring-cloud版本:Hoxton.SR12spring-cloud-alibaba版本:2.2.9.RELEASEnacos的服务端和客户端2.1.1hippo4j-config-spring-boot-starter版本:1.4.3-upgrade

springCloudAlibaba官方的版本对应关系图:

3.4问题的根本原因


3.4.1Hippo4j的SpingClouAlibaba的刷新配置的监听器配置如下


3.4.2DynamicTp的SpingClouAlibaba的刷新配置的监听器配置如下

DynamicTp的SpingClouAlibaba的刷新配置依赖了nacos的这个刷新配置的自动装配的(估计)


3.4.5SpingClouAlibaba的NacosContextRefresher监听器代码如下


3.4.6 根本原因有两个

  1. spring-cloud-alibaba版本:2.2.9.RELEASE的这个版本的扩展配置和共享配置更新不会热更新,只有项目主配置文件在nacos后台配置修改会发生热更新

  2. Hippo4j和DynamicTp的SpingClouAlibaba的刷新配置的自动装配是直接在装配适配类的 afterPropertiesSet()方法中直接加入的,由于这个spring-cloud-alibaba版本2.2.9.RELEASE的这个版本是匿名内部类的方式加入的配置变革的监听器,所以Hippo4j和DynamicTp的方式加入的监听器无效,由于nacos2.1.1的客户端使用的是grpc的方式,nacos2.0以下使用的是http长链接的方式,nacos客户端都用一个ClientWork有一个线程以一定的时间间隔去拉取服务端的配置变更,如果 开启了服务端变更推送的话,在nacos服务端改了配置客户端立马就可以感知到变化,然后拉取变更的配置信息和上一次本地的配置信息的md5去比较,如果两次的md5的值不同,说明配置变更了,会触发cacheData的safeNotifyListener(),如下代码是nacos2.1.1的客户端代码,如果是nacos1.4.1的会有所有不同,但是大致的流程是这种的:

  void checkListenerMd5() {        for (ManagerListenerWrap wrap : listeners) {            if (!md5.equals(wrap.lastCallMd5)) {                safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);            }        }    }

safeNotifyListener方法如下:

private void safeNotifyListener(final String dataId, final String group, final String content, final String type,            final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {        final Listener listener = listenerWrap.listener;        if (listenerWrap.inNotifying) {            LOGGER.warn(                    "[{}] [notify-currentSkip] dataId={}, group={}, md5={}, listener={}, listener is not finish yet,will try next time.",                    name, dataId, group, md5, listener);            return;        }        Runnable job = () -> {            long start = System.currentTimeMillis();            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();            ClassLoader appClassLoader = listener.getClass().getClassLoader();            try {                if (listener instanceof AbstractSharedListener) {                    AbstractSharedListener adapter = (AbstractSharedListener) listener;                    adapter.fillContext(dataId, group);                    LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);                }                // Before executing the callback, set the thread classloader to the classloader of                // the specific webapp to avoid exceptions or misuses when calling the spi interface in                // the callback method (this problem occurs only in multi-application deployment).                Thread.currentThread().setContextClassLoader(appClassLoader);                                ConfigResponse cr = new ConfigResponse();                cr.setDataId(dataId);                cr.setGroup(group);                cr.setContent(content);                cr.setEncryptedDataKey(encryptedDataKey);                configFilterChainManager.doFilter(null, cr);                String contentTmp = cr.getContent();                listenerWrap.inNotifying = true;                listener.receiveConfigInfo(contentTmp);//这里就是触发监听器回调的钩子方法,这两个动态线程池的nacos的动态刷新就是利用了这个个点实现了动态的效果                // compare lastContent and content                if (listener instanceof AbstractConfigChangeListener) {                    Map data = ConfigChangeHandler.getInstance()                            .parseChangeData(listenerWrap.lastContent, contentTmp, type);                    ConfigChangeEvent event = new ConfigChangeEvent(data);                    ((AbstractConfigChangeListener) listener).receiveConfigChange(event);                    listenerWrap.lastContent = contentTmp;                }                                listenerWrap.lastCallMd5 = md5;                LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ,cost={} millis.", name, dataId,                        group, md5, listener, (System.currentTimeMillis() - start));            } catch (NacosException ex) {                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,                        dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());            } catch (Throwable t) {                LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={}", name, dataId, group, md5,                        listener, t);            } finally {                listenerWrap.inNotifying = false;                Thread.currentThread().setContextClassLoader(myClassLoader);            }        };              ............................          ............................    }


3.5demo运行和验证

        demo运行起来是像这种的,不会报错的,如果跑起来就挂了绝大部分原因是因为网络的问题,注意:要使用本地环境来跑的配置,使用上面给那个本地环境搭建的方法来搭建本地环境最好不过,然后使用本文提供的demo来跑,我已经试过了这种修复这个问题是OK的

验证:

保存发布后访问控制器,发现message-consume的核心线程数为28,已经变更了:

4.解决办法

       修改spring-cloud-alibaba版2.2.9.RELEASE的NacosContextRefresher类,在这个类里面匹配到Hippo4j和DynamicTp配置所使用的主配置yaml的data-id和group,然后去触发各自适配类的动态刷新的逻辑即可,修改代码如下所示:

/* * Copyright 2013-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package com.alibaba.cloud.nacos.refresh;
import cn.hippo4j.common.config.ApplicationContextHolder;import cn.hippo4j.config.springboot.starter.config.BootstrapConfigProperties;import cn.hippo4j.config.springboot.starter.refresher.NacosCloudRefresherHandler;import com.alibaba.cloud.nacos.NacosConfigManager;import com.alibaba.cloud.nacos.NacosConfigProperties;import com.alibaba.cloud.nacos.NacosPropertySourceRepository;import com.alibaba.cloud.nacos.client.NacosPropertySource;import com.alibaba.nacos.api.config.ConfigService;import com.alibaba.nacos.api.config.listener.AbstractSharedListener;import com.alibaba.nacos.api.config.listener.Listener;import com.alibaba.nacos.api.exception.NacosException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.context.event.ApplicationReadyEvent;import org.springframework.cloud.endpoint.event.RefreshEvent;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.ApplicationListener;
import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicBoolean;import java.util.concurrent.atomic.AtomicLong;
/** * On application start up, NacosContextRefresher add nacos listeners to all application * level dataIds, when there is a change in the data, listeners will refresh * configurations. * * @author juven.xuxb * @author pbting */public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
private final static Logger log = LoggerFactory .getLogger(NacosContextRefresher.class);
private static final AtomicLong REFRESH_COUNT = new AtomicLong(0);
private NacosConfigProperties nacosConfigProperties;
private final boolean isRefreshEnabled;
private final NacosRefreshHistory nacosRefreshHistory;
private final ConfigService configService;
private ApplicationContext applicationContext;
private AtomicBoolean ready = new AtomicBoolean(false);
private Map listenerMap = new ConcurrentHashMap<>(16);
private BootstrapConfigProperties bootstrapConfigProperties;
private NacosCloudRefresherHandler nacosCloudRefresherHandler;

public NacosContextRefresher(NacosConfigManager nacosConfigManager, NacosRefreshHistory refreshHistory) { this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties(); this.nacosRefreshHistory = refreshHistory; this.configService = nacosConfigManager.getConfigService(); this.isRefreshEnabled = this.nacosConfigProperties.isRefreshEnabled(); this.bootstrapConfigProperties = ApplicationContextHolder.getBean(BootstrapConfigProperties.class); this.nacosCloudRefresherHandler = ApplicationContextHolder.getBean(NacosCloudRefresherHandler.class); }
/** * recommend to use * {@link NacosContextRefresher#NacosContextRefresher(NacosConfigManager, NacosRefreshHistory)}. * * @param refreshProperties refreshProperties * @param refreshHistory refreshHistory * @param configService configService */ @Deprecated public NacosContextRefresher(NacosRefreshProperties refreshProperties, NacosRefreshHistory refreshHistory, ConfigService configService) { this.isRefreshEnabled = refreshProperties.isEnabled(); this.nacosRefreshHistory = refreshHistory; this.configService = configService; }
@Override public void onApplicationEvent(ApplicationReadyEvent event) { // many Spring context if (this.ready.compareAndSet(false, true)) { this.registerNacosListenersForApplications(); } }
@Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; }
/** * register Nacos Listeners. */ private void registerNacosListenersForApplications() { if (isRefreshEnabled()) { for (NacosPropertySource propertySource : NacosPropertySourceRepository .getAll()) { if (!propertySource.isRefreshable()) { continue; } String dataId = propertySource.getDataId(); String group = propertySource.getGroup(); registerNacosListener(group, dataId); } } }
private void registerNacosListener(final String groupKey, final String dataKey) { String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey); Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() { @Override public void innerReceive(String dataId, String group, String configInfo) { refreshCountIncrement(); nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); applicationContext.publishEvent( new RefreshEvent(this, null, "Refresh Nacos config")); if (log.isDebugEnabled()) { log.debug(String.format( "Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo)); } //这里匹配到对应的配置然后触发各自的动态刷新的逻辑即可 Map nacosConfig = bootstrapConfigProperties.getNacos(); String dataId1 = nacosConfig.get("data-id"); String group1 = nacosConfig.get("group"); if (dataId1.equals(dataId) && group1.equals(group)) { nacosCloudRefresherHandler.dynamicRefresh(configInfo); } } }); try { configService.addListener(dataKey, groupKey, listener); } catch (NacosException e) { log.warn(String.format( "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), e); } }
public NacosConfigProperties getNacosConfigProperties() { return nacosConfigProperties; }
public NacosContextRefresher setNacosConfigProperties( NacosConfigProperties nacosConfigProperties) { this.nacosConfigProperties = nacosConfigProperties; return this; }
public boolean isRefreshEnabled() { if (null == nacosConfigProperties) { return isRefreshEnabled; } // Compatible with older configurations if (nacosConfigProperties.isRefreshEnabled() && !isRefreshEnabled) { return false; } return isRefreshEnabled; }
public static long getRefreshCount() { return REFRESH_COUNT.get(); }
public static void refreshCountIncrement() { REFRESH_COUNT.incrementAndGet(); }
}

demo中代码修改如下图所示:


5.总结

        这种开源框架使用确实会遇到各种各样的问题,但是不要害怕、不要灰心,办法总比困难多,联系作者提issue或者联系官方提issue,都不得行的话就只能自己debug然后找问题改源码了,上面的demo只是一个影子,至于什么监控搭建和通知啥的这个就靠大家自己去探索了,我觉得那些都是次要的,主要的就是要能实现动态管理和配置,说白了,如果没有这些动态开源的框架出现,我们的操作还是可以一波梭哈,完全没有这些花里胡哨的东西,只不过是那些大佬觉麻烦,需要偷懒,所以才搞了这种骚操作,但是可以从这些开源实现中学习到一些东西的,如果我的分享对你有帮助,请一键三连,么么么哒!



请使用浏览器的分享功能分享到微信等