BizStationブログ

ビズステーション株式会社の公式ブログです。

PDOより高速なORMライブラリ! Transactd PHP ORM リリース

2016年12月22日にTransactd PHP ORMをリリースしました。
これはTransactdを使用したMySQL/MariaDB用のORMライブラリです。

今回はこのTransactd PHP ORMを紹介します。

Contents

主な特徴

高速なDBアクセス

Transactd PHP ORMは、ORMでありながらPDOを直接使用したアクセスよりも高速なデータアクセスができます。
下図は、TransactdとPDO、Laravel EloquentORMで、findメソッドによる1レコードアクセスを(異なるidで)100回行ったときの時間を計測した結果です。PDOはSELECT * FROM table WHERE id=xで計測しています。

f:id:bizstation:20170111094719p:plain

TransactdはPDOの約2倍、Laravelの5.3倍高速に処理しています。
(100回ループすることで、クラスのロード時間などの影響を小さくし、純粋なデータアクセスにかかる時間を求めました。)

通常、PHPのORMはPDOを使って実装されます。それらは必ずPDOネイティブより遅くなります。MySQLに対して、PDOより高速に動作するのはおそらくTransactd PHP ORMだけでしょう。

省メモリ

Transactd PHP ORMは、他のORMに比べて非常に少ないメモリで動作します。読み取ったモデルの数にもよりますが、1つのモデルでLaravel 5.3の1/5程度、多くのモデルでも1/2以下のメモリ使用量です。

下の表は、上記のテストの前後においてmemory_get_usage関数で使用メモリの増分を計測したものです。

Transactd PDO Laravel
メモリ増加量 (KB) 81 459 442

スループット

高速なDBアクセスと省メモリによって、高いスループットが実現できます。Webリクエストに対するスループットは、処理全体に対するデータ処理の比率によって変わりますが、比較的データ処理が重いケースでは、Laravel 5.3に比べて2倍以上のスループットを実現しています。

下図は、Apache JMeterを使ってLaravelフレームワーク上にEloquentORMとTransactd PHP ORMで同じ内容のモデルを動作させ、処理時間を計測した結果です。
計測ページは2つのリレーションを持った200個のモデルをテーブル形式にしたHTMLを返しています。
f:id:bizstation:20170111115318p:plain
現実的なレスポンス時間を約300msec以下とすると、スループットはLaravelで秒間20クライアント、Transactdでは40クライントで、約2倍になっています。
同じLaravelのフレームワーク上なので、異なるのはORMのみです。ORMを変えることでWeb全体のスループットを向上できることがわかります。

高可用性

デフォルトで、マスター・スレーブ構成で、読み取りをスレーブ、書き込みをマスターに振り分ける機能があります。また、THAによる高可用運用が可能です。

自在なトランザクションとロック、スナップショット

Transactdには、1レコードずつインデックス順に自在にアクセスできるサーバーカーソルがあります。サーバーカーソルを使うと細かな行ロックとトランザクションの制御が可能です。

また、一貫性読取のためのスナップショットも簡単に行えます。
ミッションクリティカルなアプリケーションにも最適です。ロックとトランザクションについては、MySQL/MariaDBとTransactdのInnoDBロック制御詳細 その1 - BizStationブログを参照してください。

詳細なドキュメント

ドキュメントもライブラリの一部です。とにかく分かりやすくなるよう努力しました。

特に販売管理のサンプルアプリケーションでは、実際に動作するアプリケーションを作成しながら、ミッションクリティカルなポイントに重点を置いて説明しています。ORMを使わなくても、MySQLを使用している方には参考になる点があるかと思います。

欠点

欠点は、現在のところマイグレーションがサポートされていないことです。ただ、データベースがMySQLなので、フレームワークマイグレーションがあればそれをそのまま使用可能です。

詳細

すでにORMを使用されている方向けに、ORMの一般的な問題についてのTransactdでの対応内容を説明します。

ORMインターフェース

ORMのAPIインタフェースは、Ruby on Railsの流れを汲んだLaravel 5に非常に近いものです。TransactdのネイティブAPIとORMは非常に相性が良く、少ないコードでORMを構成しています。モデルの生成はC++で実装されたエクステンション内で行われ、非常に高速に処理されます。

リレーションのロードタイミング

Transactd PHP ORMのリレーションはデフォルトで、アクセス時にロードする遅延ロードです。また、モデルのコレクションのリレーションを一括して取得するEager loadingもサポートしています。
参考:モデル - パフォーマンス

インピーダンスミスマッチ

テーブルとモデルで扱う領域が異なることを、インピーダンスミスマッチと呼びます。Transactdでは、テーブルのフィールドごとに、別のクラスのプロパティへのマッピングを指定することで、ORMに値の読み書きをさせることが可能です。
参考:モデル - インピーダンスミスマッチ

モデルのキャッシュ

findメソッドを使用したユニークなキー値検索は頻繁に発生します。また、その値はあまり変化しない場合が多く、その都度データベースにアクセスするのは無駄が多くなります。そこでTransactd PHP ORMでは、そのようなモデルをキャッシュし、検索時にはキャッシュがあればそれを返します。
キャッシュの効果は非常に大きく、数十倍の速度アップになることもあります。
参考:モデル-キャッシュ

IDEのコード補完支援

モデルのプロパティは、モデル内に明示的に宣言する必要がありません。記述が簡単な反面、開発用のIDEからはどのような属性があるのかわからなくなります。
Transactd PHP ORMでは、モデルのソースコードジェネレータがPHPDocに対応した@propertyコメントを出力します。これによりIDEでのコード補完が可能になります。
参考:モデル-自動生成

プロパティアクセス速度

PHPでは、プロパティの実装でマジックメソッドを使うことができます。しかし、この方法は非常に低速です。Transactd PHP ORMは通常のプロパティの実装にマジックメソッドを使用しません。データベースアクセスの高速化だけでなく、プロパティアクセス速度にもフォーカスし、全体での処理速度の向上を図っています。

下の表は、取得済みのモデル200個について、プロパティの読取と設定を各モデル10回ずつ行ったときの時間を計測したものです。

Transactd Laravel 5.3 倍率
0.0002528 0.018078 71.5

TransactdはLaravel 5.3の70倍も高速にプロパティへの読み書きができます。

複雑なデータベース処理

ORMを使う/使わないの判断をする際に、複雑な処理になるとORMだけでデータベースアクセスを完結しにくいという問題があります。TransactdはORMとネイティブAPIの境目がありません。ORMを生かしながら自然にネイティブAPIを利用した複雑な処理にも対応できます。

まとめ

Transactd PHP ORMは、高速レスポンス、省メモリ、高スループットでミッションクリティカルな開発に適したORMです。
以下のようなORMを探している方に最適です。

  • MySQLへのアクセスのあるWebページのレスポンスを良くしたい
  • MySQLへのアクセスのあるWebサイトのスループットを良くしたい(垂直スケールアップしたい)
  • MySQLアクセスをマスターとスレーブに簡単に分散したい
  • 現在使用しているORMでレコードのロックや排他制御に不安がある
  • モデルのプロパティへのアクセスが遅いと感じている

Transactdは、ミッションクリティカルで高速なレスポンスと高いスループットを求めるアプリケーションに最適です。小さな規模から大きなものまで対応できます。
ORMのインタフェースはLaravel 5に近いので、分かりやすく学習コストも少なくてすみます。
インストールはComposerで簡単に行えます。Packagist transactd/ormをご覧ください。

是非、サクサクで堅牢なTransactd PHP ORMを体感してみてください。

MySQL/MariaDB Transactd 超 高可用運用 (THA)

MySQL/MariaDB用 NoSQLプラグイン Transactdでの高可用運用(Transactd High Availability)ツール「THA」と、対応したプラグイン(Version 3.5)の提供を開始しました。
MySQL/MariaDBのHAツールは既にいくつかありますが、THAはTransactdに合わせて最適化されると同時に、他にはない工夫がされています。

元々Transactdは、ミッションクリティカルでレスポンスを追及したエンタープライズアプリケーションのために開発されました。高可用運用はとても大事な課題であり、今回ようやくその課題を達成できました。

データアクセスにTransactd APIは使用しなくても、HAのためだけにTransactdプラグインを使用することも可能です。


THAについては、Transactdドキュメントの高可用運用に詳しく書かれていますので詳細はそちらをご覧ください。

ここでは主な内容を紹介します。

