关于netty:Java-技术栈中间件优雅停机方案设计与实现全景图

54次阅读

共计 54389 个字符,预计需要花费 136 分钟才能阅读完成。

本系列 Netty 源码解析文章基于 4.1.56.Final 版本

本文概要

在上篇文章 我为 Netty 奉献源码 | 且看 Netty 如何应答 TCP 连贯的失常敞开,异样敞开,半敞开场景 中笔者为大家具体介绍了 Netty 在解决连贯敞开时的残缺过程,并具体介绍了 Netty 如何应答 TCP 连贯在敞开时会遇到的各种场景。

在连贯敞开之后,接下来就轮到 Netty 的谢幕时刻了,本文笔者会为大家详尽 Java 技术栈中间件中对于优雅停机计划的具体设计和实现。

笔者会从日常开发工作中常见的版本公布,服务高低线的场景聊起,引出服务优雅启停的需要,并从这个需要登程,一步一步带大家探索各个中间件里的优雅停机的相干设计。

相熟笔者文风的读者敌人应该晓得,笔者必定不会只是简略的介绍,要么不讲,要讲就要把整个技术体系的前世今生给大家讲清楚,讲明确。

基于这目标,笔者会先从反对优雅停机的底层技术基石 – 内核中的信号量开始聊起。

从内核层咱们接着会聊到 JVM 层,在 JVM 层一探优雅停机底层的技术玄机。

随后咱们会从 JVM 层一路奔袭到 Spring 而后到 Dubbo。在这个过程中,笔者还会带大家一起 Shooting Dubbo 在优雅停机下的一个 Bug,并为大家具体介绍修复过程。

最初由 Dubbo 层的优雅停机,引出咱们的配角 –Netty 优雅停机的设计与实现:

上面咱们来正式开始本文的内容~~

1. Java 过程的优雅启停

在咱们的日常开发工作中,业务需要的迭代和优化随同围绕着咱们整个开发周期,当咱们加班加点实现了业务需要的开发,而后又历经各种艰难险阻通过了测试的验证,最初通过和产品经理的各种纠缠相爱相杀之后,终于到了最最激动人心的时刻程序要部署上线了。

那么在程序部署上线的过程中势必会波及到线上服务的敞开和重启,对于对线上服务的启停这外面有很多的考究,万万不能简略粗犷的进行敞开和重启,因为此时线上服务可能承载着生产的流量,可能正在进行重要的业务解决流程。

比方:用户正在购买商品,钱曾经付了,恰好这时赶上程序上线,如果咱们这时简略粗犷的对服务进行敞开,重启,可能就会导致用户付了钱,然而订单未创立或者商品未呈现在用户的购物清单中,给用户造成了本质的损失,这是十分重大的结果。

为了保障能在程序上线的过程中做到业务无损,所以线上服务的 优雅敞开 优雅启动 显得就十分十分重要了。

1.1 优雅启动

在 Java 程序的运行过程中,程序的运行速度个别会随着程序的运行缓缓的进步,所以从线上体现上来看 Java 程序在运行一段时间后往往会比程序刚启动的时候会快很多。

这是因为 Java 程序在运行过程中,JVM 会一直收集到程序运行时的动态数据,这样能够将高频执行代码通过即时编译成机器码,随后程序运行就间接执行机器码,运行速度齐全不输 C 或者 C++ 程序。

同时在程序执行过程中,用到的类会被加载到 JVM 中缓存,这样当程序再次应用到的时候不会触发长期加载,影响程序执行性能。

咱们能够将以上几点当做 JVM 带给咱们的性能红利,而当应用程序重新启动之后,这些性能红利也就隐没了,如果咱们让新启动的程序持续承当之前的流量规模,那么就会导致程序在刚启动的时候在没有这些性能红利的加持下间接进入高负荷的运行状态,这就可能导致线上申请大面积超时,对业务造成影响。

所以说优雅地启动一个程序是十分重要的,优雅启动的核心思想就是让程序在刚启动的时候不要承当太大的流量,让程序在低负荷的状态下运行一段时间,使其晋升到最佳的运行状态时,在逐渐的让程序承当更大的流量解决。

上面咱们就来看下罕用于优雅启动场景的两个技术计划:

1.1.1 启动预热

启动预热就是让刚刚上线的应用程序不要一下就承当之前的全副流量,而是在一个工夫窗口内缓缓的将流量打到刚上线的应用程序上,目标是让 JVM 先迟缓的收集程序运行时的一些动态数据,将高频代码即时编译为机器码。

这个技术计划在泛滥 RPC 框架的实现中咱们都能够看到,服务调用方会从注册核心拿到所有服务提供方的地址,而后从这些地址中通过特定的负载平衡算法从中选取一个服务提供方的发送申请。

为了可能使刚刚上线的服务提供方有工夫去预热,所以咱们就要从源头上管制服务调用方发送的流量,服务调用方在发动 RPC 调用时应该尽量少的去负载平衡到刚刚启动的服务提供方实例。

那么服务调用方如何能力判断哪些是刚刚启动的服务提供方实例呢?

服务提供方在启动胜利后会向注册核心注册本人的服务信息,咱们能够将服务提供方的实在启动工夫蕴含在服务信息中一起向注册核心注册,这样注册核心就会告诉服务调用方有新的服务提供方实例上线并告知其启动工夫。

服务调用方能够依据这个启动工夫,缓缓的将负载权重减少到这个刚启动的服务提供方实例上。这样就能够解决服务提供方冷启动的问题,调用方通过在一个工夫窗口内将申请缓缓的打到提供方实例上,这样就能够让刚刚启动的提供方实例有工夫去预热,达到平滑上线的成果。

1.1.2 提早裸露

启动预热更多的是从服务调用方的角度通过升高刚刚启动的服务提供方实例的负载平衡权重来实现优雅启动。

而提早裸露则是从服务提供方的角度,提早裸露服务工夫,利用提早的这段时间,服务提供方能够事后加载依赖的一些资源,比方:缓存数据,spring 容器中的 bean。等到这些资源全副加载结束就位之后,咱们在将服务提供方实例裸露进来。这样能够无效升高启动后期申请解决出错的概率。

比方咱们能够在 dubbo 利用中能够配置服务的提早裸露工夫:

// 提早 5 秒裸露服务
<dubbo:service delay="5000" /> 

1.2 优雅敞开

优雅敞开须要思考的问题和解决的场景要比优雅启动要简单的多,因为一个失常在线上运行的服务程序正在承当着生产的流量,同时也正在进行业务流程的解决。

要对这样的一个服务程序进行优雅敞开保障业务无损还是十分有挑战的,一个好的敞开流程,能够确保咱们业务实现平滑的高低线,防止上线之后减少很多不必要的额定运维工作。

上面咱们就来探讨下具体应该从哪几个角度着手思考实现优雅敞开:

1.2.1 切走流量

第一步必定是要将程序承当的现有流量全副切走,通知服务调用方,我要进行敞开了,请不要在给我发送申请。那么如果进行切流呢??

在 RPC 的场景中,服务调用方通过服务发现的形式从注册核心中动静感知服务提供者的高低线变动。在服务提供方敞开之前,首先就要本人从注册核心中勾销注册,随后注册核心会告诉服务调用方,有服务提供者实例下线,请将其从本地缓存列表中剔除。这样就能够使得服务调用方之后的 RPC 调用不在申请到下线的服务提供方实例上。

然而这里会有一个问题,就是通常咱们的注册核心都是 AP 类型的,它只会保障最终一致性,并不会保障实时一致性,基于这个起因,服务调用方感知到服务提供者下线的事件可能是延后的,那么在这个延迟时间内,服务调用方极有可能会向正在下线的服务发动 RPC 申请。

因为服务提供方曾经开始进入敞开流程,那么很多对象在这时可能曾经被销毁了,这时如果在收到申请过去,必定是无奈解决的,甚至可能还会抛出一个莫名其妙的异样进去,对业务造成肯定的影响。

那么既然这个问题是因为注册核心可能存在的提早告诉引起的,那么咱们就很天然的想到了让筹备下线的服务提供方被动去告诉它的服务调用方。

这种服务提供方 被动告诉 在加上注册核心 被动告诉 的两个计划联合在一起应该就能确保十拿九稳了吧。

事实上,在大部分场景下这个计划是可行的,然而还有一种极其的状况须要应答,就是当服务提供方告诉调用方本人下线的网络申请在达到服务调用方之前的很极限的一个工夫内,服务调用者向正在下线的服务提供方发动了 RPC 申请,这种极其的状况,就须要服务提供方和调用方一起配合来应答了。

首先服务提供方在筹备敞开的时候,就把本人设置为正在敞开状态,在这个状态下不会承受任何申请,如果这时遇到了上边这种极其状况下的申请,那么就抛出一个 CloseException(这个异样是提供方和调用方提前约定好的),调用方收到这个 CloseException,则将该服务提供方的节点剔除,并从残余节点中通过负载平衡选取一个节点进行重试,通过让这个申请疾速失败从而保障业务无损。

这三种计划联合在一起,笔者认为就是一个比拟完满的切流计划了。

1.2.2 尽量保障业务无损

当把流量全副切走后,可能此时将要敞开的服务程序中还有正在解决的局部业务申请,那么咱们就必须得等到这些业务解决申请全副处理完毕,并将业务后果响应给客户端后,在对服务进行敞开。

当然为了保障敞开流程的可控,咱们须要引入敞开超时工夫限度,当剩下的业务申请解决超时,那么就强制敞开。

为了保障敞开流程的可控,咱们只能做到尽可能的保障业务无损而不是百分之百保障。所以在程序上线之后,咱们应该对业务异样数据进行监控并及时修复。


通过以上介绍的优雅敞开计划咱们晓得,当咱们将要优雅敞开一个应用程序时,咱们须要做好以下两项工作:

  1. 咱们首先要做的就是将以后将要敞开的应用程序上承载的生产流量全副切走,保障不会有新的流量打到将要敞开的应用程序实例上。
  2. 当所有的生产流量切走之后,咱们还须要保障以后将要敞开的应用程序实例正在解决的业务申请要使其处理完毕,并将业务处理结果响应给客户端。以保障业务无损。当然为了使敞开流程变得可控,咱们须要引入敞开超时工夫。

以上两项工作就是咱们在应用程序将要被敞开时须要做的,那么问题是咱们如何能力晓得应用程序要被敞开呢?换句话说,咱们在应用程序里怎么能力感知到程序过程的敞开事件从而触发上述两项优雅敞开的操作执行呢?

既然咱们有这样的需要,那么操作系统内核必定会给咱们提供这样的机制,事实上咱们能够通过捕捉操作系统给过程发送的信号来获取敞开过程告诉,并在相应信号回调中触发优雅敞开的操作。

接下来让咱们来看一下操作系统内核提供的信号机制:

2. 内核信号机制

信号是操作系统内核为咱们提供用于在过程间通信的机制,内核能够利用信号来告诉过程,以后零碎所产生的的事件(包含敞开过程事件)。

信号在内核中并没有用特地简单的数据结构来示意,只是用一个代号一样的数字来标识不同的信号。Linux 提供了几十种信号,别离代表不同的意义。信号之间依附它们的值来辨别

信号能够在任何时候发送给过程,过程须要为这个信号配置信号处理函数。当某个信号产生的时候,就默认执行对应的信号处理函数就能够了。这就相当于一个操作系统的应急手册,当时定义好遇到什么状况,做什么事件,提前准备好,出了事件照着做就能够了。

内核收回的信号就代表以后零碎遇到了某种状况,咱们须要应答的步骤就封装在对应信号的回调函数中。

信号机制引入的目标就在于:

  • 让利用过程晓得以后曾经产生了某个特定的事件(比方过程的敞开事件)。
  • 强制过程执行咱们当时设定好的信号处理函数(比方封装优雅敞开逻辑)。

通常来说程序一旦启动就会始终运行上来,除非遇到 OOM 或者咱们须要从新公布程序时会在运维脚本中调用 kill 命令关闭程序。Kill 命令从字面意思上来说是杀死过程,然而其本质是向过程发送信号,从而敞开过程。

