Go 优雅退出实现方法 & context原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 优雅退出实现方法 & context原理相关的知识,希望对你有一定的参考价值。

参考技术A

1.通过endless包实现

2.通过shutdown实现

在go 1.8.x后,golang在http里加入了shutdown方法,用来控制优雅退出。什么是优雅退出? 简单说就是不处理新请求,但是会处理正在进行的请求,把旧请求都处理完,也就是都response之后,那么就退出。

shutdown通过context上下文实现

社区里不少http graceful动态重启,平滑重启的库,大多是基于http.shutdown做的。平滑启动的原理很简单,fork子进程,继承listen fd, 老进程优雅退出。

3.context原理

context 是 Go 并发编程中常用到一种编程模式。

在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。熟悉 channel 的朋友应该都见过使用 done channel 来处理此类问题。比如以下这个例子:

上述例子中定义了一个 buffer 为0的 channel done , 子协程运行着定时任务。如果主协程需要在某个时刻发送消息通知子协程中断任务退出,那么就可以让子协程监听这个 done channel ,一旦主协程关闭 done channel ,那么子协程就可以推出了,这样就实现了主协程通知子协程的需求。这很好,但是这也是有限的。

如果我们可以在简单的通知上附加传递额外的信息来控制取消:为什么取消,或者有一个它必须要完成的最终期限,更或者有多个取消选项,我们需要根据额外的信息来判断选择执行哪个取消选项。

考虑下面这种情况:假如主协程中有多个任务1, 2, …m,主协程对这些任务有超时控制;而其中任务1又有多个子任务1, 2, …n,任务1对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号。

如果还是使用 done channel 的用法,我们需要定义两个 done channel ,子任务们需要同时监听这两个 done channel 。嗯,这样其实好像也还行哈。但是如果层级更深,如果这些子任务还有子任务,那么使用 done channel 的方式将会变得非常繁琐且混乱。

我们需要一种优雅的方案来实现这样一种机制:

这个时候 context 就派上用场了。

我们首先看看 context 的结构设计和实现原理。

先看 Context 接口结构,看起来非常简单。

Context 接口包含四个方法:

可以看到 Done 方法返回的 channel 正是用来传递结束信号以抢占并中断当前任务; Deadline 方法指示一段时间后当前 goroutine 是否会被取消;以及一个 Err 方法,来解释 goroutine 被取消的原因;而 Value 则用于获取特定于当前任务树的额外信息。而 context 所包含的额外信息键值对是如何存储的呢?其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到 key 所对应的值,就会向上去父节点里找,直到根节点,具体后面会说到。

emptyCtx 是一个 int 类型的变量,但实现了 context 的接口。 emptyCtx 没有超时时间,不能取消,也不能存储任何额外信息,所以 emptyCtx 用来作为 context 树的根节点。

但我们一般不会直接使用 emptyCtx ,而是使用由 emptyCtx 实例化的两个变量,分别可以通过调用 Background 和 TODO 方法得到,但这两个 context 在实现上是一样的。那么 Background 和 TODO 方法得到的 context 有什么区别呢?可以看一下官方的解释:

Background 和 TODO 只是用于不同场景下:

Background 通常被用于主函数、初始化以及测试中,作为一个顶层的 context ,也就是说一般我们创建的 context 都是基于 Background ;

而 TODO 是在不确定使用什么 context 的时候才会使用。

下面将介绍两种不同功能的基础 context 类型: valueCtx 和 cancelCtx 。

valueCtx 利用一个 Context 类型的变量来表示父节点 context ,所以当前 context 继承了父 context 的所有信息; valueCtx 类型还携带一组键值对,也就是说这种 context 可以携带额外的信息。 valueCtx 实现了 Value 方法,用以在 context 链路上获取 key 对应的值,如果当前 context 上不存在需要的 key ,会沿着 context 链向上寻找 key 对应的值,直到根节点。

WithValue 用以向 context 添加键值对:

