一文搞懂如何实现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基础之while循环及if判断
Aug 24 Python
Flask框架通过Flask_login实现用户登录功能示例
Jul 17 Python
Flask之flask-session的具体使用
Jul 26 Python
在python中安装basemap的教程
Sep 20 Python
对python中dict和json的区别详解
Dec 18 Python
在Python中,不用while和for循环遍历列表的实例
Feb 20 Python
详解python执行shell脚本创建用户及相关操作
Apr 11 Python
Pycharm简单使用教程(入门小结)
Jul 04 Python
python pandas 时间日期的处理实现
Jul 30 Python
Python 切分数组实例解析
Nov 07 Python
python查看矩阵的行列号以及维数方式
May 22 Python
用python发送微信消息
Dec 21 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实现的获取URL信息的类
2007/01/02 PHP
PHP Error与Logging函数的深入理解
2013/06/03 PHP
深入php self与$this的详解
2013/06/08 PHP
php实现求相对时间函数
2015/06/15 PHP
盘点PHP和ASP.NET的10大对比!
2015/12/24 PHP
crontab无法执行php的解决方法
2016/01/25 PHP
PHP面向对象程序设计之对象的遍历操作示例
2019/06/12 PHP
优化JavaScript脚本的性能的几个注意事项
2006/12/22 Javascript
IE7中javascript操作CheckBox的checked=true不打勾的解决方法
2009/12/07 Javascript
Javascript和Ajax中文乱码吐血版解决方案
2009/12/21 Javascript
jqGrid日期格式的判断示例代码(开始日期与结束日期)
2013/11/08 Javascript
js获取判断上传文件后缀名的示例代码
2014/02/19 Javascript
jquery实现弹出层效果实例
2015/05/19 Javascript
基于JavaScript代码实现微信扫一扫下载APP
2015/12/30 Javascript
Node.js实现数据推送
2016/04/14 Javascript
JS实现的简单标签点击切换功能示例
2017/09/21 Javascript
vue-cli3 配置开发与测试环境详解
2019/05/17 Javascript
ES6数组与对象的解构赋值详解
2019/06/14 Javascript
Vue watch响应数据实现方法解析
2020/07/10 Javascript
Vue中的this.$options.data()和this.$data用法说明
2020/07/26 Javascript
[07:20]2018DOTA2国际邀请赛寻真——逐梦Mineski
2018/08/10 DOTA
Python 解析XML文件
2009/04/15 Python
python实现对excel进行数据剔除操作实例
2017/12/07 Python
pycharm 主题theme设置调整仿sublime的方法
2018/05/23 Python
基于python实现名片管理系统
2018/11/30 Python
Python设计模式之模板方法模式实例详解
2019/01/17 Python
中外合拍动画首获奥斯卡提名,“上海出品”《飞奔去月球》能否拿下最终大奖?
2021/03/16 国漫
法拉利英国精品店:Ferraris Boutique UK
2019/07/20 全球购物
Deux par Deux官方网站:设计师童装
2020/01/03 全球购物
意大利时尚奢侈品店:D’Aniello Boutique
2021/01/19 全球购物
镇党委书记群众路线整改措施思想汇报
2014/10/13 职场文书
自主招生学校推荐信范文
2015/03/26 职场文书
计划生育目标责任书
2015/05/09 职场文书
python基础之类属性和实例属性
2021/10/24 Python
90后经典动画片排行:《数码宝贝》第二,《小鲤鱼历险记》在榜
2022/03/18 日漫
Java Lambda表达式常用的函数式接口
2022/04/07 Java/Android