上面咱们应用 kill -l 命令查看下 kill 命令能够向过程发送哪些信号:

# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

笔者这里提取几个常见的信号来简要阐明下:

  • SIGINT:信号代号为 2。比方咱们在终端以非后盾模式运行一个过程实例时,要想敞开它,咱们能够通过 Ctrl+C 来敞开这个前台程序。这个 Ctrl+C 向过程发送的正是 SIGINT 信号。
  • SIGQUIT:信号代号为 3。比方咱们应用 Ctrl+\ 来敞开一个前台过程,此时会向过程发送 SIGQUIT 信号,与 SIGINT 信号不同的是,通过 SIGQUIT 信号终止的过程会在退出时,通过 Core Dump 将以后过程的运行状态保留在 core dump 文件外面,不便后续查看。
  • SIGKILL:信号代号为 9。通过 kill -9 pid 命令完结过程是十分十分危险的动作,咱们应该坚定禁止这种敞开过程的行为 ,因为 SIGKILL 信号是不能被过程捕捉和疏忽的,只能执行内核定义的默认操作间接敞开过程。 而咱们的优雅敞开操作是须要通过捕捉操作系统信号,从而能够在对应的信号处理函数中执行优雅敞开的动作。因为 SIGKILL 信号不能被捕捉,所以优雅敞开也就无奈实现。当初大家就赶快查看下本人公司生产环境的运维脚本是否是通过 kill -9 pid 命令来完结过程的,肯定要防止用这种形式,因为这种形式是极其有情并且略带仁慈的敞开过程行为。
  • SIGSTOP:信号代号为 19。该信号和 SIGKILL 信号一样都是无奈被应用程序疏忽和捕捉的。向过程发送 SIGSTOP 信号也是无奈实现优雅敞开的。通过 Ctrl+Z 来敞开一个前台过程,发送的信号就是 SIGSTOP 信号。
  • SIGTERM:信号代号为 15。咱们通常会应用 kill 命令来敞开一个后盾运行的过程,kill 命令发送的默认信号就是 SIGTERM,该信号也是本文要探讨的优雅敞开的根底,咱们通常会应用 kill pid 或者 kill -15 pid 来向后盾过程发送 SIGTERM 信号用以实现过程的优雅敞开。大家如果发现自己公司生产环境的运维脚本中应用的是 kill -9 pid 命令来完结过程,那么就要马上换成 kill pid 命令。

以上列举的都是咱们罕用的一些信号,大家也能够通过 man 7 signal 命令查看每种信号对应的含意:

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction


SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
……

而利用过程对于信号的解决个别分为以下三种形式:

  • 内核定义的默认操作: 零碎内核对每种信号都规定了默认操作,比方下面列表 Action 列中的 Term,就是终止过程的意思。前边介绍的 SIGINT 信号和 SIGTERM 信号的默认操作就是 Term。Core 的意思是 Core Dump,即终止过程后会通过 Core Dump 将以后过程的运行状态保留在文件外面,不便咱们预先进行剖析问题在哪里。前边介绍的 SIGQUIT 信号默认操作就是 Core。
  • 捕捉信号:应用程序能够利用内核提供的零碎调用来捕捉信号,并将优雅敞开的步骤封装在对应信号的处理函数中。当向过程发送敞开信号 SIGTERM 的时候,在过程内咱们能够通过捕捉 SIGTERM 信号,随即就会执行咱们自定义的信号处理函数。咱们从而能够在信号处理函数中执行过程优雅敞开的逻辑。
  • 疏忽信号:当咱们不心愿解决某些信号的时候,就能够疏忽该信号,不做任何解决,然而前边介绍的 SIGKILL 信号和 SIGSTOP 是无奈被捕捉和疏忽的,内核会间接执行这两个信号定义的默认操作间接敞开过程。

当咱们不心愿信号执行内核定义的默认操作时,咱们就须要在过程内捕捉信号,并注册信号的回调函数来执行咱们自定义的信号处理逻辑。

比方咱们在本文中要探讨的优雅敞开场景,当过程接管到 SIGTERM 信号时,为了实现过程的优雅敞开,咱们并不心愿过程执行 SIGTERM 信号的默认操作间接敞开过程,所以咱们要在过程中捕捉 SIGTERM 信号,并将优雅敞开的操作步骤封装在对应的信号处理函数中。

2.1 如何捕捉信号

在介绍完了内核信号的分类以及过程对于信号处理的三种形式之后,上面咱们来看下如何来捕捉内核信号,并在对应信号回调函数中自定义咱们的解决逻辑。

内核提供了 sigaction 零碎调用,来供咱们捕捉信号以及与相应的信号处理函数绑定起来。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
  • int signum:示意咱们想要在过程中捕捉的信号,比方本文中咱们要实现优雅敞开就须要在过程中捕捉 SIGTERM 信号,对应的 signum = 15。
  • struct sigaction *act:内核中会用一个 sigaction 构造体来封装咱们自定义的信号处理逻辑。
  • struct sigaction *oldact:这里是为了兼容老的信号处理函数,理解一下就能够了,和本文主线无关。

sigaction 构造体用来封装信号对应的处理函数,以及更加精细化管制信号处理的信息。

struct sigaction {
  __sighandler_t sa_handler;
  unsigned long sa_flags;
        .......
  sigset_t sa_mask; 
};
  • __sighandler_t sa_handler:其实实质上是一个函数指针,用来保留咱们为信号注册的信号处理函数,优雅敞开的逻辑就封装在这里
  • long sa_flags:为了更加精细化的管制信号处理逻辑,这个字段保留了一些管制信号处理行为的选项汇合。常见的选项有:

    • SA_ONESHOT:意思是咱们注册的信号处理函数,仅仅只起一次作用。响应完一次后,就设置回默认行为。
    • SA_NOMASK:示意信号处理函数在执行的过程中会被中断。比方咱们过程捕捉到一个感兴趣的信号,随后会执行注册的信号处理函数,然而此时过程又收到其余的信号或者和上次雷同的信号,此时正在执行的信号处理函数会被中断,从而转去执行最新到来的信号处理函数。如果间断产生多个雷同的信号,那么咱们的信号处理函数就要做好同步,幂等等措施
    • SA_INTERRUPT:当过程正在执行一个十分耗时的零碎调用时,如果此时过程接管到了信号,那么这个零碎调用将会被信号中断,过程转去执行相应的信号处理函数。那么当信号处理函数执行完时,如果这里设置了 SA_INTERRUPT,那么零碎调用将不会继续执行并且会返回一个 -EINTR 常量,通知调用方,这个零碎调用被信号中断了,怎么解决你看着办吧。
    • SA_RESTART:当零碎调用被信号中断后,相应的信号处理函数执行结束后,如果这里设置了 SA_RESTART 零碎调用将会被主动重新启动。
  • sigset_t sa_mask:这个字段次要指定在信号处理函数正在运行的过程中,如果间断产生多个信号,须要屏蔽哪些信号。也就是说当过程收到屏蔽的信号时,正在进行的信号处理函数不会被中断。

屏蔽并不意味着信号肯定失落,而是暂存,这样能够使雷同信号的处理函数,在过程间断接管到多个雷同的信号时,能够一个一个的解决。

最终通过 sigaction 函数会调用到底层的零碎调用 rt_sigaction 函数,在
rt_sigaction 中会将上边介绍的用户态 struct sigaction 构造拷贝为内核态的
k_sigaction,而后调用 do_sigaction 函数。

最初在 do_sigaction 函数中将用户要在过程中捕捉的信号以及相应的信号处理函数设置到过程描述符 task_struct 构造里。

过程在内核中的数据结构 task_struct 中有一个 struct sighand_struct 构造的属性 sighand,struct sighand_struct 构造中蕴含一个 k_sigaction 类型的数组 action[],这个数组保留的就是过程中须要捕捉的信号以及对应的信号处理函数在内核中的构造体 k_sigaction,数组下标为过程须要捕捉的信号。

#include <signal.h>

static void sig_handler(int signum) {if (signum == SIGTERM) {..... 执行优雅敞开逻辑....}

}

int main (Void) {

    struct sigaction sa_usr; // 定义 sigaction 构造体
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_handler;   // 设置信号处理函数

    sigaction(SIGTERM, &sa_usr, NULL);// 过程捕捉信号,注册信号处理函数
        
        ,,,,,,,,,,,,
}

咱们能够通过如上简略的示例代码,将 SIGTERM 信号及其对应的自定义信号处理函数注册到过程中,当咱们执行 kill -15 pid 命令之后,过程就会捕捉到 SIGTERM 信号,随后就能够执行优雅敞开步骤了。

3. JVM 中的 ShutdownHook

在《2. 内核信号机制》大节中为大家介绍的内容是操作系统内核为咱们实现过程的优雅敞开提供的最底层零碎级别的反对机制,在内核的强力反对下,那么本文的主题 Java 过程的优雅敞开就很容易实现了。

咱们要想实现 Java 过程的优雅敞开性能,只须要在过程启动的时候将优雅敞开的操作封装在一个 Thread 中,随后将这个 Thread 注册到 JVM 的 ShutdownHook 中就好了,当 JVM 过程接管到 kill -15 信号时,就会执行咱们注册的 ShutdownHook 敞开钩子,进而执行咱们定义的优雅敞开步骤。

        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {..... 执行优雅敞开步骤.....}
        });

3.1 导致 JVM 退出的几种状况

  1. JVM 过程中最初一个非守护线程退出。
  2. 在程序代码中被动调用 java.lang.System#exit(int status) 办法,会导致 JVM 过程的退出并触发 ShutdownHook 的调用。参数 int status 如果是非零值,则示意本次敞开是在一个非正常状况下的敞开行为。比方:过程产生 OOM 异样或者其余运行时异样。
public static void main(String[] args) {
        try {...... 过程启动 main 函数.......} catch (RuntimeException e) {logger.error(e.getMessage(), e);
            // JVM 过程被动敞开触发调用 shutdownHook
            System.exit(1);
        }
}
  1. 当 JVM 过程接管到第二大节《2. 内核信号机制》介绍的那些敞开信号时,JVM 过程会被敞开。因为 SIGKILL 信号和 SIGSTOP 信号不可能被过程捕捉和疏忽,这两个信号会间接粗犷地敞开 JVM 过程,所以个别咱们会发送 SIGTERM 信号,JVM 过程通过捕捉 SIGTERM 信号,从而能够执行咱们定义的 ShutdownHook 实现优雅敞开的操作。
  2. Native Method 执行过程中产生谬误,比方试图拜访一个不存在的内存,这样也会导致 JVM 强制敞开,ShutdownHook 也不会运行。

3.2 应用 ShutdownHook 的注意事项

  1. ShutdownHook 其实实质上是一个曾经被初始化然而未启动的 Thread,这些通过 Runtime.getRuntime().addShutdownHook 办法注册的 ShutdownHooks,在 JVM 过程敞开的时候会被启动 并发执行 ,然而并 不会保障执行程序

所以在编写 ShutdownHook 中的逻辑时,咱们应该确保程序的线程安全性,并尽可能防止死锁。最好是一个 JVM 过程只注册一个 ShutdownHook。

  1. 如果咱们通过 java.lang.Runtime#runFinalizersOnExit(boolean value) 开启了 finalization-on-exit,那么当所有 ShutdownHook 运行结束之后,JVM 在敞开之前将会持续调用所有未被调用的 finalizers 办法。默认 finalization-on-exit 选项是敞开的。

留神:当 JVM 开始敞开并执行上述敞开操作的时候,守护线程是会持续运行的,如果用户应用 java.lang.System#exit(int status) 办法被动发动 JVM 敞开,那么敞开期间非守护线程也是会持续运行的。

  1. 一旦 JVM 过程开始敞开,个别状况下这个过程是不能够被中断的,除非操作系统强制中断或者用户通过调用 java.lang.Runtime#halt(int status) 来强制敞开。
   public void halt(int status) {SecurityManager sm = System.getSecurityManager();
        if (sm != null) {sm.checkExit(status);
        }
        Shutdown.halt(status);
    }

