Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Yasuharu Goto
December 06, 2019

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

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

Yasuharu Goto

December 06, 2019
Tweet

Other Decks in Programming

Transcript

  1. @ono_matope

    go-circuitbreakerのご紹介


    View Slide

  2. ● Yasuharu Goto

    ○ Backend Engineer
    ○ 前職:ヤフー:Goでオブジェクトストレージ開発
    ● 株式会社メルペイ (2019年9月1日-)

    ○ ID Platformチーム所属
    ○ 認証認可Microservicesの開発

    小野マトペ (ono_matope)

    2

    View Slide

  3. Go用 Circuit Breaker ライブラリ

    go-circuitbreaker

    を公開しました

    3

    View Slide

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


    View Slide

  5. Background

    5

    View Slide

  6. Failures in Microservices Architecture

    ● Microservices Architecture においては、依存サービスの障害は平常状態

    ● 下位サービスの障害時

    ○ リクエストのブロック時間が上位サービスに伝播
    ○ リトライすると下位サービスへの負荷をさらに高める悪循環
    6
    Microsrervice B Microsrervice C
    10s Latency
    10s+ Latency
    リトライによる
    過負荷
    Fail!
    Microsrervice A

    View Slide

  7. Failures in Microservices Architecture

    連鎖的な障害を防ぐため、障害サービスへのリクエストを遮断したい

    7
    Microsrervice C
    Fail!
    Fail Fast
    Fail Fast
    Microsrervice B
    Microsrervice A

    View Slide

  8. Circuit Breaker パターン

    Circuit Breaker = ブレーカー

    外部リクエストのエラーカウンタがしきい値を超えたら

    Circuit Openし、しばらくは処理を省略して

    即座にエラーを返す(Fail Fast)パターン


    ・障害時の連鎖的なレイテンシの増加を防げる

    ・リトライによる下位サービスの過負荷を緩和

    8
    External
    Service
    Success
    Error (1)
    Error (2)
    Error (3)
    Trip
    Circuit Open!
    Trip
    Fail!

    View Slide

  9. CLOSED
    Circuit Breaker

    ● CLOSED: リクエストが一定回数失敗したら OPEN に遷移

    ● OPEN: 一定の時間リクエストを止める (ブレーカーが落ちている状態)

    ● HALF-OPEN: リクエストが失敗したら OPEN に戻る。一定回数成功したら CLOSED に復帰。

    9
    External Service
    HALF-OPEN
    OPEN
    CLOSED

    View Slide

  10. Circuit Breaker

    =障害を検知して遮断するパターン


    View Slide

  11. Circuit Breaker in Go

    11

    View Slide

  12. Circuit Breaker packages

    ● github.com/rubyist/circuitbreaker

    ● github.com/sony/gobreaker

    12

    View Slide

  13. github.com/sony/gobreaker

    13
    ● Execute メソッドに外部呼び出し処理を渡す

    ○ 戻り値 error がnil → 成功カウンタ++

    ○ 戻り値 error がnon-nil → 失敗カウンタ++

    ○ Circuit Open時 → 関数が実行されず ErrOpenState がreturn


    View Slide

  14. 現実はもう少し複雑

    ● Goの関数が返すエラーにはいろいろな種類がある 

    ○ 障害起因のエラー
    ○ その他のエラー
    ● Goコード上のerrorをすべて 「失敗」とみなしていいのだろうか? 

    14

    View Slide

  15. 現実の例

    HTTPレスポンスボディ全体を読むコードを
    サーキットブレーカーで保護


    View Slide

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

    無視したい


    View Slide

  17. HTTPリクエスト失敗:「失敗」

    リクエスト作成エラー:リクエストしてない。

    無視したい


    View Slide

  18. HTTPリクエスト失敗:「失敗」

    HTTP 5xx系ステータス:「失敗」

    リクエスト作成エラー:リクエストしてない。

    無視したい


    View Slide

  19. HTTPリクエスト失敗:「失敗」

    HTTP 5xx系ステータス:「失敗」

    HTTP 4xx系ステータス:サービス自体は生きてる。

    場合によるが「成功」とみなしたい

    リクエスト作成エラー:リクエストしてない。

    無視したい


    View Slide

  20. レスポンスボディ読み込み失敗 => 「失敗」

    HTTPリクエスト失敗:「失敗」

    HTTP 5xx系ステータス:「失敗」

    HTTP 4xx系ステータス:サービス自体は生きてる。

    場合によるが「成功」とみなしたい

    リクエスト作成エラー:リクエストしてない。

    無視したい


    View Slide

  21. context

    ・contextを使ってユーザーがリクエストをキャ
    ンセルしたり、タイムアウト時間を指定したりす
    ると、リクエストがエラー終了する。


    →障害とは無関係


    ・ユーザーが発火可能。失敗として扱うとユー
    ザーがサーキットブレイカーを落とすことが出
    来てしまう。


    (gRPCではユーザーがキャンセルも

    タイムアウトも指定可能)

    https://www.irasutoya.com/2018/06/blog-post_972.html

    View Slide

  22. 課題

    ● 現実のコードには様々な error が

    ○ 障害起因のerror
    ■ 失敗として扱いたい
    ○ 障害と関係ないerror
    ■ 「失敗」として扱うとFalse-Positive
    ■ 「成功」として扱うとFalse-Negativeのケースも
    ○ context起因のerror
    ■ contexに対応した処理は、ユーザーがエラー終了させる可能性
    ● これらをCircuit Breakerに正しく伝えることは難しい

    22

    View Slide

  23. go-circuitbreaker

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


    View Slide

  24. Doメソッド

    24
    ● Doメソッドが保護対象関数とともにcontext.Context を受け取る 

    ● 基本的にはgobreakerと同じ 

    ○ 戻り値 error が nil : 成功
    ○ 戻り値 error が non-nil : 失敗
    ○ ブレーカーが落ちたら関数は実行されずエラー

    View Slide

  25. 特徴1: 特定のエラーを無視・成功扱いに指定できる

    25
    func Ignore(err) error 

    func MarkAsSuccess(err) error 

    ラップしたエラーをサーキットブレーカに
    「無視」させる。無視されたエラーは失敗
    にも成功にもカウントされない。 

    ラップしたエラーをサーキットブレーカー
    に「成功」としてカウントさせる。 

    どちらもfuncの外にはアンラップしてreturnさ
    れる


    View Slide

  26. 特徴2: context のキャンセル・タイムアウトを無視

    26
    ● オプションで変更可能 

    ● returnしたエラーはそのままDoの外側で受け取れる 


    View Slide

  27. 外部リクエストのタイムアウトを失敗とみなす方法

    27
    ● Do() の内側でcontextを派生してください。 

    ● 内側のcontextがtimeoutしても外側はtimeoutしない→失敗としてカウント 


    View Slide

  28. 実装上の工夫

    28

    View Slide

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

    ● 単純な実装では分岐が多く複雑になる


    View Slide

  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 (抜粋)

    View Slide

  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 (抜粋)

    View Slide

  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


    View Slide

  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 (抜粋)


    View Slide

  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 (抜粋)


    View Slide

  35. State デザインパターン

    ・内部状態を型として表現

    ・分岐の少ない保守性の高いコードに

    35

    View Slide

  36. まとめ

    36

    View Slide

  37. Go用Circuit Breakerを作りました

    既存のアプリケーションに組み込みやすいはず

    使ってみてください!

    01
    02
    03
    37

    View Slide

  38. Thank you!

    38

    View Slide

  39. 39

    View Slide