一文搞懂如何实现Go 超时控制


Posted in Python onMarch 30, 2021

为什么需要超时控制?

  • 请求时间过长,用户侧可能已经离开本页面了,服务端还在消耗资源处理,得到的结果没有意义
  • 过长时间的服务端处理会占用过多资源,导致并发能力下降,甚至出现不可用事故

Go 超时控制必要性

Go 正常都是用来写后端服务的,一般一个请求是由多个串行或并行的子任务来完成的,每个子任务可能是另外的内部请求,那么当这个请求超时的时候,我们就需要快速返回,释放占用的资源,比如goroutine,文件描述符等。

一文搞懂如何实现Go 超时控制

服务端常见的超时控制

  • 进程内的逻辑处理
  • 读写客户端请求,比如HTTP或者RPC请求
  • 调用其它服务端请求,包括调用RPC或者访问DB等

没有超时控制会怎样?

为了简化本文,我们以一个请求函数 hardWork 为例,用来做啥的不重要,顾名思义,可能处理起来比较慢。

func hardWork(job interface{}) error {
  time.Sleep(time.Minute)
  return nil
}

func requestWork(ctx context.Context, job interface{}) error {
 return hardWork(job)
}

这时客户端看到的就一直是大家熟悉的画面

<img src="https://gitee.com/kevwan/static/raw/master/doc/images/loading.jpg" width="25%">

绝大部分用户都不会看一分钟菊花,早早弃你而去,空留了整个调用链路上一堆资源的占用,本文不究其它细节,只聚焦超时实现。

下面我们看看该怎么来实现超时,其中会有哪些坑。

第一版实现

大家可以先不往下看,自己试着想想该怎么实现这个函数的超时,第一次尝试:

func requestWork(ctx context.Context, job interface{}) error {
  ctx, cancel := context.WithTimeout(ctx, time.Second*2)
  defer cancel()

  done := make(chan error)
  go func() {
    done <- hardWork(job)
  }()

  select {
  case err := <-done:
    return err
  case <-ctx.Done():
    return ctx.Err()
  }
}

我们写个 main 函数测试一下

func main() {
  const total = 1000
  var wg sync.WaitGroup
  wg.Add(total)
  now := time.Now()
  for i := 0; i < total; i++ {
    go func() {
      defer wg.Done()
      requestWork(context.Background(), "any")
    }()
  }
  wg.Wait()
  fmt.Println("elapsed:", time.Since(now))
}

跑一下试试效果

➜ go run timeout.go
elapsed: 2.005725931s

超时已经生效。但这样就搞定了吗?

goroutine 泄露

让我们在main函数末尾加一行代码看看执行完有多少goroutine

time.Sleep(time.Minute*2)
fmt.Println("number of goroutines:", runtime.NumGoroutine())

sleep 2分钟是为了等待所有任务结束,然后我们打印一下当前goroutine数量。让我们执行一下看看结果

➜ go run timeout.go
elapsed: 2.005725931s
number of goroutines: 1001

goroutine泄露了,让我们看看为啥会这样呢?首先,requestWork 函数在2秒钟超时后就退出了,一旦 requestWork 函数退出,那么 done channel 就没有goroutine接收了,等到执行 done <- hardWork(job) 这行代码的时候就会一直卡着写不进去,导致每个超时的请求都会一直占用掉一个goroutine,这是一个很大的bug,等到资源耗尽的时候整个服务就失去响应了。

那么怎么fix呢?其实也很简单,只要 make chan 的时候把 buffer size 设为1,如下:

done := make(chan error, 1)

这样就可以让 done <- hardWork(job) 不管在是否超时都能写入而不卡住goroutine。此时可能有人会问如果这时写入一个已经没goroutine接收的channel会不会有问题,在Go里面channel不像我们常见的文件描述符一样,不是必须关闭的,只是个对象而已,close(channel) 只是用来告诉接收者没有东西要写了,没有其它用途。

改完这一行代码我们再测试一遍:

➜ go run timeout.go
elapsed: 2.005655146s
number of goroutines: 1

goroutine泄露问题解决了!

panic 无法捕获

让我们把 hardWork 函数实现改成

panic("oops")

修改 main 函数加上捕获异常的代码如下:

go func() {
 defer func() {
  if p := recover(); p != nil {
   fmt.Println("oops, panic")
  }
 }()

 defer wg.Done()
 requestWork(context.Background(), "any")
}()

此时执行一下就会发现panic是无法被捕获的,原因是因为在 requestWork 内部起的goroutine里产生的panic其它goroutine无法捕获。

解决方法是在 requestWork 里加上 panicChan 来处理,同样,需要 panicChan 的 buffer size 为1,如下:

func requestWork(ctx context.Context, job interface{}) error {
  ctx, cancel := context.WithTimeout(ctx, time.Second*2)
  defer cancel()

  done := make(chan error, 1)
  panicChan := make(chan interface{}, 1)
  go func() {
    defer func() {
      if p := recover(); p != nil {
        panicChan <- p
      }
    }()

    done <- hardWork(job)
  }()

  select {
  case err := <-done:
    return err
  case p := <-panicChan:
    panic(p)
  case <-ctx.Done():
    return ctx.Err()
  }
}

改完就可以在 requestWork 的调用方处理 panic 了。

超时时长一定对吗?

上面的 requestWork 实现忽略了传入的 ctx 参数,如果 ctx 已有超时设置,我们一定要关注此传入的超时是不是小于这里给的2秒,如果小于,就需要用传入的超时,go-zero/core/contextx 已经提供了方法帮我们一行代码搞定,只需修改如下:

ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)

Data race