java.lang.Runtime#halt(int status) 办法是用来强制敞开正在运行的 JVM 过程的,它会导致咱们注册的 ShutdownHook 不会被运行和执行,如果此时 JVM 正在执行 ShutdownHook,当调用该办法后,JVM 过程将会被强制敞开,并不会期待 ShutdownHook 执行结束。

  1. 当 JVM 敞开流程开始的时候,就不能在向其注册 ShutdownHook 或者勾销注册之前曾经注册好的 ShutdownHook 了,否则将会抛出 IllegalStateException 异样。
  2. ShutdownHook 中的程序应该尽快的实现优雅敞开逻辑,因为当用户调用 System#exit 办法的时候是心愿 JVM 在保障业务无损的状况下尽快实现敞开动作。这里并不适宜做一些须要长时间运行的工作或者和用户交互的操作。

如果是因为物理机关闭从而导致的 JVM 敞开,那么操作系统只会容许 JVM 限定的工夫内尽快的敞开,超过限定工夫操作系统将会强制敞开 JVM。

  1. ShutdownHook 中可能也会抛出异样,而 ShutdownHook 对于 JVM 来说实质上是一个 Thread,那么对于 ShutdownHook 中未捕捉的异样,JVM 的解决办法和其余一般的线程一样,都是通过调用 ThreadGroup#uncaughtException 办法来解决。此办法的默认实现是将异样的堆栈跟踪打印到 System#err 并终止异样的 ShutdownHook 线程。

留神:这里只会进行异样的 ShutdownHook,但不会影响其余 ShutdownHook 线程的执行更不会导致 JVM 退出。

  1. 最初也是十分重要的一点是,当 JVM 过程接管到 SIGKILL 信号和 SIGSTOP 信号时,是会强制敞开,并不会执行 ShutdownHook。另外一种导致 JVM 强制敞开的状况就是 Native Method 执行过程中产生谬误,比方试图拜访一个不存在的内存,这样也会导致 JVM 强制敞开,ShutdownHook 也不会运行。

3.3 ShutdownHook 执行原理

咱们在 JVM 中通过 Runtime.getRuntime().addShutdownHook 增加敞开钩子,当 JVM 接管到 SIGTERM 信号之后,就会调用咱们注册的这些 ShutdownHooks。

本大节介绍的 ShutdownHook 就相似于咱们在第二大节《内核信号机制》中介绍的信号处理函数。

大家这里肯定会有个疑难,那就是在介绍内核信号机制大节中,咱们能够通过零碎调用 sigaction 函数向内核注册过程要捕捉的信号以及对应的信号处理函数。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

然而在本大节介绍的 JVM 中,咱们只是通过 Runtime.getRuntime().addShutdownHook 注册了一个敞开钩子。然而并未注册 JVM 过程所须要捕捉的信号。那么 JVM 是怎么捕捉敞开信号的呢?

        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {..... 执行优雅敞开步骤.....}
        });

事实上,JVM 捕捉操作系统信号的局部在 JDK 中曾经帮咱们解决好了,在用户层咱们并不需要关注捕捉信号的解决,只须要关注信号的解决逻辑即可。

上面咱们就来看一下 JDK 是如何帮忙咱们将要捕捉的信号向内核注册的?

当 JVM 第一个线程被初始化之后,随后就会调用 System#initializeSystemClass 函数来初始化 JDK 中的一些零碎类,其中就包含注册 JVM 过程须要捕捉的信号以及信号处理函数。

public final class System {private static void initializeSystemClass() {

           ....... 省略.......

            // Setup Java signal handlers for HUP, TERM, and INT (where available).
           Terminator.setup();

           ....... 省略.......

    }

}

从这里能够看出,JDK 在向 JVM 注册须要捕捉的内核信号是在 Terminator 类中进行的。


class Terminator {
    // 信号处理函数
    private static SignalHandler handler = null;

    static void setup() {if (handler != null) return;
        SignalHandler sh = new SignalHandler() {public void handle(Signal sig) {Shutdown.exit(sig.getNumber() + 0200);
            }
        };
        handler = sh;

        try {Signal.handle(new Signal("HUP"), sh);
        } catch (IllegalArgumentException e) { }
        try {Signal.handle(new Signal("INT"), sh);
        } catch (IllegalArgumentException e) { }
        try {Signal.handle(new Signal("TERM"), sh);
        } catch (IllegalArgumentException e) {}}

}

JDK 向咱们提供了 sun.misc.Signal#handle(Signal signal, SignalHandler signalHandler) 函数来实现在 JVM 过程中对内核信号的捕捉。底层依赖于咱们在第二大节介绍的零碎调用 sigaction。

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

sun.misc.Signal#handle 函数的参数含意和零碎调用函数 sigaction 中的参数含意是一一对应的:

  • Signal signal:示意要捕捉的内核信号。从这里咱们能够看出 JVM 次要捕捉三种信号:SIGHUP(1),SIGINT(2),SIGTERM(15)。

除了上述的这三种信号之外,JVM 如果接管到其余信号,会执行零碎内核默认的操作,间接敞开过程,并不会触发 ShutdownHook 的执行。

  • SignalHandler handler:信号响应函数。咱们看到这里间接调用了 Shutdown#exit 函数。
    SignalHandler sh = new SignalHandler() {public void handle(Signal sig) {Shutdown.exit(sig.getNumber() + 0200);
            }
        };

咱们这里应该很容易就会猜测出 ShutdownHook 的调用应该就是在 Shutdown#exit 函数中被触发的。

class Shutdown {static void exit(int status) {

          ........ 省略.........

          synchronized (Shutdown.class) {
              // 开始 JVM 敞开流程,执行 ShutdownHooks
              sequence();
              // 强制敞开 JVM
              halt(status);
          }

    }

    private static void sequence() {synchronized (lock) {if (state != HOOKS) return;
        }
        // 触发 ShutdownHooks
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        // 如果 runFinalizersOnExit = true
        // 开始运行所有未被调用过的 Finalizers
        if (rfoe) runAllFinalizers();}
}

Shutdown#sequence 函数中的逻辑就是咱们在《3.2 应用 ShutdownHook 的注意事项》大节中介绍的 JVM 敞开时的运行逻辑:在这里会触发所有 ShutdownHook 的 并发运行。留神这里并不会保障运行程序。

当所有 ShutdownHook 运行结束之后,如果咱们通过 java.lang.Runtime#runFinalizersOnExit(boolean value) 开启了 finalization-on-exit 选项,JVM 在敞开之前将会持续调用所有未被调用的 finalizers 办法。默认 finalization-on-exit 选项是敞开的。

3.4 ShutdownHook 的执行

如上图所示,在 JDK 的 Shutdown 类中,蕴含了一个 Runnable[] hooks 数组,容量为 10。JDK 中的 ShutdownHook 是以类型来分类的,数组 hooks 每一个槽中寄存的是一种特定类型的 ShutdownHook。

而咱们通常在程序代码中通过 Runtime.getRuntime().addShutdownHook 注册的是 Application hooks 类型的 ShutdownHook,寄存在数组 hooks 中索引为 1 的槽中。

当在 Shutdown#sequence 中触发 runHooks() 函数开始运行 JVM 中所有类型的 ShutdownHooks 时,会在 runHooks() 函数中顺次遍历数组 hooks 中的 Runnable,进而开始运行 Runnable 中封装的 ShutdownHooks。

当遍历到数组 Hooks 的第二个槽(索引为 1)的时候,Application hooks 类型的 ShutdownHook 得以运行,也就是咱们通过 Runtime.getRuntime().addShutdownHook 注册的 ShutdownHook 在这个时候开始运行起来。


    // The system shutdown hooks are registered with a predefined slot.
    // The list of shutdown hooks is as follows:
    // (0) Console restore hook
    // (1) Application hooks
    // (2) DeleteOnExit hook
    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

    /* Run all registered shutdown hooks
     */
    private static void runHooks() {for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // acquire the lock to make sure the hook registered during
                    // shutdown is visible here.
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();} catch(Throwable t) {if (t instanceof ThreadDeath) {ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }

上面咱们就来看一下,JDK 是如果通过 Runtime.getRuntime().addShutdownHook 函数将咱们自定义的 ShutdownHook 注册到 Shutdown 类中的数组 Hooks 里的。

3.5 ShutdownHook 的注册

public class Runtime {public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();
        if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        // 留神 这里注册的是 Application 类型的 hooks
        ApplicationShutdownHooks.add(hook);
    }

}

从 JDK 源码中咱们看到在 Runtime 类中的 addShutdownHook 办法里,JDK 会将咱们自定义的 ShutdownHook 封装在 ApplicationShutdownHooks 类中,从这类的命名上看,它里边封装的就是咱们在上大节《3.4 ShutdownHook 的执行》提到的 Application hooks 类型的 ShutdownHook,由用户自定义实现。

class ApplicationShutdownHooks {
    // 寄存用户自定义的 Application 类型的 hooks
    private static IdentityHashMap<Thread, Thread> hooks;

    static synchronized void add(Thread hook) {if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {threads = hooks.keySet();
            hooks = null;
        }
        // 程序启动 shutdownhooks
        for (Thread hook : threads) {hook.start();
        }
        // 并发调用 shutdownhooks,期待所有 hooks 运行结束退出
        for (Thread hook : threads) {
            try {hook.join();
            } catch (InterruptedException x) {}}
    }
}

ApplicationShutdownHooks 类中也有一个汇合 IdentityHashMap<Thread, Thread> hooks,专门用来寄存由用户自定义的 Application hooks 类型的 ShutdownHook。通过 ApplicationShutdownHooks#add 办法增加进 hooks 汇合中。

而后在 runHooks 办法里挨个启动 ShutdownHook 线程,并发执行。留神这里的 runHooks 办法是 ApplicationShutdownHooks 类中的

在 ApplicationShutdownHooks 类的动态代码块 static{…..} 中会将 runHooks 办法封装成 Runnable 增加进 Shutdown 类中的 hooks 数组中。留神这里 Shutdown#add 办法传递进的索引是 1。

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;

    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {public void run() {runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
}

Shutdown#add 办法的逻辑就很简略了:

class Shutdown {

    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {synchronized (lock) {if (hooks[slot] != null)
                throw new InternalError("Shutdown hook at slot" + slot + "already registered");

            if (!registerShutdownInProgress) {if (state > RUNNING)
                    throw new IllegalStateException("Shutdown in progress");
            } else {if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
                    throw new IllegalStateException("Shutdown in progress");
            }

            hooks[slot] = hook;
        }
    }
}
  • 参数 Runnable hook 就是在 ApplicationShutdownHooks 中的动态代码块 static{….} 中将 runHooks 办法封装成的 Runnable。
  • 参数 int slot 示意将封装好的 Runnable 放入 hooks 数组中的哪个槽中。这里咱们注册的是 Application hooks 类型的 ShutdonwHook,所以这里的索引为 1。
  • 参数 registerShutdownInProgress 示意是否容许在 JVM 敞开流程开始之后,持续向 JVM 增加 ShutdownHook。默认为 false 示意不容许。否则将会抛出 IllegalStateException 异样。这一点笔者在大节《3.2 应用 ShutdownHook 的注意事项》中强调过。

以上就是 JVM 如何捕捉操作系统内核信号,如何注册 ShutdownHook,以及何时触发 ShutdownHook 的执行的一个全面介绍。

读到这里大家应该彻底明确了为什么不能应用 kill -9 pid 命令来敞开过程了吧,当初赶快去检查一下你们公司生产环境的运维脚本吧!!


俗话说的好 talk is cheap! show me the code!,在介绍了这么多对于优雅敞开的实践计划和原理之后,我想大家当初肯定很好奇到底咱们该如何实现这一套优雅敞开的计划呢?

那么接下来笔者就从一些出名框架源码实现角度,为大家具体论述一下优雅敞开是如何实现的?

4. Spring 的优雅敞开机制

后面两个大节中咱们从反对优雅敞开最底层的内核信号机制开始聊起而后到 JVM 过程实现优雅敞开的 ShutdwonHook 原理,通过这一系列的介绍,咱们当初对优雅敞开在内核层和 JVM 层的相干机制原理有了肯定的理解。

那么在实在 Java 利用中,咱们到底该如何基于上述机制实现一套优雅敞开计划呢?本大节咱们来从 Spring 源码中获取下答案!!

在介绍 Spring 优雅敞开机制源码实现之前,笔者先来带大家回顾下,在 Spring 的利用上下文敞开的时候,Spring 到底给咱们提供了哪些敞开时的回调机制,从而能够让咱们在这些回调中编写 Java 利用的优雅敞开逻辑。

4.1 公布 ContextClosedEvent 事件

在 Spring 上下文开始敞开的时候,首先会公布 ContextClosedEvent 事件,留神此时 Spring 容器的 Bean 还没有开始销毁,所以咱们能够在该事件回调中执行优雅敞开的操作。

@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
       @Override
       public void onApplicationEvent(ContextClosedEvent event) {........ 优雅敞开逻辑.....}
}

