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

mercari.go #17 naruse

mercari.go #17 naruse

2021/10/21に行われたmercari.go #17の登壇資料です

Makoto Naruse

October 21, 2021
Tweet

More Decks by Makoto Naruse

Other Decks in Programming

Transcript

  1. 2 • この発表のターゲット󰢏 ◦ Go学習を始める前の人 ◦ Goを学習中の人 ◦ Goの並列処理に興味のある人 ◦

    context cancelを何気なく使っている人 • 今回のスコープ外󰢃 ◦ Goを用いた専門的なツール ◦ Goの新たな機能 この発表のスコープ
  2. 5 2020年5月ごろ 初めてGoを触ってみる。 Tour of Goをやったり、簡単なツール を作ってみたり、、 Goで競プロやコーディングテストを受 けてみる Goとの出会い

    2020年8月 APIを作成する課題型のインターンを行 う。 初めて複雑な構造体やメソッドを扱った り、 軽くコードレビューやペアプロなどを 行った
  3. 6 2020年8月~9月 イベント型で行われたmercariのサ マーインターン • Goについての全般的な講義 • Goを使った静的解析の講義 • 静的解析ツールの作成

    mercariのサマーインターン 資料は公開されてます! 「プログラミング言語 Go完全入門」の 「完全」公開のお知らせ
  4. 7 2020年9月~11月ごろ A社での実務型インターン • 既存のmicroserviceの機能改善 • 新しいmicroserviceの構築 実際にGoが業務にどう使われているか を体感 実務型インターン

    2020年11月~ mercariの実務型インターン • もちろん業務でGo • Goで書かれた数々の社内ライブラリを 読んでいくことで、より実践的なGoの使 い方を習得 • 同時に、書籍等での知識のindex化
  5. 10 • 出典 ◦ context package - context - pkg.go.dev

    ◦ Go Concurrency Patterns: Context - go.dev • Go blogより引用 ◦ At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request. • つまり、contextには大きく以下の役割がある ◦ request-scopedなvalueを渡す役割 ◦ 複数のgoroutineについてcancelやdeadlineを伝搬させる 前提: Goでのcontextについて
  6. 12 contextとgoroutineの一例 package main // 略 func main() { ctx

    := context.Background() cancelCtx, cancelFunc := context.WithCancel(ctx) toCtx, toCancelFunc := context.WithTimeout(ctx, 500*time.Millisecond) wg := sync.WaitGroup{} wg.Add(1) go func() { // cancelされるgoroutine }() wg.Add(1) go func() { // timeoutされるgoroutine }() time.Sleep(1 * time.Second) cancelFunc() toCancelFunc() wg.Wait() } Go Playgroundで動かす
  7. 18 非同期での大量配信: channelとcontext package main // 略 func worker(ctx context.Context)

    { handleCtx, pullDone := context.WithCancel(context.Background()) ackCtx, handleDone := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(ctx) msgs := make(chan []*message) result := make(chan result) eg.Go(func() error { defer pullDone() return pull(ctx, msgs) }) eg.Go(func() error { defer handleDone() return handle(handleCtx, msgs, result) }) eg.Go(func() error { return ack(ackCtx, result) }) return eg.Wait() } ← cancelを伝搬させるcontextの作成 ← errgroupの作成 ref ← pullするgoroutine ← handleするgoroutine 実際の処理はさらに子供の goroutineで行う ←ackするgoroutine ←全てnilで返るかerrorが1つでも返るとreturn
  8. 19 context cancel時の流れ package main // 略 func worker(ctx context.Context)

    { handleCtx, pullDone := context.WithCancel(context.Background()) ackCtx, handleDone := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(ctx) msgs := make(chan []*message) result := make(chan result) eg.Go(func() error { defer pullDone() return pull(ctx, msgs) }) eg.Go(func() error { defer handleDone() return handle(handleCtx, msgs, result) }) eg.Go(func() error { return ack(ackCtx, result) }) return eg.Wait() } ← main関数がSIGTERMを受信し、worker自 体のcontextがcancelされる ← pullの処理を止めて全ての messageを channelに流した上でpullDone()してhandle のcontextをキャンセル ← handleの処理を完了または打ち切った上で 全ての結果をresult channelに流して handleDone(), ackをキャンセル ← result channel経由で受け取った結果を全 てackしてからreturn ← 関数が返る
  9. 20 ※workerに限らずserverでもあり得る話 • 2つのAPIに原子性を保ってリクエストしたい • Api1に先にリクエストし、 api2が失敗した場合は api1をrollback • rollbackに失敗した場合はdead

    letterなどで 特別な処理(2つのAPIのSLOを掛け合わせると 数はかなり少ない) • api2のcall中にcontextのcancelが行われた 場合、api2のcallはcancelされ、後続のapi1の rollbackもcontext canceledで失敗する • 結果、大量にdead letterへ context cancel時の課題①: 外部APIとの通信の整合性 package main // 略 func handleFunc(ctx context.Context, ...) error { if err := api1.Call(ctx); err != nil { // リトライされる return err } if err := api2.Call(ctx); err != nil { if err := api1.Rollback(ctx); err != nil { // 特別な処理 return err } // リトライされる return err } }
  10. 22 • PubSubのmessageはackのAPIを叩くことで 正しく処理されたと記録される • 最後にしっかりとackしないと予期せぬ再配信が 起こってしまう • 以下のような箇所に処理されなかった ackIDが

    残ってしまう可能性があった ◦ result channelの中 ◦ ackID変数の中 • 最後までしっかりと処理するためには context cancel時にどのchannelや変数にデータが残っ ている可能性があるかを考察し、しっかりと処理 を追加してあげる ◦ ※これ重要 課題②: channelやvariableに残ったデータ func ack(ctx context.Context, ...) error { // 略 for { var ackID string select { case <-ctx.Done(): return ctx.Err() case res := <-result: ackID := res.ackID } for { var err error err = pubsub.Ack(ctx, ackID, ...) // リトライ... if cannotRetry {// retryLimit,cancel return err } } } }
  11. 23 • goroutineが止まる際、deferなどで残っている データの処理をしっかりと行ってあげる • この際もtimeout contextを用いるので、全体 にかかる時間には留意する • deferでのAPIコールのretryは難しいので、

    100%のack保証はできない • context cancel時の挙動は直感的に分かりづ らくテストもしにくいため、今も奮闘中 • 解決法は1つではない、これからもより良い方法 がないか模索していく 課題②: 1つの解決法(格闘中?) func ack(ctx context.Context, ...) error { defer func() { // Ack all pending messages. toctx, cancel := // 略 defer cancel() _ = pubsub.Ack(toctx, remainedAckID, ...) select { case res := <-result: _ = pubsub.Ack(toctx, res.AckID, ..) } }() // 略 if cannotRetry { remainedAckID = ackID return err } // 略 }
  12. 24 • Go学習について ◦ まずは色々作ってみてトップダウン的に学ぶことが大切。 Goの良さに気づく。 ◦ 加えて、書籍等を用いたボトムアップな知識の積み上げも非常に重要になる。 ▪ 自分の場合は静的解析ツールの作成で非常にボトムアップ的学習ができた

    • context cancelについて ◦ 皆さんはしっかりと意識して context cancelできてましたか? ◦ 僕は全然意識していませんでした。 ▪ (ライブラリがよしなにやってくれるっしょ ) ◦ しかし、goの並行処理は非常に強力で、マスターすると武器になる 一緒にこれからも学んでいきましょう! まとめ