注記
この記事ではお題を満たす状況が実現できなかったため、結論は出ていません。
なので、何かしらの結論を求めている場合は参考にならないと思われます。
お題
前回、1つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限(※1)について、「100」を超えたらどうなるのか? そもそも超えることはできるのか? これらについて実験(※2)してみようとした。
※1:以下に記載
https://cloud.google.com/appengine/docs/standard/go/cloud-sql/pricing-access-limits?hl=ja#app_engine
※2:
毎秒のリクエスト数を「100」として、App EngineインスタンスからCloud SQLへ接続する数を設定で制限しない場合に、そのまま毎秒「100」同時接続(対Cloud SQL)をもたらす想定。(結果としてこの想定がそもそも甘かった様子)
が、結果は、毎秒100
であっても200
であっても1000
であっても、各回一定のリクエストタイムアウトが発生するものの、毎秒リクエストが「100」を超えたからといって急激にエラー率があがってリクエストが処理されなくなるといった事象は確認されなかった。
その結果についてコメントをもらい、目的が以下の理由で満たせていない可能性を検討。
- SQL発行処理時間が短くコネクションが(新規に張られず)使いまわされ、同時接続数「100」を超えていない可能性
- App Engineがオートスケールによりインスタンスが増えたため、Cloud SQL上で同時接続数「100」を超えていても1App Engineインスタンスあたりでは超えていない可能性
各種ダッシュボードの内容から上記は確からしいので、今回は問題をシンプルにするよう条件を整えてみて再度トライしてみる。
前提
- GCPは知っている。
- Google App Engineのことも名前とどういったものかくらいは知っている。
- MySQL等、リレーショナルデータベースについて、ざっくりとはわかっている。
以下は済んだ上での作業。
- GCPプロジェクトの作成
- Cloud SDKのインストールと初期化・認証
開発環境
# 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は特に条件を指定しない場合、デフォルトでオートスケールする設定のため、1インスタンスからCloud SQLに接続する際の限界値を確認しようにも、負荷をかけると勝手にインスタンスが増えてしまう。
まずは、スケールしないように明示的に設定をしてみる。
App Engineの設定
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接続時の処理時間を意図的に操作できるようにする。
数ミリ〜数10ミリくらいの処理時間であればコネクション使いまわされるかもしれないが、1つの処理に1秒かかれば(その間は別のリクエストはこのコネクションを使えないのだから)コネクションの使い回しは防げる想定。
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はスケールせず1インスタンスのまま。1インスタンス内のWebAPIはリクエストを受けたら受けた分だけ際限なくCloud SQLへコネクションを張りにいくはず。
いざ、実験開始
■1■お試しで毎秒 10 リクエスト(各リクエストは1秒のSQL処理を含む)を5秒間流す
No. | Success(%) | Latencies-Mean |
---|---|---|
01 | 100 | 2.81388263s |
02 | 100 | 1.114989947s |
03 | 100 | 1.114687352s |
AppEngine-Memory | CloudSQL-ActiveConnection |
---|---|
23.4MB | 4 |
■2■続いて毎秒 50 リクエスト(各リクエストは1秒のSQL処理を含む)を5秒間流す
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 |
既に、捌ききれない。。。
以下、3回分の詳細。
1回目
$ 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
2回目
$ 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
3回目
$ 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
■3■App Engineインスタンスのスペックを上げて、毎秒 50 リクエスト(各リクエストは1秒のSQL処理を含む)を5秒間流す
以下の通り、「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 |
■4■毎秒 110 リクエスト(各リクエストは1秒のSQL処理を含む)を5秒間流す
インスタンスタイプを上げてもエラーが減るわけではなかったので、リクエスト100%の成功は諦める。
1App 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 リクエストを5秒間流した後が以下。
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 50 |
+----------------------+-------+
1 row in set (0.04 sec)
そもそものお題は「1つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限について、「100」を超えたらどうなるのか?
」なので、半分にしか至っていない。。。
毎秒 110 リクエスト(1リクエスト毎 select sleep(1)
)では同時接続 100 が満たせていない様子。
■5■毎秒 110 リクエスト(各リクエストは2
秒のSQL処理を含む)を5秒間流す
結果、変わらず。
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 リクエスト(各リクエストは
5
秒のSQL処理を含む)を5秒間流す - 毎秒 300 リクエスト(各リクエストは
3
秒のSQL処理を含む)を2秒間流す
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)
■6■App Engineを2
インスタンス起動して毎秒 110 リクエスト(各リクエストは2秒のSQL処理を含む)を3秒間流す
下記の通り、2App Engineインスタンスに増やすと捌き切った。
App Engineの設定
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側の接続数は、1App Engineインスタンス数の時は 50
を超えなかったが、今回は 55
。
ただ、2App Engineインスタンスで 55
なので1インスタンスあたりは当然もっと低い。
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 55 |
+----------------------+-------+
1 row in set (0.04 sec)
念の為、同じ2App Engineで、以下の条件でも試してみた。
- ■4■毎秒 110 リクエスト(各リクエストは1秒のSQL処理を含む)を5秒間流す
その結果は下記。
MySQL [fs14db03]> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 77 |
+----------------------+-------+
1 row in set (0.04 sec)
まとめ
軽い気持ちで、「1つのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「100」までという制限について、「100」を超えたらどうなるのか?
」を確認してみようと思ったものの、
そもそもその状況が作れないという事態に陥り頓挫。。。
自分の知識の無さが恨めしい。