4.2 Spring 容器中的 Bean 销毁前回调

当 Spring 开始销毁容器中治理的 Bean 之前,会回调所有实现 DestructionAwareBeanPostProcessor 接口的 Bean 中的 postProcessBeforeDestruction 办法。

@Component
public class DestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor {

    @Override
    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {........Spring 容器中的 Bean 开始销毁前回调.......}
}

4.3 回调标注 @PreDestroy 注解的办法

@Component
public class Shutdown {
    @PreDestroy
    public void preDestroy() {...... 开释资源.......}
}

4.4 回调 DisposableBean 接口中的 destroy 办法

@Component
public class Shutdown implements DisposableBean{

    @Override
    public void destroy() throws Exception {...... 开释资源......}

}

4.5 回调自定义的销毁办法

<bean id="Shutdown" class="com.test.netty.Shutdown"  destroy-method="doDestroy"/>
public class Shutdown {public void doDestroy() {..... 自定义销毁办法....}
}

4.6 Spring 优雅敞开机制的实现

Spring 相干应用程序实质上也是一个 JVM 过程,所以 Spring 框架想要实现优雅敞开机制也必须依靠于咱们在本文第三大节中介绍的 JVM 的 ShutdownHook 机制。

在 Spring 启动的时候,须要向 JVM 注册 ShutdownHook,当咱们执行 kill - 15 pid 命令时,随后 Spring 会在 ShutdownHook 中触发上述介绍的五种回调。

上面咱们来看一下 Spring 中 ShutdownHook 的注册逻辑:

4.6.1 Spring 中 ShutdownHook 的注册

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext, DisposableBean {

    @Override
    public void registerShutdownHook() {if (this.shutdownHook == null) {
            // No shutdown hook registered yet.
            this.shutdownHook = new Thread() {
                @Override
                public void run() {synchronized (startupShutdownMonitor) {doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }
}

在 Spring 启动的时候,咱们须要调用 AbstractApplicationContext#registerShutdownHook 办法向 JVM 注册 Spring 的 ShutdownHook,从这段源码中咱们看出,Spring 将 doClose() 办法封装在 ShutdownHook 线程中,而 doClose() 办法里边就是 Spring 优雅敞开的逻辑。

这里须要强调的是,当咱们在一个纯 Spring 环境下,Spring 框架是不会为咱们被动调用 registerShutdownHook 办法去向 JVM 注册 ShutdownHook 的,咱们须要手动调用 registerShutdownHook 办法去注册。

public class SpringShutdownHook {public static void main(String[] args) throws IOException {GenericApplicationContext context = new GenericApplicationContext();
                      ........
        // 注册 Shutdown Hook
        context.registerShutdownHook();
                      ........
    }
}

而在 SpringBoot 环境下,SpringBoot 在启动的时候会为咱们调用这个办法去被动注册 ShutdownHook。咱们不须要手动注册。

public class SpringApplication {public ConfigurableApplicationContext run(String... args) {

                  ............... 省略.................

                  ConfigurableApplicationContext context = null;
                  context = createApplicationContext();
                  refreshContext(context);

                  ............... 省略.................
    }

    private void refreshContext(ConfigurableApplicationContext context) {refresh(context);
        if (this.registerShutdownHook) {
            try {context.registerShutdownHook();
            }
            catch (AccessControlException ex) {// Not allowed in some environments.}
        }
    }

}

4.6.2 Spring 中的优雅敞开逻辑

    protected void doClose() {
        // 更新上下文状态
        if (this.active.get() && this.closed.compareAndSet(false, true)) {if (logger.isInfoEnabled()) {logger.info("Closing" + this);
            }
            // 勾销 JMX 托管
            LiveBeansView.unregisterApplicationContext(this);

            try {
                // 公布 ContextClosedEvent 事件
                publishEvent(new ContextClosedEvent(this));
            }
            catch (Throwable ex) {logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
            }

            // 回调 Lifecycle beans, 相干 stop 办法
            if (this.lifecycleProcessor != null) {
                try {this.lifecycleProcessor.onClose();
                }
                catch (Throwable ex) {logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
                }
            }

            // 销毁 bean,触发后面介绍的几种回调
            destroyBeans();

            // Close the state of this context itself.
            closeBeanFactory();

            // Let subclasses do some final clean-up if they wish...
            onClose();

            // Switch to inactive.
            this.active.set(false);
        }
    }

在这里咱们能够看出最终是在 AbstractApplicationContext#doClose 办法中触发本大节开始介绍的五种回调:

  1. 公布 ContextClosedEvent 事件。留神这里是一个同步事件,也就是说 Spring 的 ShutdownHook 线程在这里公布完事件之后会持续同步执行事件的解决,等到事件处理完毕后,才会去执行前面的 destroyBeans() 办法对 IOC 容器中的 Bean 进行销毁。

所以在 ContextClosedEvent 事件监听类中,能够释怀地去做优雅敞开相干的操作,因为此时 Spring 容器中的 Bean 还没有被销毁。

  1. destroyBeans() 办法中顺次触发剩下的四种回调。

最初联合前边大节中介绍的内容,总结 Spring 的整个优雅敞开流程如下图所示:

5. Dubbo 的优雅敞开

本大节优雅敞开局部源码基于 apache dubbo 2.7.7 版本,该版本中的优雅敞开是有 Bug 的,上面让咱们一起来 Shooting Bug !

在前边几个大节的内容中,咱们从内核提供的底层技术支持开始聊到了 JVM 的 ShutdonwHook,而后又从 JVM 聊到了 Spring 框架的优雅敞开机制。

在理解了这些内容之后,本大节咱们就来看下 dubbo 中的优雅敞开实现,因为当初简直所有 Java 利用都会采纳 Spring 作为开发框架,所以 dubbo 个别是集成在 Spring 框架中供咱们应用的,它的优雅敞开和 Spring 有着严密的分割。

5.1 Dubbo 在 Spring 环境下的优雅敞开

在本文第四大节《4. Spring 的优雅敞开机制》的介绍中,咱们晓得在 Spring 的优雅敞开流程中,Spring 的 ShutdownHook 线程会首先公布 ContextClosedEvent 事件,该事件是一个同步事件,ShutdownHook 线程公布完该事件紧接着就会同步执行该事件的监听器,当在事件监听器中解决完 ContextClosedEvent 事件之后,在回过头来执行 destroyBeans() 办法并顺次触发剩下的四种回调来销毁 IOC 容器中的 Bean。

因为在解决 ContextClosedEvent 事件的时候,Dubbo 所依赖的一些要害 bean 这时还没有被销毁,所以 dubbo 定义了一个 DubboBootstrapApplicationListener 用来监听 ContextClosedEvent 事件,并在 onContextClosedEvent 事件处理办法中调用 dubboBootstrap.stop() 办法开启 dubbo 的优雅敞开流程。

public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener
        implements Ordered {

    @Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        // 这里是 Spring 的同步事件,publishEvent 和解决 Event 是在同一个线程中
        if (event instanceof ContextRefreshedEvent) {onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {onContextClosedEvent((ContextClosedEvent) event);
        }
    }

    private void onContextClosedEvent(ContextClosedEvent event) {
        // spring 在 shutdownhook 中会先触发 ContextClosedEvent,而后在销毁 spring beans
        // 所以这里 dubbo 开始优雅敞开时,依赖的 spring beans 并未销毁
        dubboBootstrap.stop();}

}

当服务提供者 ServiceBean 和服务消费者 ReferenceBean 被初始化时, 会将 DubboBootstrapApplicationListener 注册到 Spring 容器中。并开始监听 ContextClosedEvent 事件和 ContextRefreshedEvent 事件。

public class ServiceClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,
        ResourceLoaderAware, BeanClassLoaderAware {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        // @since 2.7.5 注册 spring 启动 敞开事件的 listener
        // 在事件回调中中调用启动类 DubboBootStrap 的 start  stop 来启动 敞开 dubbo 利用
        registerBeans(registry, DubboBootstrapApplicationListener.class);
      
                  ........ 省略.......
    }
}

5.2 Dubbo 优雅敞开流程简介

因为本文的主题是介绍优雅敞开的一整条流程主线,所以这里笔者只是简要介绍 Dubbo 优雅敞开的主流程,相干细节局部笔者会在后续的 dubbo 源码解析系列里为大家具体介绍 Dubbo 优雅敞开的细节。为了防止本文发散太多,咱们这里还是聚焦于流程主线。

public class DubboBootstrap extends GenericEventListener {public DubboBootstrap stop() throws IllegalStateException {destroy();
        return this;
    }

}

这里的外围逻辑其实就是咱们在《1.2 优雅敞开》大节中介绍的两大优雅敞开主题:

  • 从以后正在敞开的利用实例上切走现有生产流量。
  • 保障业务无损。

这里大家只须要理解 Dubbo 优雅敞开的主流程即可,相干细节笔者后续会有一篇专门的文章具体为大家介绍。

    public void destroy() {if (destroyLock.tryLock()) {
            try {DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {

                    // 勾销注册
                    unregisterServiceInstance();
                    // 勾销元数据服务
                    unexportMetadataService();
                    // 进行裸露服务
                    unexportServices();
                    // 勾销订阅服务
                    unreferServices();
                    // 登记注册核心
                    destroyRegistries();
                    // 敞开服务
                    DubboShutdownHook.destroyProtocols();
                    // 销毁注册核心客户端实例
                    destroyServiceDiscoveries();
                    // 革除利用配置类以及相干利用模型
                    clear();
                    // 敞开线程池
                    shutdown();
                    // 开释资源
                    release();}
            } finally {destroyLock.unlock();
            }
        }
    }

从以上内容能够看出,Dubbo 的优雅敞开依靠于 Spring ContextClosedEvent 事件的公布,而 ContextClosedEvent 事件的公布又依靠于 Spring ShutdownHook 的注册。

从《4.6.1 Spring 中 ShutdownHook 的注册》大节的介绍中咱们晓得,在 SpringBoot 环境下,SpringBoot 在启动的时候会为咱们调用 ApplicationContext#registerShutdownHook 办法去被动注册 ShutdownHook。咱们不须要手动注册。

而在一个纯 Spring 环境下,Spring 框架并不会为咱们被动调用 registerShutdownHook 办法去向 JVM 注册 ShutdownHook 的,咱们须要手动调用 registerShutdownHook 办法去注册。

所以 Dubbo 这里为了兼容 SpringBoot 环境和纯 Spring 环境下的优雅敞开,引入了 SpringExtensionFactory 类,只有在 Spring 环境下都会调用 registerShutdownHook 去向 JVM 注册 Spring 的 ShutdownHook。

public class SpringExtensionFactory implements ExtensionFactory {private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);

    private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();

    public static void addApplicationContext(ApplicationContext context) {CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 在 spring 启动胜利之后设置 shutdownHook(兼容非 SpringBoot 环境)((ConfigurableApplicationContext) context).registerShutdownHook();}
    }

}

当服务提供者 ServiceBean 和服务消费者 ReferenceBean 在初始化实现之后,会回调 SpringExtensionFactory#addApplicationContext 办法注册 ShutdownHook。

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
        ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware {

   @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        SpringExtensionFactory.addApplicationContext(applicationContext);
    }

}
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean,
        ApplicationContextAware, InitializingBean, DisposableBean {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        SpringExtensionFactory.addApplicationContext(applicationContext);
    }

}

