Slide 1

Slide 1 text

バッチ処理をパイプライン パターンで上手くやる Yusuke Misawa MediaDo.go@2019/09/17

Slide 2

Slide 2 text

Yusuke Misawa(三澤 悠介) ● 決済系の自社サービスの会社 バックエンドエンジニア ● 仕事でGoを使うようになって1年 ● Goで個人情報周りの マイクロサービス開発 ● 趣味:ボルダリング‍♂ ● Twitter: https://twitter.com/FpmpAmpm

Slide 3

Slide 3 text

話の流れ ● なにかとバッチ処理を作る機会があるという話 ● バッチ処理作成時の課題がいろいろあるよねという話 ● パイプラインパターンでうまくやろうという話 ○ パイプラインとはステージと呼ばれるデータ操作の組み合わせのパターン ○ ステージをうまくやるためのパターン(ファンアウト・ファンイン・キャンセル) ● パイプラインを使えばうまく作れそうだが残る課題として ○ ゴルーチンがリークしないようにする話 ○ チャネルのブロックと panicを防ぐ話 ● まとめ

Slide 4

Slide 4 text

なにかとバッチ処理を作る機会がある ● ユーザーデータの不整合を解消したい ● 非同期で動くイベント送信が失敗したので再送したい (なお自動でリカバリーする仕組みはない) ● ユーザーが個人情報の削除を要請してきた ● 追加機能開発で増えたDBカラムの既存レコード分のデータ作成 ● とにかくいろいろある

Slide 5

Slide 5 text

なにかとバッチ処理を作る機会がある ● 組織・チーム体制にもよるけど前職だと関わっていたサービスの規模が大きく専業 のバッチチームやインフラチームがあった。 ● なので個人的には最近まであまりバッチ処理を作る機会はあまりなかった ● マイクロサービス構成だと各サービスの開発者がなんとかするしかない。

Slide 6

Slide 6 text

バッチ処理作成時の課題 ● 処理量が多いので並行処理しようとするが複雑化 ● 動きそうだがオレオレ実装で可読性が悪くレビューもし辛い ● できれば再利用・拡張性あるものにしたい ● ゴルーチンがうまく扱えているか不安 ● チャネルがうまく扱えているか不安 →コードの見通しが良く拡張性があり  安全でいい感じの実装方式があれば真似したい

Slide 7

Slide 7 text

「Goで並行処理するときにはパ ターンがあったな・・・」

Slide 8

Slide 8 text

並行処理のパターンに関する情報源 ● The Go Blog:Go Concurrency Patterns: Pipelines and cancellation ○ https://blog.golang.org/pipelines ● 書籍「Go言語による並行処理」 ○ オライリージャパン ○ Katherine Cox-Buday 著、山口 能迪 訳 ○ 原書: Concurrency in Go ○ https://www.oreilly.co.jp/books/9784873118468/ 

Slide 9

Slide 9 text

● 入力を受け取り何らかの一連の操作(ステージと呼ぶ)を行い出力する この組み合わせをパイプラインと呼ぶ(Go特有の概念ではない) パイプラインと呼ばれる考え方 ステージ1 ステージ2 ステージ3 入力 出力

Slide 10

Slide 10 text

● Goで実装する場合ステージ間はチャネルでデータを共有する パイプラインと呼ばれる考え方 ステージ1 ステージ2 ステージ3 入力 出力 データを渡すチャネルを作りステージ 2に 渡し、入力をチャネルに流す データ受信チャネルを引数に持ち、そこ から入力を受信後データ操作を行う

Slide 11

Slide 11 text

● 各ステージは独立して修正や入れ替えが可能 ステージをファンアウト・ファンインにすることで 並行処理とその結果の集約もできる。 コードの見通しはよくなりそう。 パイプラインと呼ばれる考え方 ステージ1 ステージ2 ステージ3 入力 出力

Slide 12

Slide 12 text

● ステージ1で出力されたデータを複数のステージ2で処理 複数のゴルーチンでステージ2を行う ファンアウト ステージ1 ステージ2 ステージ3 入力 出力

Slide 13

Slide 13 text

● ステージ2で別々に出力されたデータをステージ3で集約 ファンインで結果の集約 ステージ1 ステージ2 ステージ3 入力 出力

Slide 14

Slide 14 text

パイプライン全体の擬似コード // ステージ1(ctx以下は入力) in := gen(ctx, 1,2,3,4,5) //genは受信専用チャネルinを返す // ステージ2 (ファンアウト) c1 := sq(ctx,in) // 何らかの操作を並行実行 c2 := sq(ctx,in) // c1,c2は実行結果の受信専用チャネル // ステージ3(ファンイン) for n := range merge(ctx,c1, c2) { // mergeもc1,c2をまとめた受信専用チャネルを返す fmt.Println(n) }

Slide 15

Slide 15 text