THAの主な機能

  • マスター死活監視
  • 自動フェイルオーバー(切替時間 数秒以内)
  • 仮想ホスト名アクセスと実ホスト名解決
  • スイッチオーバー
  • THAの状態検査

THAの特徴

  • 障害検知からマスター切替・名前解決まで、フェイルオーバーに必要な処理をすべて網羅
  • 障害時の最小限のアプリケーションエラー
  • OS、外部機器やDNSなどによる支援が不要
  • 数行のアプリケーションプログラム変更で利用可能
  • MySQL/MariaDBLinux/Windows/Mac OS XC++/PHP/Ruby/COMなど幅広い動作環境
  • Transactd PluginとTransactd client以外のライブラリは不要

THAに必要なもの

  • Transactd Plugin(MySQL 5.6以上 MariaDB 10.0.13以上)
  • Transactd クライアント(C++/PHP/Ruby/COMのいずれも対応しています)
  • THA管理マネージャ(haMgr)(Client utilities に含まれるコマンドラインプログラム)

これだけでTHAを開始できます。

最小限のアプリケーションエラーと持続性

一般的にフェイルオーバーのトリガーは、専用の監視デーモンやheartbeatなどを使用しますが、THAではそのようなものはありません。代わりに、アプリケーション内のTransactd クライアントが、受け取ったエラーの内容からサーバーの問題であるかを判断し、フェイルオーバーを開始します。
また、クライアントには、ネットワークエラーが発生した際にサーバー再接続する機能が組み込まれています。 再接続できた場合、カレントレコードやロック状態などを復元させます。

これによって、アプリケーションにエラーが返る前にマスターが切替えられ、再接続することで何事もなかったように処理を継続できます。(ただし、トランザクション中のエラーはこれらの処理を行いません。トランザクションがアボートされてから行われます。)

専用の監視デーモンなどの場合は監視サイクル(30秒など)があるので、障害発生から検出までの間のアクセスはすべてエラーになります。THAの場合はトランザクション中に限り(クライアントとの接続ごとに)その1つだけがエラーになります。

トランザクション中のアクセスエラーを正しく(アボート)処理すればアプリケーションはそのまま持続して実行を続けることができます。
アプリケーションの持続性で言えば、マスターダウンによるダウンタイムはゼロです。
これが「超高可用」と題した理由です。

ネームリゾルバー(THNR)

クライアントにはHAのためのネームリゾルバー(THNR)が内蔵されました。アプリケーションは接続先のホスト名を、実ホスト名でなく仮想名で指定します。THNRは指定された仮想ホスト名から実ホスト名に名前解決してアクセスを行います。これによって、マスターを切替えに付随したVIPの変更やARPキャッシュのクリアといったOSやネットワーク機器などに対する追加処理は一切不要です。
解決された名前はキャッシュされますので、名前解決のためのオーバーヘッドはほとんどありません。

オンプレミスからクラウドLinux/Windows/Macなどさまざまな環境で全く同じように使用できます。

フェイルオーバー

フェイルオーバーはMySQL/MariaDBのGTIDレプリケーションを使用してスレーブ群の中から1台をマスターに昇格させます。そして残りのスレーブのレプリケーション先を新マスターに切り替えます。
フェイルオーバーはクライアントにインストールされたhaMgrがTHNRから指令を受けて開始します。フェイルオーバーにかかる時間はスレーブの台数にもよりますが概ね1秒以内にすべて完了します。

THAの状態検査

フェイルオーバーは頻繁に起こるものではありません。いざというときに助けになる仕組みです。ところが、いざというときに正しく動作させるのは以外と難しかったりします。

そこで、haMgrにはフェールオーバーが正しくできる状態にあるかどうかをチェックする機能があります。定期的なタスクでチェックを行うことで問題時に確実にフェイルオーバーできるようにます。

まとめ

THAはMySQL/MariaDBで従来にない超高可用運用を可能にします。使い方やリファレンスなど詳細はTransactdドキュメントの高可用運用にありますので是非ご覧ください。

MySQL/MariaDB レプリケーション復旧 テーブルロックをせず特定テーブルのみコピーする その2

前回は、レプリケーション復旧においてMySQLのコマンドを使って最もロック時間を短くコピーし直す方法を紹介しました。
今回は、全くテーブルをロックせずに特定のテーブルやデータベースをコピーし直す方法を紹介します。

Nonlocking Replcopy

Nonlocking Replcopyは前回の記事の内容を実装したPHPスクリプトです。ブログのタイトル通り、テーブルロックをせず特定テーブルのみコピーできます。
スクリプトGitHubからダウンロードできます。実行環境や使い方の概要はreadmeをご覧ください。

github.com

また、テスト用のTransactdサーバー環境を手元のWindowsマシンに3分でセットアップできるTransactdTestSrvSetupもあります。是非活用してみてください。

Nonlocking Replcopyは以下のことができます。

それでは、このスクリプトを使った具体的な新規セットアップと、エラー別の修復方法を説明します。どの例もサーバーは稼働中のままでOKです。

新規にレプリケーションをセットアップする

設定はすべて設定ファイルに記述します。スクリプトコマンドラインパラメータには設定ファイル名を渡します。

$php replcopy.php repl_config.ini

具体的な新規セットアップ条件は以下の内容とします。

サーバーの種類MySQL 5.6
対象データサーバー全体
マスターホスト名server1
スレーブホスト名server2
以下は上記条件で新規レプリケーションをセットアップしたときの実行結果です。

[replcopy]# php dreplcpy.php ./repl_config_centOS.ini
Nonlocking replcopy version 1.0.0
--- Start replication setup  ---
Open slave database ...  done!
Stop slave ...  done!
Open master database ...  done!
Open master tables ...  done!
Begin snapshot on master ...  done!
Wait for stop slave until binlog pos ...  done!
Copying tables ...
  [database : test_v3]
    table : fieldtest ...  done!
    table : groups ...  done!
    table : nullkey ...  done!
    table : nullvalue ...  done!
    table : packrecord_test ...  done!
    table : scores ...  done!
    table : setenumbit ...  done!
    table : test ...  done!
    table : timetest ...  done!
    table : users ...  done!
Reset slave ...  done!
Change master ...
        set global gtid_purged='d30d02d6-3fe4-11e5-97db-00ffa4dbde57:1-8';
        change master to master_host='server1', master_port=3306, master_user='replication_user', master_password='123', master_auto_position = 1;  done!
Start slave ...  done!
Slave_IO_Running = Yes
Slave_SQL_Running = Yes
--- Replication setup has been completed ---
[replcopy]#

こんな感じで実行されます。どのような処理をしているかは、この実行結果の通りです。成功/失敗は最後のSlave_IO_Running = YesSlave_SQL_Running = Yesで判断しています。

設定のポイント

設定ファイルは以下のような内容です。

[master]
host=server1
repl_port=3306
repl_user=replication_user
repl_passwd=123

databases=
tables=
ignore_tables=

[slave]
host=server2
master_resettable=1
log_bin=0

[gtid]
using_mysql_gtid=1
type=2
  • databasestablesに何も指定しないとホスト全体をコピーします。
  • GTIDを使った新規セットアップはRESET MASTERの実行は必須です。master_resettable=1をセットして有効にします。
  • 新規セットアップなのでコピー処理のログは不要です。log-bin=0を指定してOFFにします。
  • MySQL 5.6のGTIDを使用するので、using_mysql_gtid=1をセットします。
  • MySQL 5.6のGTIDによるポジション指定でレプリケーションをセットアップするので、type=2を指定します。(0は非GTID、1はMariaDBのGTID)

mysql システムデータベース

mysqlという名前のデフォルトのデータベースがあります。このデータベースのテーブルはほとんどがMyISAMのテーブルです。MyISAMはMVCCをサポートしておらず、スナップショットが取れません。そのためコピー中にマスターで変更を加えると不整合を起こす場合があります。

Nonlocking Replcopyでは、ホスト全体を指定してもこのmysqlデータベースは含まれません。ホスト全体コピーの前にdatabases=mysqlとして実行し、その後ホスト全体を指定してコピーする分割方式で、完全な全体コピーを行うことができます。ただし、mysqlデータベースのコピー中はマスターでこのデータベースを変更しないことが条件です。

レプリケーションエラーを修復する

レプリケーションの修復を行う理由として

  • SQLスレッドエラー
  • I/Oスレッドエラー
  • データの不一致