这里添加键值对不是在原 context 结构体上直接添加,而是以此 context 作为父节点,重新创建一个新的 valueCtx 子节点,将键值对添加在子节点上,由此形成一条 context 链。获取 value 的过程就是在这条 context 链上由尾部上前搜寻:



跟 valueCtx 类似, cancelCtx 中也有一个 context 变量作为父节点;变量 done 表示一个 channel ,用来表示传递关闭信号; children 表示一个 map ,存储了当前 context 节点下的子节点; err 用于存储错误信息表示任务结束的原因。

再来看一下 cancelCtx 实现的方法:

可以发现 cancelCtx 类型变量其实也是 canceler 类型,因为 cancelCtx 实现了 canceler 接口。 Done 方法和 Err 方法没必要说了, cancelCtx 类型的 context 在调用 cancel 方法时会设置取消原因,将 done channel 设置为一个关闭 channel 或者关闭 channel ,然后将子节点 context 依次取消,如果有需要还会将当前节点从父节点上移除。

WithCancel 函数用来创建一个可取消的 context ,即 cancelCtx 类型的 context 。 WithCancel 返回一个 context 和一个 CancelFunc ,调用 CancelFunc 即可触发 cancel 操作。直接看源码:

之前说到 cancelCtx 取消时,会将后代节点中所有的 cancelCtx 都取消, propagateCancel 即用来建立当前节点与祖先节点这个取消关联逻辑。

这里或许有个疑问,为什么是祖先节点而不是父节点?这是因为当前 context 链可能是这样的:



当前 cancelCtx 的父节点 context 并不是一个可取消的 context ,也就没法记录 children 。

timerCtx 是一种基于 cancelCtx 的 context 类型,从字面上就能看出,这是一种可以定时取消的 context 。

timerCtx 内部使用 cancelCtx 实现取消,另外使用定时器 timer 和过期时间 deadline 实现定时取消的功能。 timerCtx 在调用 cancel 方法,会先将内部的 cancelCtx 取消,如果需要则将自己从 cancelCtx 祖先节点上移除,最后取消计时器。

WithDeadline 返回一个基于 parent 的可取消的 context ,并且其过期时间 deadline 不晚于所设置时间 d 。

与 WithDeadline 类似, WithTimeout 也是创建一个定时取消的 context ,只不过 WithDeadline 是接收一个过期时间点,而 WithTimeout 接收一个相对当前时间的过期时长 timeout :

首先使用 context 实现文章开头 done channel 的例子来示范一下如何更优雅实现协程间取消信号的同步:

这个例子中,只要让子线程监听主线程传入的 ctx ,一旦 ctx.Done() 返回空 channel ,子线程即可取消执行任务。但这个例子还无法展现 context 的传递取消信息的强大优势。

阅读过 net/http 包源码的朋友可能注意到在实现 http server 时就用到了 context , 下面简单分析一下。

1、首先 Server 在开启服务时会创建一个 valueCtx ,存储了 server 的相关信息,之后每建立一条连接就会开启一个协程,并携带此 valueCtx 。

2、建立连接之后会基于传入的 context 创建一个 valueCtx 用于存储本地地址信息,之后在此基础上又创建了一个 cancelCtx ,然后开始从当前连接中读取网络请求,每当读取到一个请求则会将该 cancelCtx 传入,用以传递取消信号。一旦连接断开,即可发送取消信号,取消所有进行中的网络请求。

3、读取到请求之后,会再次基于传入的 context 创建新的 cancelCtx ,并设置到当前请求对象 req 上,同时生成的 response 对象中 cancelCtx 保存了当前 context 取消方法。

这样处理的目的主要有以下几点:

在整个 server 处理流程中,使用了一条 context 链贯穿 Server 、 Connection 、 Request ,不仅将上游的信息共享给下游任务,同时实现了上游可发送取消信号取消所有下游任务,而下游任务自行取消不会影响上游任务。

context 主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式 。另外在使用 context 时有两点值得注意:上游任务仅仅使用 context 通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说 context 的取消操作是无侵入的; context 是线程安全的,因为 context 本身是不可变的( immutable ),因此可以放心地在多个协程中传递使用。

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

false)