ファンインで複数のチャネルをまとめる 引数のチャネル個数分それぞれデータを取り出し返却済のチャネル に送信する データ集約先のチャネルは先に返却

Slide 16

Slide 16 text

バッチ処理作成時の課題(再掲) ● 処理量が多いので並行処理しようとするが複雑化 ○ ◎パイプラインで見通し良さそう ● 動きそうだがオレオレ実装で可読性が悪くレビューもし辛い ○ ◎パイプラインの考え方が共有されていれば話が早そうだしコードの見通し良さそう。 ● できれば再利用・拡張性あるものにしたい ○ ◎パイプラインで拡張性良さそう ● ゴルーチンがうまく扱えているか不安 ○ パイプラインのステージ内の実装による ● チャネルがうまく扱えているか不安 ○ パイプラインのステージ内の実装による

Slide 17

Slide 17 text

ゴルーチンのリークが起きないようにしたい ● リーク(=漏れ) ○ 情報のリーク:関係者がマスコミに秘密を漏らしちゃう ● ゴルーチンのリーク ○ 非メインのゴルーチンが終了せず残り続けること。 ○ ガベージコレクションで回収されずその分メモリを圧迫する。 ○ バッチ処理の場合どこかでプロセスが終わるので永遠に増え続けることはな いが意図しないリークは避けたい。

Slide 18

Slide 18 text

ゴルーチンのリーク例 https://play.golang.org/p/_YJcrlApJRq

Slide 19

Slide 19 text

コンテキストキャンセルで解決 https://play.golang.org/p/U18DkwkN0SO メインゴルーチンのdefer cancel()が伝播してctx.Doneがcloseされこ ちらのゴルーチンも終了する。

Slide 20

Slide 20 text

パイプラインでチャネルをうまく扱う ● panicが起きる条件を知る ○ closedチャネルへの送信(https://play.golang.org/p/puTTlBWgSTh) ○ closedチャネルのclose ○ nilチャネルのclose ● 意図せずブロックしないようにする ○ nilチャネルの読み書きをしない(https://play.golang.org/p/jCnURn3qaiU ) ○ 所有権(誰が初期化するか、送受信の制限)を決める

Slide 21

Slide 21 text

チャネルのpanicを回避するパターン defer でcloseして二重にcloseを防ぎ、初期化 スコープでやることで nilチャネルのcloseを防ぐ 初期化したスコープでのみ送信する チャネルは初期化して受信専用で返却する(外 部で勝手に書き込ませない)

Slide 22

Slide 22 text

チャネルのブロックを回避するパターン ● 作成者(ジェネレーター) ○ パイプラインに流すデータのチャネルの作成、初期化し返却 ○ 並行してデータ送信、チャネルのclose ● 消費者(コンシューマー) ○ ジェネレーターから受け取ったチャネルからデータ受信 ○ もちろん必要な業務上の処理をやる

Slide 23

Slide 23 text

バッチ処理作成時の課題(再々掲) ● 処理量が多いので並行処理しようとするが複雑化 ○ ◎パイプラインで見通し良さそう ● 動きそうだがオレオレ実装で可読性が悪くレビューもし辛い ○ ◎パイプラインの考え方が共有されていれば話が早そうだしコードの見通し良さそう。 ● できれば再利用・拡張性あるものにしたい ○ ◎パイプラインで拡張性良さそう ● ゴルーチンがうまく扱えているか不安 ○ ◎コンテキストで処理終了を伝えゴルーチンのリークを回避 ● チャネルがうまく扱えているか不安 ○ ◎チャネルの作成者と消費者で役割分担し不要なブロックや panicを回避

Slide 24

Slide 24 text

パイプラインまとめ ステージ1 ステージ2 ステージ3 入力 出力 ● パイプラインは入力を受け取りステージと呼ばれる独立したデータ操作の組み合わせるパターン ● ステージの組み合わせで柔軟に処理の分割、並行実行が可能になる。 ○ ステージ同士はチャネルでデータを受け渡しする ● ステージ(個々のデータ操作を上手くやる)のためのパターン ○ ファンアウト・ファンイン(ステージ同士の連携のためのパターン) ○ コンテキストキャンセル(ステージを安全に終了するためのパターン) ○ ジェネレーターとコンシューマー(ステージを安全に終了するための役割分担のパターン)

Slide 25

Slide 25 text

最後に ● バッチ処理作成時に課題と思ったいくつかのトピックは解決できた。 しかし並行処理の沼はまだまだ深い。 ○ 競合状態 ○ ロック ○ エラー処理 ○ ゴルーチンのスケジューリング ○ 並行処理の速度・律速問題 etc… ● 今ここで話すために並行処理の勉強をしたけどまだ氷山の一角に過ぎないと感じ た。これからも備えていきたい。 ● 懇親会では是非声をかけてください。