🔥

【Go】【Docker】DBにconnection refusedされた時の対処

2022/06/12に公開

検証環境

Go: 1.16.5
docker compose: 3.9
Mysql: 5.7
Gorm: 1.23.3

GoコンテナからDBに接続できなことありませんか?

環境構築の際に、以下のようにとりあえずのファイルを用意したとします。

docker-compose.yml
version: '3.9'

services:
  go:
    build:
      context: .
      dockerfile: ./deployments/dev/go/Dockerfile
    ports:
      - 8080:8080
    tty: true
    depends_on:
      - mysql
  mysql:
    container_name: testDB
    build: ./docker/mysql
    ports:
      - 3306:3306
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=1
      - MYSQL_DATABASE=xxxx
      - MYSQL_USER=xxxx
      - MYSQL_PASSWORD=xxxx
      - TZ=Asia/Tokyo
volumes:
  mysql_data:
    driver: local
main.go
var gormDB *gorm.DB

func main() {
	_gormDB, err := gorm.Open(mysql.Open("xxx:mysql@tcp(xxx)/xxx?parseTime=true"), &gorm.Config{})
	db, _ := _gormDB.DB()
	defer db.Close()
	...
}

Dockerfileの中身は割愛
この後、docker compose up --buildをしてみると…
次のようなエラーが出て、Goコンテナが落ちてしまうことがあると思います。

"failed to initialize database, got error dial tcp 192.168.0.2:3306: connect: connection refused"

データベースへの接続に失敗しているようですが
さぁ何が原因でしょうか?

DB初期化の前にGoがDBに接続しにいって失敗しとる

上述のエラーについて、さまざまの原因があると思います。
原因の1つとして、GoがDBに接続しにいくタイミングが早すぎて、DBの初期化が終わってないことが挙げられます。
(定量的な原因特定ではありません)
事実、docker compose up mysqlとして、先DBのコンテナだけを立ち上げ、しばらく待ったの後に、docker compose up goとすると問題なく立ち上がるのです。
問題は無くなるのですが、できればdocker compose upと1回で立ち上げってくれるとありがたいですよね。

解決策(先人(神)): リトライ処理を書く

上述のエラーを解決できず、途方に暮れていた私に手を差し伸べてくれたのがこちらの記事。
https://zenn.dev/ajapa/articles/aa9b59dd30c501
詳しくはこちらの記事を見ていただきたいのですが、簡単に何をしているかを説明しますと、
DB接続に失敗しても、任意の回数リトライする処理を書かれています。
(コードを引用していいのかわからないので引用元参照してね)

解決策(オレ流): 先人のに少しカスタマイズ

先人様のコードで十分と言えますが、若干弊社のコーディングスタイルとマッチしていなかったので、カスタマイズしました。

main.go
package main

import (
	// import something
)

var db *gorm.DB

func dbConnect(dialector gorm.Dialector, config gorm.Option, count uint) (err error) {
	// countで指定した回数リトライする
	for count > 1 {
		if db, err = gorm.Open(dialector, config); err != nil {
			time.Sleep(time.Second * 2)
			count--
			log.Printf("retry... count:%v\n", count)
			continue
		}
		break
	}
	// エラーを返す
	return err
}

func main() {
	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Asia%%2FTokyo",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_HOST"),
		os.Getenv("DB_PORT"),
		os.Getenv("DB_DBNAME"),
	)

	dialector := mysql.Open(dsn)
	option := &gorm.Config{Logger: ...}
	// optionを渡すことができる
	if err = dbConnect(dialector, option, 100); err != nil {
		log.Fatalln(err)
	}
	
	// do something

先人様との違いは以下になります。

  • リトライの文をforで表現した
  • すべてのリトライに失敗した場合,エラーを返すようにした(panicにしない)
  • DB接続の関数にGormのOptionを渡すようにした

まとめ

神はzennにおわしたんやなぁ〜(しみじみ)

Discussion