MastodonのPostgreSQLをpg_rmanを使ってバックアップする

2017-04-25 23:26 typo修正


しばらくブログを更新していなかったが、最近は「ぼくもあの日、有給を取ってインスタンスを立ててさえいれば今頃年収3億ぐらいでドワンゴに雇われていたのでは...。」と思う日々を過ごしている。

とはいえ、インスタンス運用しつつ運用ネタでも書けば、まだ年収1億ぐらいでなら雇われるチャンスがあると思うので、運用ネタでも書くことにする。バックアップだ。

ユーザが20人程度いる弱小インスタンスを回しているが、つまりデータを飛ばすと20人から怒られてしまう。小心者なのであまり怒られたくはない。ということでPostgresのバックアップを構成することにした。

本記事では、Dockerで運用しているMastodonのDBを、Dockerの恩恵を受けつつ比較的楽にバックアップする方法について言及する。内容自体はMastodonに限らず、Postgresを利用するシステム全般で参考になるだろう。

pg_rman

色々調べてみると、Postgresでオンラインバックアップを取るにはpg_rmanを使うのが便利そうであることが分かった1 2pg_rmanは 1)DBの実体ファイルの物理的なコピーと 2)コピー中のトランザクションログ(WAL)をマークするコマンドの発行をまとめてやってくれるユーティリティだ。加えて、実際にリストアをする際もよしなに面倒をみてくれるらしい。

導入

弱小インスタンスの良いところは、パフォーマンスを気にせずDockerコンテナから剥がさずほぼ本家そのままのdocker-compose.ymlで動かせるところで、ぼくももれなくコンテナで運用している。そういうわけで、pg_rmanもコンテナにしてしまって楽に動かしたい。Docker Hubに誰か上げてくれてるやろって思いながら探したら意外と無かった。なんでや...。仕方ないので作った。

https://hub.docker.com/r/mecab/docker-pg_rman/ (なんか9.3と9.4版がビルド通らなくてイメージ作れてないんですがあとでどうにかします...。たぶん。)

1. postgres.confの編集

pg_rmanを使うためには、postgresql.confを編集してPostgres側でWALをアーカイブするように設定する必要がある。デフォルトでコメントアウトされている部分を編集し、以下のように有効化する。

#wal_level = minimal            #minimal, replica, or logical
wal_level = replica

#archive_mode = off             # enables archiving; off, on, or always
                                # (change requires restart)
archive_mode = on

#archive_command = ''           # command to use to archive a logfile segment
                                # placeholders: %p = path of file to archive
                                #               %f = file name only
                                # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'

archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'  

wal_level=archiveに設定すると書かれているドキュメントもあるが、PostgreSQL 9.6からreplicaに変更されたらしい 3

archive_commandで書いたコピー先/archiveは実際はホスト側のストレージで、volumeとしてマウントする。このため、下で説明するようにdocker-compose.ymlにも設定が必要である。

2. docker-compose.yml の編集

  db:
    restart: always
    image: postgres:alpine
### Uncomment to enable DB persistance
    volumes:
      - ./postgres:/var/lib/postgresql/data
      - /host/path/to/wal_archive:/archive

このような感じで、ホスト上の/host/path/to/wal_archiveをコンテナ内の/archiveに対応付ける。

また、pg_rmanのコンテナを追加する。

  rman:
    image: mecab/pg_rman
    env_file: .env.production
    user: "70:root"
    volumes:
      - ./postgres:/pg_data
      - /host/path/to/backup:/backup
    depends_on:
      - db

このような感じで/pg_dataを実際のPosgtresのデータの実体の場所(=dbでマウントする場所)、/backupをバックアップを置きたい場所に対応付ける。また、depends_onを設定することで、ネットワーク的にrmanコンテナからdbコンテナをdbというホスト名で見えるようにする。userを設定したのはデフォルトではposgtresのコンテナがUID:GID=70:0で動いてファイルを作るので、バックアップのそれに合わせておきたかったということで気分の問題。

pg_rmanはPostgresのバージョンに応じて別のビルドを使う必要があるので、実際はdbrmanのイメージをタグで固定したほうが良いと思う。

あとは、.env.productionに環境変数を設定して、dbのホスト名と必要な資格情報を与えてあげるだけだ。

3. .env.production の編集

# Service dependencies
...
DB_HOST=db  
DB_USER=postgres  
DB_NAME=postgres  
DB_PASS=  
DB_PORT=5432

# For pg_rman
PGHOST=db  
PGUSER=postgres  
PGDATABASE=postgres  
PGPASS=  
PGPORT=5432  

こんな感じで、PGHOSTDB_HOSTを、PGUSERDB_USERを...という感じで同じものを与える。

あとは、docker-compose down; docker-compose up -d;でコンテナたちを作り直す。以下で書くようにpg_rmandocker-compose runだけで使いたいのだけど、docker-compose.ymlに書いちゃうとdocker-compose upの時に必ず無駄にコンテナが作られてしまうので悲しい。作ったpg_rmanのDockerfileは引数無しで実行されると正常終了するようにしているので、気になるなら直後にdocker-compose rm rmanとかしてしまっても良い。

バックアップ

ということで準備ができたので早速バックアップする。初回のみ

$ docker-compose run --rm rman init

で初期化して

$ docker-compose run --rm rman backup --backup-mode=full --compress-data --progress

でバックアップする。バックアップ後

$ docker-compose run --rm rman validate

で検証する。バックアップは

$ docker-compose run --rm rman show

で見える。オプションの詳細については公式ドキュメント1 4 あたりを参照のこと。

とりあえず、

/usr/local/bin/docker-compose run --rm rman backup \
  --backup-mode=full --compress-data --progress \
  --keep-data-generations=3 --keep-data-days=14 --keep-arclog-days=14
/usr/local/bin/docker-compose run --rm rman validate

こんな感じのをcronに仕込んでみた。--backup-mode=incrementalで増分バックアップもできるみたいだけどなんか事故ったときにめんどくさそうなのでfullにした。インスタンスが小さいことと、幸いにもストレージに余裕があること、またバックアップ先はzfsにしてdedupしているのでフルバックアップしてもあんまり容量は消費しないんじゃないかなーという楽観的な観測だ。辛くなってきたらそのうち考える。

とりあえず今日はここまで。リカバリの実験してないけどあとでやる(死亡フラグ)。