一文搞懂如何实现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 相关文章推荐
python3+PyQt5实现使用剪贴板做复制与粘帖示例
Jan 24 Python
python中从str中提取元素到list以及将list转换为str的方法
Jun 26 Python
将Django项目部署到CentOs服务器中
Oct 18 Python
pyttsx3实现中文文字转语音的方法
Dec 24 Python
对python条件表达式的四种实现方法小结
Jan 30 Python
python for 循环获取index索引的方法
Feb 01 Python
解决python3 安装不了PIL的问题
Aug 16 Python
python检查目录文件权限并修改目录文件权限的操作
Mar 11 Python
Python3标准库之threading进程中管理并发操作方法
Mar 30 Python
python实现简易版学生成绩管理系统
Jun 22 Python
python报错: 'list' object has no attribute 'shape'的解决
Jul 15 Python
利用Python实现模拟登录知乎
May 25 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 Smarty初体验二 获取配置信息
2011/08/08 PHP
解析Extjs与php数据交互(增删查改)
2013/06/25 PHP
ThinkPHP的I方法使用详解
2014/06/18 PHP
解决PHP使用CURL发送GET请求时传递参数的问题
2019/10/11 PHP
解决Laravel无法使用COOKIE和SESSION的问题
2019/10/16 PHP
CLASS_CONFUSION JS混淆 全源码
2007/12/12 Javascript
namespace.js Javascript的命名空间库
2011/10/11 Javascript
js 限制input只能输入数字、字母和汉字等等
2013/12/18 Javascript
使用js画图之饼图
2015/01/12 Javascript
VUE使用vuex解决模块间传值问题的方法
2017/06/01 Javascript
使用prop解决一个checkbox选中后再次选中失效的问题
2017/07/05 Javascript
在Js页面通过POST传递参数跳转到新页面详解
2017/08/25 Javascript
详解使用angular的HttpClient搭配rxjs
2017/09/01 Javascript
微信小程序使用picker实现时间和日期选择框功能【附源码下载】
2017/12/11 Javascript
图片懒加载imgLazyLoading.js使用详解
2020/09/15 Javascript
angular 内存溢出的问题解决
2018/07/12 Javascript
Array数组对象中的forEach、map、filter及reduce详析
2018/08/02 Javascript
cdn模式下vue的基本用法详解
2018/10/07 Javascript
详解基于mpvue微信小程序下载远程图片到本地解决思路
2019/05/16 Javascript
element el-table表格的二次封装实现(附表格高度自适应)
2021/01/19 Javascript
Python合并多个装饰器小技巧
2015/04/28 Python
Python实现从URL地址提取文件名的方法
2015/05/15 Python
Python数据类型之列表和元组的方法实例详解
2019/07/08 Python
Django实现web端tailf日志文件功能及实例详解
2019/07/28 Python
PyTorch预训练的实现
2019/09/18 Python
python深copy和浅copy区别对比解析
2019/12/26 Python
GUESS德国官网:美国牛仔服装品牌
2017/02/14 全球购物
台湾前三大B2C购物网站:MOMO购物网
2017/04/27 全球购物
小橄榄树:Le Petit Olivier
2018/04/23 全球购物
比利时家具购买网站:Home24
2019/01/03 全球购物
eDreams葡萄牙:全球最大的在线旅行社之一
2019/04/15 全球购物
亚洲航空公司官方网站:AirAsia
2019/11/25 全球购物
托管代码(Managed Code)和非托管代码(Unmanaged Code)有什么区别
2014/09/29 面试题
大学开学典礼新闻稿
2015/07/17 职场文书
营销策划分析:怎么策划才能更好销量产品?
2019/09/04 职场文书
MySQL数据库实验之 触发器和存储过程
2022/06/21 MySQL