のタイムアウトを使用しての落とし穴.


問題


  • タイムアウトは外部リソースに接続するプログラムのために重要です.
  • あまりにも長いサーバー側の処理はあまりにも多くのリソースがかかります.結果として、並行性の低下、そしてサービスの利用不可になります.
  • なぜタイムアウト制御が必要か?


    goは通常バックエンドサービスを書くのに使用されます.一般に、リクエストは複数のシリアルまたは並列サブタスクによって完了する.各サブタスクは別の内部リクエストを発行することがあります.リクエストがタイムアウトすると、すぐに戻り、Goroutinesやファイルディスクリプタなどの占有しているリソースを解放します.

    サーバー側の共通タイムアウト制御

  • インプロセス処理
  • 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)
    }
    
    我々がサーブするためにこの種のコードを使うとき、身近なイメージは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 sizemake 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 .
    解決策はpanicChanrequestWork . 同様に、バッファサイズ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())
    }
    

    タイムアウト例

  • go-zero/core/fx/timeout.go
  • go-zero/zrpc/internal/clientinterceptors/timeoutinterceptor.go
  • go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go
  • ギタブプロジェクト


    https://github.com/zeromicro/go-zero
    使用にようこそgo-zero 私たちをサポートするスター!