每日一博 | 线程隔离浅析
前言
随着微服务的流行,单体应用被拆分成一个个独立的微进程,可能一个简单的请求,需要多个微服务共同处理,这样其实是增加了出错的概率,所以如何保证在单个微服务出现问题的时候,对整个系统的负面影响降到最低,这就需要用到我们今天要介绍的线程隔离。
线程模型
在介绍线程隔离之前,我们先了解一下主流容器,框架的线程模型,因为微服务是一个个独立的进程,之间的调用其实就是走网络io,网络io的处理容器如tomcat,通信框架如netty,微服务框架如dubbo,都很好的帮我们处理了底层的网络io流,让我们可以更加的关注于业务处理;
Netty
Netty是基于java nio的高性能通信框架,使用了主从多线程模型,借鉴Netty系列之 Netty线程模型的一张图片如下所示:
主线程负责认证,连接,成功之后交由从线程负责连接的读写操作,大致如下代码:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup);
主线程是一个单线程,从线程是一个默认为cpu*2个数的线程池,可以在我们的业务handler中做一个简单测试:
public void channelRead(ChannelHandlerContext ctx, Object msg) { System.out.println("thread name=" + Thread.currentThread().getName() + " server receive msg=" + msg); }
服务端在读取数据的时候打印一下当前的线程:
thread name=nioEventLoopGroup-3-1 server receive msg="..."
可以发现这里使用的线程其实和处理io线程是同一个;
Dubbo
Dubbo的底层通信框架其实使用的就是Netty,但是Dubbo并没有直接使用Netty的io线程来处理业务,可以简单在生产者端输出当前线程名称:
thread name=DubboServerHandler-192.168.1.115:20880-thread-2,...
可以发现业务逻辑使用并不是nioEventLoopGroup线程,这是因为Dubbo有自己的线程模型,可以看看官网提供的模型图:
其中的Dispatcher调度器可以配置消息的处理线程:
all
所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。direct
所有消息都不派发到线程池,全部在 IO 线程上直接执行。message
只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO 线程上执行。execution
只有请求消息派发到线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。connection
在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。
Dubbo默认使用FixedThreadPool,线程数默认为200;
Tomcat
Tomcat可以配置四种线程模型:BIO,NIO,APR,AIO;Tomcat8开始默认配置NIO,此模型和Netty的线程模型很像,可以理解为都是Reactor模式,在此不过多介绍;其中maxThreads参数配置专门处理IO的Worker数,默认是200;可以在业务Controller中输出当前线程名称:
ThreadName=http-nio-8888-exec-1...
可以发现处理业务的线程就是Tomcat的io线程;
为什么要线程隔离
从上面的介绍的线程模型可以知道,处理业务的时候还是使用的io线程比如Tomcat和netty,这样会有什么问题那,比如当前服务进程需要同步调用另外三个微服务,但是由于某个服务出现问题,导致线程阻塞,然后阻塞越积越多,占满所有的io线程,最终当前服务无法接受数据,直至奔溃;
Dubbo本身做了IO线程和业务线程的隔离,出现问题不至于影响IO线程,但是如果同样有以上的问题,业务线程也会被占满;
做线程隔离的目的就是如果某个服务出现问题可以把它控制在一个小的范围,不至于影响到全局;
如何做线程隔离
做线程隔离原理也很简单,给每个请求分配单独的线程池,每个请求做到互不影响,当然也可以使用一些成熟的框架比如Hystrix(已经不更新了),Sentinel等;
线程池隔离
SpringBoot+Tomcat做一个简单的隔离测试,为了方便模拟配置MaxThreads=5,提供隔离Controller,大致如下所示:
@RequestMapping("/h1") String home() throws Exception { System.out.println("h1-->ThreadName=" + Thread.currentThread().getName()); Thread.sleep(200000); return "h1"; } @RequestMapping("/h3") String home3() { System.out.println("h3-->ThreadName=" + Thread.currentThread().getName()); return "h3"; }
请求5次/h1请求,再次请求/h3,观察日志:
h1-->ThreadName=http-nio-8888-exec-1 h1-->ThreadName=http-nio-8888-exec-2 h1-->ThreadName=http-nio-8888-exec-3 h1-->ThreadName=http-nio-8888-exec-4 h1-->ThreadName=http-nio-8888-exec-5
可以发现h1请求占满了5条线程,请求h3的时候Tomcat无法接受请求;改造一下h1请求使用使用线程池来处理:
ExecutorService executorService = Executors.newFixedThreadPool(2); List<Future<String>> list = new CopyOnWriteArrayList<Future<String>>(); @RequestMapping("/h2") String home2() throws Exception { Future<String> result = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("h2-->ThreadName=" + Thread.currentThread().getName()); Thread.sleep(200000); return "h2"; } }); list.add(result); //降级处理 if (list.size() >= 3) { return "h2-fallback"; } String resultStr = result.get(); list.remove(result); return resultStr; }
如上部分伪代码,使用线程池异步执行,并且超出限制范围做降级处理,这样再次请求h3的时候,就不受影响了;当然上面代码比较简陋,我们可以使用成熟的隔离框架;
Hystrix
Hystrix 提供两种隔离策略:线程池隔离(Bulkhead Pattern)和信号量隔离,其中最推荐也是最常用的是线程池隔离。Hystrix的线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败,并可以提供fallback机制;可以看一个简单的实例:
public class HelloCommand extends HystrixCommand<String> { public HelloCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup")) .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name)) .andCommandPropertiesDefaults( HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(20000)) .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withMaxQueueSize(5) // 配置队列大小 .withCoreSize(2) // 配置线程池里的线程数 )); } @Override protected String run() throws InterruptedException { StringBuffer sb = new StringBuffer("Thread name=" + Thread.currentThread().getName() + ","); Thread.sleep(2000); return sb.append(System.currentTimeMillis()).toString(); } @Override protected String getFallback() { return "Thread name=" + Thread.currentThread().getName() + ",fallback order"; } public static void main(String[] args) throws InterruptedException, ExecutionException { List<Future<String>> list = new ArrayList<>(); System.out.println("Thread name=" + Thread.currentThread().getName()); for (int i = 0; i < 8; i++) { Future<String> future = new HelloCommand("hystrix-order").queue(); list.add(future); } for (Future<String> future : list) { System.out.println(future.get()); } Thread.sleep(1000000); } }
如上配置了处理此业务的线程数为2,并且指定当线程满了之后可以放入队列的最大数量,运行此程序结果如下:
Thread name=main Thread name=hystrix-hystrix-order-1,1589776137342 Thread name=hystrix-hystrix-order-2,1589776137342 Thread name=hystrix-hystrix-order-1,1589776139343 Thread name=hystrix-hystrix-order-2,1589776139343 Thread name=hystrix-hystrix-order-1,1589776141343 Thread name=hystrix-hystrix-order-2,1589776141343 Thread name=hystrix-hystrix-order-2,1589776143343 Thread name=main,fallback order
主线程执行可以理解为就是io线程,业务执行使用的是hystrix线程,线程数2+队列5可以同时处理7条并发请求,超过的部分直接fallback;
信号量隔离
线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的开销比较大,特别是对低延时的调用有比较大的影响;
上面对线程模型的介绍,我们发现Tomcat默认提供了200个io线程,Dubbo默认提供了200个业务线程,线程数已经很多了,如果每个命令在使用一个线程池,线程数会非常多,对系统的影响其实也很大;有一种更轻量的隔离方式就是信号量隔离,仅限制对某个资源调用的并发数,而不是显式地去创建线程池,所以开销比较小;Hystrix和Sentinel都提供了信号量隔离方式,Hystrix已经停止更新,而Sentinel干脆就没有提供线程隔离,或者说线程隔离是没有必要的,完全可以用更轻量的信号量隔离代替;
总结
本文从线程模型开始,讲到了IO线程,以及为什么要分开IO线程和业务线程,具体如何去实现,最后简单介绍了一下更加轻量的信号量隔离,为什么说更加轻量哪,其实业务还是在IO线程处理,只不过会限制某个资源的并发数,没有多余的线程产生;当然也不是说线程隔离就没有价值了,其实还是要根据实际情况来定,根据你使用的容器,框架本身的线程模型来决定
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
GuiLite 3.3 发布:要灵活扩展,还要简单粗暴
本次更新,解决了:定制多样化问题,并优化了软件启动速度。 定制需求: 随着开发群的日益壮大,有想法的开发者层出不穷;倒是群主太过正常,显得跟大家有点格格不入了。大家需求是这样的: 我的硬件,没有framebuffer(这种情况在内存较小的单片机平台很常见),但我也想用GuiLite画出漂亮流畅的效果 我需要一个可以滚动的UI效果,界面上的所有元素(窗口)可以一起上下左右滑动(例如:手机通信录) 我需要一个半透明的窗口,其中窗口的背景是半透明的,文字则是不透明的 我的硬件有2D加速功能,需要GuiLite给与支持 软件方案: 以上任何一个要求都足以推倒原有框架,因为太过底层,一旦修改就是对基础关键部分的手术。如何用最少的代码简单粗暴的解决问题,不伤执行效率,又不伤结构,是对软件架构的一个重大考验。我们的方案如下: 对于有经验的开发者来说,这已经是旧闻了,不仅被解决了,而且已经广泛应用在单片机方案中了;为什么旧事重提,就是因为它与需求2、3、4有很大的相似性,有必要一起做个总结:从c_surface派生一个类c_surface_no_fb,主要的绘制(渲染)接口没有变,只是重新实现draw...
- 下一篇
Visual Studio 已整合 ML.NET 模型构建器
ML.NET 是面向 .NET 开发者的跨平台机器学习框架,模型构建器则是 Visual Studio 中的 UI 工具,使用自动机器学习(AutoML)来训练和使用 .NET 应用程序中的自定义 ML.NET 模型。开发者可以使用 ML.NET 和模型构建器来创建自定义的机器学习模型,而无需具备机器学习经验,也无需离开 .NET 生态系统。 以前,模型构建器属于 Visual Studio 扩展,开发者如果需要使用 ML.NET 模型构建器,必须从 VS Marketplace 进行安装,现在这项工具已被直接整合到Visual Studio 最新版本 16.6 中。 当开发者在 VS 中启用“模型构建器”功能后,在解决方案资源管理器中右键单击项目,并添加"Machine Learning"即可开始构建模型。 另外,微软还更新了模型构建器的“场景”选择画面,以更现代化的界面设计,让开发者可以将要解决的问题,对应到模型构建器所提供的机器学习场景选项上。除此之外,微软还增加异常检测、聚类、预测和对象侦测作为示例场景,不过这些示例目前还不提供 AutoML 支持,仅提供 ML.NET 支持。...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- 设置Eclipse缩进为4个空格,增强代码规范
- MySQL8.0.19开启GTID主从同步CentOS8