目的
フェイルオーバー時のエラーレートを下げたい
RDS Proxy の公式ドキュメントに書かれている
Doesn’t drop idle connections during failover, which reduces the impact on client connection pools
を試す
環境
- 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()}) } }
もっとクエリ数を増やす
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()}) } } } }
最大 16,000/qps が流れる。
RDS Proxy を”使う”場合
742
まとめ
RDS Proxy は月 1vCPU 辺り $0.018/h なので、案外安い
SetConnMaxLifetime これをこっちで意識しなくてよくなるし、フェイルオーバーの速さと、フェイルオーバー時のエラーレートも下がるので SLA/SLO が厳しい要件では入れおいたほうが良さそう。
RDS Proxy を通すことでホップ数が増えるのと、RDS Proxy 内部でも処理が走るので少なからずパフォーマンスが落ちるはず。