这里 requestWork 只是返回了一个 error 参数,如果需要返回多个参数,那么我们就需要注意 data race,此时可以通过锁来解决,具体实现参考 go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go,这里不做赘述。

完整示例

package main

import (
  "context"
  "fmt"
  "runtime"
  "sync"
  "time"

  "github.com/tal-tech/go-zero/core/contextx"
)

func hardWork(job interface{}) error {
  time.Sleep(time.Second * 10)
  return nil
}

func requestWork(ctx context.Context, job interface{}) error {
  ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)
  defer cancel()

  done := make(chan error, 1)
  panicChan := make(chan interface{}, 1)
  go func() {
    defer func() {
      if p := recover(); p != nil {
        panicChan <- p
      }
    }()

    done <- hardWork(job)
  }()

  select {
  case err := <-done:
    return err
  case p := <-panicChan:
    panic(p)
  case <-ctx.Done():
    return ctx.Err()
  }
}

func main() {
  const total = 10
  var wg sync.WaitGroup
  wg.Add(total)
  now := time.Now()
  for i := 0; i < total; i++ {
    go func() {
      defer func() {
        if p := recover(); p != nil {
          fmt.Println("oops, panic")
        }
      }()

      defer wg.Done()
      requestWork(context.Background(), "any")
    }()
  }
  wg.Wait()
  fmt.Println("elapsed:", time.Since(now))
  time.Sleep(time.Second * 20)
  fmt.Println("number of goroutines:", runtime.NumGoroutine())
}

更多细节

请参考 go-zero 源码:

  • go-zero/core/fx/timeout.go
  • go-zero/zrpc/internal/clientinterceptors/timeoutinterceptor.go
  • go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go

项目地址
https://github.com/tal-tech/go-zero

到此这篇关于一文搞懂如何实现Go 超时控制的文章就介绍到这了,更多相关Go 超时控制内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python读文件逐行处理的示例代码分享
Dec 27 Python
python实现查找两个字符串中相同字符并输出的方法
Jul 11 Python
浅谈django model的get和filter方法的区别(必看篇)
May 23 Python
Python 调用PIL库失败的解决方法
Jan 08 Python
python3去掉string中的标点符号方法
Jan 22 Python
bluepy 一款python封装的BLE利器简单介绍
Jun 25 Python
Django关于admin的使用技巧和知识点
Feb 10 Python
GDAL 矢量属性数据修改方式(python)
Mar 10 Python
4行Python代码生成图像验证码(2种)
Apr 07 Python
详解python中groupby函数通俗易懂
May 14 Python
Python命名空间及作用域原理实例解析
Aug 12 Python
python百行代码实现汉服圈图片爬取
Nov 23 Python
golang中的空接口使用详解
Mar 30 #Python
在 Golang 中实现 Cache::remember 方法详解
Mar 30 #Python
Python离线安装openpyxl模块的步骤
解决Jupyter-notebook不弹出默认浏览器的问题
Python爬取科目四考试题库的方法实现
Python如何使用logging为Flask增加logid
Mar 30 #Python
如何在Python中创建二叉树
You might like
php将12小时制转换成24小时制的方法
2015/03/31 PHP
JavaScript基本概念初级讲解论坛贴的学习记录
2009/02/22 Javascript
javascript window对象属性整理
2009/10/24 Javascript
12个非常有创意的JavaScript小游戏
2010/03/18 Javascript
JS 图片缩放效果代码
2010/06/09 Javascript
javascript仿php的print_r函数输出json数据
2013/09/13 Javascript
javascript根据像素点取位置示例
2014/01/27 Javascript
node.js中的socket.io入门实例
2014/04/26 Javascript
JavaScript和CSS交互的方法汇总
2014/12/02 Javascript
BootStrap智能表单实战系列(四)表单布局介绍
2016/06/13 Javascript
node跨域请求方法小结
2017/08/25 Javascript
代码详解JS操作剪贴板
2018/02/11 Javascript
浅谈Koa2框架利用CORS完成跨域ajax请求
2018/03/06 Javascript
VUE实现可随意拖动的弹窗组件
2018/09/25 Javascript
Vue 配合eiement动态路由,权限验证的方法
2018/09/26 Javascript
vue+layui实现select动态加载后台数据的例子
2019/09/20 Javascript
详解element上传组件before-remove钩子问题解决
2020/04/08 Javascript
vue 保留两位小数 不能直接用toFixed(2) 的解决
2020/08/07 Javascript
python抓取最新博客内容并生成Rss
2015/05/17 Python
在Django的上下文中设置变量的方法
2015/07/20 Python
Python使用scrapy爬取阳光热线问政平台过程解析
2019/08/14 Python
python使用梯度下降和牛顿法寻找Rosenbrock函数最小值实例
2020/04/02 Python
Python基于当前时间批量创建文件
2020/05/07 Python
python实现scrapy爬虫每天定时抓取数据的示例代码
2021/01/27 Python
如何在网站上添加谷歌定位信息
2016/04/16 HTML / CSS
web字体加载方案优化小结
2019/11/29 HTML / CSS
台湾乐天市场:日本No.1的网路购物网站
2017/03/22 全球购物
可打印的优惠券、杂货和优惠券代码:Coupons.com
2018/06/12 全球购物
REISS美国官网:伦敦最受欢迎的时尚品牌
2019/08/16 全球购物
如何写毕业求职自荐信
2013/11/06 职场文书
会计自荐书
2013/12/02 职场文书
致百米运动员广播稿
2014/01/29 职场文书
公司聚餐通知
2015/04/22 职场文书
小学中队活动总结
2015/05/11 职场文书
用Python提取PDF表格的方法
2021/04/11 Python
2022年显卡天梯图(6月更新)
2022/06/17 数码科技