goでデータベースのバックアップをとる
はじめに
cron とシェルスクリプト(.sh)ファイルで実行してもいいですが、もっと汎用性、再現性、管理のしやすさを高めたかったので go でやろうと思いました。
こういったライブラリもあるみたいですが、go の標準のパッケージだけでやりたいと思い使いませんでした。その際少し苦戦したのでまとめました。
RDS とかを料金面で気兼ねなく使える人は素直にそちらのバックアップ機能を使いましょう。
os/exec
シェルスクリプトの実行には os/exec を使います。
この子はなかなか癖があって面白い子です。みなさんも愛でてあげてください。
使い方は簡単。
exec.Command(<name>, <args>)
でコマンドを作って Run, Output, Start などで実行するだけ。
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 のバックアップをとる
本題
失敗例
まずは失敗例
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 を使って標準出力を受け取り、それをファイルに書き込む方法です。
因みに出力先は相対パス指定でも大丈夫です。
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!!
Discussion