GCP初心者がGAE/Go入門する時に学んだことまとめ
社会になったのですが、とあるWebアプリケーションを4月の間作っていた際に入門しました。
その際にそこそこ多くの点でハマったので共有しておきます。
要件
- データ保存して整形して返すよくある Webアプリ
- GET超過多
- 10,000 req / sec
- データ量はそこそこ多く
最初の認識
- Google App Engineが安くて早いらしい
- めっちゃスケールするらしい
- 数msでするという噂を聞いてええやんとなった
学んだこと
はじめに: GAEには1世代と2世代が存在する
ここで気づくのが遅れて色々と記事を読むのに苦労していた気がします。 Standard環境とFlexible環境が存在していることは様々なドキュメントに書いてあり、ほぼ明記されています。 前者はGoogleが提供しているランタイム上で動作させるもので、後者はDockerなどカスタムランタイムも動作できるもののようです。 Standard環境のほうがスケールも高速なのでGAEを使いたい人は大体こっちを選択するでしょう。
ただここで気にする必要があるのが世代間の違いです。 GAE/Go に限って言うなればGo 1.9とGo 1.11のランタイムを選択出来ますが、ここは単なるバージョンの違いではなく内部の扱いがかなり別です。
GCPUGさんのnouhauリポジトリには大変お世話になりました。 こちらにこの世代間でどのような違いがあるのかまとめられています。
我々も実装している間に古い資料が多く存在しているなということは気づいていたのですが、最も気づいたのがtimakinさんのこちらの記事を読んでいたときです。
「共有のmemcacheが無料で使えるなら、とりあえずそこに載せてみればいいじゃん!」と思って調べて実装していたのですが、なぜか手元の dev_appserver.py
ではうまく動作せず頭を捻っていたところ上記のnouhauリポジトリにある移行のドキュメントを発見し全てが腑に落ちた感覚でした。
マネージドなRedisサービスであるCloud MemoryStoreも上記のリポジトリに記載があるとおりアクセスできず(RedisインスタンスにはローカルIPのみ払い出された & GAEをVPC内に入れるのが面倒だった)、その他様々な制約があり2日だけGCEインスタンスを立ち上げてRedis-serverをインストールするなどしていました。
…と、ここまで書いていたのですが先ほどapstndbさんのこちらのQiitaを見つけました。
というわけでGo1.11は第1世代となったようです。ややこしいですね……。
オートスケールの設定
こちらを大きく参考にさせて頂きました。
我々はGoアプリケーションだったのでかなり最初は抑えめに設定していましたが綺麗に動作してくれました。 参考までに貼っておきます。(100は既に数字を増やした後です)
automatic_scaling: target_cpu_utilization: 0.75 target_throughput_utilization: 0.75 min_instances: 0 max_instances: 100 # 本番の時は増やす min_idle_instances: 0 max_idle_instances: 10 min_pending_latency: automatic max_pending_latency: automatic max_concurrent_requests: 80
最後のベンチマークの際に、max_instances
を100に設定していたのですがStackDriverで見てみるとインスタンス数が600台以上になっていて焦ったりもしました。
max_idel_instances
は10に設定していたのであり得ないとは思いますが、謎の挙動でしたね…。
実際に600台の起動課金は行われなかったようなので、たまたま起動していただけだったのでしょうか…?
ところで、 max_concurrent_requests
の最大値が80というのは少しつらかったですね…。
CloudSQLが意外と遅い
max_concurrent_requests
の最大値が80というのが少しつらかった理由がここにあります。
ベンチマークとして大量のGETリクエストを送信しながらStackDriver MonitoringでGAEやCloudSQLの負荷状況を見ていたのですが、それほど負荷が上がらない割に捌けるリクエスト数が増えないなあと思っていたのですが、調べてみるとCloudSQL (というよりはMySQL) とGAE/Go側の接続数が上限に達していました。
CloudSQLは一定以上のスペックだとmax_connectionは4000が上限です。
これが少しつらくて、1リクエストごとに1SQL発行するユースケースだと非常に遅かったです。 恐らく第1世代だと共有や専有のmemcacheを使ったり、第2世代でもCloud Datastoreなどを使うのが良いのではないかと思いましたが、リクエストが多い状況では高額になりがちだったので利用は見送りました。。
(しっかり検証できませんでしたが、SELECTの返答が感覚値よりも遅かった気がします。この辺は場合によって使うプロダクトを適切に合わせようという気持ちが大事なのだと思います)
CloudSQLの接続方法
GAEからCloudSQLに対してアクセスする方法を検索すると様々な手法が出てきます。
色々と試したのですが、最終的に最初のUnix Domain Socket経由でアクセスする手法でうまく接続が出来ました。MySQLのDSNだと以下のようになります。
username:password@unix(/cloudsql/${project_id}:${db_region}:${db_instance_name})/${db_name}
この辺のことは以下のドキュメントに記載があります。
これが英語ドキュメントを参照しないと分かりにくく、日本語ドキュメントとしては謎の /cloudsql/~~~
のような文字列だけを見てソケットファイルがあることを気づく必要があり少しつらかったですね。
また、手元で開発する場合は自分のIPアドレスをCloudSQLのファイアウォールから接続を許可した上でTCP接続するDSNを書いて接続していたりしました。
GAEには秘匿する環境変数を設定できない
私が触りはじめる際にはHerokuのようなPaaSを想像していたので、Webコンソールから環境変数を気軽に変更することが出来るのかなと考えていました。
秘匿したいパスワードなどはgitリポジトリに入れたくない、ただ環境変数を設定するためにはGAEの設定ファイルである app.yaml
に書く必要がある…という状況になりました。
さて困ったとなったのですが、この時もGCPUGさんのnouhauリポジトリにお世話になりました。
上記のファイルを見てもらえれば分かるのですが、Cloud KMSを使って暗号化したパスワードなどをCloud Build (GCPのCIサービス)で復号する流れになるようです。クラウドですね……。
Cloud KMSの対応に少し戸惑ったのですが、以下のようにするのが正しそうでした。
- 1 KeyRing : 1アプリケーション
- 1 Key : 1環境変数
GoアプリケーションでのMySQLの取り扱い
GAEに限った話ではないですが…。
methaneさんの記事を多く参照させて頂きました。後半は methane golang mysql
などで検索したりしていました。
- Go の MySQL ドライバの効率の良い使い方 - methaneのブログ
- GoのMySQLドライバはコネクションプールを適切にやってくれる
- 最初
row.Close()
などの扱いが悪くてコネクションプールが上手く動作していなかったのですが、その辺を直した後は感覚道理に動いてくれました
- 最初
db.Query
とdb.Exec
の使い分け
- GoのMySQLドライバはコネクションプールを適切にやってくれる
- DSAS開発者の部屋:Re: Configuring sql.DB for Better Performance / コネクションプールのチューニング - Qiita
まとめ
ひとまずハマった部分について、思い出しただけ書き出してみました。
色々とハマるところはあったものの、GAEに乗ることでかなり楽することが出来ました。
app.yaml
にHTTPルーティングを書くだけで横にスケールし、エンドポイントごとのレスポンスタイムなどの計測 (StackDriver Monitoring) 、アプリケーションなど複数のログを透過的に管理できるインターフェース (StackDriver Logging)など、インフラ側は高速 / 安価に構築することができたのが非常に体験が良かったです。
記事中に書いたとおりsinmetalさんを始めとしたコミュニティの皆さまが残した記事によって多くのハマリポイントから抜け出すことが出来たので、恩返しとして書いてみました。 またどなたかの助けになれば幸いです。