Advertisement
Advertisement

More Related Content

Advertisement

リトライでtime.Sleepを使ったら積んだ話

  1. 1 リトライでtime.Sleep()使ったら積んだ話 mercari.go#14
  2. 2 名前: 田村弘(rossy) ● 経歴: 信州大学 → メルペイ(4月~) ● ポジション: バックエンド ● Go歴: 9ヶ月 ● 趣味: 筋トレ、読書 @rossy_0213 @rossy0213
  3. 3 何が起きたのか ● 新しいマイクロサービスのテストをしていました
  4. 4 何が起きたのか ● タイムアウトが結構の割合で発生していた
  5. 5 ● 実際どこで時間かかっていたかというと 調査
  6. 6 ● 実際どこで時間かかっていたかというと 調査
  7. 7 サービス間通信について ● タイムアウト付きのcontextを付けてリクエストしている
  8. 8 サービス間通信について ● timeout付きのcontextを付けてリクエストしている
  9. 9 リトライについて ● 一時的障害による失敗をカバーする ○ 例えば、ネットワークの損失、高負荷時による不調
  10. 10 リトライについて ● リクエストを少しでも早く返したい ○ 例えば、99%のリクエストが200msで終わり、残り1%が極端に遅 い場合、下図の仕組みを入れる
  11. 11 サービス間通信について ● サービスBでchildCtxを生成してリクエストをしている ○ context.WithTimeout(parentCtx, time)でchildCtxを生成する
  12. 12 context.WithTimeout()について ● context.WithTimeout()の中身はcontext.WithDeadline() ○ 子contextのdeadlineは親のdeadlineより大きくなることはない contextのソースコード
  13. 13 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) }
  14. 14 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } contextの有効性を確認する
  15. 15 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } 実際の処理を行って、結果を代入する
  16. 16 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } リトライ可能なエラーであるかを確認する
  17. 17 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } 待機時間を計算して待機する
  18. 18 ● 実際どこで時間かかっていたかというと 調査
  19. 19 ● 実際どこで時間かかっていたかというと 調査
  20. 20 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } 怪しい...
  21. 21 retryのコード(一部抜粋) for { select { case <-ctx.Done(): // context.Cancel()類が呼び出されたエラーを返す return ctx.Err() default: } if err = fn(); err == nil { // リトライしたい処理を行う return nil // エラーがないなら処理終わり } if !bc.checkRetryable(err) { // retryできないエラーならエラーを返す、例えば、400番台 return err } n := bc.Next() // 回復を待つための待機時間を計算する time.Sleep(n) } 設定ミスにより、常に100s以上の待機時間が出力されていた
  22. 22 ● リトライ前の待機時間により処理時間が伸びる 障害ストーリー
  23. 23 ● 待機時間がサービスAのDeadlineを超える 障害ストーリー
  24. 24 ● Deadline以上の待機時間を算出できないようにする 解決策 Part1 func (bc *backoffWithContext) Next() time.Duration { select { case <-bc.ctx.Done(): return Stop default: } next := bc.exponentialBackoff.Next() if deadline, ok := bc.ctx.Deadline(); ok && deadline.Sub(time.Now()) < next { return Stop } return next } 待機時間算出する前に contextの有効性を確認する
  25. 25 ● Deadline以上の待機時間を算出できないようにする 解決策 Part1 func (bc *backoffWithContext) Next() time.Duration { select { case <-bc.ctx.Done(): return Stop default: } next := bc.exponentialBackoff.Next() if deadline, ok := bc.ctx.Deadline(); ok && deadline.Sub(time.Now()) < next { return Stop } return next } 待機完了の時間がDeadlineを越えていないことを確認する
  26. 26 ● context.Done()の検知とリトライ前の待機を両立させる ○ time.Timerを導入する ■ timeパッケージにあるありがたい関数 ■ 決められた時間にチャネルにイベントを送信する ● つまり、ctx.Done()イベントの受信と両立できる ○ 仕組み ■ 一つのゴルチーンで複数のタイマーを管理する ■ 複数のタイマーをヒープで管理して、一番近い時間までsleepし て、時間になったらチャネルにイベント送信する 解決策 Part2
  27. 27 ● time.Timerを使ったリトライ 解決策 Part2(コード一部抜粋) for { if err = fn(); err == nil { return nil } if !be.checkRetryable(err) { return err } if next = be.Next(); next == Stop { return err } t.Start(next) select { case <-ctx.Done(): return ctx.Err() case <-t.C(): } } 今まで通り
  28. 28 ● time.Timerを使ったリトライ 解決策 Part2(コード一部抜粋) for { if err = fn(); err == nil { return nil } if !be.checkRetryable(err) { return err } if next = be.Next(); next == Stop { return err } t.Start(next) select { case <-ctx.Done(): return ctx.Err() case <-t.C(): } } Stopだったらエラーをそのまま返 す
  29. 29 ● time.Timerを使ったリトライ 解決策 Part2(コード一部抜粋) for { if err = fn(); err == nil { return nil } if !be.checkRetryable(err) { return err } if next = be.Next(); next == Stop { return err } t.Start(next) select { case <-ctx.Done(): return ctx.Err() case <-t.C(): } } ctx.Done()と指定時間になること のどちらか先に発火する
  30. 30 ● 原因 ○ パラメータの設定ミス ○ time.Sleepによって、context.Done()が検知できなかった ● 根本対策 ○ Deadline以上の待機時間を算出できないようにする ○ timerでcontext.Done()の検知と待機を両立させる まとめ
  31. 31 ● https://github.com/rossy0213/retry ○ contextを扱うretryライブラリー ○ 実例目的なので、機能はあまりない ○ ご意見、指摘、要望のissueお待ちしてます 今回のライブラリー例を公開してます
  32. 32 余談1 ● リトライの副作用 ○ 正しいレスポンスが帰ってこない可能性がある
  33. 33 余談1 ● 冪等性 ○ 同じリクエストなら何度しても同じ結果を返す
  34. 34 余談2 Retryについて記事を書きました - Software Design 9月号 - 作品で魅せるGoプログラミン グ - Goで作るRetryライブラリー - Retryの基礎とGoで実装する 方法 - amazon
  35. 35 ● リトライの基礎知識 ○ retry patten(デザインパターン) ○ Exponential-backoff(待機時間の計算方法) ○ backoff-with-jitter(リトライを分散する手法) ○ How Do They Do It: Timers in Go (time.Timerの仕組み説明) ● ライブラリー ○ https://github.com/avast/retry-go ○ https://github.com/cenkalti/backoff (context扱うならおすすめ) ○ https://github.com/App-vNext/Polly ○ https://github.com/spring-projects/spring-retry ● トランザクション処理と冪等性 ○ マイクロサービスにおける決済トランザクション管理 参考
Advertisement