GAE 2ndへの移行準備

こんにちは、バックエンドグループの冨永と申します。

弊社のタクシーアプリ『GO』にはGAE Standard 1st Go1.11を使用しているAPIサーバーがあり、このサーバーの2nd Go1.12以上への移行を計画しています。

他サイトでもGAE 2ndへの移行記事が増えてきましたが、実際に取り組んでみると新たに得られた知見がありましたので共有したいと思います。

移行計画

1stのGo1.11は1st用のappengineパッケージと2nd対応のパッケージが併用できるため、少しずつ置き換えていくことができます。タイトルが 移行準備 となっているのはこれが理由です。

現在は一部のAPI endpointだけ移行しており、残りのendpointを動作確認しつつ移行していく予定です。

urlfetchの置き換え

net/httpのClientに置き換えるだけですが、デフォルトのtimeoutが設定されていないためurlfetchのデフォルトtimeoutに依存しているコードがある場合はtimeoutを設定してください。

CloudTasks

基本的には公式のサンプルコードの通りでいいのですが、クライアントのキャッシュなどいくつか必要な実装を追加しました。

GAE versionの指定

google.golang.org/appengine/taskqueueではtaskのhostを設定することで、タスクを処理するサーバーversionを指定することができます。 Blue-Green Deploymentでは古いサーバーversionで作成したタスクの処理は新しいサーバーではなく古いサーバーで実行して欲しいため、以下のようにtaskspb.AppEngineRouting を作成します。

appEngineRouting := &taskspb.AppEngineRouting{
    Service: os.Getenv("GAE_SERVICE"),
    Version: os.Getenv("GAE_VERSION"),
}
req := &taskspb.CreateTaskRequest{
    Parent: queuePath,
    Task: &taskspb.Task{
        MessageType: &taskspb.Task_AppEngineHttpRequest{
            AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{
                Headers: map[string]string{
                    "Content-Type": "application/json",
                },
                AppEngineRouting: appEngineRouting,
                HttpMethod:       taskspb.HttpMethod_POST,
                RelativeUri:      "/task_handler",
            },
        },
    },
}

レイテンシの悪化への対応

GAE 1stではGAEの内部APIを使用しているためタスク作成処理のレスポンスは数msで受け取れていましたが、GAE 2ndでは外部のCloudTasksのAPIを使用するため50ms~150msほどかかるようになりました。regionはasia-northeast1で作成しており、GCP Supportから想定の範囲内のレイテンシであるとの回答を頂いています。 弊社ではバッチ処理で高頻度にtaskを作成している実装があり、バッチ完了までの時間が大幅に伸びてしまうため、その箇所についてはtaskの数自体を減らす工夫やgoroutineの使用を予定しています。

Datastore

Datastoreのclientライブラリはgoonからboomへ移行中です。冒頭で述べました通り、少しずつ移行するために併用している状態です。 goonのデフォルト機能にクエリ結果の透過的なキャッシュ機能がありますが、弊社の2ndへの移行対象のプロダクトではキャッシュ対象になるクエリはほとんど使われていないため、キャッシュについては考慮せず、そのままboomへの移行を行いました。

具体的なコード変更はBoom replacing goonが参考になります。

Memorystore for Memcached

Auto Discovery

bradfitz/gomemcacheなど通常のmemcachedクライアントを使用するとnodeを増減させる度に手動でクライアントに設定したnodeのIPリストの更新が必要になります。そのため 検出endpointから定期的にnodeのIPリストを取得するために google/gomemcache クライアントを使います。これは bradfitz/gomemcache のforkなのですが、インストール時に問題が発生するためgo.modのreplaceで回避しました。

replace github.com/bradfitz/gomemcache => github.com/google/gomemcache v0.0.0-20200326162346-94281991662a

間違ってAuto Discoveryに対応していないmemcachedクライアントで検出endpointのIPに接続してもエラーにはならないため注意してください。 nodeへのset,getは成功しますが、正しくkeyが分散しないためキャッシュの不整合が発生します。

.circleci/config.yml

memcachedの起動はテストを実行するコンテナの中で行い、Datastoreエミューレータは以下のように別で起動しています。

docker:
  - image: gcr.io/google.com/cloudsdktool/cloud-sdk:292.0.0
    command: ['gcloud', '--project=test-project-id', 'beta', 'emulators', 'datastore', 'start', '--host-port=localhost:8088', '--no-store-on-disk', '--consistency=1.0']

gcloudコマンドのversionに注意

古いversionのgcloudでdeployするとapp.yaml内の vpc_access_connectorが削除された状態でデプロイされMemorystoreに接続できなくなります。 確認した限りではGoogle Cloud SDK 263.0.0 は対応しておらず、Google Cloud SDK 292.0.0では正しく動作しました。

おわりに

GAE 1stがとても優秀だったため、他社もまだまだ1stのままのところが多いと思います。しかしGoのバージョンが1.11のままだと周辺のエコシステムに置いていかれるため、積極的にupdateしていきたいと思います。