Golang服务中context超时处理的方法详解

 更新时间:2023年05月22日 10:19:23   作者:皇家茶壶  
在Go语言中,Context是一个非常重要的概念,它存在于一个完整的业务生命周期内,Context类型是一个接口类型,在实际应用中,我们可以使用Context包来传递请求的元数据,本文将给大家介绍Golang服务中context超时处理的方法和超时原因,需要的朋友可以参考下
(福利推荐:【腾讯云】服务器最新限时优惠活动,云服务器1核2G仅99元/年、2核4G仅768元/3年,立即抢购>>>:9i0i.cn/qcloud

(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅2998元/3年,立即抢购>>>:9i0i.cn/aliyun

前言

公司运行的服务代码中,随处可见各种各样的日志信息,其中大多数是用来记录各种异常的日志,一方面,当出现问题时,通过日志我们可以快速的定位引发问题的原因;另外我们可以通过日志平台,对一些错误级别比较高的日志进行监控,从而能够快速响应系统可能会出现的问题。

起因:日志告警引发的思考

虽然日志告警很有用,但如果告警次数过于频繁,反而会降低开发人员对于系统异常的敏感度,使得告警变得毫无意义。因此,我们需要对告警进行治理。最近,由于一次治理线上频发的超时告警,使得笔者开始思考起context deadline exceed异常的问题。

什么是context

在Go语言中,Context是一个非常重要的概念,它存在于一个完整的业务生命周期内,Context类型是一个接口类型,它定义了四个方法:Deadline()、Done()、Err()和Value()。其中,Deadline()方法返回context的截止日期,Done()方法返回一个只读的channel,当Context被取消或超时时,该channel会被关闭,Err()方法返回Context被取消的原因,Value()方法返回Context中与key相关联的值。

context的作用

在实际应用中,我们可以使用Context包来传递请求的元数据,例如请求ID、超时信息等等。此外,我们还可以使用context包来控制goroutine的生命周期(最常见的),例如在HTTP请求处理程序中,我们可以使用context包来取消正在处理的请求。

可以说,我们的服务里,随处可见携带context参数的方法。

context超时之后

先来看一段例子

package main

import (
	"context"
	"fmt"
	"time"
)

func timeConsuming(ctx context.Context, costTime int) {

	ctx.Done()

	for i := 1; i <= costTime; i++ {
		// 模拟一些耗时操作
		time.Sleep(1 * time.Second)
		fmt.Printf("协程正在运行第%v次...\n", i)
	}
}

func main() {
	// 创建一个父级 context,设置超时时间为 5 秒钟
	parentCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 创建一个子级 context,用于控制协程
	childCtx, childCancel := context.WithCancel(parentCtx)
	defer childCancel()

	costTime := 5 // 模拟耗时 5 秒钟

	// 启动一个协程
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				// 如果收到取消信号,退出协程
				fmt.Println("协程退出")
				return
			case <-time.After(15 * time.Second):
				fmt.Println("协程超时")
			default:
				timeConsuming(childCtx, costTime)
			}
		}
	}(childCtx)

	// 等待 3 秒钟,然后取消子级 context
	time.Sleep(3 * time.Second)
	fmt.Println("取消协程")
	childCancel()

	// 继续等待 3 秒钟,模拟主协程的一些其他操作
	time.Sleep(3 * time.Second)
	fmt.Println("主协程退出")
}

上面代码的执行结果如下

协程正在运行第1次...
协程正在运行第2次...
取消协程
协程正在运行第3次...
协程正在运行第4次...
协程正在运行第5次...
协程退出
主协程退出

 虽然说Context可以用来管理goroutine,但是可以看到,Context超时之后,goroutine仍然在执行完成之后才会退出,Context无法真正做到强制杀死goroutine

回到文章最开始提到的线上超时告警频发的问题,经过排查我们发现,一波超时告警的出现实际上只是几条请求引起的(都是同一个trace_id)。究其原因,是我们下游的服务在单次业务请求中,会与很多第三方接口发生交互(在本篇文章的case是并发调用redis),而在业务执行到并发调用redis之前,业务逻辑就已经发生了超时。

超时后,上游调用端不再继续等待响应,直接返回了超时异常。

