2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go製の負荷ツール「Vegeta」でApp Engine+Cloud SQLに負荷をかける(準備編)

Last updated at Posted at 2019-05-14

お題

前回、「Vegeta」を使ってローカル環境でMySQLに接続するWebAPIに対する負荷をかけてみた。
お次は、実際にアプリが動く環境として Google App EngineCloud SQL を相手に負荷をかけてみるということになるのだけど、まずは、準備。

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"

# Cloud SDK

$ gcloud version
Google Cloud SDK 245.0.0

# Golang

$ go version
go version go1.11.4 linux/amd64

# Vegeta

$ vegeta -version
Version: cli/v12.2.0
Commit: 65db074680f5a0860d495e5fd037074296a4c425
Runtime: go1.11.4 linux/amd64
Date: 2019-01-20T15:07:37Z+0000

実践

デプロイ対象のソース

アプリ本体のソース

大した内容ではないのと、前回も多少説明したので、細かいソースの説明は省略。
要するに、アプリ起動時にCloud SQLにコネクションを張り、「/users」にPOSTリクエストすると1リクエストにつき1レコードを「user」テーブルに登録するWebAPIサーバを起動する内容になっている。

[main.go]
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/jinzhu/gorm"

	"github.com/google/uuid"

	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/echo"
)

func main() {
	// ローカル環境での実行時は、環境変数にセットしておく。例:「export IS_LOCAL='yes'」
	isLocal := os.Getenv("IS_LOCAL") != ""

	// Cloud SQL (ローカルでは MySQL) への接続
	db, closeDBFunc, err := connectDB(isLocal)
	if err != nil {
		os.Exit(-1)
	}
	defer closeDBFunc(db)

	// WebAPIサーバ起動
	e := echo.New()
	routing(e, db)

	port := fmt.Sprintf(":%s", os.Getenv("PORT"))
	fmt.Printf("[GWFGS] PORT: %s\n", port)
	if isLocal {
		port = ":8080"
	}
	e.Logger.Fatal(e.Start(port))
}

func createDatasource(isLocal bool) string {
	if isLocal {
		return "testuser:testpass@tcp(localhost:3306)/testdb?charset=utf8&parseTime=True&loc=Local"
	}

	// GCP - CloudSQL への接続情報は環境変数から取得
	var (
		connectionName = os.Getenv("CLOUDSQL_CONNECTION_NAME")
		user           = os.Getenv("CLOUDSQL_USER")
		password       = os.Getenv("CLOUDSQL_PASSWORD")
		database       = os.Getenv("CLOUDSQL_DATABASE")
	)

	return fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=True", user, password, connectionName, database)
}

type closeDB func(db *gorm.DB)

func closeDBFunc(db *gorm.DB) {
	if db == nil {
		return
	}
	_ = db.Close()
}

func connectDB(isLocal bool) (*gorm.DB, closeDB, error) {
	db, err := gorm.Open("mysql", createDatasource(isLocal))
	if err != nil {
		return nil, nil, err
	}

	db.LogMode(true)
	if err := db.DB().Ping(); err != nil {
		return nil, nil, err
	}

	db.DB().SetMaxIdleConns(10000)

	return db, closeDBFunc, nil
}

