🐸

goでデータベースのバックアップをとる

4 min read

はじめに

cron とシェルスクリプト(.sh)ファイルで実行してもいいですが、もっと汎用性、再現性、管理のしやすさを高めたかったので go でやろうと思いました。

https://github.com/JamesStewy/go-mysqldump
こういったライブラリもあるみたいですが、go の標準のパッケージだけでやりたいと思い使いませんでした。その際少し苦戦したのでまとめました。

RDS とかを料金面で気兼ねなく使える人は素直にそちらのバックアップ機能を使いましょう。

os/exec

シェルスクリプトの実行には os/exec を使います。
この子はなかなか癖があって面白い子です。みなさんも愛でてあげてください。

https://pkg.go.dev/os/exec

使い方は簡単。
exec.Command(<name>, <args>)でコマンドを作って Run, Output, Start などで実行するだけ。

sample.go
package main

import (
	"os/exec"
	"log"
)

func main() {
	cmd := exec.Command("ls", "-la")
	if result, err := cmd.Output; err != nil {
		log.Print(err)
	} else {
		log.Print(string(result))
	}
}

func Command(name string, arg ...string) *Cmd
Command は name と arg を渡すと Cmd のポインタ型を返してくれます。

ただこのくらいの短いコマンドなら簡単なんですが、この Command は name と arg をうまく使わないと期待通りにコマンドを実行してくれないんです。

ちなみに name について godoc にはこのように書いてあります。

If name contains no path separators, Command uses LookPath to resolve name to a complete path if possible. Otherwise it uses name directly as Path.

name に"/"が入っている場合は相対パスや絶対パス(書き方による)で、無ければ$PATH を参照してコマンドを実行する。

arg をどう区切るかでうまく行くかいかないか決まります。
たとえば mysql だと

// OK: 動作する
cmd := exec.Command(
	"mysqldump", "--single-transaction", "--skip-lock-tables",
	fmt.Sprintf("-u%s", configs.MysqlUser),
	fmt.Sprintf("-p%s", configs.MysqlPassword),
	fmt.Sprintf("%s", configs.MysqlDataBase),
)

// OK: 動作する
cmd := exec.Command(
	"mysqldump", "--single-transaction", "--skip-lock-tables",
	"-u", configs.MysqlUser,
	fmt.Sprintf("-p%s", configs.MysqlPassword),
	fmt.Sprintf("%s", configs.MysqlDataBase),
)

// BAD: 動作しない
cmd := exec.Command(
	"mysqldump", "--single-transaction", "--skip-lock-tables",
	fmt.Sprintf("-u %s", configs.MysqlUser),
	fmt.Sprintf("-p%s", configs.MysqlPassword),
	fmt.Sprintf("%s", configs.MysqlDataBase),
)

// BAD: 動作しない (全てnameに入れる)
cmd := exec.Command(
	fmt.Sprintf(
		"mysqldump --single-transaction --skip-lock-tables -u%s -p%s %s",
		configs.MysqlUser, configs.MysqlPassword, configs.MysqlDataBase,
	)
)
// "mysqldump --single- ..."のようなコマンドにパスは勿論通ってないので失敗する

DB のバックアップをとる

本題

失敗例

まずは失敗例

bad.go
package database

import (
	".../configs"
	"fmt"
	"log"
	"os/exec"
)

func BackupMainDatabase() {
	cmd := exec.Command(
		"mysqldump", "--single-transaction", "--skip-lock-tables",
		fmt.Sprintf("-u%s", configs.MysqlUser),
		fmt.Sprintf("-p%s", configs.MysqlPassword),
		fmt.Sprintf("%s", configs.MysqlDataBase),
		fmt.Sprintf("> %s", configs.MysqlBackupFile),
	)

	if err := cmd.Run(); err != nil {
		log.Print(err)
	}
}

これだとこんなエラーがでます。
Couldn't find table: "> <dumpfile>"
いや、テーブルを指定しているわけではないんですが、、、

いろいろ策を講じて arg の区切りを変えてみたりしてこの部分はクリアしましたが、dumpfile の部分でno such file or directory等に遭遇しました。
相対パスでも絶対パスでもやってみたりいろいろ試行錯誤しましたが、ダメでした。
3 時間くらいもがきました orz

そこでやり方を変えました。

成功例

StdoutPipe を使って標準出力を受け取り、それをファイルに書き込む方法です。
因みに出力先は相対パス指定でも大丈夫です。

database.go
package database

import (
	".../configs"
	"fmt"
	"log"
	"io/ioutil"
	"os/exec"
)

func BackupMainDatabase() {
	// 書き込み先は指定しない(出力するだけ)
	cmd := exec.Command(
		"mysqldump", "--single-transaction", "--skip-lock-tables",
		fmt.Sprintf("-u%s", configs.MysqlUser),
		fmt.Sprintf("-p%s", configs.MysqlPassword),
		fmt.Sprintf("%s", configs.MysqlDataBase),
	)

	// 標準出力を受け取る
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		fmt.Print(err)
	}

	// コマンド実行
	if err := cmd.Start(); err != nil {
		log.Print(err)
	}

	bytes, err := ioutil.ReadAll(stdout)
	if err != nil {
		log.Print(err)
	}
	// permissionは600くらいが妥当かなと... 640でもいいかも
	if err = ioutil.WriteFile(configs.MysqlBackupFile, bytes, 0600); err != nil {
		log.Print(err)
	}
}

あとはこの関数を batch/main.go 等で実行&それをビルドします。
ビルドしたバイナリファイルを cron から呼び出すことで定時実行されます。

crontab -e

// 1時間に1回ならこんな感じ
00 * * * * /home/<user>/<path>/main

まとめ

クラウド時代に需要があるのかわかりませんが、go で mysql のバックアップをとる方法を紹介しました。
os/exec の勉強になって面白かったです。

Let's os/exec!!