Fukuoka.go#15 2020 @online
Yuichi Watanabe at Nulab Inc.
### 概要
所謂ジョブキューの1つのパターンである、competing consumers の実装を提供する。
competing consumersとは
キューに登録されたメッセージ(以下、ジョブ)は、キューを参照するいずれかのコンシューマ(以下、ワーカ)へ、少なくとも一回は配信されるパターン。
永続ストレージにRDBを使用したジョブキューの他に、
既存のミドルウェアやパブリッククラウドの
マネージドサービスを接続先として透過的に使用可能。
### Go JWDKの開発動機
仮にヌーラボのサービスを例にすると...
クラウドサービスとして提供している backlog.com と、ユーザ自身でオンプレミスやパブリッククラウドにセルフホストするパッケージ版、開発者のローカルマシン等、環境の差異における制約。それらの多様なコンテキストによって、最適な基盤を選定する余地を作る。
### Go JWDKの主な機能
### Go JWDKの主な機能
ワーカプロセスのスケールアウト
ワーカプロセス自体を複数並べてスケールアウトすることも可能。
(同一のキューを参照してもジョブは多重取得されない)
ジョブの永続化ストレージの選択
ジョブの永続化先として以下のRDBをストレージとして選択できる。
MySQL, Postgres, SQLite3
既存ミドルウェア・パブリッククラウドのメッセージグサービスの使用
共通のインタフェースにより、既存のミドルウェアや、パブリッククラウドのマネージドサービスを接続先として透過的に使用できる。
Amazon SQS, ActiveMQ
プライマリ・セカンダリ間の自動フェイルオーバ
プライマリの接続先がダウンした場合、セカンダリの接続先にジョブをエンキューする。
### ジョブワーカを実装するための基本的なAPI
ジョブワーカを実装するための基本的なAPIは、以下 jobworker/JobWorker に定義されている。
API | Discription |
---|---|
Enqueue | 単一のジョブを登録する。 |
EnqueueBatch | 複数のジョブを一括登録する。 |
Register | 参照するキュー名とジョブを処理するワーカの実態を登録する。 |
RegisterFunc | 参照するキュー名とジョブを処理となる関数を登録する。 |
Work | キューの読み取りとジョブの処理のループを開始する。 |
Shutdown | グレイスフルシャットダウンする。 |
RegisterOnShutdown | Shutdown実行時のイベントフックとなる関数を登録する。 |
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### ワーカプロセスの実装
package main import ( ... // Amazon SQSへの接続するためのコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) go func() { // キューの監視とジョブの処理のループを開始する。 // ブロックされるので新たなゴルーチン内で実行する。 err := w.Work(&jw.WorkSetting{ // Jobの並列実行数を設定する。 WorkerConcurrency: 5, }) }() ...
... // シグナル受信用のチャンネルを定義する。 quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // シグナルを受け取るまでブロックする。 <-quit // シャットダウンのタイムアウトを設定する。 ctx := context.Background() ctx, cancel := context.WithTimeout(ctx,time.Minute) defer cancel() // Shutdown関数で処理中のジョブが完了するまで // ブロッキングする。 err := w.Shutdown(ctx) } // Worker interface を実装する // type Worker interface { // Work(*Job) error // } type HelloWorker struct { } func (HelloWorker) Work(job *jw.Job) error { log.Println("[HelloWorker]", job.Payload().Content) return nil }
Amazon SQSのコネクタを使用したワーカプロセスの実装例
### エンキューの実装
Amazon SQSのコネクタを使用したエンキューの実装例
package main import ( ... // Amazon SQSへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 単一のジョブを「hello」キューへ追加する ctx := context.Background() err := w.Enqueue(ctx, jw.EnqueueInput{ Queue: "hello", Content: `{"msg:": "foo"}`, }) ...
... // 複数のジョブを「hello」キューへ一括で追加する ctx = context.Background() r, err := w.EnqueueBatch(ctx, jw.EnqueueBatchInput{ Queue: "hello", Id2Content: map[string]string{ "60b72": `{"msg:": "foo"}`, "5f10c": `{"msg:": "bar"}`, "9c85c": `{"msg:": "baz"}`, }, }) }
### エンキューの実装
Amazon SQSのコネクタを使用したエンキューの実装例
package main import ( ... // Amazon SQSへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // sqsのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, }) // 単一のジョブを「hello」キューへ追加する ctx := context.Background() err := w.Enqueue(ctx, jw.EnqueueInput{ Queue: "hello", Content: `{"msg:": "foo"}`, }) ...
... // 複数のジョブを「hello」キューへ一括で追加する ctx = context.Background() r, err := w.EnqueueBatch(ctx, jw.EnqueueBatchInput{ Queue: "hello", Id2Content: map[string]string{ "60b72": `{"msg:": "foo"}`, "5f10c": `{"msg:": "bar"}`, "9c85c": `{"msg:": "baz"}`, }, }) }
### プライマリ/セカンダリの設定
package main import ( ... // Amazon SQSへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // MySQLへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/db-connector/mysql" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // mysqlのコネクタの実態を生成する。 mysql, err := jw.Open("mysql", map[string]interface{}{ "DSN": os.Getenv("DSN"), }) // sqsとmysqlのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, Secondary: mysql, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) ...
プライマリにAmazon SQS、セカンダリにMySQLのコネクタを使用した実装例
### プライマリ/セカンダリの設定
package main import ( ... // Amazon SQSへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/aws-sqs-connector" // MySQLへの接続を提供するコネクタを提供する。 _ "github.com/go-jwdk/db-connector/mysql" // ジョブを処理するワーカの実装に必要なAPIを提供する。 jw "github.com/go-jwdk/jobworker" ) func main() { // sqsのコネクタの実態を生成する。 sqs, err := jw.Open("sqs", map[string]interface{}{ "Region": os.Getenv("REGION"), }) // mysqlのコネクタの実態を生成する。 mysql, err := jw.Open("mysql", map[string]interface{}{ "DSN": os.Getenv("DSN"), }) // sqsとmysqlのコネクタを渡してJobWorkerの実態を生成する。 w, err := jw.New(&jw.Setting{ Primary: sqs, Secondary: mysql, }) // 参照するキューの名前とJobの処理を実装したワーカを紐つけて // 登録する。 w.Register("hello", &HelloWorker{}) ...
プライマリにAmazon SQS、セカンダリにMySQLのコネクタを使用した実装例
ジョブワーカ
コネクタを使用して、ジョブの追加・更新、サブスクリプションの発行・停止、を制御する。
コネクタ
各ジョブキューへ接続するための部品。
ジョブ追加・更新、サブスクリプションの発行を行うAPIを提供する。
ジョブ
ジョブの実態。
処理に必要なパラメータ(コンテンツ、メタデータ)を保持する。
サブスクリプション
各ジョブキューの購読の実態。
ジョブキューに追加されたジョブは
サブスクリプションを経由して
ジョブワーカに配信される。
ジョブキュー
RDB、ActiveMQ、Amazon SQS等の
ジョブキューの実態。
### Go JWDK を構成する主要な要素
### パッケージの依存関係
ジョブワーカを開発するためのAPIや各コネクタ等の主要なパッケージはリポジトリ単位で分割。
MySQL, Postgres, SQLite3
各コネクタは go-jwdk/jobworker で定義した、Connector、Subscription インタフェースを実装。
### ジョブの処理フロー
### ジョブの処理フロー
### ジョブの処理フロー
### ジョブの処理フロー
### ジョブの処理フロー
### ジョブの処理フロー
### ベータリリースに向けた整備
### モニタリング画面の提供
### サポートするコネクタの追加
ユニットテストの拡充
CIの整備
ベンチマークの計測
多岐にわたるコネクタ毎のパラメータのドキュメント化
RDBの独自実装のキューに対してのモニタリング画面の提供。
Amazon SQSやActiveMQをキューとして使用した場合は、それら自身のモニタリング画面を使用してキューの詳細を閲覧できる。しかし、RDBの独自実装のキューを使用した場合は、RDBに接続しなければキューの詳細は閲覧できない。
AWS以外のパブリッククラウドや他のミドルウェアのメッセージングサービスをサポート。
worker.Shutdown(ctx)
👋👋👋