以上就是 Dubbo 在 Spring 集成环境下的优雅敞开全流程,上面咱们来看下 Dubbo 在非 Spring 环境下的优雅敞开流程。

5.3 Dubbo 在非 Spring 环境下的优雅敞开

在上大节的介绍中咱们晓得 Dubbo 在 Spring 环境下依靠 Spring 的 ShutdownHook,通过监听 ContextClosedEvent 事件,从而触发 Dubbo 的优雅敞开流程。

而到了非 Spring 环境下,Dubbo 就须要定义本人的 ShutdownHook,从而引入了 DubboShutdownHook,间接将优雅敞开流程封装在本人的 ShutdownHook 中执行。

public class DubboBootstrap extends GenericEventListener {private DubboBootstrap() {configManager = ApplicationModel.getConfigManager();
        environment = ApplicationModel.getEnvironment();

        DubboShutdownHook.getDubboShutdownHook().register();
        ShutdownHookCallbacks.INSTANCE.addCallback(new ShutdownHookCallback() {
            @Override
            public void callback() throws Throwable {DubboBootstrap.this.destroy();
            }
        });
    }

}
public class DubboShutdownHook extends Thread {public void register() {if (registered.compareAndSet(false, true)) {DubboShutdownHook dubboShutdownHook = getDubboShutdownHook();
            Runtime.getRuntime().addShutdownHook(dubboShutdownHook);
            dispatch(new DubboShutdownHookRegisteredEvent(dubboShutdownHook));
        }
    }

    @Override
    public void run() {if (logger.isInfoEnabled()) {logger.info("Run shutdown hook now.");
        }

        callback();
        doDestroy();}

   private void callback() {callbacks.callback();
    }

}

从源码中咱们看到,当咱们的 Dubbo 应用程序接管到 kill -15 pid 信号时,JVM 捕捉到 SIGTERM(15) 信号之后,就会触发 DubboShutdownHook 线程运行,从而通过 callback() 又回调了上大节中介绍的 DubboBootstrap#destroy 办法(dubbo 的整个优雅敞开逻辑全副封装在这里)。

public class DubboBootstrap extends GenericEventListener {public void destroy() {if (destroyLock.tryLock()) {
            try {DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {

                    ........ 勾销注册......
                  
                    ........ 勾销元数据服务........
                  
                    ........ 进行裸露服务........
                 
                    ........ 勾销订阅服务........
                 
                    ........ 登记注册核心........
                 
                    ........ 敞开服务........
                  
                    ........ 销毁注册核心客户端实例........
                 
                    ........ 革除利用配置类以及相干利用模型........
                
                    ........ 敞开线程池........
                 
                    ........ 开释资源........
                 
                }
            } finally {destroyLock.unlock();
            }
        }
    }

}

5.4 啊哈!Bug!

前边咱们在《5.1 Dubbo 在 Spring 环境下的优雅敞开》大节和《5.3 Dubbo 在非 Spring 环境下的优雅敞开》大节中介绍的这两个环境的下的优雅敞开计划,当它们在各自的场景下运行的时候是没有任何问题的。

然而当这两种计划联合在一起运行,就出大问题了~~~

还记得笔者在《3.2 应用 ShutdownHook 的注意事项》大节中特别强调的一点:

  • ShutdownHook 其实实质上是一个曾经被初始化然而未启动的 Thread,这些通过 Runtime.getRuntime().addShutdownHook 办法注册的 ShutdownHooks,在 JVM 过程敞开的时候会被启动 并发执行,然而并不会保障执行程序

所以在编写 ShutdownHook 中的逻辑时,咱们应该确保程序的线程安全性,并尽可能防止死锁。最好是一个 JVM 过程只注册一个 ShutdownHook。

那么当初 JVM 中咱们注册了两个 ShutdownHook 线程,一个 Spring 的 ShutdownHook,另一个是 Dubbo 的 ShutdonwHook。那么这会引出什么问题呢?

通过前边的内容介绍咱们晓得,无论是在 Spring 的 ShutdownHook 中触发的 ContextClosedEvent 事件还是在 Dubbo 的 ShutdownHook 中执行的 CallBack。最终都会调用到 DubboBootstrap#destroy 办法执行真正的优雅敞开逻辑。

public class DubboBootstrap extends GenericEventListener {private final Lock destroyLock = new ReentrantLock();

    public void destroy() {if (destroyLock.tryLock()) {
            try {DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {.......dubbo 利用的优雅敞开.......}
            } finally {destroyLock.unlock();
            }
        }
    }

}

让咱们来构想一个这种的场景:当 Spring 的 ShutdownHook 线程和 Dubbo 的 ShutdownHook 线程同时执行并且在同一个工夫点来到 DubboBootstrap#destroy 办法中抢夺 destroyLock。

  • Dubbo 的 ShutdownHook 线程取得 destroyLock 进入 destroy() 办法体开始执行优雅敞开逻辑。
  • Spring 的 ShutdownHook 线程没有取得 destroyLock,退出 destroy() 办法。

在 Spring 的 ShutdownHook 线程退出 destroy() 办法之后紧接着就会执行 destroyBeans() 办法销毁 IOC 容器中的 Bean,这里边必定波及到一些要害业务 Bean 的销毁,比方:数据库连接池,以及 Dubbo 相干的外围 Bean。

于此同时 Dubbo 的 ShutdownHook 线程开始执行优雅敞开逻辑,《1.2 优雅敞开》大节中咱们提到,优雅敞开要保障业务无损。所以须要将剩下正在进行中的业务流程持续处理完毕并将业务处理结果响应给客户端。然而这时依赖的一些业务要害 Bean 曾经被销毁,比方数据库连接池,这时执行数据库操作就会抛出 CannotGetJdbcConnectionException。导致优雅敞开失败,对业务造成了影响。

5.5 Bug 的修复

该 Bug 最终在 apache dubbo 2.7.15 版本中被修复

详情可查看 Issue:https://github.com/apache/dub…

通过上大节的剖析,咱们晓得既然这个 Bug 产生的起因是因为 Spring 的 ShutdownHook 线程和 Dubbo 的 ShutdownHook 线程并发执行所导致的。

那么当咱们处于 Spring 环境下的时候,就将 Dubbo 的 ShutdownHook 登记掉即可。

public class SpringExtensionFactory implements ExtensionFactory {private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);

    private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();

    public static void addApplicationContext(ApplicationContext context) {CONTEXTS.add(context);
        if (context instanceof ConfigurableApplicationContext) {
            // 注册 Spring 的 ShutdownHook
            ((ConfigurableApplicationContext) context).registerShutdownHook();
            // 在 Spring 环境下将 Dubbo 的 ShutdownHook 勾销掉
            DubboShutdownHook.getDubboShutdownHook().unregister();
        }
    }
}

而在非 Spring 环境下,咱们仍然保留 Dubbo 的 ShutdownHook。

public class DubboBootstrap {private DubboBootstrap() {configManager = ApplicationModel.getConfigManager();
        environment = ApplicationModel.getEnvironment();

        DubboShutdownHook.getDubboShutdownHook().register();
        ShutdownHookCallbacks.INSTANCE.addCallback(DubboBootstrap.this::destroy);
    }

}

以上内容就是 Dubbo 的整个优雅敞开主线流程,以及优雅敞开 Bug 产生的起因和修复计划。


在 Dubbo 的优雅敞开流程中最终会通过 DubboShutdownHook.destroyProtocols() 敞开底层服务。

public class DubboBootstrap extends GenericEventListener {private final Lock destroyLock = new ReentrantLock();

    public void destroy() {if (destroyLock.tryLock()) {
            try {DubboShutdownHook.destroyAll();

                if (started.compareAndSet(true, false)
                        && destroyed.compareAndSet(false, true)) {
                    
                        .......dubbo 利用的优雅敞开.......
                    // 敞开服务
                    DubboShutdownHook.destroyProtocols();

                        .......dubbo 利用的优雅敞开.......

                }
            } finally {destroyLock.unlock();
            }
        }
    }

}

在 Dubbo 服务的销毁过程中,会通过调用 server.close 敞开底层的 Netty 服务。

public class DubboProtocol extends AbstractProtocol {

   @Override
    public void destroy() {for (String key : new ArrayList<>(serverMap.keySet())) {ProtocolServer protocolServer = serverMap.remove(key);
            RemotingServer server = protocolServer.getRemotingServer();
            server.close(ConfigurationUtils.getServerShutdownTimeout());
             ........... 省略........
        }

         ........... 省略........
}

最终触发 Netty 的优雅敞开。

public class NettyServer extends AbstractServer implements RemotingServer {

    @Override
    protected void doClose() throws Throwable {
        .......... 敞开底层 Channel......
        try {if (bootstrap != null) {
                // 敞开 Netty 的主从 Reactor 线程组
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();}
        } catch (Throwable e) {logger.warn(e.getMessage(), e);
        }
        ......... 清理缓存 Channel 数据.......
    }

}

6. Netty 的优雅敞开

通过上大节介绍 dubbo 优雅敞开的相干内容,咱们很天然的引出了 Netty 的优雅敞开触发机会,那么在本大节中笔者将为大家具体介绍下 Netty 是如何优雅地装 ………. 优雅地谢幕的~~

在之前的系列文章中,咱们围绕下图所展现的 Netty 整个外围框架的运行流程介绍了主从 ReactorGroup 的创立,启动,运行,接管网络连接,接管网络数据,发送网络数据,以及如何在 pipeline 中解决相干 IO 事件的整个源码实现。

本大节就到了 Netty 优雅谢幕的时刻了,在这谢幕的过程中,Netty 会对它的主从 ReactorGroup,以及对应 ReactorGroup 中的 Reacto r 进行优雅的敞开。上面让咱们一起来看下这个优雅敞开的过程~~~

6.1 ReactorGroup 的优雅谢幕


public abstract class AbstractEventExecutorGroup implements EventExecutorGroup {

    static final long DEFAULT_SHUTDOWN_QUIET_PERIOD = 2;
    static final long DEFAULT_SHUTDOWN_TIMEOUT = 15;

   @Override
    public Future<?> shutdownGracefully() {return shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
    }

}

在 Netty 进行优雅敞开的整个过程中,这里波及到了两个十分重要的控制参数:

  • gracefulShutdownQuietPeriod:优雅敞开静默期,默认为 2s。这个参数次要来保障 Netty 整个敞开过程中的 优雅。在敞开流程开始后,如果 Reactor 中还有遗留的异步工作须要执行,那么 Netty 就不能敞开,须要把所有异步工作执行结束才能够。当所有异步工作执行结束后,Netty 为了实现更加优雅的敞开操作,肯定要保障业务无损,这时候就引入了静默期这个概念,如果在这个静默期内,用户没有新的工作向 Reactor 提交那么就开始敞开。如果在这个静默期内,还有用户持续提交异步工作,那么就不能敞开,须要把静默期内用户提交的异步工作执行结束才能够释怀敞开。
  • gracefulShutdownTimeout:优雅敞开超时工夫,默认为 15s。这个参数次要来保障 Netty 整个敞开过程的 可控。咱们晓得一个生产级的优雅敞开计划既要保障优雅做到业务无损,更重要的是要保障敞开流程的可控,不能无限度的优雅上来。导致长时间无奈实现敞开动作。于是 Netty 就引入了这个参数,如果优雅敞开超时,那么无论此时有无异步工作须要执行都要开始敞开了。

这两个控制参数是十分重要外围的两个参数,咱们在前面介绍 Netty 敞开细节的时候还会为大家具体分析,这里大家先从概念上大略了解一下。

在介绍完这两个重要外围参数之后,咱们接下来看下 ReactorGroup 的敞开流程:

咱们都晓得 Netty 为了保障整个零碎的吞吐量以及保障 Reactor 能够线程平安地,有序地解决各个 Channel 上的 IO 事件。基于这个目标 Netty 将其承载的海量连贯摊派打散到不同的 Reactor 上解决。

ReactorGroup 中蕴含多个 Reactor,每个 Channel 只能注册到一个固定的 Reactor 上,由这个固定的 Reactor 负责解决该 Channel 上整个生命周期的事件。

一个 Reactor 上注册了多个 Channel,负责解决注册在其上的所有 Channel 的 IO 事件以及异步工作。

ReactorGroup 的构造如下图所示:

ReactorGroup 的敞开流程实质上其实是 ReactorGroup 中蕴含的所有 Reactor 的敞开,当 ReactorGroup 中的所有 Reactor 实现敞开后,ReactorGroup 才算是真正的敞开。


public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {

    // Reactor 线程组中的 Reactor 汇合
    private final EventExecutor[] children;

    // 敞开 future
    private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);

