GAE/GO(1.9) Standard から Cloud PubSubに秒間15000件パブリッシュしようとして色々ハマった話

この記事は[Google Cloud Platform その２ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gcp2)の7日目の投稿です。

# tl;dr
- GAE/GoのStandard環境では、パブリッシュの実装は [`cloud.google.com/go/pubsub`](https://godoc.org/cloud.google.com/go/pubsub) のAPIは使わず、**[`google.golang.org/api/pubsub/v1`](https://godoc.org/google.golang.org/api/pubsub/v1)のAPIで実装する**
  - [`google.golang.org/api/pubsub/v1`](https://godoc.org/google.golang.org/api/pubsub/v1)のAPIはDEPRECATEDだけど気にしない！
  - 参考になる実装サンプル: [broady/pubsub.go](https://gist.github.com/broady/c79a65cb49d7b6a56448b3345a23b3d6) 
- 秒間15000件のパブリッシュとなると**GAEの[URL取得のquota](https://cloud.google.com/appengine/quotas#UrlFetch)にひっかかる**ので、複数メッセージをまとめてパブリッシュするようにする

# 前提
![スクリーンショット 2018-12-04 17.03.53.png](https://qiita-image-store.s3.amazonaws.com/0/20307/2d552694-7340-e9a8-2c20-4b6143d41d47.png)

上の図のような構成のストリーミングのログ基盤を作る際に、GAE/GOからCloud PubSubへのパブリッシュでいろいろとハマったので共有します。まず、環境はGAE/GO(1.9) Standardですので、GO1.11だったり、Flexibleの場合の参考にはならないと思います。また、秒間15000件くらいパブリッシュしようとしたときにハマった話なので、少量のパブリッシュの場合は参考にならないかもしれません。ちなみに、Cloud PubSubのSubscriberはPULL型です。

## GAE/GO Standardから現行APIでのパブリッシュ処理は遅い
GAE/GOからCloud PubSubへの実装は公式ドキュメントに丁寧に載ってます→[【これ】](https://cloud.google.com/pubsub/docs/reference/libraries#using_the_client_library)。
私は素直にそのまま実装してしまいましたが、この実装をGAE/GO Standardで動かしてみると、**めちゃくちゃパフォーマンスが悪くなります**。例えば、秒間2000件くらいのパブリッシュをしようとするとレスポンスが全然返らずタイムアウトが頻発するという状態になります。トレースしてみると、1件のパブリッシュに数秒かかったりしててもう全然駄目だめでした。

## DEPRECATEDのv1のAPIをおすすめされる
困り果ててサポートに問い合わせたところ、Standard環境からパブリッシュの実装をする場合はこの公式ドキュメントで使っている[`cloud.google.com/go/pubsub`](https://godoc.org/cloud.google.com/go/pubsub)は非推奨であると告げられました。

ドキュメントもよく見ると、Standard環境では古いAPIを使いましょうと書いてある！！（↓）

![スクリーンショット 2018-12-04 16.24.33.png](https://qiita-image-store.s3.amazonaws.com/0/20307/b6a4c520-f71d-e0e1-9b72-45503781e4d7.png)


で、代わりに[`google.golang.org/api/pubsub/v1`](https://godoc.org/google.golang.org/api/pubsub/v1)のAPI(以後v1)を使って実装することをオススメされました。ドキュメント見てみるとこのAPIはすでにDEPRECATEDとなっていますが、今はこれを使うしかないようです。というのも、現行APIのパブリッシュ処理では、PubSubにPullのサブスクライバーがついている場合、GAEからPubSubへのコネクションをプロセスをまたいで持ち続けようとしてしまうようです。これはStandardではサポートしてないので、Pushサブスクライバーに変えるか、v1のAPIを使うことがissueでは提案されています。詳しくはこのissue([pubsub: Unable to publish message from GAE Standard](https://github.com/GoogleCloudPlatform/google-cloud-go/issues/702))。

v1のAPIを使ったパブリッシュのAPIはこちらのサンプルがわかりやすいです -> [サンプル](https://gist.github.com/broady/c79a65cb49d7b6a56448b3345a23b3d6) 
これを使って実装するとパフォーマンスは改善し、タイムアウトは出なくなりました。

## 秒間12000件の壁
ようやくちゃんとさばけるようになり、負荷試験をしていくと秒間12,000件あたりからまたエラーが頻発するようになりました。調べてみると、どうやら[GAEのURLfetchのquota](https://cloud.google.com/appengine/quotas#UrlFetch)の制限にひっかかっていることがわかりました。v1のAPIのパブリッシュでは内部的にURLfetchをしているため、それが原因となって発生していたようです。

仕方ないので、GAEのメモリ上にリクエストを貯めて、貯まったリクエスト件数が閾値をこえたらまとめてパブリッシュするような実装に変えたところ問題なくさばけるようになりました。この実装の注意点としては、閾値に届かない程度の件数がGAEのメモリ上にたまった状態でinstanceが落ちてしまいログロストしてしまう危険があることです。そうならないように、件数の閾値とは別に、定期的に貯まったログをパブリッシュする仕組みを実装する必要があります。

また、GAEのメモリに貯めずに、TaskQueueを使ってキューイングしたほうががいいのではないかというご意見を何度かいただきました。しかし、秒間15000件のパブリッシュという今回の要件を満たすことを考えると、私はこの選択肢はないかなと思っています。というのも、実は過去にこのシステムではTaskqueueをキューイングに使っていましたが、リクエストが増えるとPULLが間に合わなくなる問題に悩ませれた経験があるからです。秒間3000件くらいからTaskqueueの1インスタンスのPULLが追いつかなくなり、その後複数のTaskqueueを用意したがこちらも秒間5000件くらいからPULLが追いつかなくなったりしました。また、Memcacheも考えましたが、過去にMemcacheの書き込み/読み込みエラーが頻繁に発生して苦労した経験もあったのとできるだけシンプルな実装にしたかったので、GAEのメモリ上に貯めることにしました。

# 最後に
GAE・GOからPubSubにパブリッシュする際の注意点をざっと書いてきました。
~~最近は2nd genも使えるようになり、これを採用すれば[fetchURLなどの制限はなくなる](https://cloud.google.com/appengine/docs/standard/appengine-generation?hl=en)ようですので、よりシンプルな実装が可能になるはずなので、近いうちに2nd genに上げたい気持ちです。~~

GO1.11へのバージョンアップにより、fetchURLの回数制限が撤廃されるので移行したいという意図でした。GAEの2nd genの定義が当時から変化してきているので追記変更しました(2019/06/20)