前面已经提到过,goroutine是无法强制杀死的,此时goroutine携带着已经超时的context依旧在执行着业务逻辑,在执行到并发调用redis时,由于context已经超时,调用无一例外的全部抛出超时错误(实际上并未真正发生调用redis,redis客户端代码在调用前判断了context的状态),
从而导致个位数的超时请求却引起了大量日志的超时告警。

...
//If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
if ctx.Err() != nil { // 这里抛出了context deadline exceeded 异常
	return nil, ctx.Err()
}
...

继续执行 or 中断

知道了问题,其实处理起来就比较容易了,我们将context的状态的判断改写到了合适的位置(在一些耗时的节点之间判断了context的状态,如果判断超时,则直接结束后续的业务流程)

日志告警清净了!

但是,这样的处理方式具有普适性吗?可以思考一下,在某些超时的情况中,即便上游已经返回了超时异常,我们仍然希望下游能够将这次业务完整的执行完。

举一个例子,下游在执行完返回之前,会将本次执行的结果进行缓存。而上游在调用下游之前,也会去取缓存,取到了就直接返回(假设上下游服务共用一套缓存集群)。假如某些请求耗时比较久,而且我们在判断请求超时之后直接中断下游任务的执行,那么,缓存将永远不会生成,上游后续的调用依旧会超时。这种情况下,即便是超时了,我们也希望下游任务能够完整执行,并生成缓存,后续上游就可以直接拿到业务结果返回,避免大量耗时的调用。

最后

本篇描述的本身是一个极为常见的问题及处理方案。但是在平时处理问题的过程中,如果勤加思考,仍然会有所收获和提升。

以上就是Golang服务中context超时处理的方法详解的详细内容,更多关于Golang context超时处理的资料请关注程序员之家其它相关文章!

相关文章

  • Go读写锁操作方法示例详解

    Go读写锁操作方法示例详解

    这篇文章主要为大家介绍了Go读写锁方法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Go语言中log日志库的介绍

    Go语言中log日志库的介绍

    本文给大家介绍Go语言中log日志库的概念使用技巧,log包定义了Logger类型,该类型提供了一些格式化输出的方法,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-10-10
  • 如何利用Golang写出高并发代码详解

    如何利用Golang写出高并发代码详解

    今天领导问起为什么用Golang,同事回答语法简单,语言新,支持高并发。那高并发到底如何实现,下面这篇文章主要给大家介绍了关于如何利用Golang写出高并发代码的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-09-09
  • 在golang中使用Sync.WaitGroup解决等待的问题

    在golang中使用Sync.WaitGroup解决等待的问题

    这篇文章主要介绍了在golang中使用Sync.WaitGroup解决等待的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go语言题解LeetCode下一个更大元素示例详解

    Go语言题解LeetCode下一个更大元素示例详解

    这篇文章主要为大家介绍了Go语言题解LeetCode下一个更大元素示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 使用GO实现Paxos共识算法的方法

    使用GO实现Paxos共识算法的方法

    这篇文章主要介绍了使用GO实现Paxos共识算法,本文给大家介绍的非常详细,对大家的学习或工作,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • 一文教你如何快速学会Go的struct数据类型

    一文教你如何快速学会Go的struct数据类型

    结构是表示字段集合的用户定义类型。它可以用于将数据分组为单个单元而不是将每个数据作为单独的值的地方。本文就来和大家聊聊Go中struct数据类型的使用,需要的可以参考一下
    2023-03-03
  • Go疑难杂症讲解之为什么nil不等于nil

    Go疑难杂症讲解之为什么nil不等于nil

    在日常开发中,可能一不小心就会掉进?Go?语言的某些陷阱里,而本文要介绍的?nil?≠?nil?问题,感兴趣的小伙伴可以跟随小编一起了解一下
    2022-10-10
  • Go语言多人聊天室项目实战

    Go语言多人聊天室项目实战

    这篇文章主要为大家详细介绍了Go语言多人聊天室项目实战,实现单撩或多撩等多种功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-08-08
  • 详解Go语言中的内存对齐

    详解Go语言中的内存对齐

    前面我们学习了Go语言空结构体详解,最近又在看unsafe包的知识,在查阅相关资料时不免会看到内存对齐相关的内容。虽然不会,但可以学呀,那么这篇文章,我们就一起来看下什么是内存对齐吧
    2022-10-10

最新评论

?


http://www.vxiaotou.com