    @Override
    public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {for (EventExecutor l: children) {l.shutdownGracefully(quietPeriod, timeout, unit);
        }
        return terminationFuture();}

    @Override
    public Future<?> terminationFuture() {return terminationFuture;}

}
  • EventExecutor[] children:数组中寄存的是以后 ReactorGroup 中蕴含的所有 Reactor,类型为 EventExecutor。
  • Promise<?> terminationFuture:ReactorGroup 中的敞开 Future,用户线程通过这个 terminationFuture 能够晓得 ReactorGroup 实现敞开的机会,也能够向 terminationFuture 注册一些 listener。当 ReactorGroup 实现敞开动作后,会回调用户注册的这些 listener。大家能够依据各自的业务场景灵活运用。

在 ReactorGroup 的敞开过程中,会挨个触发它所蕴含的所有 Reactor 的敞开流程。并返回 terminationFuture 给用户线程。

当 ReactorGroup 中的所有 Reactor 实现敞开之后,这个 terminationFuture 会被设置为 success,这样一来用户线程能够感知到 ReactorGroup 曾经实现敞开了。

这一点笔者也在《Reactor 在 Netty 中的实现(创立篇)》一文中的第四大节《4. 向 Reactor 线程组中所有的 Reactor 注册 terminated 回调函数》强调过。

在 ReactorGroup 创立的最初一步,会定义 Reactor 敞开的 terminationListener。在 Reactor 的 terminationListener 中会判断以后 ReactorGroup 中的 Reactor 是否全副敞开,如果曾经全副敞开,则会设置 ReactorGroup 的 terminationFuture 为 success。

    // 记录敞开的 Reactor 个数,当 Reactor 全副敞开后,ReactorGroup 才能够认为敞开胜利
    private final AtomicInteger terminatedChildren = new AtomicInteger();
    //ReactorGroup 的敞开 future
    private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {

        ........ 挨个创立 Reactor............

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {if (terminatedChildren.incrementAndGet() == children.length) {
                    // 当所有 Reactor 敞开后 ReactorGroup 才认为是敞开胜利
                    terminationFuture.setSuccess(null);
                }
            }
        };

        for (EventExecutor e: children) {
            // 向每个 Reactor 注册 terminationListener
            e.terminationFuture().addListener(terminationListener);
        }
    }

从以上 ReactorGroup 的敞开流程咱们能够看出,ReactorGroup 的敞开逻辑只是挨个去触发它所蕴含的所有 Reactor 的敞开,Netty 的整个优雅敞开外围其实是在单个 Reactor 的敞开逻辑上。毕竟 Reactor 才是真正驱动 Netty 运行的外围引擎。

6.2 Reactor 的优雅谢幕

Reactor 的状态特地重要,从《一文聊透 Netty 外围引擎 Reactor 的运行架构》一文中咱们晓得 Reactor 是在一个 for (;;) {….} 死循环中 996 不停地工作。比方轮询 Channel 上的 IO 就绪事件,解决 IO 就绪事件,执行异步工作就是在这个死循环中实现的。

而 Reactor 在每一次循环工作完结之后,都会先去判断一下以后 Reactor 的状态,如果状态变为筹备敞开状态 ST_SHUTTING_DOWN 后,Reactor 就会开启优雅敞开流程。

所以在介绍 Reactor 的敞开流程之前,笔者先来为大家捋一捋 Reactor 中的各种状态。

  • ST_NOT_STARTED = 1:Reactor 的初始状态。在 Reactor 刚被创立进去的时候,状态为 ST_NOT_STARTED。
  • ST_STARTED = 2:Reactor 的启动状态。当向 Reactor 提交第一个异步工作的时候会触发 Reactor 的启动。启动之后状态变为 ST_STARTED。

相干细节可在回顾下《具体图解 Netty Reactor 启动全流程》一文。

  • ST_SHUTTING_DOWN = 3:Reactor 筹备开始敞开状态。当 Reactor 的 shutdownGracefully 办法被调用的时候,Reactor 的状态就会变为 ST_SHUTTING_DOWN。在这个状态下,用户依然能够向 Reactor 提交工作。
  • ST_SHUTDOWN = 4:Reactor 进行状态。示意 Reactor 的优雅敞开流程曾经完结,此时用户不能在向 Reactor 提交工作,Reactor 会在这个状态下最初一次执行残余的异步工作。
  • ST_TERMINATED = 5:Reactor 真正的终结状态,该状态示意 Reactor 曾经齐全敞开了。在这个状态下 Reactor 会设置本人的 terminationFuture 为 Success。进而开始回调上大节开端提到的 terminationListener。

在咱们理解了 Reactor 的各种状态之后,上面就该来正式开始介绍 Reactor 的敞开流程了:

public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {

    //Reactor 的状态  初始为未启动状态
    private volatile int state = ST_NOT_STARTED;
 
    //Reactor 的初始状态,未启动
    private static final int ST_NOT_STARTED = 1;
    //Reactor 启动后的状态
    private static final int ST_STARTED = 2;
    // 筹备正在进行优雅敞开,此时用户依然能够提交工作,Reactor 仍能够执行工作
    private static final int ST_SHUTTING_DOWN = 3;
    //Reactor 进行状态,示意优雅敞开完结,此时用户不能在提交工作,Reactor 最初一次执行残余的工作
    private static final int ST_SHUTDOWN = 4;
    //Reactor 中的工作已被全副执行结束,且不在承受新的工作,真正的终止状态
    private static final int ST_TERMINATED = 5;

    // 优雅敞开的静默期
    private volatile long gracefulShutdownQuietPeriod;
    // 优雅敞开超时工夫
    private volatile long gracefulShutdownTimeout;

    //Reactor 的敞开 Future
    private final Promise<?> terminationFuture = new DefaultPromise<Void>(GlobalEventExecutor.INSTANCE);

    @Override
    public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {

        ...... 省略参数校验.......

        // 此时 Reactor 的状态为 ST_STARTED
        if (isShuttingDown()) {return terminationFuture();
        }

        boolean inEventLoop = inEventLoop();
        boolean wakeup;
        int oldState;
        for (;;) {if (isShuttingDown()) {return terminationFuture();
            }
            int newState;
            // 须要唤醒 Reactor 去执行敞开流程
            wakeup = true;
            oldState = state;
            if (inEventLoop) {newState = ST_SHUTTING_DOWN;} else {switch (oldState) {
                    case ST_NOT_STARTED:
                    case ST_STARTED:
                        newState = ST_SHUTTING_DOWN;
                        break;
                    default:
                        //Reactor 正在敞开或者曾经敞开
                        newState = oldState;
                        wakeup = false;
                }
            }
            if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {break;}
        }
        // 优雅敞开静默期,在该工夫内,用户还是能够向 Reactor 提交工作并且执行,只有有工作在 Reactor 中,就不能进行敞开
        // 每隔 100ms 检测是否有工作提交进来,如果在静默期内没有新的工作提交,那么才会进行敞开 保障敞开行为的优雅
        gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
        // 优雅敞开的最大超时工夫,优雅敞开行为不能超过该工夫,如果超过的话 不论以后是否还有工作 都要进行敞开
        // 保障敞开行为的可控
        gracefulShutdownTimeout = unit.toNanos(timeout);

        // 这里须要保障 Reactor 线程是在运行状态,如果曾经进行,那么就不在进行后续敞开行为,间接返回 terminationFuture
        if (ensureThreadStarted(oldState)) {return terminationFuture;}

        // 将正在监听 IO 事件的 Reactor 从 Selector 上唤醒,示意要敞开了,开始执行敞开流程
        if (wakeup) {
            // 确保 Reactor 线程在执行完工作之后 不会在 selector 上停留
            taskQueue.offer(WAKEUP_TASK);
            if (!addTaskWakesUp) {
                // 如果此时 Reactor 正在 Selector 上阻塞,则能够确保 Reactor 被及时唤醒
                wakeup(inEventLoop);
            }
        }

        return terminationFuture();}

    @Override
    public Future<?> terminationFuture() {return terminationFuture;}

}

首先在开启敞开流程之前,须要调用 isShuttingDown() 判断一下以后 Reactor 是否曾经开始敞开流程或者曾经实现敞开。如果曾经开始敞开了,这里会间接返回 Reactor 的 terminationFuture。


    @Override
    public boolean isShuttingDown() {return state >= ST_SHUTTING_DOWN;}

剩下的逻辑就是不停的在一个 for 循环中通过 CAS 不停的尝试将 Reactor 的以后 ST_STARTED 状态改为 ST_SHUTTING_DOWN 正在敞开状态。

如果通过 inEventLoop() 判断出以后执行线程是 Reactor 线程,那么示意以后 Reactor 的状态只会是 ST_STARTED 运行状态,那么就能够间接将 newState 设置为 ST_SHUTTING_DOWN。因为只有 Reactor 处于 ST_STARTED 状态的时候才会运行到这里。否则在前边就间接返回 terminationFuture 了。

如果以后执行线程为用户线程并不是 Reactor 线程的话,那么此时 Reactor 的状态可能是正在敞开状态或者曾经敞开状态,用户线程在反复发动 Reactor 的敞开流程。所以这些异样场景的解决会在 switch(oldState){….} 语句中实现。

            switch (oldState) {
                    case ST_NOT_STARTED:
                    case ST_STARTED:
                        newState = ST_SHUTTING_DOWN;
                        break;
                    default:
                        //Reactor 正在敞开或者曾经敞开
                        newState = oldState;
                        // 以后 Reactor 曾经处于敞开流程中,则无需在唤醒 Reactor 了
                        wakeup = false;
                }

如果以后 Reactor 还未发动敞开流程,比方状态为 ST_NOT_STARTED 或者 ST_STARTED,那么间接能够释怀的将 newState 设置为 ST_SHUTTING_DOWN。

如果以后 Reactor 曾经处于敞开流程中或者曾经实现敞开,比方状态为 ST_SHUTTING_DOWN,ST_SHUTDOWN 或者 ST_TERMINATED。则没有必要在唤醒 Reactor 反复执行敞开流程了 wakeup = false。Reactor 的状态维持以后状态不变。

当 Reactor 的状态确定结束后,则在 for 循环中一直的通过 CAS 批改 Reactor 的以后状态。此时 oldState = ST_STARTED,newState = ST_SHUTTING_DOWN。


          if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {break;}

随后在 Reactor 中设置咱们在《6.1 ReactorGroup 的优雅谢幕》大节开始处介绍的管制 Netty 优雅敞开的两个十分重要的外围参数:

  • gracefulShutdownQuietPeriod:优雅敞开静默期,默认为 2s。当 Reactor 中曾经没有异步工作须要在执行时,该静默期开始触发,Netty 在这里会每隔 100ms 检测一下是否有工作提交进来,如果在静默期内没有新的工作提交,那么才会进行敞开,保障敞开行为的优雅。
  • gracefulShutdownTimeout:优雅敞开超时工夫,默认为 15s。优雅敞开行为不能超过该工夫,如果超过的话不论以后是否还有工作都要进行敞开,保障敞开行为的可控。

