特殊な構成のMySQLに対するDDL適用の一例

※この記事は、"Blog Series of Introduction of Developer Productivity Engineering at Mercari"の一環で書かれています。

Author: CoreSREチーム @ichirin2501

今回は特殊なレプリカDBの制約をどう突破してオンラインでDDL適用を実現しているのかを紹介します。

はじめに

現在メルカリでは様々なシステムが動いていますが、メルカリのサービスの中核部分はモノリスなPHPで書かれたシステムで、そこのデータベースはMySQL(v5.7)で運用されています。依然としてマイクロサービス移行を加速させるためのコードフリーズ(*1)が継続されているため、PHPシステム上での機能開発は基本的にはありませんが、依存されているマイクロサービス側の機能開発の都合などで、どうしても変更が必要な場合があります。そういったときにテーブル追加やカラム追加などのDDL系の作業がモノリス側のデータベースでも発生したりします。MySQLの機能として提供されるDDLを単に実行すると、負荷調節が出来なかったり変更内容によっては書き込みがブロックされるので、オンラインスキーマチェンジ系のツール(pt-online-schema-change(以下pt-osc)やgh-ostなど)を使ってオンラインでDDLを適用する方法があります。ただ、メルカリには特殊なレプリカDBが存在し、それらのツールをそのまま実施するとそのDBが壊れてしまう問題がありました。

DB構成(一部)について

以下はモノリスPHPシステムから利用しているDB構成の一部を表したものです。

先の特殊なレプリカDBというのは上図のtriggerが定義されたレプリカDBのことです。triggerとはテーブルで特定のイベントが発生したときに動作させる処理で、その機能を利用して他とは異なる状態を持つレプリカDBを構築しています。また、メルカリのMySQLは現在v5.7ですが、binlog formatはdefault値のROWではなくMIXEDを使っています。理由としては歴史的な流れもありますが、レプリカDB側でのみに定義されているtriggerはrow-based replicationでは機能しないため(*2)、上図の特殊レプリカDBを維持するためにはbinlog formatをROWに変更できないからです。MIXEDでもSQLによってはunsafe判定となりROW formatにfallbackするケースがありますが(*3)、可能な限りそのようなSQLを避けつつ、想定外のfallbackを監視して運用でカバーしています。

オンラインスキーマチェンジのツール(pt-osc,gh-ost)を使うと特殊レプリカDBが壊れる理由

pt-oscやgh-ostでは最初にDDL適用済みの別名空テーブルを作成し、既存テーブルのデータをコピーする処理があります。この処理時に発行されるSQLが INSERT… SELECT文(*4)を使う実装になっており、MySQLにとってはunsafe判定となるSQL文のためbinlog formatがROWにfallbackしてしまいます。そのため、レプリカDB側の別名テーブルにtriggerが定義されていたとしても、そのtriggerは動作せず、同等のデータが生成されないため特殊レプリカDBの状態を維持することが出来ません。

MySQLのtriggerとDB構成による制約は明らかなので、オンラインスキーマチェンジのツールを使ってDDL適用を達成するためにツール側の仕組みに踏み込んでいきます。

gh-ost実行時の処理の流れ

メルカリのCoreSREチームではインタラクティブな操作が簡単に出来るgh-ostを採用したので、ここでは一旦gh-ostを前提に話を続けます。

以下はgh-ost実行時を簡単に表したものです。

gh-ostはROW formatのbinlogを利用するので、gh-ostがbinlogを取得する対象DBはROW formatのものを使います(ROWではない場合はgh-ost自身がDB設定を変更するオプションもあります)。元のテーブル(table)定義に対して、DDLを適用した別名の空テーブル(table_gho)を作成し、元のテーブルから既存データをコピー(rows copy)します。その間にも元テーブルにはアプリケーションから更新処理があるので、gh-ostはその更新差分をbinlogから読み込んでtable_ghoに適用し続けるようになっています。詳細はgh-ostの公式docを見ると良いでしょう。
gh-ostがbinlog_format MIXED内でROWにfallbackするINSERT…SELECT文を発行するのはrows copyの処理のときだけです。gh-ostが更新差分を適用する処理は別のレプリカ側のbinlog_format ROWの内容から実際の値を直接利用したSQL文のため、そのSQL自体がunsafe判定されてROWにfallbackすることがありません。つまり、マスターDBから出力されるbinlog内ではその更新差分の内容に関してはレプリカ側のtriggerが作動する条件を満たすStatement formatで記述されます。アプリケーションからはunsafe判定になるようなLIMIT句なしの更新SQLやUUID()関数など(*5)が利用されたものが発行されていても、その更新差分をgh-ostがtable_ghoに適用するときにはunsafe判定にならない形式のSQLで処理されるようになっています。そして、元テーブルからのrows copy処理が終わると、アプリケーションからの更新差分を適用する処理だけが残ります。

gh-ostを利用したDDL適用手順

まず、triggerが作動しないrows copy分の同等の処理をどうするかについてはメルカリ特有の事情として、今回の特殊レプリカDBのtriggerはロジックが簡単だったため、本来のデータを使えば生成されるデータの再現可能、かつ、同時にtable_gho側にも同等のtriggerを定義して作動させても問題がなく、解決が可能でした。これに関してはtriggerで定義されているロジック次第なため、今回紹介する方法が使えないケースも多々あるだろうと思います。

これらを整理すると、この特殊レプリカDBを維持したままgh-ostを実行するためのkeyポイントは3つです。

  • gh-ostの処理によってbinlogがROW formatにfallbackするのはrow copy phaseのみ
  • 本来のデータを使えばtriggerによって生成されるデータは再現可能
  • table_gho側に同等のtriggerを定義して同時に作動しても問題がない

gh-ostの最後のテーブル切り替え(cut-over)はこちらのタイミングで実施出来るので、row copy phase後に、replicationを停止し、gh-ostが生成したtable_ghoテーブル(本来のデータ)を使ってデータの再現処理を行い、table_ghoテーブルにtriggerを定義してreplicationを再開すれば良いことになります。

以下はgh-ostによるcut-over直前の状態の図

以下はcut-over後の図

table’はDDLが適用されていたtable_ghoテーブルがrenameされたテーブルのことです。

多少手間にはなってしまいますが、この方法でメルカリの特殊なレプリカDBを維持したまま、オンラインスキーマチェンジのツールの恩恵を受けています。

おわりに

特殊な構成の多くには制約があり、様々なリスクを抱え込むことがあります。特殊な構成になる経緯は色々ありますが、その当時、価値があると判断されたであろうことも忘れてはいけない、と思いながら日々サービスの改善に取り組んでいます。

CoreSREチームでは今回のような所謂レガシーシステムと向き合う機会が多いですが、システムとして巨大であったり、改善ポイントがたくさんあります。一緒にサービスの改善をすすめるメンバーを募集しているので、興味がある方は是非ご応募ください。インターンも募集中です。

参照

  1. メルカリのマイクロサービス移行の進捗 (2019年冬)
  2. https://dev.mysql.com/doc/refman/5.7/en/replication-features-triggers.html
  3. https://dev.mysql.com/doc/refman/5.7/en/replication-rbr-safe-unsafe.html
  4. https://dev.mysql.com/doc/refman/5.7/en/insert-select.html
  5. https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html
  • Twitter
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加