などがあります。
それぞれのパターンごとに修復の仕方を説明します。

データの不一致を修復

これは最もシンプルです。データのコンペアツールやユーザーからの申告などでデータの不整合に気付いた場合です。SQLスレッドは運良くまだエラーにはなっていない場合です。
この場合は不整合のあるテーブルのみ列挙してコピーします。

不一致のあるデータベース名test
不一致のあるテーブル名tabel_a
table_b

設定ファイル上で、新規セットアップと異なる点は以下の部分です。

[master]
...
databases=test
tables=tabel_a,table_b
...

この設定で実行すれば、スムーズにコピーして終わります。

SQLスレッドのエラーを修復

設定内容は、上記の「データの不一致を修復」と同じです。ただし、SQLスレッドエラーを起こしているテーブルを事前にある程度調べる必要があります。そしてそのテーブルをコピー対象に設定します。もし、エラーの中に想定外のテーブルが出現した場合は、キャンセルして設定ファイルにそのテーブルを加えて再実行します。

このスクリプトは、START SLAVE UNTILを使って指定したポジションまでSQLスレッドが進むのを待機します。ところがSQLスレッドエラーが発生してしまってはそこまで進むことができません。そこで、待機時にSQLスレッドエラーがある場合、その内容を表示しスキップするかどうかを問い合わせします。

-------------------------
SQL thread has error(s).
-------------------------
Could not execute Update_rows_v1 event on table test_v3.fieldtest; Can't find record in 'fieldtest', Error_code: 1032; handler error HA_ERR_KEY_NOT_FOUND; the event's master log mysqld-bin.000016, end_log_pos 2424
Do you want skip only this error ?
Y: Skip this error | A: Skip all error | C: Cancel replication

選択肢は以下の3つです。

  • Y : このエラーのみスキップする
  • A : RESET SLAVEしてすべてのエラーをスキップする
  • C : レプリケーション修復をキャンセルする

あらゆる条件で矛盾なく進められるのは、Yの「このエラーのみスキップする」を選択し、かつこのエラーの起きているデータベースまたはテーブルが今回のコピー対象に含まれている場合です。
エラーのスキップはすべてのエラーがなくなるまで繰り返し行う必要があります。

エラーメッセージには、対象のデータベース名やテーブル名が含まれているので、必ず確認するようにします。

Aの「RESET SLAVEしてすべてのエラーをスキップする」では容易にコピーを開始することができますが、リレーログにコピー対象に含まれないトランザクションがあるとそれがスレーブに適用されず矛盾が生じます。ホスト全体が対象であったり、コピー対象外のデータがあっても問題ない場合やコピー対象外のデータは無いとわかっている場合はこれを選択できます。

もし、コピー対象外で問題のあるSQLスレッドエラーが出現したらCの「キャンセル」をして、そのテーブルやデータベースを設定に加えて再度実行してください。ここでのキャンセルは追加の問題を発生させたりはしません。

このスキップを通過すれば、あとは新規セットアップと同じようにテーブルがコピーされレプリケーションが再開されます。

I/Oスレッドのエラーを修復

I/Oスレッドエラーの原因は

  • レプリケーション用のアカウントが無効になった
  • ファイアウォール設定が変更されブロックされた
  • マスターが停止している
  • マスターで RESET MASTERなどが実行され、スレーブの要求するログがない

など多岐に渡ります。また修復方法も原因ごとに異なります。Nonlocking Replcopyはこれらを回復するためのものではありませんが、再セットアップしてしまいたい場合には有用です。

レプリケーションの再セットアップ

この設定は新規セットアップと何も変わりありません。
ただ、既にスレーブが設定済みのため、現状の状態にI/Oスレッドエラーやバイナリログの矛盾あったりします。その場合、それらを表示して継続するかどうか問い合わせします。

-------------------------
IO thread has error(s).
-------------------------
Got fatal error 1236 from master when reading data from binary log: 'Error: connecting slave requested to start from GTID 0-1-234, which is not in the master's binlog'
Do you want stop and reset slave ?
 (Y/N) ?

Yを選択すると、I/Oエラー自体を無視して再セットアップを行います。開発やテストなどでレプリケーションを行う場合、この再セットアップはとても頻繁に使用されます。

GTIDへの対応

Nonlocking ReplcopyMySQLMariaDBの両方のGTIDに対応しています。GTIDを使用している場合でも、修復コピーの考え方は前回の記事と同様です。
また、GTIDを使ったレプリケーションについては以下の記事も是非ご覧ください。
bizstation.hatenablog.com

エラーのスキップ

エラーのスキップについては、MySQL 5.6以降でgtid_mode=onの場合、空のトランザクションに代替する方法でスキップします。それ以外のサーバーはsql_slave_skip_counter=1を使ってスキップします。

ポジション指定

type=0とした場合、従来のバイナリログ名とポジションを使ってログポジションの指定をします。MySQLgtid_mode=onの場合でもこの指定は可能です。
type=1とした場合、MariaDBmaster_use_gtid = slave_posを使ってログポジションの指定をします。
type=2とした場合、MySQLmaster_auto_position = 1を使ってログポジションの指定をします。

フェイルオーバーでの注意点

GTIDを使ったマスターと複数のスレーブによるフェイルオーバー構成をしているときの注意点を説明します。
スレーブはフェイルオーバーによってマスターになる可能性があります。(ここでは以降、スレーブから昇格したマスターを新マスターと呼びます。)
新マスターは、遅れのあるスレーブに自身のバイナリログからトランザクションを転送しなければなりません。転送する可能性のあるのは、遅れスレーブとの差分です。

意図的に遅らせたスレーブがなければ、遅れはごくわずかな時間(例えば1秒)です。
例えば、あるスレーブを修復コピーしバイナリログを消去しても、それから1秒以上経てば、新マスターになって問題ありません。修復してから1秒ですから現実的にはほとんど問題ないでしょう。
しかし、意図的に12時間遅らせる等の設定をしたスレーブがある場合、バイナリログを消去したスレーブは、それから12時間以上経たないと新マスターになる資格がありません。フェイルオーバースクリプトなどを用意し、ある時間までは昇格できないと制御する必要があります。

Nonlocking Replcopyでバイナリログに関連するパラメータは以下の2つです。

  • log_bin
  • master_resettable

MySQLのGTIDとそれ以外の場合ではこれらに対する対処が異なるので、分けて説明します。

MySQL GTID

MySQLのGTIDはマスターとGTIDセットで比較するので、スレーブ上で余計なトランザクションを実行すると、新マスターになった時にそれが転送されてしまいます。ですのでmaster_resettable=1を指定して、そのスレーブのバイナリログをリセットした方がうまくいきます。ただ、リセットするとマスターになったときに遅延スレーブからのログ転送要求に答えることができません。遅延させたスレーブがある場合は、その遅延時間が経過するまで、修復したスレーブはマスターになれないとマークすることが必要です。

MariaDB GTID

MariaDBのGTIDの場合、log_bin=0としてコピーの内容をバイナリログに記録しないようにしましょう。遅れスレーブに対しても問題ありません。

もし、この修復したスレーブが多段構成でマスターでもある場合は、log_bin=1の必要が出てきます。この場合、MySQLの場合と同じように、遅延させたスレーブがある場合は、その遅延時間が経過するまで、修復したスレーブはマスターになれないとマークすることが必要です。さらに、修復直後にマスター上で何らかのトランザクションを発生せて、それが遅延スレーブに反映されるようにしましょう。そうでないと、遅延スレーブがコピーの内容以降のポジションを要求してくれません。

まとめ

2回に渡ってレプリケーションの復旧コピーについて説明しました。また、間にGTIDの詳細の記事も書きました。この3つの記事で、レプリケーションの復旧についてはだいたい網羅できたかと思います。

今回、パターン別修復方法の記事を書いたことで、自分の中でも修復時の問題と対処をうまく整理できました。

Nonlocking Replcopyは開発環境などではとても便利です。(開発時はあれやこれやですぐにSQLスレッドが止まってたりしますので)設定ファイルさえ作っておけば、コマンド一発でレプリケーションの修復ができます。

これがTransactdを使うきっかけになってくれれば嬉しいです。コメントも大歓迎! です。

MySQL/MariaDB GTID レプリケーション詳細

今回は、MySQL/MariaDB GTID レプリケーションの詳細を説明します。これは、Transactdによるレプリケーションセットアップ(修復)ツールを構築する際に調べたものです。

