튜토리얼 칼럼에서 시간 관리가 필요한 친구들에게 도움이 되기를 바랍니다.
타임아웃 제어가 왜 필요한가요?
서버 측 공통 시간 제한 제어
hardWork
요청 기능을 예로 들어 보겠습니다. 이름에서 알 수 있듯이 처리 속도가 느려질 수 있습니다. func hardWork(job interface{}) error { time.Sleep(time.Minute) return nil}func requestWork(ctx context.Context, job interface{}) error { return hardWork(job)}이때 클라이언트는 항상 익숙한 화면을 보게 됩니다
hardWork
为例,用来做啥的不重要,顾名思义,可能处理起来比较慢。
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 <p>这时客户端看到的就一直是大家熟悉的画面</p><p><img src="https://img.php.cn/upload/article/000/000/020/ea4492ddee56842b8d2dd9e34d636408-1.jpg" alt="Go 시간 초과 제어 구현 방법에 대한 자세한 설명"></p><p>绝大部分用户都不会看一分钟菊花,早早弃你而去,空留了整个调用链路上一堆资源的占用,本文不究其它细节,只聚焦超时实现。</p><p>下面我们看看该怎么来实现超时,其中会有哪些坑。</p><h2> <span class="header-link octicon octicon-link"></span>第一版实现</h2><p>大家可以先不往下看,自己试着想想该怎么实现这个函数的超时,第一次尝试:</p><pre class="brush:php;toolbar:false">func main() { const total = 1000 var wg sync.WaitGroup wg.Add(total) now := time.Now() for i := 0; i <p>我们写个 main 函数测试一下</p><pre class="brush:php;toolbar:false">➜ go run timeout.go elapsed: 2.005725931s
跑一下试试效果
time.Sleep(time.Minute*2)fmt.Println("number of goroutines:", runtime.NumGoroutine())
超时已经生效。但这样就搞定了吗?
让我们在main函数末尾加一行代码看看执行完有多少goroutine
➜ go run timeout.go elapsed: 2.005725931s number of goroutines: 1001
sleep 2分钟是为了等待所有任务结束,然后我们打印一下当前goroutine数量。让我们执行一下看看结果
done := make(chan error, 1)
goroutine泄露了,让我们看看为啥会这样呢?首先,requestWork
函数在2秒钟超时后就退出了,一旦 requestWork
函数退出,那么 done channel
就没有goroutine接收了,等到执行 done 这行代码的时候就会一直卡着写不进去,导致每个超时的请求都会一直占用掉一个goroutine,这是一个很大的bug,等到资源耗尽的时候整个服务就失去响应了。
那么怎么fix呢?其实也很简单,只要 make chan
的时候把 buffer size
设为1,如下:
➜ go run timeout.go elapsed: 2.005655146s number of goroutines: 1
这样就可以让 done 不管在是否超时都能写入而不卡住goroutine。此时可能有人会问如果这时写入一个已经没goroutine接收的channel会不会有问题,在Go里面channel不像我们常见的文件描述符一样,不是必须关闭的,只是个对象而已,<code>close(channel)
只是用来告诉接收者没有东西要写了,没有其它用途。
改完这一行代码我们再测试一遍:
panic("oops")
goroutine泄露问题解决了!
让我们把 hardWork
函数实现改成
go func() { defer func() { if p := recover(); p != nil { fmt.Println("oops, panic") } }() defer wg.Done() requestWork(context.Background(), "any")}()
修改 main
函数加上捕获异常的代码如下:
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>此时执行一下就会发现panic是无法被捕获的,原因是因为在 <code>requestWork</code> 内部起的goroutine里产生的panic其它goroutine无法捕获。</p><p>解决方法是在 <code>requestWork</code> 里加上 <code>panicChan</code> 来处理,同样,需要 <code>panicChan</code> 的 <code>buffer size</code></p>대부분의 사용자는 국화를 잠시도 보지 않고 일찍 포기할 것이며 전체 통화 링크에 많은 리소스를 차지할 것입니다. 이 기사에서는 다른 세부 사항을 다루지 않을 것입니다. , 포커스 시간 초과 구현만 가능합니다. 🎜🎜타임아웃을 구현하는 방법과 어떤 함정이 있는지 살펴보겠습니다. 🎜🎜🎜🎜첫 번째 버전 구현🎜🎜아래 내용을 읽지 말고 이 함수의 시간 초과를 구현하는 방법에 대해 생각해 보세요. 먼저 시도해 보세요. 🎜<pre class="brush:php;toolbar:false">ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)🎜테스트할 주요 함수를 작성해 봅시다🎜
package mainimport ( "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 🎜실행해 보세요. 🎜rrreee🎜타임아웃이 적용되었습니다. 그런데 이게 다 됐나요? 🎜🎜🎜🎜고루틴 유출🎜🎜주 함수 끝에 코드 한 줄을 추가하여 얼마나 많은 고루틴이 실행되었는지 확인하겠습니다.🎜rrreee🎜잠자기 2분은 모든 작업이 끝날 때까지 기다린 후 다음을 인쇄합니다. 현재 고루틴의 수. 실행해서 결과를 볼까요🎜rrreee🎜고루틴이 유출됐는데 왜 이런 일이 일어나는지 살펴볼까요? 우선, <code>requestWork</code> 함수는 2초의 시간 초과 후에 종료됩니다. <code>requestWork</code> 함수가 종료되면 <code>done 채널</code>에는 수신할 고루틴이 없습니다. <code>done 을 실행할 때 이 코드 줄이 멈춰서 작성할 수 없게 되어 각 시간 초과 요청이 고루틴을 차지하게 됩니다. 리소스가 소진되면 전체 서비스가 응답하지 않게 됩니다. 🎜🎜그럼 어떻게 고치나요? 사실, <code>make chan</code>을 할 때 다음과 같이 <code>버퍼 크기</code>를 1로 설정하면 됩니다. 🎜rrreee🎜이 방법으로 <code>done 시간 초과 여부에 관계없이 고루틴을 차단하지 않고 쓸 수 있습니다. 이때 누군가는 고루틴이 없는 채널에 글을 써서 수신하면 문제가 없느냐고 물을 수 있는데, Go에서는 채널이 일반적인 파일 디스크립터와는 달리 닫힐 필요가 없습니다. 단지 객체일 뿐입니다. <code>close(channel)</code>는 수신자에게 쓸 내용이 없으며 다른 목적이 없음을 알리는 데에만 사용됩니다. 🎜🎜이 코드 줄을 변경한 후 다시 테스트해 보겠습니다. 🎜rrreee🎜고루틴 누출 문제가 해결되었습니다! 🎜🎜🎜🎜패닉은 포착할 수 없습니다🎜🎜 <code>hardWork</code> 함수 구현을 🎜rrreee🎜로 변경하겠습니다. <code>main</code> 함수를 수정하고 다음과 같이 예외를 캡처하는 코드를 추가합니다. 🎜rrreee🎜이때 실행해보면 패닉을 캡쳐할 수 없다는 것을 알 수 있는데, 그 이유는 <code>requestWork</code> 내부 고루틴에서 발생한 패닉은 다른 고루틴에서 캡쳐할 수 없기 때문입니다. 🎜🎜해결책은 <code>requestWork</code>에 <code>panicChan</code>을 추가하여 처리하는 것입니다. 마찬가지로 <code>panicChan</code>의 <code>버퍼 크기</code>도 필요합니다. 다음과 같이 1이 됩니다: 🎜<pre class="brush:php;toolbar:false">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>改完就可以在 <code>requestWork</code> 的调用方处理 <code>panic</code> 了。</p><h2> <span class="header-link octicon octicon-link"></span>超时时长一定对吗?</h2><p>上面的 <code>requestWork</code> 实现忽略了传入的 <code>ctx</code> 参数,如果 <code>ctx</code> 已有超时设置,我们一定要关注此传入的超时是不是小于这里给的2秒,如果小于,就需要用传入的超时,<code>go-zero/core/contextx</code> 已经提供了方法帮我们一行代码搞定,只需修改如下:</p><pre class="brush:php;toolbar:false">ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)
这里 requestWork
只是返回了一个 error
参数,如果需要返回多个参数,那么我们就需要注意 data race
,此时可以通过锁来解决,具体实现参考 go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go
,这里不做赘述。
package mainimport ( "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 <h2> <span class="header-link octicon octicon-link"></span>更多细节</h2><p>请参考 <code>go-zero</code> 源码:</p>
go-zero/core/fx/timeout.go
go-zero/zrpc/internal/clientinterceptors/timeoutinterceptor.go
go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go
github.com/tal-tech/go-zero
欢迎使用 go-zero
并 star 支持我们!
위 내용은 Go 시간 초과 제어 구현 방법에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!