1.序言
经过前面两篇文章的介绍,线程池组件主要是为了解决监控线程池的状态以及支持统计、推送告警等功能;组件的基本架构已经搭建完毕,已在项目中进行使用
2.背景
当前所有服务接口都是共用一个线程池,如果某个接口出现延时则会阻塞队列,影响到其它接口服务的正常使用
3.目标
- 线程池资源隔离
- 增强Dubbo线程池
4.具体实现
利用Dubbo SPI 机制,使用自定义线程池替换默认线程池
基于Xml格式:
4.1 先定义线程池组件Bean
@Bean(name = "userThreadPool")
public ThreadPoolExecutor executor1() {
PhxThreadPool pool = new PhxThreadPool(10, 10, 30, TimeUnit.SECONDS,
new PhxResizeLinkedBlockingQueue<>(100));
pool.setName("USER-PROVIDER-1");
return pool;
}
@Bean(name = "roleThreadPool")
public ThreadPoolExecutor executor2() {
PhxThreadPool pool = new PhxThreadPool(20, 20, 30, TimeUnit.SECONDS,
new PhxResizeLinkedBlockingQueue<>(200));
pool.setName("ROLE-PROVIDER-2");
return pool;
}
4.2 配置xml
Provider 使用了 20880 和 20881 端口,自定义线程池名称为 custom
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="demo-provider"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol id="p1" name="dubbo" port="20880" threadpool="custom" />
<dubbo:protocol id="p2" name="dubbo" port="20881" threadpool="custom"/>
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.bingo.provider.api.service.IUserService" protocol="p1"
class="com.bingo.impl.serviceImpl.UserServiceImpl" timeout="120000"/>
<dubbo:service interface="com.bingo.provider.api.service.IRoleService" protocol="p2"
class="com.bingo.impl.serviceImpl.RoleServiceServiceImpl" timeout="120000"/>
</beans>
4.3 配置自定义线程池扩展
在 META-INF/dubbo/ 目录下,创建 com.alibaba.dubbo.common.threadpool.ThreadPool 文件,内容为
custom=com.custom.CustomThreadPool
4.4 编写自定义线程拓展类
参数URL中只能获取到端口信息,无法与Spring中自定义线程池Bean相关联,需提供方设置 端口- 线程池Bean 的映射关系
@Slf4j
public class CustomThreadPool implements ThreadPool {
private static Map<Integer, String> portAndBeanNameRefMap = new HashMap<>(4);
private static boolean ALREADY_LOAD_FLAG = false;
@Override
public Executor getExecutor(URL url) {
// 自定义线程池
Executor executor = customerExecutor(url);
if(executor != null){
return executor;
}
// 默认线程池
return defaultExecutor(url);
}
private Executor customerExecutor(URL url){
Map<String, PhxThreadPool> phxThreadPoolMap = ApplicationContextHolder.getBeansOfType(PhxThreadPool.class);
if(MapUtils.isEmpty(phxThreadPoolMap)) {
return null;
}
int bindPort = url.getPort();
if(!ALREADY_LOAD_FLAG){
portAndBeanNameRefMap = getPortAndBeanNameRef();
}
if(MapUtils.isEmpty(portAndBeanNameRefMap)) {
return null;
}
for(Map.Entry<Integer, String> entry : portAndBeanNameRefMap.entrySet()){
Integer port = entry.getKey();
String beanName = entry.getValue();
if(StringUtils.isBlank(beanName)){
continue;
}
PhxThreadPool threadPool = phxThreadPoolMap.get(beanName);
if(bindPort == port && threadPool != null){
return threadPool;
}
}
return null;
}
private Executor defaultExecutor(URL url){
String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
queues == 0 ? new SynchronousQueue<>() :
(queues < 0 ? new LinkedBlockingQueue<>()
: new LinkedBlockingQueue<>(queues)),
new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}
private Map<Integer,String> getPortAndBeanNameRef(){
Map<Integer, String> resultMap = new HashMap<>();
try {
ClassLoader classLoader = CustomThreadPool.class.getClassLoader();
Enumeration<java.net.URL> urls = classLoader.getResources("META-INF/dubbo/phxThreadPool");
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(resultMap, resourceURL);
}
}catch (Exception ex){
log.error("RefreshContainlListener.threadPoolAndPortRef error: ", ex);
return resultMap;
}finally {
ALREADY_LOAD_FLAG = true;
}
return resultMap;
}
private void loadResource(Map<Integer,String> resultMap, java.net.URL resourceURL) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {
int i = line.indexOf('=');
if (i > 0) {
Integer port;
try {
port = Integer.parseInt(line.substring(0, i).trim());
}catch (Exception ex){
log.error("loadResource parseInt error : ", ex);
continue;
}
line = line.substring(i + 1).trim();
resultMap.put(port, line);
}
}
}
}
} catch (Throwable t) {
log.error("loadResource error: ", t);
}
}
}
4.5 配置端口-线程池映射关系
参考SPI读取资源的方式(可以从源码中直接拷贝读取资源代码,不香吗?)
在 META-INF/dubbo 目录下新建文件 phxThreadPool,配置内容:
20880=userThreadPool
20881=roleThreadPool
4.6 效果
效果一:
效果二:
5.原理解析
5.1 暴露服务部分流程
Dubbo是基于Netty进行传输的,那么Dubbo是怎么做的呢? 先来一张时序图,进入到开启Netty服务这一环节!
跟随时序图到达远程暴露节点 DubboProtocol,开启了服务 openServer,代码如下:
private void openServer(URL url) {
// find server.
String key = url.getAddress();
//client can export a service which's only for server to invoke
boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
if (isServer) {
ExchangeServer server = serverMap.get(key);
if (server == null) {
serverMap.put(key, createServer(url));
} else {
// server supports reset, use together with override
server.reset(url);
}
}
}
/**
大致逻辑就是获取服务机器的地址(一般为 ip:port)作为KEY,如果是一个服务则根据地址信息从缓存的Map获取已经暴露的服务:
如果不存在,则暴露服务
如果存在,则将服务的信息添加到已暴露的服务中
*/
上面几行的逻辑并不复杂,但确是一个关键节点;根据上述逻辑,只要地址不一致,则会重新创建Netty服务 ;
如果想对接口做资源隔离,那么可以创建多个Netty服务,只需要改变服务暴露对应的端口,即可做到资源隔离的效果
紧跟时序图,可以看到最后创建了 NettyServer 对象;
下面部分为对NettyHandler的封装增强
进入构造器中查看,发现对ChannelHandler对象进行了包装处理;
再次进入wrap方法:
ExtensionLoader.getExtensionLoader(Dispatcher.class).getAdaptiveExtension().dispatch(handler, url)
这行代码主要是基于SPI机制加载线程模型分发策略的具体实现 (Dispatcher 默认线程为 all)
可以看到该接口有5个实现类:
官网介绍
进入默认的分发类: AllDispatcher
/**
* default thread pool configure
*/
public class AllDispatcher implements Dispatcher {
public static final String NAME = "all";
@Override
public ChannelHandler dispatch(ChannelHandler handler, URL url) {
return new AllChannelHandler(handler, url);
}
}
进入封装的 AllChannelHandler 对象:
可以看到 AllChannelHandler 中有一些 接收、连接、异常捕获等方法,是否可以大胆假设,这些就是消费方调用时执行真正处理的方法 ?
这里调用了父类 WrappedChannelHandler 有参构造器,再来一张图看一下 ChannelHandler 的关系:
所有的子类都调用了父类的有参构造器,代码如下:
这一段代码的逻辑是基于 SPI 获取拓展线程池
5.2 SPI 解析
SPI的大概逻辑是从资源配置中加载名称和全限定名,并且利用反射机制创建对象,部分对象还会使用set形式做一些增强;创建好的对象会缓存在 ExtensionLoader 的属性中存储;
与SPI经常结合使用的有 @Adaptive 注解:
- 1、该注解在类上时,会成为一个装饰者类,利用dubbo的 ioc 机制做一些功能增强(set 形式)
- 2、该注解在方法上时,会使用 javassist 在运行期间动态生成字节码类文件并加载(代理类)
可以看看加载生成线程池的代码:
executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);
可以看到 ThreadPool 类的方法上使用到 @Adaptive 注解
/**
* ThreadPool
*/
@SPI("fixed")
public interface ThreadPool {
/**
* Thread pool
*
* @param url URL contains thread parameter
* @return thread pool
*/
@Adaptive({Constants.THREADPOOL_KEY})
Executor getExecutor(URL url);
}
因此会先组装好完整的类文件结构,运用操作字节码的javaassist类库,在运行期间动态生成并加载该类;
(生成类文件结构的方法为:com.alibaba.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClassCode)
默认情况下是不会显示的,为了调试方便,可以将Logger级别设置为debug,即可在控制台中输出该类结构
生成的文件如下:
public class ThreadPool$Adaptive implements com.alibaba.dubbo.common.threadpool.ThreadPool {
public java.util.concurrent.Executor getExecutor(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
}
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("threadpool", "fixed");
if(extName == null){
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.common.threadpool.ThreadPool) name from url(" + url.toString() + ") use keys([threadpool])");
}
com.alibaba.dubbo.common.threadpool.ThreadPool extension =
ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.threadpool.ThreadPool.class).getExtension(extName);
return extension.getExecutor(arg0);
}
}
URL中带有提供者自定义的一些属性,上述逻辑会先从url中获取 threadPool 属性,如果获取不到则默认使用fixed,之前我们在xml中配置为 custom ,拿到该属性名称后,会从 ExtensionLoader 的属性中获取之前创建好的 CustomThreadPool 对象,并且调用其 getExecutor 方法,执行我们自定义的一些逻辑。
继续回到主线:
进入父类中,除了参数获取赋值外,重点关注 doOpen 方法,可以看到调用了本类抽象的方法;
这里使用了钩子方法,重新进入子类 NettyServer 的 doOpen 方法;
5.3 处理请求
Netty分为boss线程池和work线程池,boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理;
服务接收到请求后,会委派到上图的 NettyHandler 中进行处理; 即刚才包装过的 ChannelHandler 对象
NettyHandler 类关系图可以看到实现了处理IO事件的 SimpleChannelHandler, 并且重新覆写了部分逻辑处理的关键方法;
有请求连接时,Netty识别分发至开启的服务中方法,如上述的 20880 端口,对应的Handler是 AllChannelHandler 为例:
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
try {
if (channel != null) {
channels.put(NetUtils.toAddressString((InetSocketAddress) ctx.getChannel().getRemoteAddress()), channel);
}
handler.connected(channel);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
}
}
调用了 handler.connected(channel);
/**
获取对象中线程池属性,先获取自定义的线程池,如果拿不到就用系统默认共享的线程池;
会将任务委派到线程中进行处理;
**/
@Override
public void connected(Channel channel) throws RemotingException {
ExecutorService cexecutor = getExecutorService();
try {
cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
} catch (Throwable t) {
throw new ExecutionException("connect event", channel, getClass() + " error when process connected event .", t);
}
}
public ExecutorService getExecutorService() {
ExecutorService cexecutor = executor;
if (cexecutor == null || cexecutor.isShutdown()) {
cexecutor = SHARED_EXECUTOR;
}
return cexecutor;
}
6. 总结
- 写文章期间也倒逼自己更深入的了解源码底层的一些机制原理,期间也踩了一些坑,收获满满