主に従来のバイナリログとポジションを使ったレプリケーションとGTIDによるレプリケーションの違いについて説明します。ある程度従来のレプリケーションのセットアップなどを理解していることを前提にしています。

Index

なぜGTIDが必要なのか

まず、最初になぜGTIDが必要なのでしょうか?よく言われているのは、

  • CHANGE MASTER TOバイナリログポジションをいちいち指定しなくても良い

といったものですが、さほど大した問題では無いように感じます。本当のところ何のためでしょうか?

それは、MariaDBのドキュメントに明確に書かれています。

  • マスターと複数スレーブの構成で、マスターがダウンしてスレーブのいずれかがマスターに昇格したときに、他のスレーブがマスターを切替えるためCHANGE MASTERを発行するが、その時のポジション指定の問題を解決する

マスターがダウンしたら、スレーブ群の中から、レプリケーションの遅れが最も少ないスレーブをマスターに昇格*1させます。
他のスレーブは新マスターに対してCHANGE MASTERを発行してマスターを切り替えます。問題なのはこのときです。各スレーブごとに旧マスターのバイナリログ名とポジションでどこまでレプリケートしたかはわかっています。しかし、新マスターでいうところのどのファイル名でポジションがいくつなのかはわからないのです。*2
GTIDはこの問題を解決するために生まれました。

MariaDBのGTIDの目的はこれだけです。
MySQLのGTIDにはさらに「マスターとスレーブの一貫性の判断を容易にする」という目的が加えられています。これについては MySQLでGTIDを使う の項目で説明します。)

GTIDが活きるシナリオ

マスターと複数スレーブの構成でマスターを切り替えるフェイルオーバーを行うのであれば、GTIDの恩恵を大いに受けることができます。

正確にはもう少し限定的で、フェイルオーバーを行う際にスレーブ群の中で遅れているスレーブがあってもそれを救済するフェイルオーバーです。
もし、遅れているスレーブを切り捨てるならば、フェイルオーバーした新マスターと他のスレーブは同じデータを持っているので、新マスターのバイナリログをリセットreset masterしスレーブもreset slaveしてポジション指定なしでCHANGE MASTERすれば、従来の方法でも正しく切替ができるからです。

もう一つ、手動でマスターを切替える(スイッチオーバー)際に、すべてのスレーブが同期するのを待たなくて良くなります。同期できているスレーブが1台あればそれを新マスターにすることで遅れているスレーブはその新マスターからデータを受け取ることができるようになります。迅速なスイッチオーバーができます。

上記の必要がなければGTIDの恩恵はなく、従来のバイナリログとポジションを使ったレプリケーションとほぼ同等の機能です。(追記 マルチマスターになると話がややこしくなりますのでここではそれは除いて考えます)

GTIDによるポジションの解決

まず最初に、マスター切り替え時のポジション問題の解決方法について説明します。解決の考え方自体はMariaDBMySQLもほとんど同じです。(実装と扱うためのコマンドは異なります)

従来は、マスターが切り替わったときに、スレーブが新マスターに対して「旧マスターのこのポジション」と要求しても、新マスターのバイナリログはサーバーが異なるので意味がなく、どこのことかわからない状態でした、そのため、新マスターのバイナリログをスキャンし、スレーブが適用済みの位置を自前で探すしかありませんでした。位置を探すのも明確な目印があるわけでもなく、ある程度の長さが一致する部分を探すしかなかったわけです。

