のタイムアウトを使用しての落とし穴.
問題
タイムアウトは外部リソースに接続するプログラムのために重要です.
なぜタイムアウト制御が必要か?
goは通常バックエンドサービスを書くのに使用されます.一般に、リクエストは複数のシリアルまたは並列サブタスクによって完了する.各サブタスクは別の内部リクエストを発行することがあります.リクエストがタイムアウトすると、すぐに戻り、Goroutinesやファイルディスクリプタなどの占有しているリソースを解放します.
サーバー側の共通タイムアウト制御
何がタイムアウトコントロールがない場合は?
簡単にするために、我々はリクエスト機能を取る
hardWork
例として.それは何のために使用される問題ではない.名前が示すように、それはプロセスが遅いかもしれません.func hardWork(job interface{}) error {
time.Sleep(time.Minute)
return nil
}
func requestWork(ctx context.Context, job interface{}) error {
return hardWork(job)
}
我々がサーブするためにこの種のコードを使うとき、身近なイメージは1分の間現れます.ほとんどの人は長い間待つことができないと思いますが、サーバーはまだそのページが閉じていることに取り組んでいます.そして、処理リソースは他の要求に役立つためにリリースされません.この記事は他の詳細には深く行きません.タイムアウトの実装にのみフォーカスします.
タイムアウトの仕事とどのような落とし穴をどのように注意してください.
バージョン1
さらに読み込む前に、関数のタイムアウトを実装する方法について考えてみましょう.
最初に試してみます.
func requestWork(ctx context.Context, job interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), 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()
}
}
主な機能をテストしましょう.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
タイムアウトが有効になった.しかし、それはすべて行われますか?ゴーアウトリーク
主な関数の最後にコード行を追加し、どのように多くのGoroutinesがあるかを確認しましょう.
time.Sleep(time.Minute*2)
fmt.Println("number of goroutines:", runtime.NumGoroutine())
睡眠2分はすべてのタスクを実行するのを待つことです、そして、我々はGoroutinesの現在の数を印刷します.それを実行して、結果を見ましょう.➜ go run timeout.go
elapsed: 2.005725931s
number of goroutines: 1001
おっと、ゴローンは、なぜこれが起こるか見てみましょう漏れ?第一にrequestWork
関数は2秒のタイムアウト後に終了する.時requestWork
関数が終了するdone channel
どんなGoroutineによっても受け取られていません.ときにdone <- hardWork(job)
が実行されると、常にスタックされ、書き込みできません.この種の問題は、各々のタイムアウト要求が永遠にGoroutineを占領する原因になります.これはひどい問題だ.各々のGoroutineは2 - 4 kバイトのメモリをとります、そして、メモリが使い果たされるとき、プロセスは予想外に出ます.それで、それを修理する方法?実際、それは非常に簡単です、我々がする必要がある唯一のものは
buffer size
をmake chan
, 下記のようになります.done := make(chan error, 1)
このようにdone <- hardWork(job)
それがタイムアウトかどうかに関係なく、Goroutineで立ち往生することなく書くことができます.この方法で、誰かがどんなGoroutineによっても受け取られていないチャンネルに書くならば、問題があるかどうか尋ねるかもしれません.Goでは、チャネルはファイルディスクリプタのようなリソースではない.閉じるこの動画はお気に入りから削除されています.close (channel)
は、何も書くことがない、他の目的はない受信機を伝えるために使用されます.1行のコードを変更した後、もう一度テストしましょう.
➜ go run timeout.go
elapsed: 2.005655146s
number of goroutines: 1
Goroutineの問題が漏れている.すごい!パニックは取れない
のコードを変えましょう
hardWork
関数へpanic("oops")
変更するmain
以下の例外をキャッチする関数go func() {
defer func() {
if p := recover(); p != nil {
fmt.Println("oops, panic")
}
}()
defer wg.Done()
requestWork(context.Background(), "any")
}()
コードを実行すると、パニックをキャプチャできません.その理由は、他のゴロウズが内部からGoroutineで発生したパニックを捕えることができないということですrequestWork
.解決策は
panicChan
にrequestWork
. 同様に、バッファサイズpanicChan
以下のようにする必要があります.func requestWork(ctx context.Context, job interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), 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
.タイムアウト期間は正しいですか?
上記の実装
requestWork
着信を無視するctx
パラメータIf ctx
タイムアウト設定をしているので、着信タイムアウトが2秒未満であるかどうかを注意しなければなりません.もしあれば、指定したタイムアウト設定をctx
引数.幸運にもcontext.WithTimeout
タイムアウトとセットを比較し、以下のようなコードを変更します.ctx, cancel := context.WithTimeout(ctx, time.Second*2)
データレース
例では
requestWork
だけを返しますerror
パラメータ複数のパラメータを返す必要がある場合は、注意を払う必要がありますdata race
, これはmutex
. 特定の実装のためにgo-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go , 私はここで詳細に深く行きません.完全な例
package main
import (
"context"
"fmt"
"runtime"
"sync"
"time"
)
func hardWork(job interface{}) error {
time.Sleep(time.Second * 10)
return nil
}
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()
}
}
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())
}
タイムアウト例
ギタブプロジェクト
https://github.com/zeromicro/go-zero
使用にようこそ
go-zero
私たちをサポートするスター!Reference
この問題について(のタイムアウトを使用しての落とし穴.), 我々は、より多くの情報をここで見つけました https://dev.to/kevwan/the-pitfalls-on-using-timeout-in-go-22d8テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol