目的

フェイルオーバー時のエラーレートを下げたい

RDS Proxy の公式ドキュメントに書かれている

Doesn’t drop idle connections during failover, which reduces the impact on client connection pools

を試す

aws.amazon.com

環境

  • Aurora MySQL 2.08.1
  • Lambda (Go)

やり方

Lambda から DB(もしくは RDS Proxy) に対して 0.5秒間隔で Ping を打つ
3分間起動している間に 5回フェイルオーバーをする

コード

package main
import (
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"log"
"os"
"time"
)
type PingErr struct {
Time time.Time
}
const (
executeTimeSec = 300
DBMaxOpenConn = 100
DBMaxIdleConn = 10
DBMaxLifeTime = time.Second * 10
)
func main() {
lambda.Start(realMain)
}
func realMain() {
time.Local = time.FixedZone("JST", 9*60*60)
log.Println("initializing")
// direct
rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.cluster.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
// proxy
// rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.proxy.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
defer rw.Close()
if err != nil {
panic(err)
}
rw.DB().SetMaxOpenConns(DBMaxOpenConn)
rw.DB().SetMaxIdleConns(DBMaxIdleConn)
rw.DB().SetConnMaxLifetime(DBMaxLifeTime)
log.Println("finish initialized")
now := time.Now().Unix()
var pingErrs []PingErr
for {
diff := time.Now().Unix() - now
if diff >= executeTimeSec {
if len(pingErrs) != 0 {
log.Println("ping error detected.", "count: ", len(pingErrs))
for _, v := range pingErrs {
fmt.Println(v.Time)
}
}
log.Println("finish")
os.Exit(0)
}
check(rw, &pingErrs)
time.Sleep(time.Second / 2)
}
}
func check(db *gorm.DB, pingErrs *[]PingErr) {
err := db.DB().Ping()
if err != nil {
*pingErrs = append(*pingErrs, PingErr{Time: time.Now()})
}
}

RDS Proxy を”使わない”場合

f:id:rarirureluis:20200701225305p:plain

46回 Ping でエラー

RDS Proxy を”使った”場合

f:id:rarirureluis:20200701225449p:plain

8回 Ping でエラー

もっとクエリ数を増やす

SELECT 1 を並列で流す。

SetConnMaxLifetime(DBMaxLifeTime) は直接つないだ時で使う。
RDS Proxy を経由するときはここを指定せずに全コネクションを永遠に使い回す設定にして試す。

package main
import (
"context"
"github.com/aws/aws-lambda-go/lambda"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"log"
"sync"
"time"
)
type PingErr struct {
Time time.Time
}
const (
timeout    = 120 // sec
goroutines = 100 // db.r5.large = 1000
queryCount = 1000000
DBMaxOpenConn = 900
DBMaxIdleConn = 900
DBMaxLifeTime = time.Second * 10
DBQuery       = "SELECT 1"
)
var (
wg sync.WaitGroup
)
func main() {
lambda.Start(realMain)
// realMain()
}
func realMain() {
time.Local = time.FixedZone("JST", 9*60*60)
log.Println("initializing")
// rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.cluster.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
rw, err := gorm.Open("mysql", "root:ZokWAWywPwQtO7xr@tcp(hasegawa.proxy.ap-northeast-1.rds.amazonaws.com:3306)/mysql?charset=utf8mb4&parseTime=True&loc=Local")
defer rw.Close()
if err != nil {
panic(err)
}
rw.DB().SetMaxOpenConns(DBMaxOpenConn)
rw.DB().SetMaxIdleConns(DBMaxIdleConn)
rw.DB().SetConnMaxLifetime(DBMaxLifeTime)
log.Println("finish initialized")
var pingErrs []PingErr
ctx, cancel := context.WithCancel(context.Background())
q := make(chan *gorm.DB)
for i := 0; i < goroutines; i++ {
wg.Add(1)
go check(ctx, q, &pingErrs)
}
now := time.Now().Unix()
for i := 0; i < queryCount; i++ {
diff := time.Now().Unix() - now
if diff >= timeout {
log.Println("timed out: ", timeout)
break
}
q <- rw
}
cancel()
wg.Wait()
if len(pingErrs) != 0 {
log.Println("ping error detected: ", len(pingErrs))
}
log.Println("finish")
}
func check(ctx context.Context, q chan *gorm.DB, pingErrs *[]PingErr) {
for {
select {
case <-ctx.Done():
wg.Done()
return
case db := <-q:
err := db.Exec(DBQuery).Error
if err != nil {
*pingErrs = append(*pingErrs, PingErr{Time: time.Now()})
}
}
}
}

f:id:rarirureluis:20200703162822p:plain

最大 16,000/qps が流れる。

RDS Proxy を”使わない”場合

f:id:rarirureluis:20200703162934p:plain

ping error になってるけど、実際は query error
RDS Proxy を使わない場合は 15,4056 が失敗

RDS Proxy を”使う”場合

f:id:rarirureluis:20200703163111p:plain

742

まとめ

RDS Proxy は月 1vCPU 辺り $0.018/h なので、案外安い
SetConnMaxLifetime これをこっちで意識しなくてよくなるし、フェイルオーバーの速さと、フェイルオーバー時のエラーレートも下がるので SLA/SLO が厳しい要件では入れおいたほうが良さそう。

RDS Proxy を通すことでホップ数が増えるのと、RDS Proxy 内部でも処理が走るので少なからずパフォーマンスが落ちるはず。