シ〜らかんす

プログラミングとか、カメラとか。

Lambda Web AdapterでGoのGinを動かすときに、DBコネクションの貼りすぎを防ぐ

概要

Lambda Web Adapterを使うと、それまで非サーバーレス環境下で動かしていたウェブAPIをLambda上で動かすことが可能になる。 コンテナイメージさえあれば、web APIをサーバーレス化できるし、コードの共通化も容易になって便利だ。

ただし、RDSと接続するのならば、コネクションが増えすぎないように工夫する必要がある。 対処方法として、RDS Proxyを使う方法はよく知られていて、導入はマストかと思う。

aws.amazon.com

その上で、RDS Proxyを使ったとしても、コネクション数が圧迫されることがある。 アプリケーション内部で明示的にデータベースとの接続を切断しないと、接続がアイドル状態となってしまい、アイドルコネクションのタイムアウトを迎えるまでコネクション数を消費してしまうのだ。

今回は、Lambda Web Adapter & Go & Ginを使って動かす際に、アプリケーション内部で明示的にコネクションをクローズし、コネクション数を圧迫しないようにする工夫を紹介する。

解決策1: Graceful Shutdown時にコネクションをクローズする

Lambdaはシャットダウン時にSIGTERMのシグナルを受け取るので、これをトリガーにDBとのコネクションをクローズする。

Lambdaのライフサイクル

https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html より

サンプルコードは以下の通り。

r := gin.Default()
r.GET("/healthcheck", healthcheck)

srv := &http.Server{
    Addr:    ":8000",
    Handler: r,
}
go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        panic(err)
    }
}()

// Wait for interrupt signal to gracefully shutdown the server with a timeout of 5 seconds.
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

// DBとのコネクションを切断する
DbConnector.Close()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    logging.Error(err, "Server Shutdown:")
}
// catching ctx.Done(). timeout of 5 seconds.
<-ctx.Done()

この方法の問題として、Lambdaがinvokeされなくなってからシャットダウンするまでの間もコネクションが張られ続けてしまう点がある。 実際にAPIに対して同時接続数100件の負荷試験をかけてみると、RDSプロキシのコネクション数も同じ数くらいまで上がってしまった。

↓このようにリクエスト数に合わせてコネクション数が伸びてしまった。

解決策2: エンドポイントの処理が終了したらコネクションをクローズする(これで根本解決)

Ginのmiddlewareとして、DBのコネクションを閉じる処理を追加する。Ginのmiddlewareは、エンドポイントの処理の後に処理を挟み込むことができるので、これを利用する。

サンプルコードは以下の通り。

func SetDbConnClose(dbConn *sql.DB) func(*gin.Context) {
    return func(c *gin.Context) {
        c.Next()
        dbConn.Close()
    }
}


func main() {
    // DBとのコネクションを作成
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "{RDS Proxyのエンドポイント}:3306",
        DBName: "sample",
    }
    dbConn, _ := sql.Open("mysql", cfg.FormatDSN())

    r := gin.Default()
    r.Use(SetDbConnClose())
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run()
}

こうすることで、コネクション数が伸びてしまう現象をなくすことができた。

参考

Lambda Web Adapterとは? aws.amazon.com

Lambdaのライフサイクル docs.aws.amazon.com