🤖

🤖

:gijutsu_burogu:

docker-composeでdepends_onしても起動順を制御するだけで稼働順は制御されない問題

本記事での言葉の定義

  • 起動: 動き始めること(例: PC の電源ボタンを押すこと)
  • 稼働: 働きはじめること(例: PC が利用可能になること)

はじめに

以下の Redis と MySQL を利用するアプリケーションの docker-compose を考えます。

version: "3"

services:
  app:
    build: app
    depends_on:
      - redis
      - mysql
  mysql:
    image: mysql:5.7
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
  redis:
    image: redis:6.0.5-alpine
    ports:
      - "6379:6379"

アプリーケーションが起動する前に Redis と MySQL が稼働することが期待されます。 docker-compose では、depends_on によって起動順は制御することができますが稼働順は制御することができません。

以下の Redis と MySQL へのネットワークの疎通を確認するだけのアプリケーションで実験します。

func main() {
    client := redis.NewClient(&redis.Options{Addr: "redis:6379"})
    if err := client.Ping().Err(); err != nil {
        log.Fatal(err)
    }

    db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/performance_schema")
    defer db.Close()
    if err != nil {
        log.Fatal(err)
    }
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    log.Println("success")
}

docker-compose upすると、depends_on_app_1 exited with code 1となりアプリケーションが異常終了します。 これは、MySQL の稼働前にアプリケーションが稼働し、MySQL に接続できないためです。 Redis はすぐに稼働しましたが、MySQL は稼働まで時間がかかりました。

対処法

アプリケーションでで稼働待ちする

アプリケーション内で再接続を行うことが最適解です。 何らかの理由でもデータベースへの接続に失敗した後に、接続を再度確立するようにアプリケーションを設計しておくことが必要です。 以下のようにループとスリープによって稼働待ちしました。 これはアプリケーションでの稼働待ちを表現したかっただけで、Goでのベストプラクティスは別にあるかもしません。

func main() {
    client := redis.NewClient(&redis.Options{Addr: "redis:6379"})
    if err := client.Ping().Err(); err != nil {
        log.Fatal(err)
    }

    db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/performance_schema")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 稼働待ち
    for {
        err = db.Ping()
        if err == nil {
            break
        }
        time.Sleep(3 * time.Second)
    }

    log.Println("success")
}

docker-compose で稼働待ちする

それほどに厳密性を求めない場合であれば、簡単なスクリプトで稼働順序を制御することも選択肢にあります。

github.com

以上がDockerのドキュメントでも紹介されていますが、ベースイメージで Alpine Linux を使っておりashでうまく動作しなかったため簡単な独自スクリプトを作成しました。

docker-compose.ymlでは、アプリケーションを起動する前にスクリプトを挟みます。

app:
  build: app
  depends_on:
    - redis
    - mysql
  command: ["./wait-for-it.sh", "./main"]

以下のスクリプトでRedisとMySQLの稼働待ちをします。 最後のexec $@./mainを実行します。 また、厳密に稼働待ちするためにはncコマンドではなく、MySQLクライアントによるmysql -h"$host" -u"$user" -p"$password"で疎通確認をすべきです。 しかし、アプリケーションのコンテナに初期からMySQLクライアントが入っていることはなくインストールする必要があります。 MySQLクライアントをあえてインストールするのは大袈裟すぎるので、ncコマンドで代用します。

#!/bin/ash
# wait-for-it.sh

set -e

until nc -z redis 6379; do
  >&2 echo "redis is unavailable - sleeping"
  sleep 3
done

until nc -z mysql 3306; do
  >&2 echo "mysql is unavailable - sleeping"
  sleep 3
done
>&2 echo "redis and mysql is up - executing command"

exec $@

ncコマンドでは、ネットワークの疎通確認だけでなく様々なことができます。 また、Alpine Linuxでさえ最初から含まれているので嬉しいです。 qiita.com

ソースコード全体は以下にあります。

github.com

参考

docs.docker.jp