ペパボに教えたMySQLのリアルタイム増分バックアップ(mysqlbinlogのストリームバックアップ)

TL;DR


ぬいぐるみが好きな方のDBA です。

このエントリー GMOペパボ Advent Calendar 2018 の26日目の記事です。
私の観測範囲では以下のような26日目の記事が確認されています 🙂

メリークリスマス! (プラス1日と半分)


いつだったか、ペパボテックフライデーに遊びに行った時に mysqlbinlogのストリームバックアップについて話したんですが、それが結構意外(?)だったらしいのでちょっと紹介。

基本的な考え方は サーバーが完膚なきまでに死んでもMySQLのデータを失わないための表技 P.71 に書いてあるままです。
これにいくつかひねりを加えて運用しているのがこんな感じになります。

FROM centos
RUN yum upgrade -y && yum clean all
COPY mysqlbinlog /usr/bin/mysqlbinlog
COPY mysql /usr/bin/mysql
RUN groupadd -g 600 mysql
RUN useradd -g mysql -u 600 -d /home/mysql mysql
COPY entrypoint.sh /home/mysql/entrypoint.sh

USER mysql
CMD "/home/mysql/entrypoint.sh"

まずは1インスタンスを1コンテナに押し込めるためのDockerfile。
Dockerfileの隣にはPercona Server 5.6の mysqlbinlogmysql を.tar.gzから抜き取ったものを置いています。
何故5.6のを使っているかというと、5.7とそれ以降では mysql_old_password 認証プラグインが使えないからだったんですが、さすがに mysql_old_password なアカウントは撲滅されたのでもうそろそろいいのかも知れない。

コンテナを起動する時に -v 保管先ディレクトリ:/target として、コンテナからは「ローカルの /usr/bin/mysqlbinlog/usr/bin/mysql を使って、 /target に書き込めばいい」ように固定してあります。
リトライとか通知とか省きましたが entrypoint.sh の中身はこんな感じ。

#!/bin/bash

### We need credential in $TARGET/.envrc
. /target/.envrc

cd /target
while true ; do
  latest_uncompressed=$(ls *.[0-9]*[0-9][0-9] 2> /dev/null | tail -1)
  earliest_on_master=$(mysql --user=${MYSQL_USER} -sse "SHOW MASTER LOGS" | head -1 | awk '{print $1}' 2> /dev/null)

  if [[ -n $latest_uncompressed ]] ; then
    latest=$latest_uncompressed
  elif [[ -n $earliest_on_master ]] ; then
    latest=$earliest_on_master
  else
    : Binlog not found, error handling
  fi

  ### mysqlbinlog doesn't use MYSQL_* variable except of MYSQL_PWD..
  mysqlbinlog --read-from-remote-server --stop-never --raw \
              --user=${MYSQL_USER} --port=${MYSQL_TCP_PORT} \
              --host=${MYSQL_HOST} --socket=${MYSQL_UNIX_PORT} \
              $latest
done ### Loop and retry when can't connect master.

### Retry-out
: Error notice

コンテナに渡す mysqlbinlogでストリームするユーザー情報接続先のIP, port/target/.envrc に入っていることを要求します。
俺は平文で書いちゃってますが(そもそもこのディレクトリーが見えるということは mysqlroot でシェルを持ってるってことなので)気になるなら mysql_config_editor とかで難読化してもいいんじゃないでしょうか。
mysqlbinlog --stop-never --row --read-from-remote-server でバイナリーログを取ってくる場合、ストリームを開始するバイナリーログファイル名だけは mysqlbinlog コマンドに渡してやる必要があるので、前半部分ではファイルを探して組み立てています。

  1. /target にバイナリーログがある場合は、連番の一番大きいやつ
  2. ない場合は、 SHOW MASTER LOGS で出力されたバイナリーログのうち連番の一番小さいやつ

今のところ、マスター切り替えで連番がガラっと変わる場合は手で /target のファイルを消して再起動することで対応しています。そこまで頻繁に必要なわけではないので。今のところ。
あとは、これとは別に↓のようなスクリプトがDockerホスト側で動いていて、


### /data/binlog/[a-z]* がそれぞれ /target にマウントされる find /data/binlog/[a-z]* -type f -name "*[0-9][0-9]" | while read f ; do ### Compress and transfer to S3, even if that is current streaming binlog. binlog_file=$(basename $f) instance_name=$(basename $(dirname $f)) s3path="s3://${s3_backet_binlog}/$instance_name/$binlog_file.zst" ### コンテナが生きてて開いてる = 今まさに書き込み中のbinlog = 転送はするけど削除しない(次回上書きされる) ### コンテナが生きてて閉じてる = 書き終わってスイッチしたbinlog = 転送 & 削除 ### コンテナが死んでて開いてる = ないはず ### コンテナが死んでて閉じてる = スイッチしたのかしてないのかわからない = 転送はするけど削除しない(どこから再開すればいいのかわからなくなる) ### Check $f is used or not. ### lsof returns 1 if file is unused. tmp_error=$(mktemp) sudo /sbin/lsof $f 2> $tmp_error 1> /dev/null lsof_rc=$? if [[ $(grep -v tmpfs $tmp_error | grep -v "Output information may be incomplete" | wc -l) -ne 0 ]] ; then ### Output information .. は書き込み中のbinlogにかけると出るので無視してそれ以外 queue_me "WARN" "$(cat $tmp_error)" fi rm $tmp_error ### Check streaming container is alive or not. is_container_running $instance_name container_rc=$? if [[ $lsof_rc != 0 && $container_rc = 0 ]] ; then ### When unused, validate binary-log. ${mysql_basedir}/bin/mysqlbinlog $f > /dev/null if [[ $? != 0 ]] ; then ### binlog is broken. Should be retried. queue_me "WARN" "Broken binlog detected: $f please check manually" else ### コンテナが生きてる && プロセス掴んでない && デコードに失敗しない log_me "INFO" "$f uploaded to $s3path" upload_binlog $f $s3path [[ $? = 0 ]] && rm $f fi else ### When used(current streaming binlog) or unused but container is not running, ### Don't validate(always mysqlbinlog fails because of not close binlog) log_me "INFO" "Partial(current) binary log $f uploaded to $s3path" upload_binlog $f $s3path fi done
  • mysqlbinlog コマンドを通してデコードに失敗しない = 壊れていないことを確認
  • パターン分けで転送だけか転送して削除か判定
    • コンテナが生きてて開いてる = 今まさに書き込み中のbinlog = 転送はするけど削除しない(次回上書きされる)
    • コンテナが生きてて閉じてる = 書き終わってスイッチしたbinlog = 転送 & 削除
    • コンテナが死んでて開いてる = ないはず
    • コンテナが死んでて閉じてる = スイッチしたのかしてないのかわからない = 転送はするけど削除しない(どこから再開すればいいのかわからなくなる)

あたりを判定してアップロードするスクリプトが動いています。
これで、たとえデータセンターに何かがあってもS3(と私たちDBA)さえ無事ならば、データのロストは↑のスクリプトを回している間隔までで済むようになりました。

( ´-`).oO(完全に余談ですが、このスクリプトは某国と某国が一触即発だった頃に書き始められました。たまたまなんですけどね…。