そこで、バイナリログに目印としてトランザクションごとに番号を振って、それを一緒に記録するようにしました。この番号がGTIDです。サーバーが異なってもIDが重複しないようにグローバルにユニークな番号です。(Global Transaction ID

また、スレーブはバイナリログの書き込みの際に、マスターから転送されたトランザクションのGTIDを必ず転記するようにします。そうすることで後でマスターになったとしても、スレーブからGTIDさえ教えてもらえればバイナリログのその場所を正しく探し出して返すことができます。

GTIDを使ったレプリケーションの処理をまとめると以下のようになります。

  • マスターはトランザクションごとに重複しないIDを振り、それをバイナリログに記録する
  • マスターはスレーブからのバイナリログ要求にGTIDの情報も加えて転送する
  • スレーブはマスターから受け取ったトランザクションを自身のバイナリログを記録する際にそのID(GTID)を転記する

これらの準備があったうえで、以下のようにすることで、旧マスターでのポジションが新マスターではどこかを解決できるようになります。

  • スレーブはバイナリログログポジションに代えて最後に受け取ったGTIDで要求する
  • マスターはバイナリログ中のGTIDを探し次のトランザクションを返す

具体的なGTID

GTIDはサーバーが異なっても重複しないユニークな番号ですが、MySQLMariaDBでは生成方法が異なります。

MariaDBdomain-id + server-id + サーバーごとのトランザクション番号
MySQLUUID + サーバーごとのトランザクション番号
MySQLのUUIDはWindowsでよく使われるあの長い322e3d16-355f-11e3-9ee3-00155d031304といった番号です。
MariaDBは従来のserver-idの前にdomain-idを付加することでユニークなものとしています。domain-idはmy.cnfで指定しますが何も指定しなければ0です。

実際の番号はそれぞれ以下のように文字列表現されます。

MariaDB 0-1-1234
MySQL 322e3d16-355f-11e3-9ee3-00155d031304:1234
MariaDBは要素をハイフンでつなぎます。MySQLはコロンでつなぎます。

GTIDを使うための設定

GTIDを使うためのmy.cnfの設定を説明します。

MariaDB/MySQL共通 server-id = 1
log-bin=mysqld-bin
binlog-format = ROW
log-slave-updates
MariaDB gtid_domain_id = 1 (書かなければ gtid_domain_id = 0)
MySQL gtid_mode=ON
enforce-gtid-consistency
赤色の値は、固定値ではなく任意に決めることのできる値です。それぞれの意味の詳細はMySQLのDocument等で確認いただければと思います。
MariaDBはGTIDの基本部分を自分で決めることができますが、MySQLは自動でUUIDを生成します。生成したUUIDはauto.cnfに記録されています。スレーブのデータをマスターからコピーする際にこれもコピーしてしまうと、UUIDが重複してしまうので、コピーしないようにします。

では実際の使い方を説明します。MariaDBMySQLでは異なっているのでそれぞれ分けて説明します。

MariaDBでGTIDを使う

MariaDBではバイナリログポジションがGTIDに代わっただけと考えてほぼ差支えありません。多少パラメータの指定方法が違いますが、従来の方法との互換性が高く、非常に使い易くなっています。
また、MariaDB 10.0以降では、server-idが指定されていれば自動でGTIDが振られているため、特別な設定はほとんどありません。GTIDでポジションを指定してもよいし、従来通り指定しても何ら問題ありません。

構成を行う前準備として、マスター/スレーブともにRESET SLAVE ALLRESET MASTERを事前に行い、現在のログをクリアしておくようにお勧めします。フェイルオーバーした際に、無効なログによる誤動作を防止できます。また、マスターの mysql.gtid_slave_posテーブルのレコードを必ず削除しておいてください。MariaDBのバグでRESET SLAVE ALLを行ってもその値をクリアしてくれません。ここに過去の意味のない情報が残っていると、ダウンしたマスターをスレーブ群に加える際誤動作します。

2つのGTIDポジションモード

スレーブがスタート時にマスターにバイナリログ内容の送信を要求する際には、2種類のGTIDポジションがあります。

  1. gtid_slave_pos:スレーブSQLスレッド*3が最後に実行したトランザクションのGTID
  2. gtid_current_pos:最後に処理したトランザクションのGTID

2つの違いを説明します。gtid_slave_posは、マスターから転送されたトランザクションのうち、最後に実行されたもののGTIDです。一方、gtid_current_posは、マスターから転送されたトランザクションだけでなく、スレーブで直接実行されたトランザクションも含めて最後に処理したもののGTIDです。
スレーブで直接実行されたトランザクションがなければ、gtid_slave_posgtid_current_posは同じ値になります。

SQLスレッドエラーの修復などで、スレーブで直接実行されたトランザクションがある場合は、異なった値になり得ます。
スレーブで直接実行されたトランザクションは、当然マスターには存在しません。そのため、gtid_current_posはマスターのバイナリログに存在しないGTIDの可能性があります。その場合はログ転送ができないため、スレーブはI/Oエラーで開始できません。

スレーブの開始時にgtid_current_posを使用する必要はないように思えますが、後述する「ダウンしたマスターをスレーブ群に加える」の際には便利です。

新規セットアップ

まず、マスターとスレーブのデータを事前に(データコピーやmysqldumpを使って)同じ内容にしておくのは従来と同じです。
レプリケーションを開始するバイナリログ名とポジションはCHANGE MASTER TOで指定していましたが、gtid_slave_posグローバル変数にGTIDを設定してからCHANGE MASTER TO master_use_gtid=slave_posとします。
設定すべきGTIDはマスターのselect @@gtid_binlog_pos;で取得します。また、Transactdではdatabase::beginSnapshot(binlogPos)binlogPos->gtidでスナップショットの開始と同時に取得できます。

SET GLOBAL gtid_slave_pos = "0-1-1234";
CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_useer", master_password="6789", master_use_gtid=slave_pos;
START SLAVE;

スレーブで、マスターを新マスターに切り替える

スレーブのSQLスレッドで最後に処理したGTID(このポジションをslave_posと呼びます)がわかれば、それ以降を新マスターに要求すれば済みます。しかし実際にはホストを指定し直すだけで、遅れていたスレーブも簡単に切替できます。なぜなら、gtid_slave_posグローバル変数には最後にSQLスレッドが処理したGTIDが入っているからです。
CHANGE MASTER TO master_host="master-host", master_port=3306でOKです。ポジションの指定は不要です。

スレーブをマスターに昇格させるときは、最後のGTIDを記録しましょう。旧マスターに未レプリケートのデータが残っていたときのためです。
もし、そのようなデータがある場合は、旧マスターが使えるようになったら一時的に新マスターを旧マスターのスレーブとしてそのGTIDで開始します。そうすれば、旧マスターにしかな残っていないデータを新マスターに補てんすることができます。(それができたらこの旧マスターをスレーブ群に追加することができます。)

余談ですが、仮にスレーブが従来のバイナリログ名とポジションを指定していたとします。それでも、CHANGE MASTER TO master_host="master-host", master_port=3306, master_use_gtid=current_posだけでGTIDを使ったマスター切替が行えます。

ダウンしたマスターをスレーブ群に加える

ダウンしたサーバーの回復が完了し、スレーブ群に加える際、このサーバーはスレーブになったことがないので、SQLスレッドで最後に処理したGTID(slave_pos)がありません。最後に処理したGTIDは自身が最後に処理したポジション(current_pos)です。この場合は、CHANGE MASTER TO master_use_gtid = current_posを指定します。ポジションの指定は不要です。
または、最後に処理したGTIDを select @@gtid_binlog_pos;で調べて、新規セットアップと同様に指定してもOKです。

SQLスレッドのエラーを修復する

MariaDBは従来通りの方法が使用できます。sql_slave_skip_counterでイベントをスキップするか、エラーの原因となっている問題を取り除いて再度スタートする方法です。ただし、MariaDB 10.0.12以前はsql_skip_counterが使用できなくなっていましたので、それ以降のバージョンにしましょう。
また、エラーの原因となっている問題を取り除く際、その処理がバイナリログに記録されない方がよい場合は、事前にSET sql_log_bin=OFFとし記録されないようにします。
テーブルやデータベースごとコピーし直したい場合は、以下の記事を参考にしてください。
bizstation.hatenablog.com

MySQLでGTIDを使う

MySQLでの使い方を説明する前に、MariaDBにはないGTIDのもう一つの目的「マスターとスレーブが一貫しているかどうかを判断する」について説明します。

GTIDセット

MySQLのGTIDによるログの転送は、従来のログファイル名とポジションで行っていたポイント指定(位置指定)でなく、マスター・スレーブのそれぞれで処理したすべてのトランザクション番号を保持し、それらが同じかどうかを比較し、不足分を要求するという方法です。
すなわち、ポイントを管理するのではなく、GTIDのセットを管理します。

この不足分を補う方法で「マスターとスレーブが一貫しているかどうかを判断する」を実現しようとしています。「しようとしています」としたのは、抜け道がたくさんあって「マスターとスレーブのデータが同一である」とは保証できないためです。

GTIDセットの表現方法

処理したGTIDをすべて列挙すると大変な量になってしまうため、連続したIDの場合「1-100」といったように最初と最後の番号のみを保持します。
具体的には、ID1~100であれば322e3d16-355f-11e3-9ee3-00155d031304:1-100のように表現します。マスターを切替えた場合などでは、スレーブの処理済みのGTIDセットには、最初のマスターの分と新マスターの分の両方が含まれます。GTIDセットは322e3d16-355f-11e3-9ee3-00155d031304:1-100,b17243ca-11c2-11e6-95ee-00ffcc08618a:1-1200のようにカンマで区切り列挙されます。

GTIDセットの比較

マスターとスレーブで適用されているトランザクションを比較しますが、適用されたGTIDセットをどこで確認するか説明します。

サーバー コマンド 名前
マスター SHOW MASTER STATUS Executed_Gtid_Set 322e3d16-355f-11e3-9ee3-00155d031304:1-100
スレーブ SHOW SLAVE STATUS Executed_Gtid_Set 322e3d16-355f-11e3-9ee3-00155d031304:1-100
Executed_Gtid_Setは、現在存在するバイナリログに記録されているすべてのGTID(グローバル変数gtid_executed)と、既に正しくパージ(削除)(gtid_purged)されたすべてのGTIDの両方を含めたものです。
Executed_Gtid_Setをクリアするには、マスター/スレーブに関わらず RESET MASTERを行います。RESET MASTERは同時にバイナリログを消去します。gtid_purgedは、gtid_executedが空の場合に限って変更可能です。
従って、マスターデータをスレーブにコピーしレプリケーションを開始する場合は、スレーブにてRESET MASTERを行ったあと、gtid_purgedにマスターのExecuted_Gtid_Setの内容を与えることで、双方の適用済みGTIDセットは同じであると判断させます。

GTIDと従来の方法の混在

MySQL 5.6では、GTIDを使う場合はすべてのサーバーでGTIDを使用しなければなりません。一度すべてのサーバーをシャットダウンして有効化する必要があります。5.7からはローリングアップデートと呼ばれる方法で順次有効化できるようです。(これについては未調査です。)

新規セットアップ

まず、マスターとスレーブのデータを事前に(データコピーやmysqldumpを使って)同じ内容にしておくのは従来と同じです。
MySQLのGTIDレプリケーションでは双方のGTIDセットを比較するので、gtid_purgedにマスターのExecuted_Gtid_Setをセットして、実行済みGTIDセット同じであるようにしてから開始します。
設定すべきGTIDは従来と同様マスターのSHOW MASTER STATUSで取得します。また、Transactdではdatabase::beginSnapshot(binlogPos)binlogPos->gtidで、スナップショットの開始と同時にGTIDセットを取得できます。

RESET MASTER;
SET GLOBAL gtid_purged="322e3d16-355f-11e3-9ee3-00155d031304:1-100";
CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_useer", master_password="6789", auto_position=1;
START SLAVE;

Executed_Gtid_Setは非常に長い文字列になる場合があり、従来のバイナリログ名とポジションより設定し易いとは言いがたいものです。
なお、mysqldumpを使用するとダンプファイルの最後の方にSET @@GLOBAL.GTID_PURGED='322e3d16-355f-11e3-9ee3-00155d031304:1-100';の一文を自動で含めてくれます。

スレーブで、マスターを新マスターに切り替える

これはとても簡単で、CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_user", master_password="password", auto_position=1;とするだけです。新マスターのGTIDセットとスレーブのGTIDセットを比較して、不足があれば補ってくれます。ポジションの指定は不要です。

スレーブをマスターに昇格させるときは、最後のバイナリログ名とポジションを記録しましょう。
もし、旧マスターにしかない未レプリケートデータが残っている分があった場合は、あとでその分を新マスターに補います。このとき、新マスターをもう一度旧マスターのスレーブにするだけでレプリケートしてくれるでしょうか? これはだめです。GTIDを使うとセット管理なのでわけのわからないことになってしまいます。ここは、従来のバイナリログ名とポジションを使って不足分だけをレプリケートしてください。

ダウンしたマスターをスレーブ群に加える

これもとても簡単で、CHANGE MASTER TO master_host="master-host", master_port=3306, master_user="rep_user", master_password="password", auto_position=1;とするだけです。新マスターのGTIDセットとダウンした旧マスターのGTIDセットを比較して不足があれば自動で補ってくれます。ポジションの指定は不要です。

SQLスレッドのエラーを修復する

結論から言うと、これはとても厄介です。マスター/スレーブの処理済みGTIDセットは同じでなければならず、不用意にスレーブに変更を加えると、その時はよくても、それがマスターになった際にはその変更が他のスレーブにも転送されてしまいます。内容によってはすべてのスレーブが停止してしまう原因になったりします。
また、違いをセットで比較する性質上sql_slave_skip_counterは使用できません。仮に使用できたとしても、GTIDセットに食い違いがでるのでその分を転送しようとしてしまいます。

そこで、エラーを起こすGTID番号のトランザクションの内容を他の内容に変えてしまう(代替)という方法で、SQLエラーを回避します。
これはMySQLの2つの特性を利用して行うものです。


これらを利用して以下のようにすると、sql_slave_skip_counterと同じように問題のイベントをスキップまたは修復できます。

SET gtid_next="322e3d16-355f-11e3-9ee3-00155d031304:101";
START TRANSACTION
... //ここに回復処理 単にスキップしたければ何もせずcommit
COMMIT
START SLAVE;

ここで、gtid_nextに指定するGTIDがいくつなのか調べる方法を説明します。
SQLエラーを起こした場合SHOW SLAVE STATUSLast_SQL_Errorにその内容が書かれます。しかし、バイナリログ名とポジションは記載されますが、GTIDはありません。Executed_Gtid_Setの末尾には最後に適用されたGTIDが書かれています。例えば322e3d16-355f-11e3-9ee3-00155d031304:1-101であれば322e3d16-355f-11e3-9ee3-00155d031304:101が最後のIDです。通常エラーを起こしたGTIDはこの次のID(102)です。

話は戻りますが、仮に先ほどのエラーを修正したスレーブがマスターになって、他のスレーブにその修正内容を適用するとまずい場合は、すぐに、

FLUSH LOGS;
PURGE BINARY LOGS TO 'mysql-bin.xxxx';

のようにして、ログからトランザクションの内容をパージしましましょう。遅れたスレーブがそのGTIDを要求すると「既にパージ済みで適用できない」といったエラーになってそのスレーブは停止してしまいますが、無用なトランザクションを適用して進んでしまうよりはマシなはずです。*4
もう一つの方法は、特定のテーブルやデータベースに限定できる場合にそれらのみ再コピーし、新規セットアップ同様に

RESET MASTER;
SET GLOBAL gtid_purged="322e3d16-355f-11e3-9ee3-00155d031304:1-100";
START SLAVE;

とすることです。これも以下の記事を参考にしてください。
bizstation.hatenablog.com

MySQLにおける「マスターとスレーブの一貫性の判断が容易」について

GTIDセットの一致を要求するMySQLの実装は、一見すると一貫性が保証されるように見えます。しかし、初期セットアップ時のデータ違いや、SQLスレッドのエラー修復など、運用管理の仕方によって簡単に矛盾した状態が生じます。この矛盾は従来の方法やMariaDBでも全く同様で、GTIDセットによる特別な効果はありません。

それでも、スレーブがバイナリログを書いたあとに(OSはsuccessを返したが)ディスクコントローラエラーなどで実際には記録されていなかった場合には、スレーブを再起動すると、マスターとスレーブのログの不一致を検出できるということはあるかと思います。すべての状況を考察したわけではありませんが、GTIDセットを比べることで得られるのは「スレーブで書いたはずのログに欠落があったとか、バグで転送されていない処理があった」などの検出でしょうか。

真のマスターとスレーブのデータの一貫性の保証は、従来通り双方のすべてのレコードをコンペアするしかないということには変わりないかと思います。

まとめ

  • GTIDのメリットとしてよく言われる「CHANGE MASTER TOバイナリログポジションをいちいち指定しなくてもよい」はスレーブがフェイルオーバーで昇格した新マスターに切り替える際の話であって、新規セットアップなどでは、バイナリログポジションに替わってGTID(セット)を指定する必要がある。(追記 新規レプリケーションセットアップ時にマスターのバイナリログも一緒にコピーすれば、自動でそのログをスキャンして無指定で開始することもできます。ただ、それであればそもそもすべて同じなのでGTIDによる恩恵とは言い難いものです。)
  • GTIDは、マスターと複数スレーブによる、マスターダウン時にフェイルオーバーする構成でメリットがある。
  • MariaDBのGTIDは、従来のバイナリログ名とポジションに番号を振っただけに近く、従来とほぼ同様の運用が行える。(sql_slave_skip_counterも使える)
  • MySQLのGTIDは、単一の位置情報でログを要求するのではなく、マスターとスレーブで適用されたすべてのトランザクション(GTIDセット)の比較で行われる。
  • MySQLで適用済みのGTIDセットを忘れさせるには、RESET MASTERを行う(しかない)。
  • MySQLのGTIDによる「マスターとスレーブの一貫性の判断が容易」は限定的であって、真のマスターとスレーブのデータの一貫性の保証はされない。

おまけ どっちがよいか?

レプリケーションマネージャを作成するために、数百回もレプリケーションを構築し、エラーを発生させ修復するといった作業を行ってきての感想です。

ポイントは「何かあったときの修復し易さ」です。
MySQLでGTIDを使う場合は、マスターデータとスレーブをきっちり同じにしてSQLエラーを絶対に起こさないように運用管理をしっかりしないと、フェイルオーバーしたあとで予期しない全スレーブのSQLスレッド停止なんてことになりかねません。また、そうなるとその修復でも問題が起き、収集がつかなくなります。

対してMariaDBは、もう少しラフに多少なにかあっても従来と同様に修復できます。sql-log-bin=offにして修復するか、もしくはgtid_slave_posモードであればその修復のことは忘れてくれます。

また、GTIDの使い始めもGTIDと従来の方法が混在可能なMariaDBはとても使い易いと思います。

MariaDBはデータの一貫性の保証はありません。MySQLには限定的な一貫性の保証の仕組みがありますが完全ではありません。ミッショクリチカルな用途での一貫性の保証はデータのコンペアなどをするのがベストかと思います。フェイルオーバーを中心に選択するなら、個人的には使いやすいMariaDBがいいです。

*1:昇格というと何か構成が変わるように見えますが、スレーブ機能をリセットし更新処理をそのサーバーに向けるだけのことです。他のスレーブがchange masterでそのサーバーを指定して初めて構成上のマスターになります。

*2:全く遅れがないスレーブ同士であれば、新マスターの show master statusで得られるポジションでOKです。しかし、遅れがあるスレーブだとこれではだめです。

*3:「スレーブSQLスレッド」についてはMySQL :: MySQL 5.6 リファレンスマニュアル :: 17.2.1 レプリケーション実装の詳細を参照してください。

*4:従来の方法やMariaDBの位置による管理の場合は、修復のための変更のあと、すべてのスレーブがより新しいポジションになれば、そのどれかマスター昇格した際にも修復のための変更のログが転送されることはないため、あまり問題にはなりません。

MySQLレプリケーション復旧 テーブルロックをせず特定テーブルのみコピーする その1

今回は、レプリケーションの問題発生時の復旧で、テーブルロックをせずに特定テーブルのみ再コピー(dumpとインポート)する方法について書きたいと思います。テーブルロックするとその間マスターサーバーの機能を制限してしまうの運用時にはなるべく避けたいものです。(この記事は、innoDBのデータベースやテーブルが対象です。)

初回のレプリケーションセットアップ

よくあるレプリケーションのセットアップ手順は

  1. マスターでサーバーIDとバイナリログを有効にする
  2. スレーブがアクセスするためのレプリケーションユーザーをマスターに作成する
  3. マスターでFLUSH TABLES WITH READ LOCKを開始する
  4. マスターでSHOW MASTER STATUSを実行し、binlogファイル名とポジションをメモする
  5. mysqldumpでマスターデータをダンプする
  6. マスターでUNLOCK TABLESを実行してロックを解放する
  7. スレーブでダンプしたデータをインポートする
  8. スレーブでCHANGE MASTER TOを実行してマスターホストとメモしたバイナリログポジションを登録する
  9. スレーブでSTART SLAVEを実行してレプリケーションを開始する

といった内容で、具体的には以下のようになります。

//my.cnfの設定 
server-id = 1
log-bin = mysqld-bin
binlog-format = ROW

//ユーザー作成、ロック、バイナリログポジション取得
master> GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%' IDENTIFIED BY 'password_here';
master> FLUSH TABLES WITH READ LOCK;
master> SHOW MASTER STATUS;  //--> File: mysqld-bin.000001 Position: 1101983 

//マスターデータダンプ
$> mysqldump -uroot -p --databases databaseA databaseB databaseC > ./master_dump.sql

//ロック開放
master> UNLOCK TABLES;

// -----   ここからスレーブで ----------------
//スレーブでのデータインポート
$> mysql -uroot -p --default-character-set=utf8 < ./master_dump.sql

//スレーブにマスター登録とレプリケーションの開始
slave> STOP SLAVE;
slave> RESET SLAVE;
slave> CHANGE MASTER TO MASTER_HOST = '192.168.0.2',MASTER_USER='replication_user',MASTER_PASSWORD='password_here',MASTER_LOG_FILE='mysqld-bin.000001',MASTER_LOG_POS=1101983;
slave> START SLAVE;

レプリケーションの不具合

運用中に何らかの問題でスレーブのSQLスレッドが書き込みエラーを起こすと、マスターとスレーブにデータの違いが出てしまいます。最初に試みる修復は、エラーの内容を見てスレーブのレコードを削除したり、SQL_SLAVE_SKIP_COUNTER変数を使ってイベントをスキップすることで行います。これで解消できればよいですが、データに矛盾が無いかどうか確認しにくかったり、複雑で手に負えないこともあります。そのような場合、マスターからコピーし直せば確実です。

しかし、初回と同じ手順で再度セットアップすると、機能制限の時間が問題になります。

  • マスターの書き込み機能を、FLUSH TABLES WITH READ LOCKの間停止しなければならない。
  • 1つのマスターに複数のデータベースがある場合、必要なすべてのデータベースをダンプとインポートしなければならない。特にデータサイズが大きい場合は多くの時間がかかる。

テーブルロックフリーな部分コピーによる復旧

マスターの機能制限の時間が最小限になるよう、テーブルロックフリーでの部分コピーによる修復方法を考えてみたいと思います。スレーブは壊れた状態なので、復旧担当以外アクセスできないようになっていることが前提です。

ログポジションの問題

大抵の場合、問題は特定のデータベースやテーブルに限定できることが多いと思います。そこで、全部のデータベースではなく一部のみのダンプとインポートを行うとどうでしょうか。
初回セットアップ時と同じ手順で、テーブルロックなしに部分的にコピーしようとすると、下記の問題が生じます。

  • スレーブを停止せずに行うと、SHOW MASTER STATUS以降の変更が重複してスレーブに適用されてしまう。(ログポジションをSHOW MASTER STATUS時点に戻すため)
  • 事前にスレーブを停止すると、停止時のログポジションからSHOW MASTER STATUSするまでの変更ログが無くなる。(ログポジションをSHOW MASTER STATUS時点に進めてしまうため)

部分コピーを行うには、データに矛盾を発生させないように、スレーブ停止時とダンプ時のログポジションが一致する、すなわち、SHOW MASTER STATUSで確認したログポジションまでスレーブに適用された状態でスレーブを停止する必要があることがわかります。

START SLAVE UNTIL

START SLAVEにはUNTILというオプションがあります。UNTIL master_log_file='xxxx', master_log_pos=xxxのように使用します。UNTILはSQLスレッドが指定したログポジションまで達したら停止してくれます。これを利用して以下の手順を加えます。

  1. STOP SLAVEを実行します。
  2. マスターでSHOW MASTER STATUSを実行し、スレーブでそのポジションをUNTILに加えてSTART SLAVEします。
  3. 問題が発生した場合は、SQL_SLAVE_SKIP_COUNTERに1を指定してから、再度START SLAVE UNTILを実行し、問題のあるイベントをスキップします。これを繰り返して問題をすべてスキップします。
  4. SHOW SLAVE STATUSを何度か実行して、指定したポジションまでログが適用されSQLスレッドが停止するのを待ちます。
  5. スレーブでインポートします。
  6. スレーブでRESET SLAVE、マスター再登録とレプリケーションの開始を行います。

これで、サーバーの一部のデータベースのみ矛盾なくコピーし直すことができます。ただし、SHOW MASTER STATUS時点のコピー元データが必要です。これを現在のMySQLで実現するには2つの方法があります。

  1. 事前にFLUSH TABLES WITH READ LOCKしてダンプする。
  2. 事前にFLUSH TABLES WITH READ LOCKしてその間にスナップショットを開始しダンプする。

ちなみに、START SLAVE UNTILMySQL 5.5~5.7、MariaDB 5.5~10.1 で使用できます。

テーブルロック時間を最小限でダンプする

mysqldumpには、バイナリログポジションを同時に取得する--master-dataオプションがあります。これを使うと、自分でFLUSH TABLES WITH READ LOCKを発行する必要はありません。しかしながら、--master-dataオプションは内部でこのコマンドを使っていますので、完全にテーブルロックなしにダンプすることはできません。以下はその部分を示すmysqldumpのソースコードの抜粋です。

// mysql-5.6.20 mysqldump.c : 5797
  if ((opt_lock_all_tables || opt_master_data ||
       (opt_single_transaction && flush_logs)) &&
      do_flush_tables_read_lock(mysql))
    goto err;

--single-transactionオプションを使うと、ロック開始後にスナップショットを取り(start_transaction)、SHOW MASTER STATUSしてすぐにロックを開放します。これによりロック時間をほんのわずかな時間にすることができます。

// mysql-5.6.20 mysqldump.c : 5827 
  if (opt_single_transaction && start_transaction(mysql))
    goto err;

  /* Add 'STOP SLAVE to beginning of dump */
  if (opt_slave_apply && add_stop_slave())
    goto err;


  /* Process opt_set_gtid_purged and add SET @@GLOBAL.GTID_PURGED if required. */
  if (process_set_gtid_purged(mysql))
    goto err;


  if (opt_master_data && do_show_master_status(mysql))
    goto err;
  if (opt_slave_data && do_show_slave_status(mysql))
    goto err;
  if (opt_single_transaction && do_unlock_tables(mysql)) /* unlock but no commit! */
    goto err;

 // ...このあとダンプ開始

初回の方法よりはテーブルロックを十分短い時間にできるので、

$>mysqldump -uroot -p --master-data --single-transaction databaseA tableA tableB > ./master_dump.sql

のようにダンプしましょう。
しかし、START SLAVE UNTILを実行するためには、ダンプファイルを開いてログ名とポジションを自分で読み取らなければなりません。少々面倒ですね。

ここまでの方法は、mysqlクライアントとmysqldumpを使って行うことができます。後述のまとめに手順一式を書いておきます。

テーブルロックをせずコピーする

ここまで見てきた通り、mysqldumpでは、完全にテーブルロックをせずスナップショットとその時点のログポジションを取得することはできません。
しかし、Transactdのnsdatabase::beginSnapshotメソッドを使えば、テーブルロックをせずにスナップショットを開始し、その時点でのログポジションを取得できます。

なぜテーブルロックフリーで取得可能なのでしょうか。
MySQLはXAトランザクションを使ってバイナリログとinnoDBの処理結果に矛盾が無いようにしています。XAのコミットは、最初にcommitロックを取得してシリアライズされます。Transactdnsdatabase::beginSnapshotは、このcommitロックを使って以下のように処理します。

  1. commitロックを取得(他の書き込みブロック)
  2. ログポジションを取得
  3. スナップショットを開始
  4. commitロックを開放

commitロックは通常の書き込み処理と同様のロックなので、テーブルをロックすることはありません。他の処理を止めることなくスムーズに処理されます。
あとは、このスナップショット中にマスターデータを読み取ってスレーブにコピーします。
これで完全にテーブルロックフリーでコピーできます。マスターはほんのわずかな機能停止もありません。
具体的なコードは次回改めて詳しく書きたいと思います。

まとめ

mysqlクライアントとmysqldumpを使って、可能な限りロックを短くしたレプリケーションの部分コピーを行う方法は以下のようになります。
例としてdatabaseAtableAtableBのみコピーします。「障害はtableAtableBでのみ発生している」と特定できていることが条件です。

/*
 databaseA の tableA と tableB のみ再コピーするサンプル
 master> はマスターのmysqlクライアントでの処理
 slave>  はスレーブのmysqlクライアントでの処理
*/
//スレーブ停止
slave> STOP SLAVE;
//マスターデータダンプ (ロック、スナップショット、binlog posの取得)
$> mysqldump -uroot -p --master-data --single-transaction --add-drop-table databaseA tableA tableB > ./master_dump.sql
// ./master_dump.sqlからbinlogファイル名とポジションを読み取ってメモしておく
slave> START SLAVE UNTIL master_log_file='xxxx', master_log_pos=xxx; //ファイル名とポジションは先ほどメモした値
// -- ここから エラーが発生した場合の処理 --
slave> STOP SLAVE;
slave> SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
slave> START SLAVE UNTIL master_log_file='xxxx', master_log_pos=xxx; //ファイル名とポジションは先ほどメモした値
// -- エラーが発生した場合の処理 ここまで --
//    以上をエラーが発生するたびに繰り返し、エラーをすべてスキップする
slave> SHOW SLAVE STAUS;  // Exec_Master_Log_Posが指定したポジションになってSQLスレッドが停止するまで待つ
//スレーブ停止 ダンプしたデータと同じログポジションまで適用されている状態
slave> STOP SLAVE;
//スレーブでのデータインポート
$> mysql -uroot -p --default-character-set=utf8 < ./master_dump.sql
//スレーブへのマスター登録とレプリケーションの開始
slave> RESET SLAVE;
slave> CHANGE MASTER TO MASTER_HOST = '192.168.0.2',MASTER_USER='replication_user',MASTER_PASSWORD='password_here',MASTER_LOG_FILE='xxxx',MASTER_LOG_POS=xxx;
slave> START SLAVE;

今回はタイトルにある「テーブルロックをせず」まで至っていません。次回はTrasnactdを使って完全にテーブルロックしない方法でのスクリプトを紹介します。エラーのスキップも簡単にできてとても便利なスクリプトです。

追記 (2016/05/27) その2を公開しました。
bizstation.hatenablog.com

Transactd Plugin PHP Ruby C# C++のチュートリアル

MySQL用NoSQLプラグイン Transactdのチュートリアルで使用するプログラミング言語の種類を拡充しました。
この中に慣れた言語がありましたら、是非試してみてください。SQLに比べてとても高速に処理ができるようになります。

Transactd チュートリアル


今まではJScriptしかありませんでしたが、ようやくPHPRubyC#C++の4つの言語を追加することができました。
また、内容も細かく見直しています。

内容は、このような構成です。

  • プラグインとクライアントの準備
  • プログラム言語ごとの準備
  • データベースとテーブルの操作
  • SQLライクな読取と結果セットの操作
  • データベースとテーブルの作成・削除
  • サンプルコードダウンロード

簡単なハウツー本のような内容になっています。
ダウンロード用のサンプルコードは、エラー処理も含め実用に耐えるコードにしています。

できれば、このチュートリアルを使って、ハンズオンセミナーなどもやりたいなと考えています。関連するセミナーなどで「うちでやって」とか「この時間空いてるよ」などご意見ありましたら是非お寄せください。

MySQL 5.7 + Transactd スループット117万QPSを記録


ようやく24コアのマシンでTransactdのベンチマークを取ることができました。
Xeon E5-2697 V2 2.7GHz 24コア48スレッドの物理マシンです。32または36コアでできれば良かったのですが、お借りできたこのマシン*1ベンチマークを行いました。

結果はタイトルの通りで、パフォーマンスの改善されたMySQL 5.7.7にて驚きの117万QPSを記録しました。
以下はその結果グラフです。横軸がクライアント数、縦軸が1秒間に処理したクエリー(読み取りレコード)数です。

f:id:bizstation:20150430111654p:plain
memcached-pluginなどと基本的な考え方は同じですので、100万QPSを出すには48コア位は必要かと思っていましたが、24コアでこの値には少し驚いています。

MySQL 5.6.20でも87万QPSを出しました。5.7の結果が良いので5.6が少なく見えますが、24コアで87万QPSは好結果です。
48コアマシンだったら、MySQL 5.7とTransactd 2.4で200万QPSも可能ではないかと思います。

このテストのポイントについていくつか説明しておきます。

テスト環境

MachineFujitsu RX300 S8
CPUXeon E5-2697 V2 2.7GHz 24Core
Hyper-ThreadingON
memory24GB
OSWindows Server 2012 R2
Transactd Version2.4(Not yet release)
NetworkLocal pipe (shared memory)

テスト方法

ベンチマークプログラム

ベンチマークプログラムは、シングルプロセスで2~48個のスレッドにてランダムなidのプライマリーキー値でサーバーにアクセスします。それぞれのスレッドは無限ループでアクセスを開始し、5秒間の計測シグナルがONの間にアクセスできた数を返すようになっています。
OSに制御を返す時間がほとんどない連続アクセスのため、論理コア数以下でスループットの劣化が始まります。
(ときどき、数百クライアントまでスケールする結果があったりしますが、それはサーバーの処理スレッドがOSに制御を返す空き時間が多い処理(通信)方法によるもので、空き時間がなければ論理コア以上にスケールすることはありません。)

ネットワーク

他のMySQL用のNoSQLプラグインでもベンチマークはローカルマシン内で行っています。これはネットワークレイテンシと負荷を軽減するためです。Transactdのパケットは数十バイト程度の小さなものが多く、1Gbpsのネットワークでも1Gbpsのスループットを出すことはできません。ネットワークのレイテンシによって、10~30万パケット程度で頭打ち*2になってしまいます。また、ネットワーク処理の負荷も問題になるため、今回のテストも他と同様にサーバーとクライアントは同一マシンで行っています。*3

OSはWindowsで行いました。TransactdのWindows版は共有メモリとイベント送受信を使ったプロセス間通信を行うことができます。これで通信のレイテンシと負荷を最小限にすることができます。

f:id:bizstation:20150430110038p:plain

この方法はTCPのレイヤーを一切介さず、非常に高速に通信します。

TCPでの接続においても、ネットワーク処理を除いたデータアクセスに24コアが割り当てできれば、Linux/Windowsともにほぼ同様な結果がでると思います。それには、低負荷で高速なネットワークカードが必要です。逆に言えば今回の結果はそれを示していると言えます。

Transactdの高速性を真に発揮するには高速なネットワークカードも併せて利用していただけたらと思います。

MySQL 5.7

MySQL 5.7はスループットの改善が顕著でした。Transactdのコードは5.6と5.7のインタ-ーフェースの差異を吸収するだけの違いしかなく、基本的に同じものです。それにもかかわらず大きな性能差が出ています。特に、クライアント数が物理コア数を超えたあたりからの差が大きく、Hyper -Threadingによる論理コアをうまく使えているようです。InnoDBの改善が進んでいるのがわかります。
これだけの差があると、スループットをすぐにでも上げたい場合5.7はお勧めです。

5.7は既にRC版で、間もなくGA版になるかと思います。早期の導入には不安もありますが、TransactdはMySQLのhandlerインターフェース以下の層しか使わないので、潜在的なバグもSQLでのアクセスより少なくなります。また、TransactdのAPIは十分にテストされますのでチェックも2重に働いています。

まとめ

Transactd APIにて、性能劣化を起こすことなくInnoDBのパフォーマンスを引き出せていることが数字で確認できました。

今回のテストはidによる読み取りなのでkey-valueアクセスです。このようなkey-valueアクセス用途はもちろんですが、Transactdはフル機能のAPIを備えています。複雑な集計やトランザクションもすべて高速に行えます。
みなさんも、Transactdで最高パフォーマンスのWebアプリケーションを作ってみてください。今回のテストは開発中のVersion2.4で行いましたが、パフォーマンスに関しては2.3も同様です。

共有メモリを使用したWindowsローカルでの処理は非常に高速です。BizStationでは、大きなデータで時間のかかるデータのマイグレーションなどの際に、RAMディスクと併用して極端に短時間で処理するなどに利用しています。

また、MySQL 5.7の読み取りスループットの改善は本当に大きく、リリースされたらBizStationでも積極的に使っていこうと思います。
MySQL 5.7 + Transactdのこのスピードはちょっとわくわくします。

*1:ダイワボウ様、富士通様ありがとうございます。

*2:100バイト ☓ 30万パケット ≒ 240Mbps

*3:最近の10Gbpsネットワークカードのテストなどを見ると小さなパケットでも100万PPSを超えるようです。これであれば、ネットワークがボトルネックになることを回避できるかと思います。