概要
サービスを運用していると、データベースがダウンすることがあります。
完全なダウンではなくとも、バージョンアップやメンテナンスの際のブルーグリーンによる切り替え、フェイルオーバによって一時的に数秒程度データベースへの接続が不能になることがあります。
そこで、もしもデーターベースに接続ができなくなった場合でも、間隔をあけてリトライするように Go言語でExponential Backoffというアプローチを使って対応してみようと思います。
検証
まず、リトライを考慮しないGoの簡単なコードを以下に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package main import ( "database/sql" "fmt" "log" _ "github.com/go-sql-driver/mysql" ) func main() { // MySQLデータベースに接続するためのDSN (Data Source Name) dsn := "root:@tcp(127.0.0.1:3306)/test1" // データベースへの接続を開く db, err := sql.Open("mysql", dsn) if err != nil { log.Fatalf("Error opening database: %v", err) } defer db.Close() // データベースへの接続が有効かどうかを確認 err = db.Ping() if err != nil { log.Fatalf("Error connecting to the database: %v", err) } // クエリを実行してuserテーブルから一件のユーザー情報を取得 var id int var name, email string query := "SELECT id, name, email FROM user LIMIT 1" row := db.QueryRow(query) err = row.Scan(&id, &name, &email) if err != nil { if err == sql.ErrNoRows { log.Println("No rows were returned!") } else { log.Fatalf("Error querying database: %v", err) } } else { fmt.Printf("User: ID=%d, Name=%s, Email=%s\n", id, name, email) } } |
データベースへ正常に接続できる場合は以下の結果を受け取ることができます。
1 2 3 |
% go run main.go User: ID=1, Name=shibuya taro, Email=shibuya_taro@example.com |
しかし、データベースを停止して実行してみると当然以下のような結果になります。
1 2 3 4 |
% go run main.go 2024/08/25 22:49:03 Error connecting to the database: dial tcp 127.0.0.1:3306: connect: connection refused exit status 1 |
そこで、リトライ機能を Exponential Backoff で実装してみます。
Exponential Backoff はリトライ間隔を指数関数的に増加させる方法です。リトライの間隔を徐々に延ばすことで、システムの負荷を減らし効率的にリトライを実施します。
以下に、Exponential Backoff を追加したGoのコードを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
package main import ( "database/sql" "fmt" "log" "math" "time" _ "github.com/go-sql-driver/mysql" ) // エクスポネンシャルバックオフを使用してデータベースに接続する関数 func connectWithExponentialBackoff(dsn string, maxRetries int) (*sql.DB, error) { var db *sql.DB var err error for i := 0; i < maxRetries; i++ { db, err = sql.Open("mysql", dsn) if err != nil { log.Printf("Error opening database: %v", err) } else { err = db.Ping() if err == nil { return db, nil } log.Printf("Error connecting to the database: %v", err) } // エクスポネンシャルバックオフ backoff := time.Duration(math.Pow(2, float64(i))) * time.Second log.Printf("Retrying in %v...", backoff) time.Sleep(backoff) } return nil, fmt.Errorf("could not connect to the database after %d retries", maxRetries) } func main() { // MySQLデータベースに接続するためのDSN (Data Source Name) dsn := "root:@tcp(127.0.0.1:3306)/test1" // 最大リトライ回数 maxRetries := 5 // データベースへの接続を開く db, err := connectWithExponentialBackoff(dsn, maxRetries) if err != nil { log.Fatalf("Failed to connect to the database: %v", err) } defer db.Close() // クエリを実行してuserテーブルから一件のユーザー情報を取得 var id int var name, email string query := "SELECT id, name, email FROM user LIMIT 1" row := db.QueryRow(query) err = row.Scan(&id, &name, &email) if err != nil { if err == sql.ErrNoRows { log.Println("No rows were returned!") } else { log.Fatalf("Error querying database: %v", err) } } else { fmt.Printf("User: ID=%d, Name=%s, Email=%s\n", id, name, email) } } |
接続に問題がない場合に取得できる結果は先程と変わらないので、一時的にデータベースサーバを停止させてすぐに立ち上げた際の処理結果を確認してみます。
1 2 3 4 5 6 7 8 9 10 11 |
% go run main.go 2024/08/25 22:58:57 Error connecting to the database: dial tcp 127.0.0.1:3306: connect: connection refused 2024/08/25 22:58:57 Retrying in 1s... 2024/08/25 22:58:58 Error connecting to the database: dial tcp 127.0.0.1:3306: connect: connection refused 2024/08/25 22:58:58 Retrying in 2s... 2024/08/25 22:59:00 Error connecting to the database: dial tcp 127.0.0.1:3306: connect: connection refused 2024/08/25 22:59:00 Retrying in 4s... 2024/08/25 22:59:04 Error connecting to the database: dial tcp 127.0.0.1:3306: connect: connection refused 2024/08/25 22:59:04 Retrying in 8s... User: ID=1, Name=shibuya taro, Email=shibuya_taro@example.com |
上記の結果の通りではありますが、データベースが落ちている状態から稼働して正常に結果が得られるまでリトライ間隔を延ばしながらリトライを実施し、最後に結果を取得できていることが分かります。
まとめ
多くのサービスで実装されている基本的なものではありますが改めてリトライについて触れてみました。
Exponential Backoff でリトライを実装するメリットは大きいです。しかし、リトライ回数をどこまで延ばすか、つまり何秒までリトライを可能とするかはよく考える必要があります。
バックグラウンドで非同期に処理されるものか、ユーザがリアルタイムで待つ必要がある処理かなどで変わってくると思います。負荷が軽く、より効率的なリトライとは何かを考えていく必要がありそうです。