【再実験】App Engine(Golang)からCloud SQLに接続する際の同時接続数制約

# 注記
この記事ではお題を満たす状況が実現できなかったため、結論は出ていません。
なので、何かしらの結論を求めている場合は参考にならないと思われます。

# お題
[前回](https://qiita.com/sky0621/items/8792b54ebf6bd21bcf72)、１つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限（※１）について、「100」を超えたらどうなるのか？　そもそも超えることはできるのか？　これらについて実験（※２）してみようとした。

※１：以下に記載
https://cloud.google.com/appengine/docs/standard/go/cloud-sql/pricing-access-limits?hl=ja#app_engine

※２：
毎秒のリクエスト数を「100」として、App EngineインスタンスからCloud SQLへ接続する数を設定で制限しない場合に、そのまま毎秒「100」同時接続（対Cloud SQL）をもたらす想定。（結果としてこの想定がそもそも甘かった様子）

が、[結果](https://qiita.com/sky0621/items/8792b54ebf6bd21bcf72#まとめ)は、毎秒`100`であっても`200`であっても`1000`であっても、各回一定のリクエストタイムアウトが発生するものの、毎秒リクエストが「100」を超えたからといって急激にエラー率があがってリクエストが処理されなくなるといった事象は確認されなかった。

その結果についてコメントをもらい、目的が以下の理由で満たせていない可能性を検討。

- SQL発行処理時間が短くコネクションが（新規に張られず）使いまわされ、同時接続数「100」を超えていない可能性
- App Engineがオートスケールによりインスタンスが増えたため、Cloud SQL上で同時接続数「100」を超えていても１App Engineインスタンスあたりでは超えていない可能性

各種ダッシュボードの内容から上記は確からしいので、今回は問題をシンプルにするよう条件を整えてみて再度トライしてみる。

# 前提
- [GCP](https://cloud.google.com/?hl=ja)は知っている。
- [Google App Engine](https://cloud.google.com/appengine/?hl=ja)のことも名前とどういったものかくらいは知っている。
- [MySQL](https://www.mysql.com/jp/)等、リレーショナルデータベースについて、ざっくりとはわかっている。

以下は済んだ上での作業。

- GCPプロジェクトの作成
- [Cloud SDK](https://cloud.google.com/sdk/docs/)のインストールと初期化・認証

# 開発環境
### # OS
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
```

### # Cloud SDK
```
$ gcloud version
Google Cloud SDK 247.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
```

# 実践

## 条件提示

App Engineは特に条件を指定しない場合、デフォルトでオートスケールする設定のため、１インスタンスからCloud SQLに接続する際の限界値を確認しようにも、負荷をかけると勝手にインスタンスが増えてしまう。
まずは、スケールしないように明示的に設定をしてみる。

### App Engineの設定

```yaml:[app.yaml]
runtime: go111

instance_class: F1

automatic_scaling:
  max_instances: 1
  min_instances: 1
  max_idle_instances: 1
  min_idle_instances: 1

includes:
  -  secret.yaml
```

`instance_class`部分は以下にあるように「`CPU: 600 MHz`」、「`Memory: 128 MB`」というスペック。
https://cloud.google.com/appengine/docs/standard/#instance_classes

`automatic_scaling`部分については以下を参考にしてみた。合っているのかな・・・。
https://cloud.google.com/appengine/docs/standard/go111/config/appref?hl=ja#scaling_elements

よくよく考えたら、オートスケールさせないのなら、manualスケールやbasicスケールの方を検討した方がよいかも。。。

### WebAPIソース

「`SELECT sleep(?)`」で「？」の部分にリクエスト時のパスに含んだ数値（単位は秒）をセットすることでDB接続時の処理時間を意図的に操作できるようにする。
数ミリ〜数１０ミリくらいの処理時間であればコネクション使いまわされるかもしれないが、１つの処理に１秒かかれば（その間は別のリクエストはこのコネクションを使えないのだから）コネクションの使い回しは防げる想定。

```go:[main.go]
package main

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

	"github.com/jinzhu/gorm"

	"github.com/google/uuid"

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

func main() {
	db, err := gorm.Open("mysql",
		fmt.Sprintf("root:%s@unix(/cloudsql/%s)/fs14db01?parseTime=True",
			os.Getenv("PASS"), os.Getenv("CONN")))
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := db.Close(); err != nil {
			panic(err)
		}
	}()
	// --------------------------------------------------------------
	// Pattern 1
	db.DB().SetMaxIdleConns(0)
	db.DB().SetMaxOpenConns(0)
	// Pattern 2
	//db.DB().SetMaxIdleConns(0)
	//db.DB().SetMaxOpenConns(95)
	// Pattern 3
	//db.DB().SetMaxIdleConns(95)
	//db.DB().SetMaxOpenConns(95)
	// --------------------------------------------------------------

	e := echo.New()
	e.GET("sleep/:s", func(c echo.Context) error {
		fmt.Printf("before: %v", time.Now())
		s := c.Param("s")
		rows, err := db.Raw("SELECT sleep(?)", s).Rows()
		if err != nil {
			return c.JSON(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
		}
		defer func() {
			if err := rows.Close(); err != nil {
				fmt.Println(err)
			}
		}()
		fmt.Printf("after: %v", time.Now())

		return c.JSON(http.StatusOK, "OK")
	})

	e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", os.Getenv("PORT"))))
}
```

これで、想定としては、負荷がかかってもApp Engineはスケールせず１インスタンスのまま。１インスタンス内のWebAPIはリクエストを受けたら受けた分だけ際限なくCloud SQLへコネクションを張りにいくはず。

## いざ、実験開始

### ■１■お試しで毎秒 10 リクエスト（各リクエストは１秒のSQL処理を含む）を５秒間流す

| No. | Success(%) | Latencies-Mean |
| --- | --- | --- |
| 01 | 100 | 2.81388263s |
| 02 | 100 | 1.114989947s | 
| 03 | 100 | 1.114687352s |

| AppEngine-Memory | CloudSQL-ActiveConnection |
| --- | --- |
| 23.4MB | 4 |

### ■２■続いて毎秒 50 リクエスト（各リクエストは１秒のSQL処理を含む）を５秒間流す

| No. | Success(%) | Latencies-Mean |
| --- | --- | --- |
| 01 | 97.6 | 11.090699085s |
| 02 | 98.8 | 10.97371648s | 
| 03 | 97.6 | 10.898812272s |

| AppEngine-Memory | CloudSQL-ActiveConnection |
| --- | --- |
| 26.0MB | 46 |

既に、捌ききれない。。。
以下、３回分の詳細。

#### １回目

```
$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            250, 50.20
Duration      [total, attack, wait]    28.14466916s, 4.979905969s, 23.164763191s
Latencies     [mean, 50, 95, 99, max]  11.090699085s, 10.263874159s, 20.857254022s, 24.000073269s, 25.041618113s
Bytes In      [total, mean]            3158, 12.63
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  97.60%
Status Codes  [code:count]             200:244  500:6  
Error Set:
500 Internal Server Error
```

#### ２回目

```
$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            250, 50.20
Duration      [total, attack, wait]    26.176578335s, 4.980029453s, 21.196548882s
Latencies     [mean, 50, 95, 99, max]  10.97371648s, 10.242889379s, 21.413832478s, 24.136439179s, 24.169104238s
Bytes In      [total, mean]            1235, 4.94
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  98.80%
Status Codes  [code:count]             0:3  200:247  
Error Set:
Get https://i0o0-dot-【自分のプロジェクトID】.appspot.com/sleep/1: http2: timeout awaiting response headers
```

#### ３回目

```
$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            250, 50.20
Duration      [total, attack, wait]    28.636641586s, 4.980049063s, 23.656592523s
Latencies     [mean, 50, 95, 99, max]  10.898812272s, 10.235065468s, 21.124616937s, 26.536301304s, 26.575871492s
Bytes In      [total, mean]            2189, 8.76
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  97.60%
Status Codes  [code:count]             0:3  200:244  500:3  
Error Set:
500 Internal Server Error
Get https://i0o0-dot-【自分のプロジェクトID】.appspot.com/sleep/1: http2: timeout awaiting response headers
```

### ■３■App Engineインスタンスのスペックを上げて、毎秒 50 リクエスト（各リクエストは１秒のSQL処理を含む）を５秒間流す

以下の通り、「F1」→「F2」に上げてみた。
が、結果としてはスペックの問題ではなかった様子。

```
$ git diff app.yaml
diff --git a/t04_to_cloudsql/app.yaml b/t04_to_cloudsql/app.yaml
index 702baf7..328d3e0 100644
--- a/t04_to_cloudsql/app.yaml
+++ b/t04_to_cloudsql/app.yaml
@@ -1,6 +1,6 @@
 runtime: go111
 
-instance_class: F1
+instance_class: F2
 
 automatic_scaling:
   max_instances: 1
```

| No. | Success(%) | Latencies-Mean |
| --- | --- | --- |
| 01 | 78.4 | 9.975848933s |
| 02 | 96.8 | 11.481939996s | 
| 03 | 98.0 | 10.614662261s |

### ■４■毎秒 110 リクエスト（各リクエストは１秒のSQL処理を含む）を５秒間流す

インスタンスタイプを上げてもエラーが減るわけではなかったので、リクエスト100%の成功は諦める。
１App Engineインスタンスであることはコンソールから確認できているので、負荷を上げてCloud SQL上の接続数が「100」を超える段階まで確認する。

今回の実施結果。やはり一定の率でタイムアウト。

```
$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            550, 110.19
Duration      [total, attack, wait]    57.71304566s, 4.991186186s, 52.721859474s
Latencies     [mean, 50, 95, 99, max]  26.626566708s, 26.275790562s, 51.953338553s, 54.617067253s, 55.781333601s
Bytes In      [total, mean]            2660, 4.84
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  96.73%
Status Codes  [code:count]             0:18  200:532  
Error Set:
Get https://i0o0-dot-【自分のプロジェクトID】.appspot.com/sleep/1: http2: timeout awaiting response headers
```

最大同時接続数のカウントについては、Cloud SQLのダッシュボードにおける「」を見ていたが、そもそもここで見ている数値が目的と合致しているかわかっていないので、
直接MySQLインスタンスにログインしてこれまでの最大同時接続数がどれほどなのかを確認することにする。

今回の負荷をかける前が以下の状態。

```
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 7     |
+----------------------+-------+
1 row in set (0.04 sec)
```

今回、毎秒 110 リクエストを５秒間流した後が以下。

```
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 50    |
+----------------------+-------+
1 row in set (0.04 sec)
```

そもそものお題は「`１つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限について、「100」を超えたらどうなるのか？`」なので、半分にしか至っていない。。。
毎秒 110 リクエスト（１リクエスト毎 `select sleep(1)` ）では同時接続 100 が満たせていない様子。

### ■５■毎秒 110 リクエスト（各リクエストは`２`秒のSQL処理を含む）を５秒間流す

結果、変わらず。

```
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 50    |
+----------------------+-------+
1 row in set (0.04 sec)
```

他のパターンとして、下記も試したものの上記結果と同じく同時接続数は「`50`」を超えなかった。

- 毎秒 110 リクエスト（各リクエストは`５`秒のSQL処理を含む）を５秒間流す
- 毎秒 300 リクエスト（各リクエストは`３`秒のSQL処理を含む）を２秒間流す

Cloud SQLの対象インスタンスは下記の通り、`1030`まで同時接続を許容しているので、Cloud SQL側の制約ではなさそう。

```
MySQL [fs14db03]> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 1030  |
+-----------------+-------+
1 row in set (0.04 sec)
```

### ■６■App Engineを`２`インスタンス起動して毎秒 110 リクエスト（各リクエストは２秒のSQL処理を含む）を３秒間流す

下記の通り、２App Engineインスタンスに増やすと捌き切った。

### App Engineの設定

```yaml:[app.yaml]
runtime: go111

instance_class: F1

automatic_scaling:
  max_instances: 2
  min_instances: 2
  max_idle_instances: 2
  min_idle_instances: 2

includes:
  -  secret.yaml
```

実施結果。

```
$ vegeta report /tmp/vegeta_result.bin
Requests      [total, rate]            330, 110.37
Duration      [total, attack, wait]    40.37358696s, 2.990036079s, 37.383550881s
Latencies     [mean, 50, 95, 99, max]  20.822092054s, 20.367117704s, 35.883990081s, 37.727537881s, 38.566024081s
Bytes In      [total, mean]            1650, 5.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:330  
Error Set:
```

Cloud SQL側の接続数は、１App Engineインスタンス数の時は `50` を超えなかったが、今回は `55` 。
ただ、２App Engineインスタンスで `55` なので１インスタンスあたりは当然もっと低い。

```
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 55    |
+----------------------+-------+
1 row in set (0.04 sec)
```

念の為、同じ２App Engineで、以下の条件でも試してみた。

- ■４■毎秒 110 リクエスト（各リクエストは１秒のSQL処理を含む）を５秒間流す

その結果は下記。

```
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 77    |
+----------------------+-------+
1 row in set (0.04 sec)
```

# まとめ
軽い気持ちで、「`１つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限について、「100」を超えたらどうなるのか？`」を確認してみようと思ったものの、
そもそもその状況が作れないという事態に陥り頓挫。。。
自分の知識の無さが恨めしい。