七七网站建设,怎么在互联网推广产品,wordpress收费模版,建设公司招聘一、背景
在工作中#xff0c;因报警治理标准提高#xff0c;在报警治理的过程中#xff0c;有一类context cancel报警渐渐凸显出来。
目前context cancel日志报警大致可以分为两类。 context deadline exceeded 耗时长有明确报错原因 context canceled 耗时短无明确报错…一、背景
在工作中因报警治理标准提高在报警治理的过程中有一类context cancel报警渐渐凸显出来。
目前context cancel日志报警大致可以分为两类。 context deadline exceeded 耗时长有明确报错原因 context canceled 耗时短无明确报错原因分布在各个接口
之前因为不了解原因所以一遇到这类报警统一都按照偶发超时处理可是我们发现这其中有一大半case 耗时并不长整个业务接口耗时在300ms以内甚至100ms以内于是我对超时这个缘由产生了疑惑带着这个疑惑我在业余时间学习探究最终找到了出现此类情况的一些场景。
二、底层原因探究
2.1 go context预备知识
context原理可以看我另一篇文章contextgo的上下文存储并发控制之道
这里简单解释下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 exceededcontext 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方法会取消contexterr
cancel()
这里cancel方法无论是业务层和框架层都有可能调用一旦调用下游感知到了就会返回errcontext 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参数则会设置一个超时取消这个超时取消对应着errcontext 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, were 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. Its 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信号此时会返回errcontext 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 客户端提前断开链接
这种是正常现象是服务端为了减少不必要的资源消耗把不需要的请求快速失败的做法。
这个我们需要重新配置日志报警采集策略把这部分报错过滤即可。