Go + Google Cloud Pub/Sub で GAE SE (gen1) から Publish するときの注意点

**Go3 Advent Calendar 2018 6日目**を担当する avvmoto です。よろしくお願いします。


この記事では、Go (gen 1) on Google App Engine SE (GAE SE) から Google Cloud Pub/Sub で Publish するときの注意点を解説します。
なお、go 1.11 以降の gen2 はこの記事の対象外です。

# はじめに

GAE SE は Web Application の作成に適した PaaS です。GAE から分析用途等で、 Pub/Sub へ message を Publish したいケースは珍しくないです。
これを実現するときの注意点をご紹介します。

# cloud.google.com/go/pubsub の課題

[Cloud Pub/Sub の公式ドキュメント](https://cloud.google.com/pubsub/docs/reference/libraries) をみると、冒頭で紹介されているクライアントライブラリー `cloud.google.com/go/pubsub` を使いたくなります。ただしこれは、GAE SE から使うには以下のような課題があります。実際、このドキュメントの中程には、GAE SE からだと別のライブラリを使うよう書いてあります。

> If you're running on Google App Engine standard, we recommend using the [older Google APIs Client Libraries](https://github.com/googleapis/google-api-go-client/blob/master/README.md). 

## 課題1: latancy が大きい

GAE SE の handler で、1request の中で client を作成し、 Publish する例を考えてみましょう。実装としては以下のようになります。



```go

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)
	client, err := pubsub.NewClient(ctx, appengine.AppID(ctx))
	if err != nil {
		return // todo
	}
	defer client.Close()

	topic := client.Topic("eventlog-view")
	defer topic.Stop()
	result := topic.Publish(ctx, &pubsub.Message{
		Data: []byte("hello world"),
	})

	id, err := result.Get(ctx)
	if err != nil {
		return // todo
	}
	log.Infof(ctx, "Published a message with a message ID: %s\n", id)
}
```

この実装だと latency が大きくなってしまいます。 message のサイズにもよると思いますが、筆者が実験したところ 100 ms から 300 ms かかりました。
これは Web app 中に非同期ではなく行う処理としては遅いです。

ただ、godoc によるとそもそもこのような使い方は想定されていないようです。


> Clients should be reused rather than being created as needed. A Client may be shared by multiple goroutines. [^1]

godoc によると、このように client は再利用し、複数のgoroutine から共有される使い方を想定されています。


> If the client is available for the lifetime of the program, then Close need not be called at exit. [^3]

またこのような記述もあって、client はプログラムのライフサイクルを通じてずっと起動することも想定されているようです。



1request の中で client を作成し Publish し、client を close といったユースケースでは作られていないようで、むしろずっとclient を起動しっぱなしにする想定のようです。
実際、上記コードを動かしてみると、Stackdriver Trace でみると一回のPublishで複数回通信が走っています。一回Publishしてcloseという用途には最適化されていないような印象を受けます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/25630/08e93fd9-8b68-33c1-8324-269bc7a71e26.png)



[^1]: https://godoc.org/cloud.google.com/go/pubsub#Client



[^3]: https://godoc.org/cloud.google.com/go/pubsub#Client.Close

## 課題2: Socket API の quota を消費する

`cloud.google.com/go/pubsub` は Socket API 経由で RPC を叩いているようで [^2]、Socket API 関連の quota `Sockets created per day` を消費します。
これはコンソール https://console.cloud.google.com/iam-admin/quotas から確認できます。 Socket API の quota は、GCP の他の quota と比較して少なめに設定されており、注意していないと quota limit に引っかかってしまいます。

依頼フローを経れば quota の上限を引き上げられますが、その後も quota の limit に引っかからないか、継続的にチェックする必要が出てきます。




[^2]: > Package pubsub provides an easy way to publish and receive Google Cloud Pub/Sub messages, hiding the details of the underlying server RPCs. [^4]


[^4]: https://godoc.org/cloud.google.com/go/pubsub


# 解決策

## 1. Socket API の quota を引き上げ、 cloud.google.com/go/pubsub を使う

quota を十分引き上げることで、とりあえず quota の上限に引っかかり Publish できなくなる問題は防ぐことができます。その場合は引き続き latancy が大きい問題が残ります。用途によっては許容できるかもしれませんが、許容できない場合は、 Task Queue を用いて非同期に処理しましょう。

![second-hanei2.png](https://qiita-image-store.s3.amazonaws.com/0/25630/3e4b3c29-367f-6887-dc25-9cc8ba74d44d.png)




Push 型の TaskQueue で一件ずつ処理すると、前述のとおり、このライブラリだと効率的に扱えないです。 Pull 型の Taskqueue として、Cron である程度の件数を取得し、まとめて Publish する実装が効率的でよいです。効率的に処理することで、インスタンス台数が減り、課金額をへらすことができます。



## 2. GAE SE gen2 に移行し、cloud.google.com/go/pubsub を使う

GAE gen2 となれば、Socket API の quota を気にせず良くなます。 go1.11 で cloud.google.com/go/pubsub を使いましょう。
ただし前述の latency の問題は引き続き考慮する必要があります。

## 3. google.golang.org/api/pubsub/v1 に乗り換える
[Cloud Pub/Sub の公式ドキュメント](https://cloud.google.com/pubsub/docs/reference/libraries) にあるとおり、古い方のライブラリ、[google.golang.org/api/pubsub/v1](https://godoc.org/google.golang.org/api/pubsub/v1) を利用する方法です。こちらは内部的にREST APIを叩いているようで、Socket API のquota を消費せず、URLFetch の quota を消費します。

このライブラリの godoc には `DEPRECATED` と記載ある点は今後も利用を続ける点では一定リスクですが、公式ドキュメントで利用が奨励されている点は考慮にいれて良いと思います。


筆者が実験したところ、このライブラリだと概ね 100 ms 以下でpublish でき、特に非同期にすることなく利用することにしました。


このライブラリは、grpc から自動生成されたようで godoc も構成も分かりにくいですが、 Publish するものの実装するときにはこちらのサンプルが役に立ちました。
https://gist.github.com/broady/c79a65cb49d7b6a56448b3345a23b3d6






# まとめ

Go on GAE SE (gen1) からカジュアルに `cloud.google.com/go/pubsub` を利用して Publish していると、レスポンスの悪化や、 Socket API の quota limit に気づかず障害、などの事態を引き起こす可能性があります。
事前にquota の limit を適切にあげてから利用するか、 REST API を叩く古いライブラリ [google.golang.org/api/pubsub/v1](https://godoc.org/google.golang.org/api/pubsub/v1) を利用しましょう。ただしこちらを利用するにしても、 URL Fetch の quota には注意が必要です( Socket API の quota よりシビアではないとはいえ)。また、公式ドキュメントから案内があるとはいえ、`DEPRECATED` となっている点も注意が必要です。

なおGAE SE gen2 になると状況が変わるので、別途最適な状況を考える必要があります。


# 謝辞

Pub/Sub 利用時の注意点を教えてくださった GCPUG の皆様、ありがとうございました。

# 参考資料


* https://godoc.org/cloud.google.com/go/pubsub
* https://godoc.org/google.golang.org/api/pubsub/v1
* https://cloud.google.com/pubsub/docs/reference/libraries
* https://github.com/googleapis/google-cloud-go/issues/702
* https://gist.github.com/broady/c79a65cb49d7b6a56448b3345a23b3d6


# 脚注