流程走到这里,Reactor 就开始筹备执行敞开流程了,那么在进行敞开操作之前,咱们须要确保 Reactor 线程此时应该是运行状态,如果此时 Reactor 线程还未开始运行那么就须要让它运行起来执行敞开操作。


        // 这里须要保障 Reactor 线程是在运行状态,如果曾经进行,// 那么就不在进行后续敞开行为,间接返回 terminationFuture
        if (ensureThreadStarted(oldState)) {return terminationFuture;}

    private boolean ensureThreadStarted(int oldState) {if (oldState == ST_NOT_STARTED) {
            try {doStartThread();
            } catch (Throwable cause) {STATE_UPDATER.set(this, ST_TERMINATED);
                terminationFuture.tryFailure(cause);

                if (!(cause instanceof Exception)) {
                    // Also rethrow as it may be an OOME for example
                    PlatformDependent.throwException(cause);
                }
                return true;
            }
        }
        return false;
    }

如果此时 Reactor 线程刚刚执行完异步工作或者正在 Selector 上阻塞,那么咱们须要确保 Reactor 线程被及时的唤醒,从而能够间接进入敞开流程。wakeup == true。

这里的 addTaskWakesUp 默认为 false。示意并不是只有 addTask 办法能力唤醒 Reactor 线程 还有其余办法能够唤醒 Reactor 线程,比方 SingleThreadEventExecutor#execute 办法还有本大节介绍的 SingleThreadEventExecutor#shutdownGracefully 办法都会唤醒 Reactor 线程。

对于 addTaskWakesUp 字段的具体含意和作用,大家能够回顾下《一文聊透 Netty 外围引擎 Reactor 的运行架构》一文中的《1.2.2 Reactor 开始轮询 IO 就绪事件》大节。


     // 将正在监听 IO 事件的 Reactor 从 Selector 上唤醒,示意要敞开了,开始执行敞开流程
        if (wakeup) {
            // 确保 Reactor 线程在执行完工作之后 不会在 selector 上停留
            taskQueue.offer(WAKEUP_TASK);
            if (!addTaskWakesUp) {
                // 如果此时 Reactor 正在 Selector 上阻塞,则能够确保 Reactor 被及时唤醒
                wakeup(inEventLoop);
            }
        }
  • 通过 taskQueue.offer(WAKEUP_TASK) 向 Reactor 中增加 WAKEUP_TASK,能够确保 Reactor 在执行完异步工作之后不会在 Selector 上做停留,间接执行敞开操作。
  • 如果此时 Reactor 线程正在 Selector 上阻塞,那么间接调用 wakeup(inEventLoop) 唤醒 Reactor 线程,间接来到敞开流程。
public final class NioEventLoop extends SingleThreadEventLoop {
    @Override
    protected void wakeup(boolean inEventLoop) {if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {selector.wakeup();
        }
    }
}

6.3 Reactor 线程的优雅敞开

咱们先来通过一张 Reactor 优雅敞开整体流程图来从总体上俯撼一下敞开流程:

通过 [《一文聊透 Netty 外围引擎 Reactor 的运行架构》](https://mp.weixin.qq.com/s?__…
) 一文的介绍,咱们晓得 Reacto r 是在一个 for 循环中 996 不停地解决 IO 事件以及执行异步工作。如上面笔者提取的 Reactor 运行框架所示:

public final class NioEventLoop extends SingleThreadEventLoop {

    @Override
    protected void run() {for (;;) {
            try {
                  .......1. 监听 Channel 上的 IO 事件.......
                  .......2. 解决 Channel 上的 IO 事件.......
                  .......3. 执行异步工作..........
            } finally {
                try {if (isShuttingDown()) {
                        // 敞开 Reactor 上注册的所有 Channel, 进行解决 IO 事件,触发 unActive 以及 unRegister 事件
                        closeAll();
                        // 登记掉所有 Channel 进行解决 IO 事件之后,剩下的就须要执行 Reactor 中残余的异步工作了
                        if (confirmShutdown()) {return;}
                    }
                } catch (Error e) {throw (Error) e;
                } catch (Throwable t) {handleLoopException(t);
                }
            }
        }
    }

}

在 Reactor 在每次 for 循环的开端 finally{….} 语句块中都会通过 isShuttingDown() 办法去查看以后 Reactor 的状态是否是敞开状态,如果是敞开状态则开始正式进入 Reactor 的优雅敞开流程。

咱们在本文前边《1.2 优雅敞开》大节中在探讨优雅敞开计划的时候提到,咱们要着重从以下两个方面来施行优雅敞开:

  1. 首先须要切走程序承当的现有流量。
  2. 保障现有残余的工作能够执行结束,保障业务无损。

Netty 这里实现的优雅敞开同样也听从这两个要点。

  1. 在优雅敞开流程开始之前首先会调用 closeAll() 办法,将 Reactor 上注册的所有 Channel 全副敞开掉,切掉现有流量。
  2. 随后会调用 confirmShutdown() 办法,将残余的异步工作执行结束。在该办法中只有有异步工作须要执行,就不能敞开,保障业务无损。该办法返回值为 true 时示意能够进行敞开。返回 false 时示意不能马上敞开。

6.3.1 切走流量

    private void closeAll() {
        // 这里的目标是清理 selector 中的一些有效 key
        selectAgain();
        // 获取 Selector 上注册的所有 Channel
        Set<SelectionKey> keys = selector.keys();
        Collection<AbstractNioChannel> channels = new ArrayList<AbstractNioChannel>(keys.size());
        for (SelectionKey k: keys) {
            // 获取 NioSocketChannel
            Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {channels.add((AbstractNioChannel) a);
            } else {......... 省略......}
        }

        for (AbstractNioChannel ch: channels) {
            // 敞开 Reactor 上注册的所有 Channel,并在 pipeline 中触发 unActive 事件和 unRegister 事件
            ch.unsafe().close(ch.unsafe().voidPromise());
        }
    }

首先会通过 selectAgain() 最初一次在 Selector 上执行一次非阻塞轮询操作,目标是革除 Selector 上的一些有效 Key。

对于有效 Key 的革除,具体细节大家能够回看下《一文聊透 Netty 外围引擎 Reactor 的运行架构》一文中的《3.1.3 从 Selector 中移除生效的 SelectionKey》大节。

随后通过 selector.keys() 获取在 Selector 上注册的所有 SelectionKey。进而获取到 Netty 中的 NioSocketChannel。SelectionKey 与 NioSocketChannel 的对应关系如下图所示:

最初将注册在 Reactor 上的这些 NioSocketChannel 挨个进行敞开。

Channel 的敞开流程能够回看下笔者的这篇文章《且看 Netty 如何应答 TCP 连贯的失常敞开,异样敞开,半敞开场景》

6.3.2 保障业务无损

该办法中的逻辑是保障 Reactor 进行优雅敞开的外围,Netty 这里为了保障业务无损,采取的是只有有异步工作 Task 或者 ShutdwonHooks 须要执行,就不能敞开,须要期待所有 tasks 或者 ShutdownHooks 执行结束,才会思考敞开的事件。

    protected boolean confirmShutdown() {if (!isShuttingDown()) {return false;}

        if (!inEventLoop()) {throw new IllegalStateException("must be invoked from an event loop");
        }

        // 勾销掉所有的定时工作
        cancelScheduledTasks();

        if (gracefulShutdownStartTime == 0) {
            // 获取优雅敞开开始工夫,绝对工夫
            gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();}

        // 这里判断只有有 task 工作须要执行就不能敞开
        if (runAllTasks() || runShutdownHooks()) {if (isShutdown()) {
                // Executor shut down - no new tasks anymore.
                return true;
            }

            /**
             * gracefulShutdownQuietPeriod 示意在这段时间内,用户还是能够持续提交异步工作的,Reactor 在这段时间内
             * 是会保障这些工作被执行到的。*
             * gracefulShutdownQuietPeriod = 0 示意 没有这段静默期间,以后 Reactor 中的工作执行结束后,无需期待静默期,执行敞开
             * */
            if (gracefulShutdownQuietPeriod == 0) {return true;}
            // 防止 Reactor 在 Selector 上阻塞,因为此时曾经不会再去解决 IO 事件了,分心解决敞开流程
            taskQueue.offer(WAKEUP_TASK);
            return false;
        }

        // 此时 Reactor 中曾经没有工作可执行了,是时候思考敞开的事件了
        final long nanoTime = ScheduledFutureTask.nanoTime();

        // 当 Reactor 中所有的工作执行结束后,判断是否超过 gracefulShutdownTimeout
        // 如果超过了 则间接敞开
        if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) {return true;}

        // 即便当初没有工作也还是不能进行敞开,须要期待一个静默期,在静默期内如果没有新的工作提交,才会进行敞开
        // 如果在静默期内还有工作持续提交,那么静默期将会从新开始计算,进入一轮新的静默期检测
        if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {taskQueue.offer(WAKEUP_TASK);
            try {
                //gracefulShutdownQuietPeriod 内每隔 100ms 检测一下 是否有工作须要执行
                Thread.sleep(100);
            } catch (InterruptedException e) {// Ignore}

            return false;
        }

        // 在整个 gracefulShutdownQuietPeriod 期间内没有工作须要执行或者静默期完结 则无需期待 gracefulShutdownTimeout 超时,间接敞开
        return true;
    }

在敞开流程开始之前,Netty 首先会调用 cancelScheduledTasks() 办法将 Reactor 中残余须要执行的定时工作全副勾销掉。

记录优雅敞开开始工夫 gracefulShutdownStartTime,这是为了后续判断优雅敞开流程是否超时。

调用 runAllTasks() 办法将 Reactor 中 TaskQueue 里残余的异步工作全副取出执行。

调用 runShutdownHooks() 办法将用户注册在 Reactor 上的 ShutdownHook 取出执行。

咱们能够在用户线程中通过如下形式向 Reactor 中注册 ShutdownHooks:

        NioEventLoop reactor = (NioEventLoop) ctx.channel().eventLoop();
        reactor.addShutdownHook(new Runnable() {
            @Override
            public void run() {..... 敞开逻辑....}
        });

在 Reactor 进行敞开的时候,会取出用户注册的这些 ShutdownHooks 进行运行。

public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {

   // 能够向 Reactor 增加 shutdownHook,当 Reactor 敞开的时候会被调用
   private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>();

   private boolean runShutdownHooks() {
        boolean ran = false;
        while (!shutdownHooks.isEmpty()) {List<Runnable> copy = new ArrayList<Runnable>(shutdownHooks);
            shutdownHooks.clear();
            for (Runnable task: copy) {
                try {
                    //Reactor 线程挨个程序同步执行
                    task.run();} catch (Throwable t) {logger.warn("Shutdown hook raised an exception.", t);
                } finally {ran = true;}
            }
        }

        if (ran) {lastExecutionTime = ScheduledFutureTask.nanoTime();
        }

        return ran;
    }

}

须要留神的是这里的 ShutdownHooks 是 Netty 提供的一种机制并不是咱们在《3. JVM 中的 ShutdownHook》大节中介绍的 JVM 中的 ShutdownHooks。

JVM 中的 ShutdownHooks 是一个 Thread,JVM 在敞开之前会 并发无序 地运行。而 Netty 中的 ShutdownHooks 是一个 Runnable,Reactor 在敞开之前,会由 Reactor 线程 同步有序 地执行。

这里须要留神的是只有有 tasks 和 hooks 须要执行 Netty 就会始终执行上来直到这些工作全副执行完为止

当 Reactor 没有任何工作须要执行时,这时就会判断以后敞开流程所用工夫是否超过了咱们前边设定的优雅敞开最大超时工夫 gracefulShutdownTimeout。

nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout

如果敞开流程因为前边这些工作的执行导致曾经超时,那么就间接敞开 Reactor,退出 Reactor 的工作循环。

如果没有超时,那么这时就会触发前边介绍的优雅敞开的静默期 gracefulShutdownQuietPeriod。

在静默期中 Reactor 线程会每隔 100ms 检查一下是否有用户提交工作申请,如果有的话,就须要保障将用户提交的这些工作执行结束。而后静默期将会从新开始计算,进入一轮新的静默期检测。

