context canceled 到底谁在作祟?

一、背景

在工作中,因报警治理标准提高,在报警治理的过程中,有一类context cancel报警渐渐凸显出来。

目前context cancel日志报警大致可以分为两类。

  • context deadline exceeded

    • 耗时长
    • 有明确报错原因
  • context canceled

    • 耗时短
    • 无明确报错原因
    • 分布在各个接口

之前因为不了解原因,所以一遇到这类报警,统一都按照偶发超时处理,可是我们发现,这其中有一大半case 耗时并不长,整个业务接口耗时在300ms以内,甚至100ms以内,于是我对超时这个缘由产生了疑惑,带着这个疑惑,我在业余时间学习探究,最终找到了出现此类情况的一些场景。

二、底层原因探究

2.1 go context预备知识

context原理可以看我另一篇文章:context,go的上下文存储&并发控制之道

这里简单解释下go中context的部分原理,方便后续理解。

context是go中上下文的实现关键。

在我们实际业务场景中,context通常都会被作为函数的第一个参数不断传递下去。

func (i *ItemSalesController) ItemListFilterBar(ctx context.Context, req *proto.ItemListFilterBarReq) *proto.ItemListFilterBarResp
func (i *itemSalesService) ItemListFilterBar(ctx context.Context, bizLine, bizType, schemeType int32)
func getBrandFilterBars(ctx context.Context, salesMerchantId int64, bizType int32, schemeType int32)
//用于存值,类似与Java的ThreadLocal
type valueCtx struct {Contextkey, val any
}
//用于控制并发函数的生命周期,上层方法可以通过cancel的方式结束下游的调用(前提是下游需要感知context)
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call
}

创建新的context时会将上层的context作为新的字段存入。因此最终的context会形成一个类似函数调用关系树。

context关系示意图:

在这里插入图片描述

当context 被cancel时 ,可以通过ctx.Done()来感知context的状态,并可以通过ctx.Err()获取实际的报错类型。

2.2 http包感知context cancel的时机

先看下真实业务场景中的context(断点看变量):

在这里插入图片描述

go/net/http包底层通过select ctx.Done()返回的通道来感知context,达到快速失败的效果

//代码路径:go1.18.9/src/net/http/transport.go:563
func (t *Transport) roundTrip(req *Request) (*Response, error) {
//...for {select {case <-ctx.Done():req.closeBody()return nil, ctx.Err()default:}//...}
}

这里会快速返回Context 对应的err,而内置err分为下面两个

  • context deadline exceeded
  • context canceled

在这里插入图片描述

分别在调用以下两种场景会抛出:

  • 超时自动调用
