A:老板,有个需求我搞不定,需求:

用户发送 开始消费 请求时,开启多个协程开始消费消息队列某个topic的信息;
用户发送 结束消费 请求时,把消费中的topic相关的协程关闭掉,结束消费;

A:我之前写过的并发代码,都是开个 WaitGroup 等所有协程处理完后继续执行后续逻辑,这种根据用户请求关闭协程的代码该怎么写啊?

B:控制多个协程的关闭?听起来可以用channel或Context来做。

A:噢,我把channel给忘了,这里可以用channel发信号关闭协程,不过Context怎么处理呢?

Done

A:我还真没研究过这玩意,对Context的印象就是这这玩意在项目中到处都是,很多时候调用函数时把它当第一个参数传递,打日志时也要把ctx传入函数里。不过为啥这么做我也不清楚。

B:不管是函数参数,还是打日志,都是在用context传递请求上下文,至于原理,且听我来给你介绍:

跨服务传递信息

现在具备一定规模的互联网公司都用微服务形式让各系统组合起来为用户提供服务,一个简单的业务在流程上可能需要十几个甚至几十个系统间互相调用。由于每个系统内部的正确性无法保证,若出现了case,比如用户反馈积分少发了,就需要排查这十几个系统的日志信息,看问题出在哪里。

此处需要一个ID凭证,ID是请求级别的,在各个系统中记录着与此请求相关的日志信息,我们把它叫做trace ID。把日志采集并落盘到ES这样的存储中,有case时只需要拿到请求的trace ID就可以把全流程的关键信息还原出来。如图所示:


在Golang web服务中,每个请求都是开一个协程去处理的。系统间传递信息时,若通信协议用HTTP,那trace ID等信息可放在HTTP Header中,在web框架的middle层把这些信息存入Context。demo如下:

WithValue


ctx的生命周期是 伴随请求开始而诞生、请求结束而终止的。在请求中ctx会跨越多个函数多个协程,在打日志时,第一个参数预留给ctx是因为日志库需要从Context中抽取trace ID等信息,从而记录下完整的日志。获取信息时只需要调用context的Value方法,demo如下:

这里画个图帮助理解:


若我们的系统也需要请求第三方服务,同样应把trace ID等信息放入HTTP Header后发送请求,其他服务按照同样的流程接收到trace ID后开始内部逻辑处理。这样一个请求在多个系统中就通过trace ID串联起了整个流程。除trace ID外,Context还可以传递 URL Path、请求时间、Caller等信息。


A:厉害了老板,经过你的讲解,我现在已经知道context怎么用了。

B:汗,刚刚的传递上下文只是context三大功能的一块,另外context还有一块很重要的功能是取消机制,控制子协程取消和超时取消机制还没讲呢。

A:对哦,我最开始的需求你还没告诉我用Context如何控制多个协程的关闭呢?

B:这块也挺简单,你把相关代码贴出来:

多协程消费demo:

B:要通过Context关闭所有协程,可以这样改造代码:


控制协程关闭


WithCancel
CancelFunc


但如果用户在访问网站时觉得没意思,去其他网站了。此时若你的服务收到用户请求后继续去访问其他C system、B database就是浪费资源。比较符合直觉的做法是:当业务请求取消时,你的系统也应该停止请求下游系统。前面我们介绍过context在系统中贯穿请求周期,那么当用户取消访问时,只要context监听取消事件并在用户取消时发送取消事件,就可以取消请求了。

curl localhost:8888request canceleldprocess finished


CancelFunc


A:原来cancelFunc是这么用的,我觉得我行了,context我差不多会了,除了你说的超时控制。

B:超时机制你应该也可以想象出来,你知道SLA(service level agreement)吗?

A:知道,SLA就是服务对外承诺的请求最长持续时间,比如我们的服务对外承诺的SLA就是99分位100ms。在Golang里,SLA可以通过Context做的?

B:是的,可以通过Context的WithTimeout做:


控制超时取消


WithTimeout

正常情况下,会得到这样的输出:

如果我们请求百度超时了,会得到这样的输出:


A:厉害了,原来SLA可以这样搞,这次我算了解到Context的使用场景了。现在我对Context的源码充满了好奇。

B:篇幅有限,写多了读者也不爱看,关于Context源码和使用时的注意点,我们下篇文章聊聊吧。