ちゃんるいすのブログ

オタクエンジニアの雑記

RDS Proxy を使うとフェイルオーバー時のコネクションプーリング問題が良い感じになるのでは?


目的

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

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 内部でも処理が走るので少なからずパフォーマンスが落ちるはず。