MySQL5.6のordered_commit()とHA_IGNORE_DURABILITYとinnodb_flush_log_at_trx_commit


MySQL 5.6 ではinnodb_flush_log_at_trx_commitの意味が MySQL 5.5 と違う』で、論理飛躍*1や間違い*2があったので、結論が正しいかどうかチェックしてみた。


なお、flushというかfsync()の呼び出しについて、および上記の記事に対する結論はこちらに書いたので参照。

ソースコードのトレース

MYSQL_BIN_LOG::ordered_commit()からtrx_commit_in_memory()まで辿ってみる。

(1) MYSQL_BIN_LOG::ordered_commit()

ここのStage 3で finish_commit()を呼ぶ。

 /*
  Stage #3: Commit all transactions in order. 
   */
 ... 略 ....
 
  (void) finish_commit(thd);

 ... 略 ....
(2) MYSQL_BIN_LOG::finish_commit()

内部でha_commit_low()を呼ぶ。

 ... 略 ....
   /*
      storage engine commit                               
    */
    if (thd->commit_error == THD::CE_NONE &&
        ha_commit_low(thd, all, false))
      thd->commit_error= THD::CE_COMMIT_ERROR;
 ... 略 ....
(3) ha_commit_low() @ sql/handler.cc

forループでhandlertonを拾ってcommit()を実行するのでわかりにくいが、InnoDBの場合は storage/innobase/handler/ha_innodb.ccのinnobase_commit()が実行される。

 ... 略 ....

  if (ha_info)
  {
    for (; ha_info; ha_info= ha_info_next)
    {
      int err;
      handlerton *ht= ha_info->ht();
      if ((err= ht->commit(ht, thd, all)))
      {
        my_error(ER_ERROR_DURING_COMMIT, MYF(0), err);
        error=1;
      }
      ... 略 ....
    }
 ... 略 ....
(4) innobase_commit()@storage/innobase/handler/ha_innodb.cc

ここがポイントなのだが、innobase_commit_low()を呼び出す前後で、flush_log_laterをTRUE、FLASEと明示的に設定している。
そして最後にtrx_commit_complete_for_mysql()を実行している。

 ... 略 ....
                /* Don't do write + flush right now. For group commit 
                 to work we want to do the flush later. */
                trx->flush_log_later = TRUE;
                innobase_commit_low(trx);
		trx->flush_log_later = FALSE;

                if (innobase_commit_concurrency > 0) {
                        mysql_mutex_lock(&commit_cond_m);
                        commit_threads--;
                        mysql_cond_signal(&commit_cond);
                        mysql_mutex_unlock(&commit_cond_m);
                }

                trx_deregister_from_2pc(trx);

                /* Now do a write + flush of logs. */
                trx_commit_complete_for_mysql(trx);

 ... 略 ....

ということで、innobase_commit_low()とtrx_commit_complete_for_mysql()を確認する。

(5) innobase_commit_low()以降

innobase_commit_low()の中からざくっと辿って行くと、次の経路でtrx_commit_in_memory()に到達する:

  1. trx_commit_for_mysql()
  2. trx_commit()
  3. trx_commit_low()
  4. trx_commit_in_memory()

以下、trx_commit_in_memory()の一部を掲載。

                if (trx->flush_log_later) {
[1]                        /* Do nothing yet */
                        trx->must_flush_log_later = TRUE;
                } else if (srv_flush_log_at_trx_commit == 0
                           || thd_requested_durability(trx->mysql_thd)
                           == HA_IGNORE_DURABILITY) {
[2]                        /* Do nothing */
                } else {
[3]                        trx_flush_log_if_needed(lsn, trx);
                }

"trx->flush_log_later = TRUE"でtrx_commit_in_memory()に突入するから、ifの条件分岐は[1]。
この文書による解説では[2]にいくことになっているが、そうはならない。


確かに[1]は"trx->must_flush_log_later = TRUE;”をするだけ、[2]は"do nothing"なので、flush処理をしないという意味のおいては等しい。しかし、この文書の結論:

「HA_IGNORE_DURABILITY の時は flush_log_at_trx_commit=0 のように振る舞い、
フラッシュ (fsync) を実行しません。」

はおかしい。

  • この条件分岐には到達しないから、HA_IGNORE_DURABILITYもinnodb_flush_log_at_trx_commitも無関係。
  • innodb_flush_log_at_trx_commit=0は1秒毎にwrite+flushするモードだけど、そんな動きはしないので「innodb_flush_log_at_trx_commit=0のように振る舞」わない。


この件については、こちらの追加調査も参照のこと。


(6)trx_commit_complete_for_mysql()

次は本丸のtrx_commit_complete_for_mysql()。

UNIV_INTERN
void
trx_commit_complete_for_mysql(
/*==========================*/
        trx_t*  trx)    /*!< in/out: transaction */
{
        ut_a(trx);

	if (!trx->must_flush_log_later
	    || thd_requested_durability(trx->mysql_thd)
	       == HA_IGNORE_DURABILITY) {
                return;
        }

	trx_flush_log_if_needed(trx->commit_lsn, trx);

	trx->must_flush_log_later = FALSE;
}


先にtrx_commit_in_memory()の[1]で"trx->must_flush_log_later = TRUE;"を設定してるけど、HA_IGNORE_DURABILITYなのでreturnしてflush処理をスキップしている。
確かにここでは「HA_IGNORE_DURABILITYだからflushをしない」。

結論

上記を要約すると:

  1. MySQL5.6のMYSQL_BIN_LOG::ordered_commit()は次の順番でflush処理のメソッドを呼び出すが‥
    1. finish_commit()
    2. ha_commit_low()
    3. innobase_commit()
      1. innobase_commit_low()-> trx_commit_for_mysql()-> trx_commit()-> trx_commit_low() -> trx_commit_in_memory()
      2. trx_commit_complete_for_mysql()
  2. trx_commit_in_memory()はflush_log_laterが設定されてるので、トランザクションログはflushされない
  3. trx_commit_complete_for_mysql()はHA_IGNORE_DURABILITYなので、トランザクションログはflushされない


ということで、

  • MySQL5.6のMYSQL_BIN_LOG::ordered_commit()はトランザクションログをflushしない。innodb_flush_log_at_trx_commitの値は無関係。
  • trx_commit_for_mysql()はordered_commit()以外からも呼び出され、そのときは従来通りinnodb_flush_log_at_trx_commitに依存したflush処理を行う。


要するに、innodb_flush_log_at_trx_commitはInnoDBREDOログ同期書き込みに関するパラメータなのに、バイナリログのグループコミット絡みの部分だけみてなにか結論めいたことを言ってもあまり意味がない。


COMMIT時のREDOログとバイナリログの書込みとfsync()の呼び出しについて、および上記の記事に対する結論はこちらに書いたので参照のこと。

ACIDのD

この文書からリンクされているmysqlのML。

http://lists.mysql.com/internals/38707に、理解不能だった文面。

> My questions are:
>  - in MySQL 5.6 and MariaDB 10.0, what are the fsyncing requirements
> for storage engines during prepare and commit?

In MySQL 5.6, the storage engine have to sync on prepare (or make the
state durable some other way).

On commit, the storage engine can either sync or not. If the storage
engine decides to not sync and there is a crash, crash recovery will
commit the prepared transaction if it was written to the binary log and
roll it back otherwise.

「クラッシュリカバリはバイナリログ(とUNDOログ)」?

MySQL5.6におけるInnoDBトランザクションログの位置づけが分からない。
確かにInnoDB専用のトランザクションログと、全体にかかわるバイナリログを同時に書き込み続けるのが、性能の足を引っ張るのはわかるけども、それってマルチストレージエンジンを選択した宿命であって、MySQLはそれを背負って生きて行くのだとばかり思っていた。

確かにバイナリログにCRCチェックを設けるなど、信頼性を高める施策もなされているけども……
未だに自分の理解が正しいのか疑心暗鬼。MySQLのやることは斜め上過ぎてよく分かりません。

*1:「HA_IGNORE_DURABILITYだからtrx_commit_in_memory()でinnodb_flush_log_at_trx_commit=0と同じ条件分岐する」はおかしい。後述するように、その前の条件flush_log_later==TRUEに分岐する。

*2:HA_IGNORE_DURABILITYを条件分岐に使ってるのは、trx_commit_in_memory()だけでなく、trx_commit_complete_for_mysql()もある。