- 投稿日:2022-01-31T23:34:30+09:00
Raspberry pi 4 2台を使って映像配信と受信してみた。【Gstreamer +Golang (gocv)】
はじめに OpenCV4の入力にカメラ映像ではなく、Gstreamerからの入力を受け取り表示します。 Raspberry piのカメラユニットの映像を別PCで動かしているOpenCvで受信してみます。 今回動かすシステムの構成としてはこのようになっています。 WEB camera - Raspbery pi 4(カメラ配信サーバ) ===== Raspberry pi 4(受信サーバ) Gstreamer の勉強も兼ねているので、先ずは単機で動かしてステップアップ方式で進めていきます。 記事の構成がわかりにくくてクソなのですが、「機器間配信のテスト」まではWindows10で実行しています。 それ以降はRaspberry pi4 2台を使って配信と受信を行っています。 大まかな流れは変わらないのでご容赦ください。 Gstreamerでの配信について 配信する側もゆくゆくはソフトとして作っていきますが、今回はコマンドラインで代用します。 Gstreamerの動作確認 インストール済みだとは思いますが、映像を配信できるかを確かめます。 videotestsrcで起動すると映像が配信されます。pattern=snowを選択すると、 地デジになる前のテレビで電波が届いていないときに表示されていたような映像が流れます。(伝わるかしら) 映画ドラえもん ブリキのラビリンスで、深夜子どもたちが見ていたあのジャギジャギの映像です。 参考 videotestsrc gst-launch-1.0 -v videotestsrc pattern=snow ! video/x-raw,width=1280,height=720 ! autovideosink 実行するとこんな感じで映像が取得されます。 ジャギジャギなのはあまり落ち着かないので別の映像で試します。 gst-launch-1.0 -v videotestsrc ! video/x-raw,width=1280,height=720 ! autovideosink 後ろでYoutube見ているのがバレちゃいますが、Terminalと合わせてこのように出力されているのが確認できます。 gocvでGstreamerの映像を取得する 映像の配信で使用していたautovideosink をappsinkに変更すればOKです。 gst-launch-1.0 -v videotestsrc ! video/x-raw,width=1280,height=720 ! appsink 指定されたサイズの映像が、appsinkに流れています。 Terminalでは時間が経過するばかりで、映像が表示されません。 gocvを使ってappsinkを表示するプログラムを書いていきます。 gocvのTutorialにあるHello, videoをべ-スに作ります。 OpenVideoCaptureにgstreamerのコマンドを引数にとして渡してやればOKです。 実行コード package main import ( "gocv.io/x/gocv" ) func main() { src := `videotestsrc ! video/x-raw,width=1280,height=720 ! appsink` webcam, _ := gocv.OpenVideoCapture(src) window := gocv.NewWindow("Hello") img := gocv.NewMat() for { webcam.Read(&img) window.IMShow(img) window.WaitKey(1) } } 実行結果 先ほどと表示内容に変化はないですが、Windowの名前がHelloになっていることが確認できます。 機器間配信のテスト 次は機器間をまたいで映像を取得します。 Golangで組む前にコマンドで動作することを確認しましょう。 Raspberry pi4(カメラ配信サーバ)IP:192.168.0.3 GstreamerからVideoカメラのデバイスを指定し、配信を開始します。 RTPパケットを使って映像配信を行っていきます。 udpsinkでは受信先のIPアドレスとポート番号を指定してあげます。 gst-launch-1.0 -v v4l2src device=/dev/video1 ! "image/jpeg,width=640, height=360,framerate=30/1" ! rtpjpegpay ! udpsink host=192.168.0.4 port=9000 Raspberry pi4 (映像受信サーバ) IP:192.168.0.4 gst-launch-1.0 -e -v udpsrc port=9000 ! application/x-rtp, encoding-name=JPEG,payload=26 ! rtpjpegdepay ! jpegdec ! autovideosink 実行結果 カメラ配信サーバに取り付けたUSBカメラの映像を同一ネットワークにおいている別の機器で出力してみました。 想像以上に遅延なくて少し驚いています。こんなに低遅延で実現できるのが驚いています。 tcpserversinkで配信したときは20秒ぐらい遅延があったのでそれを思うとかなり高速になっています。 あとは、WindowsとRaspberry pi2ってのも影響しているのでしょうか。 今回はRaspberry pi4を2台構成で組んでいるのでかなり安定しているように思います。いいですね。 golangから呼び出してみよう この記事の本題に入ります。 Raspberry pi4 (映像受信サーバ) IP:192.168.0.4 Golangから読み出してやるときMat型で呼び出すためにappsinkを使います。 jpegdec以下をこのように変換します。 jpegdec ! videoconvert ! appsink 全文コード package main import ( "gocv.io/x/gocv" ) func main() { src := `udpsrc port=9000 ! application/x-rtp, encoding-name=JPEG,payload=26 ! rtpjpegdepay ! jpegdec ! videoconvert ! appsink` webcam, _ := gocv.OpenVideoCapture(src) window := gocv.NewWindow("Hello") img := gocv.NewMat() for { webcam.Read(&img) window.IMShow(img) window.WaitKey(1) } } Raspberry pi4(カメラ配信サーバ)IP:192.168.0.3 Golangから呼び出すコードを書いていきます。 Gstreamerで呼び出したコマンドをexecで実行してあげます。 他に良いやり方があれば是非教えて下さい。お待ちしております。 全文コード package main import ( "fmt" "os" "os/exec" "strings" ) func main() { args := strings.Fields("-v v4l2src device=/dev/video1 ! image/jpeg,width=640, height=360,framerate=30/1 ! rtpjpegpay ! udpsink host=192.168.0.4 port=9000") out, err := exec.Command("gst-launch-1.0", args...).Output() if err != nil { fmt.Println(err.Error()) os.Exit(1) } fmt.Println(string(out)) } 実行結果 良い感じに動いています。 やりましたね!! まとめ GolangでGstreamerをいじって遊んでいる人って少ないようなので記事に残しました。 今回はRaspberry pi4とRaspberry pi4で配信と受信をやっています。 Golangで書いているのでどのOSでもあるていど使い回しが出来るんじゃないかと期待してます。 心のデブを信じろ というブログもやってますのでよければお願いします。
- 投稿日:2022-01-31T23:03:51+09:00
[Go] Structs
Structs とは Go には関連する情報をまとめするため、 structs が使われる Defining and Declaring package main import "fmt" type person struct { firstName string lastName string } func main() { var aaa person fmt.Println(aaa) // The default value is a empty string. fmt.Printf("%+v\n", aaa) // Use the "%+v" can print the fields also. // Updating Struct Values aaa.firstName = "aaa" aaa.lastName = "player" } Embedding Structs Structs に structs も宣言できる package main import "fmt" type contactInfo struct { email string zipCode int } type person struct { firstName string lastName string contact contactInfo } func main() { aaa := person{ firstName: "aaa", lastName: "player", contact: contactInfo{ email: "aaa@mail.com", zipCode: 1234567, }, } fmt.Println(aaa) } 宣言するときに、 contactInfo だけを宣言すると contactInfo contactInfo の書き方と同じになる type contactInfo struct { email string zipCode int } type person struct { firstName string lastName string contactInfo } Updating Struct Values package main import "fmt" type player struct { name string score int } func main() { a := player{"aaa", 0} fmt.Println(a) a.updateScore(1) fmt.Println(a) } func (p player) updateScore(s int) { p.score = s } output {aaa 0} {aaa 0} 上記のupdateScore functionを使ってもaのスコアが更新できない Pass By Value GoはPass By Valueなので、下記のように address value note 0001 {aaa 0} 元のa値 更新されなかった 0002 0003 {aaa 0} コピーされた値 funcがこの値を更新した 元のaのスコアが更新されなかった Pointer address value 0001 {aaa 0} Turn address into value with *address Turn value into address with &value Point Receiver ReceiverのTypeに*を追加することで (p *player) 宣言するtypeはstruct playerではなく、struct playerのaddressになる そうすれば、struct aのaddressのvalueをpassでき 元のaの値を更新できるようになる package main import "fmt" type player struct { name string score int } func main() { a := player{"aaa", 0} fmt.Println(a) pointerA := &a // Turn value into address pointerA.updateScore(1) fmt.Println(a) } func (p *player) updateScore(s int) { // Turn value into address (*p).score = s } output {aaa 0} {aaa 1} ちなみに、 aのvalueをadressに転換しなくても、Goも処理してくれる package main import "fmt" type player struct { name string score int } func main() { a := player{"aaa", 0} fmt.Println(a) a.updateScore(1) // Use a, the player struct type fmt.Println(a) } func (p *player) updateScore(s int) { (*p).score = s } 結果も同じ output {aaa 0} {aaa 1}
- 投稿日:2022-01-31T15:25:11+09:00
Go導入リンク
Go導入リンク 新しく案件でGoを使うことになったので使えそうなドキュメントをリンクしていく 随時更新予定 追記予定: デプロイ周り Unity連携 gRPC-web Goダウンロード・インストール 公式サイト ダウンロード 確認 $ go version go version go1.17.6 darwin/amd64 エディタ VSCodeやAtomなどにプラグイン入れる方法もあるが、すぐ始められて多機能なJET BRAINSのGoLandが一番いいと思う(有料だけどその価値はあった チュートリアル A Tour of Go 初学者は一通りこれを見ればいい、ポインタ、Goroutineだけ理解を深めるため他のサイトも見た。 メモ 以下、Go特有だったのでメモ Defer 遅延処理 try-cath-finalyのfinaly的に使う https://go-tour-jp.appspot.com/flowcontrol/12 Array スライスはデータ格納していない、単に元の配列の部分列を指すだけ https://go-tour-jp.appspot.com/moretypes/8 Pointer receivers ポインタレシーバ、変数レシーバーの違い 変数レシーバーはコピーを渡す。 ポイントレシーバーはレシーバー自体を更新する。 レシーバー自身を更新することが多いため、変数レシーバよりもポインタレシーバの方が使うことが多い https://go-tour-jp.appspot.com/methods/1 https://go-tour-jp.appspot.com/methods/4 使い所、構造体など https://nishinatoshiharu.com/go-structure-pointermethod/ The empty interface 型が不明な場合にvar i interface{} を使うと全ての型を入れることができる。(Any的な)a := x と違いはintとかstringとか型が違を入れ替えて使うことができる。 https://go-tour-jp.appspot.com/methods/14 Stringers fmt.Println(a, z) でString() を見にいく、これに関しては標準関数を理解していないとわからないので、他に定義済みが無いか確認しないとダメ https://go-tour-jp.appspot.com/methods/17 Goroutine 個人サイトの方がわかりやすかった。 GoroutineはGoLangで書くメリットの大きな一つなので下記でポイントを押さえておく https://qiita.com/taigamikami/items/fc798cdd6a4eaf9a7d5e https://go-tour-jp.appspot.com/concurrency/1 標準ライブラリ チュートリアルやって標準ライブラリ詳しくないとダメだと認識 テスト 標準ライブラリにテストがあり使い勝手がGOOD!! 実行はGoLandで走らすことができた gRPC 公式サイトのQuick startがわかりやすかった MacOS gRPCインストール $ brew install protobuf $ protoc --version libprotoc 3.19.4 クライアントツール 毎回Go側でクライアントコードを書くのも大変なのでクライアントツールをインストール 使い方はサイトを参考 $ brew install --cask bloomrpc Go Plugins $ go get -u google.golang.org/grpc $ go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest $ export PATH="$PATH:$(go env GOPATH)/bin" MongoDB 公式ドライバあり、ドキュメントもわかりやすい。 ダウンロード $ go get go.mongodb.org/mongo-driver/mongo $ go install -v go.mongodb.org/mongo-driver/mongo $ go install -v go.mongodb.org/mongo-driver/mongo/options $ go install -v go.mongodb.org/mongo-driver/mongo/readpref
- 投稿日:2022-01-31T13:24:51+09:00
[Go] time.AddDate() で月を加算する
はじめに Golang の time パッケージのAddDate()について、 挙動を理解していないが為に重要なバグを生んでしまったので、まとめてみました。 結論 AddDate()で月を加算すると加算前の月の日数(1月だったら31日)が加算される つまり、2022年1月31日にAddDate()で月を1加算すると、2022年3月3日になる 実際に起こったこと それは、今月の月と来月の月を返す関数getMonths()を実装した際に起こりました。 まずは、実装したコードを見てください。 func getMonths() []int { targetTime := time.Now() calendars := make([]int, 2) for i, _ := range calendars { calendars[i] = int(targetTime.Month()) targetTime = targetTime.AddDate(0, 1, 0) } return calendars } このコードはバグります。 たとえば1月30日の場合、[1, 3]が返ってしまいます。 time.AddDateの挙動を見てみる 一旦、time.AddDateの実装部分を見てみましょう。 func (t Time) AddDate(years int, months int, days int) Time { year, month, day := t.Date() hour, min, sec := t.Clock() return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec()), t.Location()) } 上のコードでわかる通り、月の部分はmonth+Month(months)となっており、 現在の月に引数の月(int型をMonthにキャストする)を足した値を返しています。 time.Date()では次のような実装になっています。 func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time { if loc == nil { panic("time: missing Location in call to Date") } // Normalize month, overflowing into year. m := int(month) - 1 year, m = norm(year, m, 12) month = Month(m) + 1 // Normalize nsec, sec, min, hour, overflowing into day. sec, nsec = norm(sec, nsec, 1e9) min, sec = norm(min, sec, 60) hour, min = norm(hour, min, 60) day, hour = norm(day, hour, 24) // Compute days since the absolute epoch. d := daysSinceEpoch(year) // Add in days before this month. d += uint64(daysBefore[month-1]) if isLeap(year) && month >= March { d++ // February 29 } // Add in days before today. d += uint64(day - 1) // Add in time elapsed today. abs := d * secondsPerDay abs += uint64(hour*secondsPerHour + min*secondsPerMinute + sec) unix := int64(abs) + (absoluteToInternal + internalToUnix) // Look for zone offset for expected time, so we can adjust to UTC. // The lookup function expects UTC, so first we pass unix in the // hope that it will not be too close to a zone transition, // and then adjust if it is. _, offset, start, end, _ := loc.lookup(unix) if offset != 0 { utc := unix - int64(offset) // If utc is valid for the time zone we found, then we have the right offset. // If not, we get the correct offset by looking up utc in the location. if utc < start || utc >= end { _, offset, _, _, _ = loc.lookup(utc) } unix -= int64(offset) } t := unixTime(unix, int32(nsec)) t.setLoc(loc) return t } 月を算出している部分は以下の部分で、 // Compute days since the absolute epoch. d := daysSinceEpoch(year) // Add in days before this month. d += uint64(daysBefore[month-1]) if isLeap(year) && month >= March { d++ // February 29 } ここでは引数のyearまでの日数を取得し、 引数の月までの日数を加算しています。 daysBefore は以下のような値です。 var daysBefore = [...]int32{ 0, 31, 31 + 28, 31 + 28 + 31, 31 + 28 + 31 + 30, 31 + 28 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31, } つまり、Date()の引数にyear=2022, month=3を渡した場合、 2022年1月1日までの総日数を取得し、 2022年1月1日〜2022年3月1日 までの日数(31+28)を加算しているわけです。 2022年1月31日でAddDate(0, 1, 0)をすると 2022年1月31日でAddDate(0, 1, 0)をすると、 内部で2022年1月1日までの総日数を取得し、 2022年1月1日から2022年2月1日までの総日数31日分が加算されます。 つまり、 2022年1月31日が2022年3月3日になります。 改めて今月の月と来月の月を返す関数を実装してみる これらを踏まえてもう一度getMonths()を実装してみると、 func getMonths() []int { loc, _ := time.LoadLocation("Asia/Tokyo") targetTime := time.Now() months := make([]int, 2) for i, _ := range months { months[i] = int(targetTime.Month()) targetTime = time.Date(targetTime.Year(), targetTime.Month()+1, 1, 0, 0, 0, 0, loc) } return months } これで意図した関数になりました。 または、ややこしいことはしないで、 func getMonths() []int { now := time.Now() month := now.Month() months := make([]int, 2) for i, _ := range months { months[i] = int(month) if month == 12 { month = 1 } else { month++ } } return months } これでももちろん十分です。 まとめ 今回はAddTime()で月を加算するとどうなるかまとめてみました。 今思えばそりゃそうだろって感じですが、雑魚なりに意外と深いところまで読めました。
- 投稿日:2022-01-31T12:34:07+09:00
Firestoreをサーバーサイドで運用するTips with Go
はじめに Firestore、便利ですよね。大好きなGoogle Cloudプロダクトのひとつです。 ネイティブモードで利用して、Web or モバイルアプリからmBaaS的に使うのが鉄板だと思います。 とはいえ、サーバから直接Firestoreを触りたくなるケースもちょくちょくあるかと思います。 例えばバッチジョブでデータを洗い替えたりする場合など。 直近、サーバからFirestoreを触る機会がそこそこあったので、その中で得た備忘録的なものをまとめました。 サンプルコードはGoですが、他の言語でも、参考になる部分もあるかと思います。 思いつき次第随時更新いたします?? Tips一覧 1. ローカル環境からFirestoreを利用する方法 開発を進める中で、手元の環境からFirestoreの動作をチェックするユースケースは多発するかと思います。 開発環境のFirestoreに直接繋ぐ方法と、エミュレータに繋ぐ方法を紹介します。 ① 開発環境のFirestoreと接続する 手元の環境から直接開発環境のFirestoreに繋ぎ込む際、サービスアカウントキーをコンソールからダウンロードする方法もありますが、セキュリティ上あまりオススメしません。管理もタイヘンです。 gcloud auth application-default login を実行し、生成されたapplication_default_credentials.jsonをクレデンシャルとして使うのが楽です。 Windowsの場合 %APPDATA%/gcloud/application_default_credentials.json macOS等の場合 $HOME/.config/gcloud/application_default_credentials.json に生成されます。 Google CloudのSDKは、クレデンシャルを下記の順番で読み込みます。 環境変数 GOOGLE_APPLICATION_CREDENTIALS が指定されていたら、そのファイルを service account の credentials として読んで使う gcloud auth application-default login を実行して well_known_file が作成済みであれば、それを authorized user の credentials として読んで使う GAE (Google App Engine) 上で実行されていれば、その built-in service account を使う GCE (Google Compute Engine) 上で実行されていれば、その build-in service account を使う ここまで該当がなければエラーとする ※well_known_fileは前述のapplication_default_credentials.jsonのこと gcloud auth application-default loginで生成されたクレデンシャルは、コード中で明示的に読み込まなくてもSDKが自動で見つけてくれるわけです。 ただし、この方法にも弱点があります。 生成されたapplication_default_credentials.jsonにはproject_idがありません。 ❯ cat ~/.config/gcloud/application_default_credentials.json { "client_id": "...", "client_secret": "...", "quota_project_id": "...", "refresh_token": "...", "type": "authorized_user" } この状態でFirestoreを呼び出してみます。 実行コード app, err := firebase.NewApp(ctx, nil) if err != nil { log.Fatal(err) } client, err := app.Firestore(ctx) if err != nil { log.Fatal(err) } iter := client.Collection("Foo").Documents(ctx) snaps, err := iter.GetAll() if err != nil { log.Fatal(err) } プロジェクトIDの指定がない!と怒られます。 結果 project id is required to access Firestore 解消する方法は主に3つあります。 まず1つ目はコード中でプロジェクトIDを明示的に与える方法です。 conf := &firebase.Config{ProjectID: "my-project"} app, err := firebase.NewApp(ctx, conf) 2つ目(非推奨。クレデンシャルは弄るべきでない)がapplication_default_credentials.jsonを直接編集するなりしてproject_idを追加する方法です。 ❯ vi ~/.config/gcloud/application_default_credentials.json { "client_id": "...", "client_secret": "...", "quota_project_id": "my-project", "project_id": "my-project", "refresh_token": "...", "type": "authorized_user" } 3つ目は、これが最も推奨なのですが、GOOGLE_CLOUD_PROJECT環境変数にプロジェクトIDを設定します。 対象のプロジェクトIDとしてSDKが自動で読み込んでくれます。 export GOOGLE_CLOUD_PROJECT="my-project" 余談ですがgcloud auth application-default loginの裏側を詳しく知りたい方はこちらの記事がオススメです!何度読んだか分からんくらい読んでます。 ② Firestoreエミュレータと接続する 次にFirestoreエミュレータを使う方法です。 Firebaseプロダクトにはローカルで実行できるエミュレータがあり、Firestoreも例に漏れず、サーバアプリケーションから利用できます? エミュレータのデータは全てメモリ上に載っており、立ち上げるたびに初期化されてしまいます(永続化する方法もアリ)。そのためFirestoreとの繋ぎ込みをモックする用途等でのみ使っています。 環境変数 FIRESTORE_EMULATOR_HOST が設定されている場合、Firebase Admin SDK は Cloud Firestore エミュレータに自動的に接続します。 エミュレータをFIRESTORE_EMULATOR_HOSTで指定したホストに立ち上げれば、SDKが環境変数を読み込み接続を試みます。 example export FIRESTORE_EMULATOR_HOST="localhost:8000" 以前作成した、Firestoreエミュレータを利用したWeb APIのサンプルコードでは、Firestoreエミュレータを8000番に立ち上げて、8080番に立ち上げたGoのアプリケーションからFIRESTORE_EMULATOR_HOST越しに接続するようにしました。 下記に、サンプルコードから(Firestoreエミュレータの)Dockerfileとdocker-compose.ymlを転記します。 Firestoreのエミュレータも、Goのアプリケーションも、docker compose upで立ち上がる形です。 GoのアプリケーションにenvironmentとしてPROJECT_IDとFIRESTORE_EMULATOR_HOSTを渡しています。 FirestoreエミュレータのプロジェクトIDは何でもOKです。 適当な任意の値(hogeでもfooでも)を入れておけば動きます。 実際の Firebase プロジェクト ID を使用する必要はありません。Cloud Firestore エミュレータは、有効な形式である限り、どのようなプロジェクト ID でも受け入れます。 Dockerfile.firestore FROM node:alpine RUN apk add openjdk11 RUN npm install -g firebase-tools WORKDIR /app CMD [ "firebase", "emulators:start", "--only", "firestore" ] docker-compose.yml version: "3.8" services: firestore: build: context: . dockerfile: Dockerfile.firestore ports: - 8000:8000 volumes: - .:/app app: build: context: . target: dev dockerfile: Dockerfile ports: - 8080:8080 environment: PROJECT_ID: foo FIRESTORE_EMULATOR_HOST: firestore:8000 volumes: - .:/go/src/app 実行例 // ユーザを作成します curl -X POST -d '{"id": "100", "name": "taro", "age": 10}' localhost:8080/users // エミュレータに作成したユーザが確認できます curl localhost:8080/users/100 2. 指数バックオフを組み込む方法 Firestoreに限らず、サーバアプリケーションではエラーが生じた際のリトライが重要です。 SDKが透過的にリトライしてくれるエラーもありますが、例えばAPI上限にぶち当たった際のResourceExhausted等は自前でリトライを組み込む必要があります。リトライの戦略としては指数バックオフが一般的です。 指数バックオフは、失敗したリクエストをクライアントが再試行する際、失敗するごとに次の再試行までの待ち時間を増やしていく処理です。これは、ネットワークアプリケーションに使われる標準的なエラー処理方法です。 指数バックオフの実装 Cloud TasksやCloud Monitoring等のクライアントライブラリの場合、指数バックオフに関するオプションを引数で受け渡せます。 example // https://pkg.go.dev/cloud.google.com/go/cloudtasks/apiv2beta3#Client.ListTasks ListTasks(ctx, req, gax.WithRetry(DefaultRetryOption)) 一方、Firestoreのクライアントライブラリには指数バックオフのオプションがありません。 Firestoreへのリクエストをラップして任意のエラー時にリトライするような処理を実装する必要があります。 指数バックオフのロジックはこだわらなければそこまで複雑ではないので、頑張って自前で実装するか、先人が実装してくれたパッケージを利用すると良いかと思います。 Goの場合、github.com/cenkalti/backoff等があります。 自前実装する場合であれ、指数バックオフを組み込む方法はググればいくらでも出てくるのでサンプルコードは割愛します。実装の基本方針は、 Firestoreから返ってきたエラーをgRPC status codeに変換する(code := status.Code(err)) リトライ対象のコードだった場合リトライ(if code == codes.ResourceExhausted) といった感じです。 Firestoreにおいてリトライすべきエラーは公式ドキュメントに載っているので、実装の際、参考になるかと思います。 3. 大量データを高速で書き込む方法 Firestoreはシリアルにデータを書き込むよりBatchで書き込んだ方が早いですが、更に速度を目指す場合、並列で書き込む必要があります。 データを一括入力するには、並列化された個別の書き込みでサーバー クライアント ライブラリを使用します。バッチ書き込みは、シリアル化された書き込みより優れたパフォーマンスを発揮しますが、並列書き込みほど優れてはいません。一括データ オペレーションには、モバイル / ウェブ SDK ではなく、サーバー クライアント ライブラリを使用する必要があります。 下記の記事にあるように、API制限に気を付けつつ、指数バックオフで適切にエラーをリトライしながら並列で書き込むのが重要です。 Goであれば、goroutineの並列数をチャネル等で制御しつつ、諸々気を付けながら大量投入を行うことになるかと思います。 4. 接続周りのオプションを設定する方法 gRPCの接続オプションはfirebase.NewApp()時に引数で渡せます。 自分の場合、下記の記事を参考に、コネクションを同期的に張るgrpc.WithBlock()を渡したりしています。 Introduce Cloud Run その他、KeepAliveの設定を渡すこと等もできます。必要に応じて設定すると良さそうです。 5. パフォーマンスを測定する方法 FirestoreのパフォーマンスはKey Visualizerで分析できます。 手前味噌ですが、Key Visualizerに関する記事を以前書きましたので参考になるかもです。 6. 監査ログを取得する方法 Firestoreに誰が(Googleアカウント、サービスアカウント、エンドユーザ...)CRUDを行ったかは、監査ログを追うことで確認できます。 こちらも手前味噌ですが、以前の記事です。よろしければ! Firestoreの監査ログがパワーアップした 最後に Firestoreはマネージドサービスですが、しっかり運用しようと思うと気を付けることが色々あります。 Key Visualizerがリリースされたり監査ログが充実する等、運用面の機能も充実してきたので、しっかりキャッチアップしながらFirestoreと付き合っていこうと思います!
- 投稿日:2022-01-31T12:34:07+09:00
Firestoreをサーバーサイドで運用するTips
はじめに Firestore、便利ですよね。大好きなGoogle Cloudプロダクトのひとつです。 ネイティブモードで利用して、Web or モバイルアプリからmBaaS的に使うのが鉄板だと思います。 とはいえ、サーバから直接Firestoreを触りたくなるケースもちょくちょくあるかと思います。 例えばバッチジョブでデータを洗い替えたりする場合など。 直近、サーバからFirestoreを触る機会がそこそこあったので、その中で得た備忘録的なものをまとめました。 サンプルコードはGoですが、他の言語でも、参考になる部分もあるかと思います。 思いつき次第随時更新いたします?? Tips一覧 1. ローカル環境からFirestoreを利用する方法 開発を進める中で、手元の環境からFirestoreの動作をチェックするユースケースは多発するかと思います。 開発環境のFirestoreに直接繋ぐ方法と、エミュレータに繋ぐ方法を紹介します。 ① 開発環境のFirestoreと接続する 手元の環境から直接開発環境のFirestoreに繋ぎ込む際、サービスアカウントキーをコンソールからダウンロードする方法もありますが、セキュリティ上あまりオススメしません。管理もタイヘンです。 gcloud auth application-default login を実行し、生成されたapplication_default_credentials.jsonをクレデンシャルとして使うのが楽です。 Windowsの場合 %APPDATA%/gcloud/application_default_credentials.json macOS等の場合 $HOME/.config/gcloud/application_default_credentials.json に生成されます。 Google CloudのSDKは、クレデンシャルを下記の順番で読み込みます。 環境変数 GOOGLE_APPLICATION_CREDENTIALS が指定されていたら、そのファイルを service account の credentials として読んで使う gcloud auth application-default login を実行して well_known_file が作成済みであれば、それを authorized user の credentials として読んで使う GAE (Google App Engine) 上で実行されていれば、その built-in service account を使う GCE (Google Compute Engine) 上で実行されていれば、その build-in service account を使う ここまで該当がなければエラーとする ※well_known_fileは前述のapplication_default_credentials.jsonのこと gcloud auth application-default loginで生成されたクレデンシャルは、コード中で明示的に読み込まなくてもSDKが自動で見つけてくれるわけです。 ただし、この方法にも弱点があります。 生成されたapplication_default_credentials.jsonにはproject_idがありません。 ❯ cat ~/.config/gcloud/application_default_credentials.json { "client_id": "...", "client_secret": "...", "quota_project_id": "...", "refresh_token": "...", "type": "authorized_user" } この状態でFirestoreを呼び出してみます。 実行コード app, err := firebase.NewApp(ctx, nil) if err != nil { log.Fatal(err) } client, err := app.Firestore(ctx) if err != nil { log.Fatal(err) } iter := client.Collection("Foo").Documents(ctx) snaps, err := iter.GetAll() if err != nil { log.Fatal(err) } プロジェクトIDの指定がない!と怒られます。 結果 project id is required to access Firestore 解消する方法は主に3つあります。 まず1つ目はコード中でプロジェクトIDを明示的に与える方法です。 conf := &firebase.Config{ProjectID: "my-project"} app, err := firebase.NewApp(ctx, conf) 2つ目(非推奨。クレデンシャルは弄るべきでない)がapplication_default_credentials.jsonを直接編集するなりしてproject_idを追加する方法です。 ❯ vi ~/.config/gcloud/application_default_credentials.json { "client_id": "...", "client_secret": "...", "quota_project_id": "my-project", "project_id": "my-project", "refresh_token": "...", "type": "authorized_user" } 3つ目は、これが最も推奨なのですが、GOOGLE_CLOUD_PROJECT環境変数にプロジェクトIDを設定します。 対象のプロジェクトIDとしてSDKが自動で読み込んでくれます。 export GOOGLE_CLOUD_PROJECT="my-project" 余談ですがgcloud auth application-default loginの裏側を詳しく知りたい方はこちらの記事がオススメです!何度読んだか分からんくらい読んでます。 ② Firestoreエミュレータと接続する 次にFirestoreエミュレータを使う方法です。 Firebaseプロダクトにはローカルで実行できるエミュレータがあり、Firestoreも例に漏れず、サーバアプリケーションから利用できます? エミュレータのデータは全てメモリ上に載っており、立ち上げるたびに初期化されてしまいます(永続化する方法もアリ)。そのためFirestoreとの繋ぎ込みをモックする用途等でのみ使っています。 環境変数 FIRESTORE_EMULATOR_HOST が設定されている場合、Firebase Admin SDK は Cloud Firestore エミュレータに自動的に接続します。 エミュレータをFIRESTORE_EMULATOR_HOSTで指定したホストに立ち上げれば、SDKが環境変数を読み込み接続を試みます。 example export FIRESTORE_EMULATOR_HOST="localhost:8000" 以前作成した、Firestoreエミュレータを利用したWeb APIのサンプルコードでは、Firestoreエミュレータを8000番に立ち上げて、8080番に立ち上げたGoのアプリケーションからFIRESTORE_EMULATOR_HOST越しに接続するようにしました。 下記に、サンプルコードから(Firestoreエミュレータの)Dockerfileとdocker-compose.ymlを転記します。 Firestoreのエミュレータも、Goのアプリケーションも、docker compose upで立ち上がる形です。 GoのアプリケーションにenvironmentとしてPROJECT_IDとFIRESTORE_EMULATOR_HOSTを渡しています。 FirestoreエミュレータのプロジェクトIDは何でもOKです。 適当な任意の値(hogeでもfooでも)を入れておけば動きます。 実際の Firebase プロジェクト ID を使用する必要はありません。Cloud Firestore エミュレータは、有効な形式である限り、どのようなプロジェクト ID でも受け入れます。 Dockerfile.firestore FROM node:alpine RUN apk add openjdk11 RUN npm install -g firebase-tools WORKDIR /app CMD [ "firebase", "emulators:start", "--only", "firestore" ] docker-compose.yml version: "3.8" services: firestore: build: context: . dockerfile: Dockerfile.firestore ports: - 8000:8000 volumes: - .:/app app: build: context: . target: dev dockerfile: Dockerfile ports: - 8080:8080 environment: PROJECT_ID: foo FIRESTORE_EMULATOR_HOST: firestore:8000 volumes: - .:/go/src/app 実行例 // ユーザを作成します curl -X POST -d '{"id": "100", "name": "taro", "age": 10}' localhost:8080/users // エミュレータに作成したユーザが確認できます curl localhost:8080/users/100 2. 指数バックオフを組み込む方法 Firestoreに限らず、サーバアプリケーションではエラーが生じた際のリトライが重要です。 SDKが透過的にリトライしてくれるエラーもありますが、例えばAPI上限にぶち当たった際のResourceExhausted等は自前でリトライを組み込む必要があります。リトライの戦略としては指数バックオフが一般的です。 指数バックオフは、失敗したリクエストをクライアントが再試行する際、失敗するごとに次の再試行までの待ち時間を増やしていく処理です。これは、ネットワークアプリケーションに使われる標準的なエラー処理方法です。 指数バックオフの実装 Cloud TasksやCloud Monitoring等のクライアントライブラリの場合、指数バックオフに関するオプションを引数で受け渡せます。 example // https://pkg.go.dev/cloud.google.com/go/cloudtasks/apiv2beta3#Client.ListTasks ListTasks(ctx, req, gax.WithRetry(DefaultRetryOption)) 一方、Firestoreのクライアントライブラリには指数バックオフのオプションがありません。 Firestoreへのリクエストをラップして任意のエラー時にリトライするような処理を実装する必要があります。 指数バックオフのロジックはこだわらなければそこまで複雑ではないので、頑張って自前で実装するか、先人が実装してくれたパッケージを利用すると良いかと思います。 Goの場合、github.com/cenkalti/backoff等があります。 自前実装する場合であれ、指数バックオフを組み込む方法はググればいくらでも出てくるのでサンプルコードは割愛します。実装の基本方針は、 Firestoreから返ってきたエラーをgRPC status codeに変換する(code := status.Code(err)) リトライ対象のコードだった場合リトライ(if code == codes.ResourceExhausted) といった感じです。 Firestoreにおいてリトライすべきエラーは公式ドキュメントに載っているので、実装の際、参考になるかと思います。 3. 大量データを高速で書き込む方法 Firestoreはシリアルにデータを書き込むよりBatchで書き込んだ方が早いですが、更に速度を目指す場合、並列で書き込む必要があります。 データを一括入力するには、並列化された個別の書き込みでサーバー クライアント ライブラリを使用します。バッチ書き込みは、シリアル化された書き込みより優れたパフォーマンスを発揮しますが、並列書き込みほど優れてはいません。一括データ オペレーションには、モバイル / ウェブ SDK ではなく、サーバー クライアント ライブラリを使用する必要があります。 下記の記事にあるように、API制限に気を付けつつ、指数バックオフで適切にエラーをリトライしながら並列で書き込むのが重要です。 Goであれば、goroutineの並列数をチャネル等で制御しつつ、諸々気を付けながら大量投入を行うことになるかと思います。 4. 接続周りのオプションを設定する方法 gRPCの接続オプションはfirebase.NewApp()時に引数で渡せます。 自分の場合、下記の記事を参考に、コネクションを同期的に張るgrpc.WithBlock()を渡したりしています。 Introduce Cloud Run その他、KeepAliveの設定を渡すこと等もできます。必要に応じて設定すると良さそうです。 5. パフォーマンスを測定する方法 FirestoreのパフォーマンスはKey Visualizerで分析できます。 手前味噌ですが、Key Visualizerに関する記事を以前書きましたので参考になるかもです。 6. 監査ログを取得する方法 Firestoreに誰が(Googleアカウント、サービスアカウント、エンドユーザ...)CRUDを行ったかは、監査ログを追うことで確認できます。 こちらも手前味噌ですが、以前の記事です。よろしければ! Firestoreの監査ログがパワーアップした 最後に Firestoreはマネージドサービスですが、しっかり運用しようと思うと気を付けることが色々あります。 Key Visualizerがリリースされたり監査ログが充実する等、運用面の機能も充実してきたので、しっかりキャッチアップしながらFirestoreと付き合っていこうと思います!