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 客户端提前断开链接
这种是正常现象,是服务端为了减少不必要的资源消耗,把不需要的请求快速失败的做法。
这个我们需要重新配置日志报警采集策略,把这部分报错过滤即可。
相关文章:
context canceled 到底谁在作祟?
一、背景 在工作中,因报警治理标准提高,在报警治理的过程中,有一类context cancel报警渐渐凸显出来。 目前context cancel日志报警大致可以分为两类。 context deadline exceeded 耗时长有明确报错原因 context canceled 耗时短无明确报错…...
windows C++ 虚拟内存的按需调拨
虚拟内存的按需调拨 windows C 虚拟内存的按需调拨 文章目录 虚拟内存的按需调拨虚拟内存的按需调拨 虚拟内存的按需调拨 /*------------------------------------------------------------------------24-SEHAndMemory.cpp演示虚拟内存的按需调拨--------------------------…...
[杂项]pugi::xml获取xml中的注释节点
前言 想到学习xml时的一句话,xml中注释也会被算作一个节点。那么我们就可以通过 pugixml 把注释节点获取出来, <?xml version"1.0"?> <mesh name"mesh_root"><!--这是一个注释节点-->some text<![CDATA[so…...
Spring Boot Admin集成与自定义监控告警
目录 一.Spring Boot Admin集成 1.引入依赖 2.添加配置 3.监控界面 二.Spring Boot Admin告警机制 1. 基本告警机制 2. 配置告警 2.1 triggers触发器讲解 3. 自定义通知 3.1 Instance 对象 三.Spring Boot Admin支持的监控属性 1.常见的Spring Boot Admin监控属性 …...
如何恢复回收站中已删除/清空的文件
回收站清空后如何恢复已删除的文件?是否可以恢复永久删除的文件?或者最糟糕的是,如果文件直接被删除怎么办?本文将向您展示清空回收站后恢复已删除数据的最佳方法。 回收站清空后如何恢复已删除的文件? “回收站清空后…...
玩短视频素材都是在哪里找的?推荐几个热门的短视频素材下载渠道
亲爱的短视频创作爱好者们,你是否在寻找视频素材时感到苦恼,觉得选择有限?别担心,今天我要为大家介绍几个超级实用的视频素材下载平台,帮助你的视频创作事半功倍! 蛙学网 我们首先要重点推荐的是蛙学网&am…...
ThinkPHP5 5.0.23-rce远程代码执行漏洞复现
启动环境,先关闭其他环境 启动 判断是否存在漏洞:访问/index.php?scaptcha页面,会出现报错 使用HackBar 插件发送 POST 请求 _method__construct&filter[]system&methodget&server[REQUEST_METHOD]dir 通过echo命令写入 Webshe…...
windows下安装并使用nvm
目录 一.准备工作:卸载node 卸载步骤 二.下载nvm 三.安装nvm 三.配置下载源【重要】 四.使用nvm安装node.js 五.nvm常用命令 六.卸载nvm 一.准备工作:卸载node 如果电脑上已经有node,那么我们需要先完全卸载node,再安装…...
mac m2 安装 nvm
踩坑-填坑 过程 红字都是 启动台-ohter-终端 里面直接输入就行了 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" brew -v 重启终端 brew uninstall nvm brew install nvm 成功提示 > Summary 🍺 /o…...
通信工程学习:什么是AN接入网络
AN接入网络 AN接入网络,全称Access Network,是电信部门业务节点与用户终端设备之间的实施系统。它可以部分或全部代替传统的用户本地线路网,并可包括复用、交叉连接和传输功能。以下是关于AN接入网络的详细解释: 一、AN接入网络的…...
MSCKF7讲:特征管理与优化
MSCKF7讲:特征管理与优化 文章目录 MSCKF7讲:特征管理与优化1 Feature.h2 OptimizationConfig3 initializePosition三角化LM优化3.1 计算归一化坐标深度初值generateInitialGuess① 理论推导② 代码分析 3.2 计算归一化误差cost① 理论推导② 代码分析 3…...
C# XML 使用教程
C# XML 使用教程 目录 C# XML 使用教程XML 是什么介绍组成XML 与 HTML 的区别 C# 中如何使用 XML序列化根元素子元素序列化方法 反序列化反序列化方法 序列化与反序列化实例 XML 是什么 介绍 可扩展标记语言 (Extensible Markup Language, XML) ,标准通用标记语言的…...
淘宝开放平台交易类API解析以及如何测试?
调用淘宝开放平台的订单接口,主要可以通过以下几种途径进行: 1. 直接使用淘宝开放平台提供的API接口 步骤概述: 注册淘宝开放平台账号:首先,你需要在淘宝开放平台注册一个开发者账号。创建应用:在注册并…...
基于聚类与LSTM对比特币价格深度分析与预测
1.项目背景 比特币作为全球最具影响力的加密货币之一,其价格受到多种复杂因素的共同作用,包括市场情绪、政策变化、大型机构的投资行为等,这些因素在不同的市场阶段对比特币价格波动产生直接或间接的影响。通过对比特币市场的深入分析&#…...
YOLOv9改进策略【Neck】| 使用CARAFE轻量级通用上采样算子
一、本文介绍 本文记录的是利用CARAFE上采样对YOLOv9的颈部网络进行改进的方法研究。YOLOv9采用传统的最近邻插值的方法,仅考虑子像素邻域,无法捕获密集预测任务所需的丰富语义信息,从而影响模型在密集预测任务中的性能。CARAFE通过在大感受…...
SpringMVC上
SpringMVC介绍 MVC模型 MVC全称Model View Controller,是一种设计创建Web应用程序的模式。这三个单词分别代表Web应用程序的三个部分: Model(模型):指数据模型。用于存储数据以及处理用户请求的业务逻辑。在Web应用…...
嵌入式软件--51单片机 DAY 2
一、数码管 1.数码管概况 2.设计 (1)硬件设计 我们可以通过阴极控制显示的位置,通过阳极控制显示的内容。两个数码管共有8个阴极引脚和16个阳极引脚,如果所有引脚都直接接入MCU,会造成MCU引脚的极大浪费。 为了节省…...
高精度加法,减法,乘法,除法
加法: 大整数该如何储存? 用数组储存: 把个位放在数下标为0的位置,十位放在数组下标为1的位置(也就是高位放在数组的后面) 因为这样,如果需要增加一位最高位,那我们就可以直接在…...
学习计划(大三上)
第二周 总结Java并发编程的艺术 学习JVM(博客文章) 第三周 学习JVM(博客文章) 图解TCP/IP 4章 第四周 完成简历项目 学习JVM(博客文章) 图解TCP/IP 4章 第五周 完成简历项目 深入学习RocketMQ底层…...
【第0006页 · 数组】寻找重复数
【前言】本文以及之后的一些题解都会陆续整理到目录中,若想了解全部题解整理,请看这里: 第0006页 寻找重复数 今天想讨论的一道题在 LeetCode 上评论也是颇为“不错”。有一说一,是道好题,不过我们还是得先理解了它才…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...
阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)
cd /home 进入home盘 安装虚拟环境: 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境: virtualenv myenv 3、激活虚拟环境(激活环境可以在当前环境下安装包) source myenv/bin/activate 此时,终端…...
针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...
spring boot使用HttpServletResponse实现sse后端流式输出消息
1.以前只是看过SSE的相关文章,没有具体实践,这次接入AI大模型使用到了流式输出,涉及到给前端流式返回,所以记录一下。 2.resp要设置为text/event-stream resp.setContentType("text/event-stream"); resp.setCharacter…...
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀”
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀” 在JavaScript中,我们经常需要处理文本、数组、对象等数据类型。但当我们需要处理文件上传、图像处理、网络通信等场景时,单纯依赖字符串或数组就显得力不从心了。这时ÿ…...
软件工程教学评价
王海林老师您好。 您的《软件工程》课程成功地将宏观的理论与具体的实践相结合。上半学期的理论教学中,您通过丰富的实例,将“高内聚低耦合”、SOLID原则等抽象概念解释得十分透彻,让这些理论不再是停留在纸面的名词,而是可以指导…...
RFID推动新能源汽车零部件生产系统管理应用案例
RFID推动新能源汽车零部件生产系统管理应用案例 一、项目背景 新能源汽车零部件场景 在新能源汽车零部件生产领域,电子冷却水泵等关键部件的装配溯源需求日益增长。传统 RFID 溯源方案采用 “网关 RFID 读写头” 模式,存在单点位单独头溯源、网关布线…...