//设置延迟3s后超时取消
ctx, cancel = context.WithTimeout(ctx,3*time.Second)
//设置固定时间超时取消
ctx, cancel = context.WithDeadline(ctx,time.Time{})
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {//...c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}//传播cancel信号,往下传递propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {//cancelc.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}//...if c.err == nil {//定时器超时取消cancelc.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
  • 主动调用cancel方法
ctx, cancel := context.WithCancel(ctx)
//主动调用cancel方法会取消context,err
cancel()

这里cancel方法,无论是业务层和框架层都有可能调用,一旦调用,下游感知到了就会返回err(context canceled)。

不过一般业务场景,这个都是由框架层面去调用的。

三、诱发场景探究

3.1排查思路

回到业务场景中,我排查了几个trace,并在本地在感知ctx.Done的地方断点调试,看整条链路中,context到底有哪些cancelCtx。

在这里插入图片描述

在这里插入图片描述

可以看到cancelCtx在整条链路中有四个,我的排查思路就是找到这四处cancelCtx,看看哪些逻辑可能导致context 被取消。

3.2 go/net/http包设置的cancelCtx

3.2.1 底层原理

底层设置的cancelCtx

//go1.18.9/src/net/http/client.go:359
func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) {//...//如果设置了timeOut参数,则会设置超时取消if req.Cancel == nil && knownTransport {var cancelCtx func()req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)return cancelCtx, func() bool { return time.Now().After(deadline) }}//...}

这里如果设置了TimeOut参数,则会设置一个超时取消,这个超时取消对应着err(context deadline exceeded)。

而这就是我们前面讲的第一类报警原因!

一般来说,调用http请求一般是context的末端,不会影响其他协程/方法,所以这里发生cancel一般都是超时取消。

3.3 框架生成的Handle中设置的cancelCtx

3.3.1底层原理

mux.Handle("GET", param1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {ctx, cancel := context.WithCancel(req.Context())defer cancel()//...
}

这里会在退出的时候主动调用cancel方法.

3.3.2延伸注意点:需要注意是否有异步协程遗留

如果该请求的主协程已经返回,退出时会调用cancel方法。

需要注意的场景的就是,如果你需要在主协程退出时,需要异步开启的协程依然正常运行,那么请对使用context做处理或者创建新的context(具体操作见文末)。

3.4 go server中cancelCtx

3.4.1底层原理

这里比较复杂,为了搞清楚来龙去脉,我们得简单捋一遍go server中的context流转。(go版本1.18.9)

我们来到最开始创建context的地方。

server 端接受新请求时会起一个协程 go c.serve(connCtx)

func (srv *Server) Serve(l net.Listener) error {//...//context最开始创建的地方baseCtx := context.Background()if srv.BaseContext != nil {baseCtx = srv.BaseContext(origListener)if baseCtx == nil {panic("BaseContext returned a nil context")}}//...for {// 从链接中读取请求w, err := c.readRequest(ctx)if c.r.remain != c.server.initialReadLimitSize() {// If we read any bytes off the wire, we're active.c.setState(c.rwc, StateActive, runHooks)}// ....// 启动协程后台读取链接if requestBodyRemains(req.Body) {registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)} else {w.conn.r.startBackgroundRead()}// ...// 这里转到具体框架的serverHttp方法serverHandler{c.server}.ServeHTTP(w, w.req)// 请求结束之后cancel掉contextw.cancelCtx()// ...}
}

这里我们看见第一处cancelCtx,会在结束时cancel。

func (c *conn) serve(ctx context.Context) {//...// HTTP/1.x from here on.ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer cancelCtx()//...//调用具体的Handler(后面就会根据路径匹配到我们写好的业务逻辑)serverHandler{c.server}.ServeHTTP(w, w.req)//...
}

这里我们看见第二处cancelCtx,依然是结束后cancel。

目前为止,我们看到是**请求结束之后才会 cancel 掉 context,而不是 cancel 掉 context 导致的请求结束。

那我们第二类报警到底是什么原因呢,经过多个链路分析,可以确定的是业务逻辑中并没有“遗漏”的协程,都是所有业务逻辑结束,请求才会返回。

直到我看到一篇博文,才恍然大悟,

context canceled,谁是罪魁祸首? | Go 技术论坛 (learnku.com)

这篇博文提到了另一个我们很容易忽略的地方

func (cr *connReader) startBackgroundRead() {// ...go cr.backgroundRead()
}func (cr *connReader) backgroundRead() {n, err := cr.conn.rwc.Read(cr.byteBuf[:])// ...if ne, ok := err.(net.Error); ok && cr.aborted && ne.Timeout() {// Ignore this error. It's the expected error from// another goroutine calling abortPendingRead.} else if err != nil {cr.handleReadError(err)}// ...
}func (cr *connReader) handleReadError(_ error) {// 这里cancel了contextcr.conn.cancelCtx()cr.closeNotify()
}

当服务端在处理业务的同时,后台有个协程监控链接的状态,如果链接有问题就会把 context cancel 掉(cancel 的目的就是快速失败 —— 业务不用处理了,就算服务端返回结果,客户端也不会处理了)

3.4.2 验证复现场景

这里我们拿报警的case接口在本地简单验证。

准备:

  • 本地项目调试,对以下逻辑打断点
    • 用于监控链接的状态的协程中,进入cancel逻辑的入口
    • 业务逻辑入口
    • http包底层感知context的地方
  • 代开Wireshark,过滤目标端口进行抓包

步骤:

  • 用apifox模拟客户端发送请求
  • 调试进入断点后
  • 取消请求,模拟链接断开

验证:

  • 观察断点是否进入监控链接的状态的协程中,进入cancel逻辑的入口
  • 观察断开链接后context中的cancelCtx 状态是否改变

果然,取消请求后,后台开启的协程会监听到Fin 请求,会返回EOF 错误,此时会进入处理错误逻辑,调用context cancel方法。

抓包看对应的就是 FIN 报文。

在这里插入图片描述

在http包底层监听到了cancel信号,此时会返回err(context canceled)

在这里插入图片描述

而上层感知到err时就把这个err打印报警出来,这就是为什么会出现第二类报错err context canceled。

我们看下抓的包,

在这里插入图片描述

所以验证结果证实了这种可能。

当客户端断开链接时,服务端感知到了(FIN报文),会在框架层主动调用context cancel方法,而下游感知该context的地方就会抛出context canceled的err。

四、原因总结

至此,我们分析了整条链路中可能cancel的地方,我们回到我们最开始的问题——报警日志中context cancel原因是什么?

对于context deadline exceeded报错,它是定时器cancel的,可能诱发的操作场景:

  • 配置的超时时间,http调用超时触发
  • 业务代码中设置的context.WithTimeout、context.WithDeadline方法超时导致

对于context canceled报错,它是代码中主动cancel的,可能诱发的操作场景:

  • 请求中异步开启协程,主协程返回,开启的协程并未退出
  • 客户端调用链接提前断开,服务感知到FIN请求,后台协程执行cancel快速失败

五、解决建议

针对不同场景我们需要有对应的解决措施

5.1超时返回

需要case by case 排查超时原因,核心是解决超时问题,而非context cancel问题。

思考几个问题:

  • 是偶发的还是经常的?
  • 链路中谁的耗时最长?
  • 对业务是否有影响

如果对业务无影响,可以选择调高超时时间,但这种方式实际上是一种掩耳盗铃的做法,请谨慎评估。

5.2 异步线程遗留

判断主协程提前返回是否有必要?

如果必要,那么开启协程时可以对传入的context做处理,可以新建一个context,也可以对context做处理,比如重新实现一个cancelCtx

原理:利用自己的Context(类似于面向对象的重写)来阻断上层cancel信号传递到下层

// WithoutCancelCtx ... 不带取消的 context
type WithoutCancelCtx struct {ctx context.Context
}// Deadline ...
func (c WithoutCancelCtx) Deadline() (time.Time, bool) { return time.Time{}, false }// Done ...
func (c WithoutCancelCtx) Done() <-chan struct{} { return nil }// Err ...
func (c WithoutCancelCtx) Err() error { return nil }// Value ...
func (c WithoutCancelCtx) Value(key interface{}) interface{} { return c.ctx.Value(key) }

5.3 客户端提前断开链接

这种是正常现象,是服务端为了减少不必要的资源消耗,把不需要的请求快速失败的做法。

这个我们需要重新配置日志报警采集策略,把这部分报错过滤即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/53425.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android13_SystemUI下拉框新增音量控制条

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 Android13_SystemUI下拉框新增音量控制条 一、必备知识二、源码分析对比1.brightness模块分析对比2.statusbar/phone 对应模块对比对比初始化类声明对比构造方法 三、源码修改…

OpenCV结构分析与形状描述符(11)椭圆拟合函数fitEllipse()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 围绕一组2D点拟合一个椭圆。 该函数计算出一个椭圆&#xff0c;该椭圆在最小二乘意义上最好地拟合一组2D点。它返回一个内切椭圆的旋转矩形。使…

树莓派3B串口通信

树莓派3B串口通信 文章目录 树莓派3B串口通信一、串口的基本认知1.1 关于电器标准和协议&#xff1a;RS232RS422RS485 1.2 关于串口的电平&#xff1a;UARTRS232电平TTL电平 1.3 串口通信引脚接线&#xff1a;1.4 串口的通信协议&#xff1a; 二、树莓派串口通信开发2.1 树莓派…

TensorRT-For-YOLO-Series项目:实现yolov10模型的python-tensorrt推理(对比int8与fp16推理差异)

项目地址&#xff1a;https://github.com/Linaom1214/TensorRT-For-YOLO-Series/tree/cuda-python 算法支持状态&#xff1a; 2024.6.16 Support YOLOv9, YOLOv10, changing the TensorRT version to 10.0 2023.8.15 Support cuda-python 2023.5.12 Update 2023.1.7 support YO…

观趋势 谋发展 2024 SSHT上海智能家居展有哪些创新呈现?

引言&#xff1a;大数跨境发布的《2024全球智能家居市场洞察报告》显示&#xff0c;智能家居市场正迎来快速增长&#xff0c;预计从2024年的1215.9亿美元增长至2032年的6332.0亿美元&#xff0c;复合年增长率为22.9%。 近年来&#xff0c;随着物联网、AI等底层技术的飞速进步&…

kubernetes微服务之ingress-nginx

目录 1 ingress-nginx 介绍 2 Ingress-nginx 的工作原理 3 使用 Ingress-nginx 的步骤 4 部署 ingress &#xff1a; 4.1 开启ipvs 模式 4.2 下载部署文件 4.3 上传镜像到harbor 4.4 修改文件中镜像地址,与harbor仓库路径保持一致 4.5 检查是否running 4.6 将ingress的命名…

轻松上手,高效产出:音频剪辑工具年度精选

不知道你有没有拍vlog记录生活的习惯&#xff0c;有时候视频里穿插进自己的声音能让视频更加丰富贴上自己的标签。这次我们一起探讨当下有哪些好用的在线音频剪辑工具。 1.FOXIT音频剪辑 链接直达>>https://www.foxitsoftware.cn/audio-clip/ 这个工具是一款专业的音…

Java 数据类型详解:基本数据类型与引用数据类型

在 Java 编程语言中&#xff0c;数据类型主要分为两大类&#xff1a;基本数据类型和引用数据类型。理解这两种类型的区别、使用场景及其转换方式是学习 Java 的基础。本文将深入探讨这两类数据类型的特点&#xff0c;并展示自动类型转换、强制类型转换以及自动拆箱和封箱的使用…

虚拟现实辅助工程技术助力多学科协同评估

在当今高速发展的经济环境中&#xff0c;制造业面临着多重挑战&#xff0c;包括提高产品性能、压缩设计周期、实现轻量化设计和降低成本。为了有效应对这些挑战&#xff0c;多学科协同评估成为缩短研发周期和提升研制质量的关键手段。 传统的多学科评估面临着数据孤立与融合困难…

《‌黑神话:‌悟空》‌游戏攻略‌

时光荏苒&#xff0c;岁月如梭&#xff0c;不知不觉已经来到了2024年的9月份了。 ‌突然想写一篇关于《‌黑神话&#xff1a;‌悟空》‌的游戏攻略‌。 在《‌黑神话&#xff1a;‌悟空》‌这款以中国古代名著《‌西游记》‌为背景的动作角色扮演游戏中&#xff0c;‌玩家将扮…

LeetCode 热题 100 回顾9

干货分享&#xff0c;感谢您的阅读&#xff01;原文见&#xff1a;LeetCode 热题 100 回顾_力code热题100-CSDN博客 一、哈希部分 1.两数之和 &#xff08;简单&#xff09; 题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标…

【redis】redis的特性和主要应用场景

文章目录 redis 的特性在内存中存储数据可编程的扩展能力持久化集群高可用快 redis 的应用场景实时数据存储缓存消息队列 redis 的特性 redis 的一些特性&#xff08;优点&#xff09;成就了它 在内存中存储数据 In-memory data structures MySQL 主要是通过“表”的方式来…

JavaEE-HTTPHTTPS

目录 HTTP协议 一、概念 二、http协议格式 http请求报文 http响应报文 URL格式 三、认识方法 四、认识报头 HTTP响应中的信息 HTTPS协议 对称加密 非对称加密 中间人攻击 解决中间人攻击 HTTP协议 一、概念 HTTP (全称为 "超⽂本传输协议") 是⼀种应⽤…

Mysql中的锁机制详解

一、概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。 在数据库中&#xff0c;除了传统的计算资源&#xff08;如CPU、RAM、I/O等&#xff09;的争用以外&#xff0c;数据也是一种供需要用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决…

一文讲懂Spring Event事件通知机制

目录 一 什么是spring event 二 怎么实现spring event 一 什么是spring event 我不会按照官方的解释来说什么是spring event&#xff0c;我只是按照自己的理解来解释&#xff0c;可能原理上会和官方有偏差&#xff0c;但是它的作用和功能就是这个&#xff0c;我更加偏向于从他…

详解React setState调用原理和批量更新的过程

1. React setState 调用的原理 setState目录 1. React setState 调用的原理2. React setState 调用之后发生了什么&#xff1f;是同步还是异步&#xff1f;3. React中的setState批量更新的过程是什么&#xff1f; 具体的执行过程如下&#xff08;源码级解析&#xff09;&#x…

安卓13带有系统签名的应用不能正常使用webview 调用webview失败 系统应用app apk

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.彩蛋1.前言 android版本高一些的平台,经常会遇到一些权限安全问题,像客户的应用如果带有系统签名,会导致不能正常使用webview问题。 2.问题分析 我们log信息,可以发现下面的提示: Fo…

网络层ip协议

一.概念 ip协议主要是为了在复杂的网络环境中确定一个合适的路径来传输主机间的数据。简单来说就是用来确定主机的位置。 ip协议中的一些设备如下&#xff1a; 主机: 配有 IP 地址, 但是不进行路由控制的设备;路由器: 即配有 IP 地址, 又能进行路由控制;节点: 主机和路由器的统…

传统CV算法——边缘算子与图像金字塔算法介绍

边缘算子 图像梯度算子 - Sobel Sobel算子是一种用于边缘检测的图像梯度算子&#xff0c;它通过计算图像亮度的空间梯度来突出显示图像中的边缘。Sobel算子主要识别图像中亮度变化快的区域&#xff0c;这些区域通常对应于边缘。它是通过对图像进行水平和垂直方向的差分运算来…

5.2.数据结构-c/c++二叉树详解(下篇)(算法面试题)

本章所有代码请见&#xff1a;5.3.数据结构-c/c二叉树代码-CSDN博客 上篇:5.数据结构-c/c二叉树详解(上篇)&#xff08;遍历方法&#xff0c;完全二叉树&#xff09;-CSDN博客 目录 1 求二叉树 第k层的节点 2 查找一个节点是否在二叉树中 3 求二叉树节点的个数 4 求二叉树…