Type.SERVLET)
ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
     ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
     ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
     ServletWebServerFactoryConfiguration.EmbeddedUndertow.class )
class ServletWebServerFactoryAutoConfiguration 
    // ... ...
static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware 
   // ... ...
         logger.info(          createWebServer();     logger.info(    (Throwable ex)      ApplicationContextException(   WebServer webServer =  ServletContext servletContext = getServletContext(); (webServer == && servletContext ==   ServletWebServerFactory factory = getWebServerFactory();      (servletContext !=      getSelfInitializer().onStartup(servletContext);    (ServletException ex)    ApplicationContextException(    initPropertySources();

WebServer ( Registry.disableRegistry(); Tomcat tomcat = Tomcat(); File baseDir = ( tomcat.setBaseDir(baseDir.getAbsolutePath()); (LifecycleListener listener : tomcat.getServer().addLifecycleListener(listener); Connector connector = Connector( connector.setThrowOnFailure( tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy( configureEngine(tomcat.getEngine()); (Connector additionalConnector : tomcat.getService().addConnector(additionalConnector); prepareContext(tomcat.getHost(), initializers); getTomcatWebServer(tomcat);TomcatWebServer TomcatWebServer(tomcat, getPort() >= Assert.notNull(tomcat, GracefulShutdown(tomcat) : initialize(); logger.info(+ getPortsDescription( synchronized ( addInstanceIdToEngineName(); Context context = findContext(); context.addLifecycleListener(( (context. removeServiceConnectors(); ); rethrowDeferredStartupExceptions(); ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); (NamingException ex) startDaemonAwaitThread(); (Exception ex) stopSilently(); destroySilently(); WebServerException( clearResourceCaches(); initLifecycleProcessor(); getLifecycleProcessor().onRefresh(); publishEvent(ContextRefreshedEvent( (!NativeDetector.inNativeImage()) LiveBeansView.registerApplicationContext( startBeans(

  • the target VM, address: code (interrupted signal 2: SIGINT)

    4. 例行回顾

    本文采取 Debug 的方式跟了一下 Spring Boot 源码,梳理了一下 Spring Boot 内嵌 Tomcat 的主线脉略,并对内嵌 Tomcat 启动、关闭进行了深入了解。


    其实回头瞅瞅,Spring Boot 内嵌的 Tomcat 启动、关闭的实现方式,大体可以简化成下面的类图。


  • 内嵌容器启动或关闭时,经过 DefaultLifecycleProcessor 调用一系列方法进行最终是由 WebServerStartStopLifecycle、 WebServerGracefulShutdownLifecycle 来处理。


  • 而WebServerStartStopLifecycle、 WebServerGracefulShutdownLifecycle 持有 webServer 对象,最后调用持有的 X

    xxWebServer 对象的 start、stop 等方法来完成容器的启停操作


  • 本次就分享到这里,这种内嵌 Tomcat 的实现方式,对于轮子的开发或许有点帮助,你们 get 到了没?

    世上没有轻而易举就能获得的成功。所有出众的背后,都有着超乎寻常的努力。真正能够登顶远眺的,都是那些心无旁骛、坚持着往前走的人。再坚持一下,也许你离成功就差那一步。

    一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,欢迎关注,敬请期待!

    参考资料:

    https://spring.io/

    https://start.spring.io/

    https://spring.io/projects/spring-boot

    https://github.com/spring-projects/spring-boot

    https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

    https://stackoverflow.com/questions/tagged/spring-boot

    《Spring Boot实战》《深入浅出Spring Boot 2.x》

    《一步一步学Spring Boot:微服务项目实战(第二版)》

    《Spring Boot揭秘:快速构建微服务体系》

    以上是关于Go 优雅退出实现方法 & context原理的主要内容,如果未能解决你的问题,请参考以下文章

    Android 应用技巧: 手把手教你 优雅实现 “一键退出 App”

    Node.js & Kubernetes Graceful Shutdown

    如何优雅地关闭 boost asio ssl 客户端?

    玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

    优雅关闭web服务的方式

    Go值方法 & 指针方法