mercari.go #12 go-circuitbreakerのご紹介

0391a8643fe7a08340be46cdf98e3a8f?s=47 Yasuharu Goto
December 06, 2019

mercari.go #12 go-circuitbreakerのご紹介

mercari.go #12 の登壇資料です。

0391a8643fe7a08340be46cdf98e3a8f?s=128

Yasuharu Goto

December 06, 2019
Tweet

Transcript

  1. @ono_matope
 go-circuitbreakerのご紹介


  2. • Yasuharu Goto
 ◦ Backend Engineer ◦ 前職:ヤフー:Goでオブジェクトストレージ開発 • 株式会社メルペイ

    (2019年9月1日-)
 ◦ ID Platformチーム所属 ◦ 認証認可Microservicesの開発 
 小野マトペ (ono_matope)
 2
  3. Go用 Circuit Breaker ライブラリ
 go-circuitbreaker
 を公開しました
 3

  4. https://github.com/mercari/go-circuitbreaker


  5. Background
 5

  6. Failures in Microservices Architecture
 • Microservices Architecture においては、依存サービスの障害は平常状態
 • 下位サービスの障害時


    ◦ リクエストのブロック時間が上位サービスに伝播 ◦ リトライすると下位サービスへの負荷をさらに高める悪循環 6 Microsrervice B Microsrervice C 10s Latency 10s+ Latency リトライによる 過負荷 Fail! Microsrervice A
  7. Failures in Microservices Architecture
 連鎖的な障害を防ぐため、障害サービスへのリクエストを遮断したい
 7 Microsrervice C Fail! Fail

    Fast Fail Fast Microsrervice B Microsrervice A
  8. Circuit Breaker パターン
 Circuit Breaker = ブレーカー
 外部リクエストのエラーカウンタがしきい値を超えたら
 Circuit Openし、しばらくは処理を省略して


    即座にエラーを返す(Fail Fast)パターン
 
 ・障害時の連鎖的なレイテンシの増加を防げる
 ・リトライによる下位サービスの過負荷を緩和
 8 External Service Success Error (1) Error (2) Error (3) Trip Circuit Open! Trip Fail!
  9. CLOSED Circuit Breaker
 • CLOSED: リクエストが一定回数失敗したら OPEN に遷移
 • OPEN:

    一定の時間リクエストを止める (ブレーカーが落ちている状態)
 • HALF-OPEN: リクエストが失敗したら OPEN に戻る。一定回数成功したら CLOSED に復帰。
 9 External Service HALF-OPEN OPEN CLOSED
  10. Circuit Breaker
 =障害を検知して遮断するパターン


  11. Circuit Breaker in Go
 11

  12. Circuit Breaker packages
 • github.com/rubyist/circuitbreaker
 • github.com/sony/gobreaker
 12

  13. github.com/sony/gobreaker
 13 • Execute メソッドに外部呼び出し処理を渡す
 ◦ 戻り値 error がnil →

    成功カウンタ++
 ◦ 戻り値 error がnon-nil → 失敗カウンタ++
 ◦ Circuit Open時 → 関数が実行されず ErrOpenState がreturn

  14. 現実はもう少し複雑
 • Goの関数が返すエラーにはいろいろな種類がある 
 ◦ 障害起因のエラー ◦ その他のエラー • Goコード上のerrorをすべて

    「失敗」とみなしていいのだろうか? 
 14
  15. 現実の例
 HTTPレスポンスボディ全体を読むコードを サーキットブレーカーで保護


  16. リクエスト作成エラー:リクエストしてない。
 無視したい


  17. HTTPリクエスト失敗:「失敗」
 リクエスト作成エラー:リクエストしてない。
 無視したい


  18. HTTPリクエスト失敗:「失敗」
 HTTP 5xx系ステータス:「失敗」
 リクエスト作成エラー:リクエストしてない。
 無視したい


  19. HTTPリクエスト失敗:「失敗」
 HTTP 5xx系ステータス:「失敗」
 HTTP 4xx系ステータス:サービス自体は生きてる。
 場合によるが「成功」とみなしたい
 リクエスト作成エラー:リクエストしてない。
 無視したい


  20. レスポンスボディ読み込み失敗 => 「失敗」
 HTTPリクエスト失敗:「失敗」
 HTTP 5xx系ステータス:「失敗」
 HTTP 4xx系ステータス:サービス自体は生きてる。
 場合によるが「成功」とみなしたい
 リクエスト作成エラー:リクエストしてない。


    無視したい

  21. context
 ・contextを使ってユーザーがリクエストをキャ ンセルしたり、タイムアウト時間を指定したりす ると、リクエストがエラー終了する。
 
 →障害とは無関係
 
 ・ユーザーが発火可能。失敗として扱うとユー ザーがサーキットブレイカーを落とすことが出 来てしまう。


    
 (gRPCではユーザーがキャンセルも
 タイムアウトも指定可能)
 https://www.irasutoya.com/2018/06/blog-post_972.html
  22. 課題
 • 現実のコードには様々な error が
 ◦ 障害起因のerror ▪ 失敗として扱いたい ◦

    障害と関係ないerror ▪ 「失敗」として扱うとFalse-Positive ▪ 「成功」として扱うとFalse-Negativeのケースも ◦ context起因のerror ▪ contexに対応した処理は、ユーザーがエラー終了させる可能性 • これらをCircuit Breakerに正しく伝えることは難しい
 22
  23. go-circuitbreaker
 23 https://github.com/mercari/go-circuitbreaker


  24. Doメソッド
 24 • Doメソッドが保護対象関数とともにcontext.Context を受け取る 
 • 基本的にはgobreakerと同じ 
 ◦

    戻り値 error が nil : 成功 ◦ 戻り値 error が non-nil : 失敗 ◦ ブレーカーが落ちたら関数は実行されずエラー
  25. 特徴1: 特定のエラーを無視・成功扱いに指定できる
 25 func Ignore(err) error 
 func MarkAsSuccess(err) error

    
 ラップしたエラーをサーキットブレーカに 「無視」させる。無視されたエラーは失敗 にも成功にもカウントされない。 
 ラップしたエラーをサーキットブレーカー に「成功」としてカウントさせる。 
 どちらもfuncの外にはアンラップしてreturnさ れる

  26. 特徴2: context のキャンセル・タイムアウトを無視
 26 • オプションで変更可能 
 • returnしたエラーはそのままDoの外側で受け取れる 


  27. 外部リクエストのタイムアウトを失敗とみなす方法
 27 • Do() の内側でcontextを派生してください。 
 • 内側のcontextがtimeoutしても外側はtimeoutしない→失敗としてカウント 


  28. 実装上の工夫
 28

  29. 29 https://docs.microsoft.com/ja-jp/azure/architecture/patterns/circuit-breaker • Circuit Breakerは成功・失敗カウンターと ク ローズ, オープン, ハーフオープン と3つの内

    部状態を持つ
 • 単純な実装では分岐が多く複雑になる

  30. State パターン in Go
 CBの内部状態 state を interface 定義。
 30

    type state interface { onEntry(cb *CircuitBreaker) // 状態開始時の処理 onExit(cb *CircuitBreaker) // 状態終了時の処理 onSuccess(cb *CircuitBreaker) // 成功時の処理
 onFail(cb *CircuitBreaker) // 失敗時の処理
 ready() bool // 処理を開始できるかの問い合わせ // ... } state.go (抜粋)
  31. State パターン in Go
 CircuitBreaker は state を保持し、
 特有の処理を移譲する。
 (カウンタの増分は共通処理なので移譲していない)


    31 func (cb *CircuitBreaker) Success() { cb.mu.Lock() defer cb.mu.Unlock() cb.cnt.incrementSuccesses() cb.state.onSuccess(cb) } func (cb *CircuitBreaker) Fail() { cb.mu.Lock() defer cb.mu.Unlock() cb.cnt.incrementFailures() cb.state.onFail(cb) } breaker.go (抜粋) type CircuitBreaker struct { state state cnt Counters // ... } breaker.go (抜粋)
  32. state.go (抜粋)
 func (st *stateClosed) onEntry(cb *CircuitBreaker) { cb.cnt.resetFailures() //

    ... } func (st *stateClosed) onSuccess(cb *CircuitBreaker) {} func (st *stateClosed) onFail(cb *CircuitBreaker) { if cb.shouldTrip(&cb.cnt) { cb.setState(&stateOpen{}) } } 32 https://docs.microsoft.com/ja-jp/azure/architecture/patt erns/circuit-breaker Closed

  33. func (st *stateOpen) onEntry(cb *CircuitBreaker) { // ... cb.clock.AfterFunc(timeout, cb.setStateWithLock(&stateHalfOpen{}))

    } func (st *stateOpen) onSuccess(cb *CircuitBreaker) {} func (st *stateOpen) onFail(cb *CircuitBreaker) {} 33 https://docs.microsoft.com/ja-jp/azure/architecture/patterns/circuit-b reaker Open
 state.go (抜粋)

  34. func (st *stateHalfOpen) onEntry(cb *CircuitBreaker) { cb.cnt.resetSuccesses() } func (st

    *stateHalfOpen) onSuccess(cb *CircuitBreaker) { if cb.cnt.Successes >= cb.halfOpenMaxSuccesses { cb.setState(&stateClosed{}) } } func (st *stateHalfOpen) onFail(cb *CircuitBreaker) { cb.setState(&stateOpen{}) } 34 https://docs.microsoft.com/ja-jp/azure/architecture/patterns/circuit-b reaker Half Open
 state.go (抜粋)

  35. State デザインパターン
 ・内部状態を型として表現
 ・分岐の少ない保守性の高いコードに
 35

  36. まとめ
 36

  37. Go用Circuit Breakerを作りました
 既存のアプリケーションに組み込みやすいはず
 使ってみてください!
 01 02 03 37

  38. Thank you!
 38

  39. 39