如果在整个静默期内,没有任何工作提交,则无需期待 gracefulShutdownTimeout 超时,间接敞开 Reactor,退出 Reactor 的工作循环。

从以上过程咱们能够看出 Netty 的优雅敞开至多须要期待一个静默期的工夫。还有一点是 Netty 优雅敞开的工夫可能会超出 gracefulShutdownTimeout,因为 Netty 须要保障遗留残余的工作被执行结束。当所有工作执行结束之后,才会去检测是否超时。

6.4 Reactor 的最终敞开流程

当在静默期内没有任何工作提交或者敞开流程超时时,上大节中介绍的 confirmShutdown() 就会返回 true。随即 Reactor 线程就会退出工作循环。

public final class NioEventLoop extends SingleThreadEventLoop {

    @Override
    protected void run() {for (;;) {
            try {
                  .......1. 监听 Channel 上的 IO 事件.......
                  .......2. 解决 Channel 上的 IO 事件.......
                  .......3. 执行异步工作..........
            } finally {
                try {if (isShuttingDown()) {
                        // 敞开 Reactor 上注册的所有 Channel, 进行解决 IO 事件,触发 unActive 以及 unRegister 事件
                        closeAll();
                        // 登记掉所有 Channel 进行解决 IO 事件之后,剩下的就须要执行 Reactor 中残余的异步工作了
                        if (confirmShutdown()) {return;}
                    }
                } catch (Error e) {throw (Error) e;
                } catch (Throwable t) {handleLoopException(t);
                }
            }
        }
    }

}

咱们在《具体图解 Netty Reactor 启动全流程》一文中的《1.3.3 Reactor 线程的启动》大节中的介绍中提到,Reactor 线程的启动是通过第一个异步工作被提交到 Reactor 中的时候被触发的。在向 Reactor 提交工作的办法 SingleThreadEventExecutor#execute(java.lang.Runnable, boolean) 中会触发上面 doStartThread() 办法的调用,在这里会调用前边提到的 Reactor 工作循环 run() 办法。

在 doStartThread() 办法的 finally{…} 语句块中会实现 Reactor 的最终敞开流程,也就是 Reactor 在退出 run 办法中的 for 循环之后的后续收尾流程。

最终 Reactor 的优雅敞开残缺流程如下图所示:

public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {

                .......... 省略.........

                try {
                    //Reactor 线程开始轮询解决 IO 事件,执行异步工作
                    SingleThreadEventExecutor.this.run();
                    // 前面的逻辑为用户调用 shutdownGracefully 敞开 Reactor 退出循环 走到这里
                    success = true;
                } catch (Throwable t) {logger.warn("Unexpected exception from an event executor:", t);
                } finally {
                    // 走到这里示意在静默期内曾经没有用户在向 Reactor 提交工作了,或者达到优雅敞开超时工夫,开始对 Reactor 进行敞开
                    // 如果以后 Reactor 不是敞开状态则将 Reactor 的状态设置为 ST_SHUTTING_DOWN
                    for (;;) {
                        int oldState = state;
                        if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {break;}
                    }

                    try {for (;;) {
                            // 此时 Reactor 线程尽管曾经退出,而此时 Reactor 的状态为 shuttingdown,但工作队列还在
                            // 用户在此时仍然能够提交工作,这里是确保用户在最初的这一刻提交的工作能够失去执行。if (confirmShutdown()) {break;}
                        }

                        for (;;) {
                            // 当 Reactor 的状态被更新为 SHUTDOWN 后,用户提交的工作将会被回绝
                            int oldState = state;
                            if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {break;}
                        }

                        // 这里 Reactor 的状态曾经变为 SHUTDOWN 了,不会在承受用户提交的新工作了
                        // 但为了避免用户在状态变为 SHUTDOWN 之前,也就是 Reactor 在 SHUTTINGDOWN 的时候 提交了工作
                        // 所以此时 Reactor 中可能还会有工作,须要将残余的工作执行结束
                        confirmShutdown();} finally {
                        try {
                            //SHUTDOWN 状态下,在将全副的残余工作执行结束后,则将 Selector 敞开
                            cleanup();} finally {
                            // 清理 Reactor 线程中的 threadLocal 缓存,并告诉相应 future。FastThreadLocal.removeAll();

                            //ST_TERMINATED 状态为 Reactor 真正的终止状态
                            STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                            
                            // 使得 awaitTermination 办法返回
                            threadLock.countDown();

                            // 统计一下以后 reactor 工作队列中还有多少未执行的工作,打出日志
                            int numUserTasks = drainTasks();
                            if (numUserTasks > 0 && logger.isWarnEnabled()) {
                                logger.warn("An event executor terminated with" +
                                        "non-empty task queue (" + numUserTasks + ')');
                            }

                            /**
                             * 告诉 Reactor 的 terminationFuture 胜利,在创立 Reactor 的时候会向其 terminationFuture 增加 Listener
                             * 在 listener 中减少 terminatedChildren 个数,当所有 Reactor 敞开后 ReactorGroup 敞开胜利
                             * */
                            terminationFuture.setSuccess(null);
                        }
                    }
                }
            }
        });
    }
}

流程走到 doStartThread 办法中的 finally{…} 语句块中的时候,这个时候示意在优雅敞开的静默期内,曾经没有工作持续向 Reactor 提交了。或者敞开耗时曾经超过了设定的优雅敞开最大超时工夫。

当初正式来到了 Reactor 的敞开流程。在流程开始之前须要确保以后 Reactor 的状态为 ST_SHUTTING_DOWN 正在敞开状态。

留神此刻用户线程仍然能够向 Reactor 提交工作。当 Reactor 的状态变为 ST_SHUTDOWN 或者 ST_TERMINATED 时,用户向 Reactor 提交的工作就会被回绝,然而此时 Reactor 的状态为 ST_SHUTTING_DOWN,仍然能够承受用户提交过去的工作。

public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
  @Override
  public boolean isShutdown() {return state >= ST_SHUTDOWN;}

  private void execute(Runnable task, boolean immediate) {boolean inEventLoop = inEventLoop();
        addTask(task);
        if (!inEventLoop) {startThread();
            // 当 Reactor 的状态为 ST_SHUTDOWN 时,回绝用户提交的异步工作,然而在优雅敞开 ST_SHUTTING_DOWN 状态时还是能够承受用户提交的工作的
            if (isShutdown()) {
                boolean reject = false;
                try {if (removeTask(task)) {reject = true;}
                } catch (UnsupportedOperationException e) { }
                if (reject) {reject();
                }
            }
        }

        ......... 省略........
    }
}

所以 Reactor 从工作循环 run 办法中退出随后流程一路走到这里来的这段时间,用户依然有可能向 Reactor 提交工作,为了确保敞开流程的优雅,这里会在 for 循环中不停的执行 confirmShutdown() 办法直到所有的工作全副执行结束。

随后会将 Reactor 的状态改为 ST_SHUTDOWN 状态,此时用户就不能在向 Reactor 提交工作了。如果此时在提交工作就会收到 RejectedExecutionException 异样。

大家这里可能会有疑难,Netty 在 Reactor 的状态变为 ST_SHUTDOWN 之后,又一次调用了 confirmShutdown() 办法,这是为什么呢?

其实这样做的目标是为了避免 Reactor 状态在变为 SHUTDOWN 之前,在这个极限的工夫里,用户又向 Reactor 提交了工作,所以还须要最初一次调用 confirmShutdown() 将在这个极限工夫内提交的工作执行结束。

以上逻辑步骤就是真正优雅敞开的精华所在,确保工作全副执行结束,保障业务无损。

在咱们优雅解决流程介绍完了之后,上面就是敞开 Reactor 的流程了:

Reactor 会在 SHUTDOWN 状态下,将 Selector 进行敞开。

    @Override
    protected void cleanup() {
        try {selector.close();
        } catch (IOException e) {logger.warn("Failed to close a selector.", e);
        }
    }

清理 Reactor 线程中遗留的所有 ThreadLocal 缓存。

FastThreadLocal.removeAll();

将 Reactor 的状态由 SHUTDOWN 改为 ST_TERMINATED 状态。此时 Reactor 就算真正的敞开了

 STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);

用户线程可能会调用 Reactor 的 awaitTermination 办法阻塞期待 Reactor 的敞开,当 Reactor 敞开之后会调用 threadLock.countDown() 使得用户线程从 awaitTermination 办法返回。

public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {private final CountDownLatch threadLock = new CountDownLatch(1);

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        
         ........ 省略.......

        // 期待 Reactor 敞开
        threadLock.await(timeout, unit);
        return isTerminated();}

    @Override
    public boolean isTerminated() {return state == ST_TERMINATED;}
}

当这所有处理完毕之后,最初就会设置 Reactor 的 terminationFuture 为 success。此时注册在 Reactor 的 terminationFuture 上的 listener 就会被回调。

这里还记得咱们在《Reactor 在 Netty 中的实现(创立篇)》一文中介绍的,在 ReactorGroup 中的所有 Reactor 被挨个全副创立胜利之后,会向所有 Reactor 的 terminationFuture 注册一个 terminationListener。

在 terminationListener 中检测以后 ReactorGroup 中的所有 Reactor 是否全副实现敞开,如果曾经全副敞开,则设置 ReactorGroup 的 terminationFuture 为 Success。此刻 ReactorGroup 敞开流程完结,Netty 正式优雅谢幕结束~~


public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {

    //Reactor 线程组中的 Reactor 汇合
    private final EventExecutor[] children;
    // 记录敞开的 Reactor 个数,当 Reactor 全副敞开后,才能够认为敞开胜利
    private final AtomicInteger terminatedChildren = new AtomicInteger();
    //ReactorGroup 敞开 future
    private final Promise<?> terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
      
        ........ 挨个创立 Reactor........

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {if (terminatedChildren.incrementAndGet() == children.length) {
                    // 当所有 Reactor 敞开后 才认为是敞开胜利
                    terminationFuture.setSuccess(null);
                }
            }
        };

        for (EventExecutor e: children) {e.terminationFuture().addListener(terminationListener);
        }

        ........ 省略........
    }

}

到当初为止,Netty 的整个优雅敞开流程,笔者就为大家具体介绍完了,下图为整个优雅敞开的残缺流程图,大家能够对照上面这副总体流程图在回顾下咱们后面介绍的源码逻辑。

6.5 Reactor 的状态变更流转

在本文的最初,笔者再来带着大家回顾下 Reactor 的状态变更流程。

  • 在 Reactor 被创立进去之后状态为 ST_NOT_STARTED。
  • 随着第一个异步工作的提交 Reactor 开始启动随后状态为 ST_STARTED。
  • 当调用 shutdownGracefully 办法之后,Reactor 的状态变为 ST_SHUTTING_DOWN。示意正在进行优雅敞开。此时用户仍可向 Reactor 提交异步工作。
  • 当 Reactor 中遗留的工作全副执行结束之后,Reactor 的状态变为 ST_SHUTDOWN。此时如果用户持续向 Reactor 提交异步工作,会被回绝,并收到 RejectedExecutionException 异样。
  • 当 Selector 实现敞开,并清理掉 Reactor 线程中所有的 TheadLocal 缓存之后,Reactor 的状态变为 ST_TERMINATED。

总结

到这里对于优雅敞开的前世今生笔者就位大家全副交代结束了,信息量比拟大,须要好好消化一下,很拜服大家可能一口气看到这里。

本文咱们从过程优雅启停计划开始聊起,以优雅敞开的实现计划为终点,先是介绍了优雅敞开的底层基石 - 内核的信号量机制,从内核又聊到了 JVM 的 ShutdownHook 原理以及执行过程,最初通过三个出名的开源框架为案例,别离从 Spring 的优雅敞开机制聊到了 Dubbo 的优雅敞开,最初通过 Dubbo 的优雅敞开引出了 Netty 优雅敞开的具体实现计划,前后响应。

好了,本文的内容就到这里了,大家辛苦了,置信大家认真看完之后肯定会播种很大,咱们下篇文章见~~~

正文完
 0