Jetty9源码剖析 - Connector组件 - EPC(ExecuteProduceConsume)

转载自ph0ly:http://www.ph0ly.com

一、概念

对于常规的IO操作来说,通常我们会拿一个线程作为生产者,阻塞在select调用上面,等待新的IO事件,当IO到来时,生产者将该事件以及一些数据包装起来放到队列,另一个线程去消费这些事件,然后根据事件类型处理,这样解耦了事件的生产与消费,让IO事件生产和IO事件消费互不阻塞,这种模式在Jetty任务执行模型里面称为ProduceExecuteConsume(简称PEC)。这样确实带来了很多好处,不过对于大量的IO操作来说,例如读事件,这时候唤醒select,说明内核态已经处理完成了,CPU的某个核的寄存器可能还存在热缓存(Hot Cache),用户态去读取数据时,如果放到另一个线程,很大概率并不一定是这个核来执行,而且还存在线程调度的时间开销,因此从底层来看,Jetty做了一个很大的优化,那就是让IO事件生产和消费放到同一个线程,这样很大概率仍然可以在同一个核执行并能利用寄存器热缓存。于是Jetty就搞了个新的任务执行模型ExecuteProduceConsume(简称EPC,又称为EatWhatYouKill)出来,让同一个线程去执行生产和消费,经过Jetty官方测试,这种模型比PEC模型性能高出了近10倍,如下图

Jetty-EPC性能图

二、继承体系

继承体系

继承图还是比较简洁,ExecuteProduceConsume继承自ExecutingExecutionStrategy,实现了ExecutionStrategy策略,而ExecutionStrategy内部定义了Producer接口和Factory执行工厂,比较简单,这里从简,重点关注EPC的实现

三、总体架构

总体架构

正如第一节提到的,EPC是一个任务执行模型,它执行传入的生产者来生产任务,并在同一个线程中执行生产的任务,通常Jetty就拿来执行IO或其他动作,如上图,EPC利用ManagedSelector提供的SelectorProducer.produce方法来生产任务,通常这些任务是IO事件或一些动作(连接、EndPoint创建或销毁等等),如图SelectorProducer.produce主要包含的几个步骤:processSelected处理IO事件、runActions获取动作任务、updateKeys动态调整SelectionKey的读写模式、select等待IO事件,这些方法中processSelected、runActions会返回一个任务,update、select仅是执行自身逻辑,拿到任务(通常就是IO读写、动作)后就在当前的线程执行

四、源码剖析

1. 构造函数

构造函数

构造函数比较简单,没啥说的

2. 启动

由于EPC不是具有生命周期的,因此也就不存在doStart的说法,它的启动是由ManagedSelector的run方法触发(不清楚的读者,请回到上一篇文章了解详情),其实就是EPC的execute方法

执行

execute方法首先拿锁,保证状态改得是原子的,不解释。如果当前EPC是空闲状态,那就改成生产中状态,如果是生产中,那下面的produceConsume方法就能执行,也就是可以执行生产消费,否则当前EPC处于非生产中状态,那就不会执行生产消费

produceConsume

produceConsume首先会判断当前线程池是否处于低线程状态(也就是线程几乎被打满了),那这个时候就执行produceExecuteConsume(PEC,也就是概念里面提到的传统任务执行模型,生产者和消费者在不同线程执行),如果不是低线程状态,那就执行EPC

读者肯定会问,这个低线程模式下为什么要执行PEC?
原因是EPC本身是在同一线程执行生产和消费,它的执行仍然是利用qtp来执行,那如果这个线程被业务代码阻塞,那如果阻塞的多了,线程自然就不够用了,到最后qtp直接被打满,打满了那自然连基础的生产都不行(IO事件没法处理了),这就是线程饥饿状态,那这个时候Jetty做了一个优化,低线程模式下,就执行PEC,如下图,我们来分析下PEC

produceExecuteConsume

executeProduct

PEC会一直判断当前是否是低线程模式,如果是那就会一直占住当前这个线程,并执行生产操作,有任务后,执行executeProduct,其实就是放到了Executor来执行,那其实就是生产和消费在不同的线程执行了,这样就保证了在低线程的情况下,Jetty仍然能够完成生产任务,这样线程饥饿的场景,Jetty也能应付

executeProduceConsume-1

executeProduceConsume-2

接下来分析EPC真正的逻辑
Runnable task = _producer.produce()这行就是调前面的SelectorProducer来生产任务,生产完了改状态_producing为false,如果拿到了任务,如果当前的EPC处于生产状态,那就会将dispatch置位true,也就是当前EPC已经在干活了,线程已经被占住了,于是会进入下面if(dispatch),execute(this),执行自己,EPC本身就是一个Runnable,所以这里在把自己扔到线程池,执行run方法,开启下一个EPC任务(注意这里是生产完成后就会开下一个,并不是等这个生产后的任务执行完了才开下一个EPC线程),开了新的EPC后,会执行拿到的任务,task.run(),之后改状态。下面再来看下EPC的run在干什么

run

run其实就是改状态,然后执行produceConsume,其实和execute逻辑基本一致,这里就不再多说

五、 总结

EPC其实就是Jetty对任务执行的优化,传统的ProduceExecuteConsume(PEC)模型,IO事件监听放到一些线程,而IO事件处理则放到另一些线程,这其实不能很好利用寄存器缓存,Jetty作为高性能Web容器自然要做大量优化,因此就有了EPC的诞生,EPC其实算是一个综合的产物,并不是所有情况全部在同一线程执行任务,而是结合PEC在低线程模式下,仍能够完成生产消费,这是Jetty考虑的非常周全的地方,点赞。所以要做好高性能还得考虑底层的硬件层面的操作,这样才能做到更高的性能。这篇文章就写到这里,看起来还是很复杂,希望对这块有兴趣的读者,仔细阅读,相信你能收获一些东西。接下来的文章我将会深入HTTP的逻辑连接、数据解析、数据的响应,欢迎大家继续关注~

六、参考资料

  1. Eat What You Kill
  2. Thread Starvation with Eat What You Kill
  3. Mechanical Sympathy

猜你喜欢

转载自blog.csdn.net/qq_41084324/article/details/83343109