func routing(e *echo.Echo, db *gorm.DB) {
	// ユーザー登録
	e.POST("/users", func(c echo.Context) error {
		id := uuid.New().String()
		u := &User{
			ID:   id,
			Name: fmt.Sprintf("ユーザー%s", id),
			Mail: fmt.Sprintf("mail-%s@example.com", id),
		}
		if err := db.Save(u).Error; err != nil {
			return c.JSON(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
		}
		return c.JSON(http.StatusOK, "OK")
	})
}

type User struct {
	ID   string `gorm:"column:id"`
	Name string `gorm:"column:name"`
	Mail string `gorm:"column:mail"`
}

func (u *User) TableName() string {
	return "user"
}

接続先テーブルのDDL

CREATE TABLE IF NOT EXISTS `user` (
  `id` varchar(64) NOT NULL,
  `name` varchar(256) NOT NULL,
  `mail` varchar(256) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;

App Engineデプロイ定義ファイル

[app.yaml]
runtime: go111

includes:
  -  secret.yaml
[secret.yaml]
env_variables:
  # For Cloud SQL 2nd generation instances, this should be in the form of "project:region:instance".
  CLOUDSQL_CONNECTION_NAME: 【自分のプロジェクトID】:asia-northeast1:sample-001
  CLOUDSQL_USER: root
  CLOUDSQL_PASSWORD: 【rootユーザーのパスワード】
  CLOUDSQL_DATABASE: fs14db01

負荷のかけ先の情報

Clous SQL のセットアップ

インスタンス作成

最も低スペックな設定「db-f1-micro」でインスタンス作っておく。

screenshot-console.cloud.google.com-2019-05-13-07-35-28-662.png

そして、前回同様、最大同時接続数に余裕を持たせておくことにする。
現在の設定では「280」らしい。で、「1000」にあげようとすると、

MySQL [(none)]> show variables like "%max_connections%";
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 280   |
+-----------------+-------+
1 row in set (0.04 sec)

MySQL [(none)]> set global max_connections = 10000;
ERROR 1227 (42000): Access denied; you need (at least one of) the SUPER privilege(s) for this operation

あれ、変更できない。SUPER権限が必要?
なのだけど、FAQ見てみたら、「Cloud SQL は SUPER 権限をサポートしない」と書いてある。
https://cloud.google.com/sql/faq#grantall
そうなると、
こんなレベルの最大同時接続数では実務に使えないのでは・・・。
あ、「db-f1-micro」だからか。
一番、低スペックなインスタンスにしたせいかも。
https://cloud.google.com/sql/docs/quotas?hl=ja#fixed-limits
そのようだ。
db-f1-micro」の最大同時接続数は「250」となってる。
250」? 実測値は「280」のようだけど。
まあ、どちらにせよ、このインスタンスでは負荷をかける対象としては適さない。
db-g1-small」インスタンスなら「1000」までいけるとのことなので、インスタンスを変更。

screenshot-console.cloud.google.com-2019-05-14-08-10-35-794.png

サイズアップにより、インスタンス再起動が必要になった。

screenshot-console.cloud.google.com-2019-05-14-08-13-18-368.png

db-g1-small」インスタンスに変更完了。(メモリ搭載量の違いしかわからないけど・・・)

screenshot-console.cloud.google.com-2019-05-14-08-17-34-721.png

実際、最大同時接続数も増えた。(ドキュメント記載の数値より「30」余分なのはなぜだろう。。。)

screenshot-console.cloud.google.com-2019-05-14-08-23-49-415.png

データベース作成

アプリからは「fs14db01」という名前で接続を試みるので、その名前で作っておく。

screenshot-console.cloud.google.com-2019-05-14-08-31-13-206.png

MySQL [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| fs14db01           |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.03 sec)

テーブル作成

アプリからは「user」という名前のテーブルにアクセスするので、その名前で作っておく。

MySQL [(none)]> use fs14db01;
Database changed
MySQL [fs14db01]>
MySQL [fs14db01]> CREATE TABLE IF NOT EXISTS `user` (
    ->   `id` varchar(64) NOT NULL,
    ->   `name` varchar(256) NOT NULL,
    ->   `mail` varchar(256) NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
Query OK, 0 rows affected (0.05 sec)

MySQL [fs14db01]>
MySQL [fs14db01]> show tables;
+--------------------+
| Tables_in_fs14db01 |
+--------------------+
| user               |
+--------------------+
1 row in set (0.04 sec)

MySQL [fs14db01]>
MySQL [fs14db01]> desc user;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | varchar(64)  | NO   | PRI | NULL    |       |
| name  | varchar(256) | NO   |     | NULL    |       |
| mail  | varchar(256) | NO   |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
3 rows in set (0.03 sec)

App Engine デプロイ

ローカル環境のコンソールにて、デプロイ先プロジェクトの設定確認。

$ gcloud config list
[compute]
region = asia-northeast1
zone = asia-northeast1-c
[core]
account = 【自分のアカウント】@gmail.com
disable_usage_reporting = False
project = 【自分のプロジェクトID】

Your active configuration is: [default]

デプロイ。

$ gcloud app deploy
Services to deploy:

descriptor:      [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t03_vegeta_gae/app.yaml]
source:          [/home/sky0621/work/src/go111/src/github.com/sky0621/try-gae-go111/t03_vegeta_gae]
target project:  [【自分のプロジェクトID】]
target service:  [default]
target version:  [20190514t083802]
target url:      [https://【自分のプロジェクトID】.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 0 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.                                                                                                                                                                           
Setting traffic split for service [default]...done.                                                                                                                                                          
Deployed service [default] to [https://【自分のプロジェクトID】.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

screenshot-console.cloud.google.com-2019-05-14-08-52-25-567.png

負荷をかける

1リクエスト

$ cat vegeta/gae_post_user.txt 
POST https://【自分のプロジェクトID】.appspot.com/users
$ vegeta attack -targets=vegeta/gae_post_user.txt -output=/tmp/vegeta_result.bin -rate=1 -duration=1s

結果。
レイテンシー高いな・・・。ローカルから叩いてるせいかな。
ちょっとあとで、プログラム内にログ仕込んで、1リクエストのINとOUT、及びDBアクセス前後の処理時間を計測してみよう。

$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            1, 1.00
Duration      [total, attack, wait]    923.851771ms, 0s, 923.851771ms
Latencies     [mean, 50, 95, 99, max]  923.851771ms, 923.851771ms, 923.851771ms, 923.851771ms, 923.851771ms
Bytes In      [total, mean]            5, 5.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:1  
Error Set:

ちゃんとDBにも入ってる。

MySQL [fs14db01]> select count(*) from user;
+----------+
| count(*) |
+----------+
|        1 |
+----------+
1 row in set (0.03 sec)

MySQL [fs14db01]> select * from user;
+--------------------------------------+--------------------------------------------------+-------------------------------------------------------+
| id                                   | name                                             | mail                                                  |
+--------------------------------------+--------------------------------------------------+-------------------------------------------------------+
| bf6874da-7c0d-41ff-a2ca-4aae8c64c1dd | ユーザーbf6874da-7c0d-41ff-a2ca-4aae8c64c1dd     | mail-bf6874da-7c0d-41ff-a2ca-4aae8c64c1dd@example.com |
+--------------------------------------+--------------------------------------------------+-------------------------------------------------------+
1 row in set (0.03 sec)

1リクエスト(処理時間計測版)

ソースに処理時間計測用のログを仕込む

[main.go(抜粋)]
        e.POST("/users", func(c echo.Context) error {
+               start := time.Now()
+               fmt.Printf("[START] %v\n", start)
+
                id := uuid.New().String()
                u := &User{
                        ID:   id,
                        Name: fmt.Sprintf("ユーザー%s", id),
                        Mail: fmt.Sprintf("mail-%s@example.com", id),
                }
+
+               dbSave := time.Now()
+               fmt.Printf("[from start to db.Save] %v\n", dbSave.Sub(start))
                if err := db.Save(u).Error; err != nil {
                        return c.JSON(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
                }
+
+               fmt.Printf("[from db.Save to end] %v\n", time.Now().Sub(dbSave))
+               fmt.Printf("[from start to end] %v\n", time.Now().Sub(start))
+
+               fmt.Printf("[ END ] %v\n", time.Now())
                return c.JSON(http.StatusOK, "OK")
        })

再度、1リクエスト送ってみる。

$ cat vegeta/gae_post_user.txt 
POST https://【自分のプロジェクトID】.appspot.com/users
$ vegeta attack -targets=vegeta/gae_post_user.txt -output=/tmp/vegeta_result.bin -rate=1 -duration=1s

結果。

$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            1, 1.00
Duration      [total, attack, wait]    937.806167ms, 0s, 937.806167ms
Latencies     [mean, 50, 95, 99, max]  937.806167ms, 937.806167ms, 937.806167ms, 937.806167ms, 937.806167ms
Bytes In      [total, mean]            5, 5.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:1  
Error Set:

StackdriverLoggingで仕込んだログを確認。

screenshot-console.cloud.google.com-2019-05-14-09-39-50-702.png

レイテンシーの「937ms」に対して、1リクエスト内の処理時間としては「22ms」。
ほとんどがプログラムの処理時間外でかかった時間。

検証

試みとしては、ちゃんとした負荷試験では到底なく、お試しでやってるだけではあるものの、負荷をかけることを考えると、ローカル環境からではなく、Vegetaコマンド自体の発行をApp Engineの別サービスを立ててやる、ないし、別でGCEインスタンス立ててやるといったことも考えた方がいいかも。

Vegetaはライブラリとして使ってGoのアプリでラップもできるようなので、HTTPリクエストで負荷度合いのパラメータをもらったら、対象のApp Engineインスタンスにパラメータに応じた負荷をかけるGoアプリを別途作って、複数のGCEインスタンスに配置して一斉に負荷をかけるとかも考えられる。

[2019-05-15]追加検証

レイテンシーが「937ms」かかったのは、ウォームアップのためだった様子。
下記によると、デプロイしたばかりのApp Engineインスタンスには、アプリのコードを読み込むためのリクエストが走ることがあり、その分、別リクエストのレイテンシーが大きくなる可能性があるとのこと。
https://cloud.google.com/appengine/docs/standard/go111/configuring-warmup-requests?hl=ja

実際、5回リクエストを発行したところ、以下の通りとなった。

レイテンシー
1 937.806167ms
2 814.035009ms
3 162.590719ms
4 129.247249ms
5 175.187796ms

まとめ

準備編。ひとまずは、ここまで。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?