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<
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 上评论也是颇为“不错”。有一说一,是道好题,不过我们还是得先理解了它才…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...

Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...