一文搞懂如何实现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编写一个简单的FUSE文件系统的教程
Apr 02 Python
Python实现获取照片拍摄日期并重命名的方法
Sep 30 Python
python里使用正则表达式的组嵌套实例详解
Oct 24 Python
Python爬虫_城市公交、地铁站点和线路数据采集实例
Jan 10 Python
Python搭建FTP服务器的方法示例
Jan 19 Python
简单了解python PEP的一些知识
Jul 13 Python
python 多进程共享全局变量之Manager()详解
Aug 15 Python
python 实现两个npy档案合并
Jul 01 Python
python中HTMLParser模块知识点总结
Jan 25 Python
10张动图学会python循环与递归问题
Feb 06 Python
详解OpenCV获取高动态范围(HDR)成像
Apr 29 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获取本周开始日期和结束日期的方法
2015/03/09 PHP
简单谈谈php浮点数精确运算
2016/03/10 PHP
JavaScript创建命名空间(namespace)的最简实现
2007/12/11 Javascript
javascript 流畅动画实现原理
2009/09/08 Javascript
jQuery创建插件的代码分析
2011/04/14 Javascript
通过js动态操作table(新增,删除相关列信息)
2012/05/23 Javascript
浅谈Javascript事件处理程序的几种方式
2012/06/27 Javascript
IE下双击checkbox反应延迟问题的解决方法
2014/03/27 Javascript
浅谈jQuery事件绑定原理
2015/01/02 Javascript
jQuery统计上传文件大小的方法
2015/01/24 Javascript
JS判断元素是否在数组内的实现代码
2016/03/30 Javascript
最细致的vue.js基础语法 值得收藏!
2016/11/03 Javascript
JS实现自动阅读单词(有道单词本添加功能)
2016/11/14 Javascript
详解Vue + Vuex 如何使用 vm.$nextTick
2017/11/20 Javascript
利用vue.js实现被选中状态的改变方法
2018/02/08 Javascript
Vue自定义弹窗指令的实现代码
2018/08/13 Javascript
JavaScript实现轮播图片完整代码
2020/03/07 Javascript
原生JavaScript实现轮播图
2021/01/10 Javascript
利用python实现微信头像加红色数字功能
2018/03/26 Python
Django ImageFiled上传照片并显示的方法
2019/07/28 Python
python生成大写32位uuid代码
2020/03/03 Python
安装pyinstaller遇到的各种问题(小结)
2020/11/20 Python
英国儿童图书网站:Scholastic
2017/03/26 全球购物
机械专业个人求职自荐信格式
2013/09/21 职场文书
建筑专业自荐信范文
2014/01/05 职场文书
电脑专业个人求职信范文
2014/02/04 职场文书
《雨霖铃》听课反思
2014/02/13 职场文书
大学四年个人自我小结
2014/03/05 职场文书
党支部换届选举方案
2014/05/08 职场文书
基层党组织整改方案
2014/10/25 职场文书
2014年科技工作总结
2014/11/26 职场文书
管理人员岗位职责
2015/02/14 职场文书
优秀团员个人总结
2015/02/26 职场文书
企业安全隐患排查治理制度
2015/08/05 职场文书
你会写报告?产品体验报告到底该怎么写?
2019/08/14 职场文书
个人销售励志奋斗口号
2019/12/05 职场文书