サービス無停止を実現するデータ移行戦略

サービス無停止を実現するデータ移行戦略

はじめに

こんにちは、ECプラットフォーム部会員基盤ブロックのturbofishです。弊社ではモノリスのプログラムで動いているZOZOTOWNをマイクロサービス化する取り組みを行なっており、複数チームが1つの大きなオンプレシステムをマイクロサービスでリプレイスしています。その中で私が所属する会員基盤ブロックでは、ZOZOTOWNの会員情報を管理するマイクロサービスを開発しています。

本記事では、弊チームを含む複数のマイクロサービス開発チームにおいて、既存のアプリケーションの一部をマイクロサービスを使用する処理に置き換えた際、サービス無停止でオンプレ環境にあるDBからマイクロサービスが使用するクラウド環境のDBにデータを移行した戦略を紹介します。

ディスクレイマー

本記事で紹介するデータ移行方法には下記の制約があり、全ての状況に対応できるわけではありません。

  • DBへの書き込み処理と読み取りの処理の実装リリースタイミングを分けられない場合は実行できません
  • プライマリキーを持たず、テーブル内のレコードを一意に識別できない場合はデータの正しさを保証できません

また、この記事で紹介する方法は、手間をかけてでもデータ移行の際にサービス停止を発生させないことにフォーカスしています。同様の戦略を検討する際は、データ移行を実行する環境において全ての過程を実現できるか、サービス停止を回避するためにどの程度の手間をかけられるかなどを考慮の上、状況に応じた戦略を決定する必要があります。

本記事において、随所で「オンプレ」「クラウド」という単語を用いていますが、サーバーの場所は本記事で紹介する戦略に影響しません。あくまで本記事に登場する複数のDBを見分けるための名前だと考えてください。

目次

データ移行の背景と前提

ZOZOTOWNのマイクロサービス化の背景については、過去の記事をご覧ください。

techblog.zozo.com

弊社では各マイクロサービスが専用のDBを持ち、リプレイスを通じてDBMSとDBスキーマを変更したいと考えていました。そのため、マイクロサービスを使用する実装をリリースする際に、オンプレ環境で使用しているDBからマイクロサービスで使用するクラウド環境にあるDBへ、データをコピーする必要がありました。
以降、オンプレ環境で使用しているデータ移行元のDBを「オンプレDB」、クラウド環境にあるマイクロサービスで使用しているデータ移行先のDBを「クラウドDB」と表記します。また、オンプレDBからデータを抽出し、クラウドDBへ格納するETL処理のことを「データ移行」と呼びます。

本来、安全にデータを移行するためには、一旦サービスを停止してDBに書き込みがされない状態にしてからデータをコピーすることが最も確実です。特に会員基盤チームでは、ユーザーの個人情報を含むデータを扱うため、データの欠損や不整合は許されませんでした。しかし、ZOZOTOWNは非常にアクセスが多く、サービス停止による機会損失が大きいことから、多少の手間を許容してでもデータ移行に伴うサービス停止を無くす方法を考える必要がありました。

データ移行の要件と課題

弊社のマイクロサービス化のプロジェクトに共通する、データ移行の要件は下記の通りです。

  • 移行元と移行先のDBMSの種類が異なる
    • オンプレDBはMicrosoft SQL Server、クラウドDBはAmazon Aurora MySQLを使用
  • 移行元と移行先のDBでスキーマが異なる
    • 移行元のDBは長い歴史を経て最適な状態ではなくなっており、マイクロサービス側のDBでテーブルを新設する際にスキーマを再設計する必要がある
  • データ移行1回につき、扱うデータ量は大体の場合多くても3千万件程度
  • ダウンタイムなしでデータを移行する必要がある

上記の要件を考慮し方針を検討したところ、下記の課題がありました。

  • DBの種類やテーブルスキーマが違うため、レプリケーションによって複製したDBをそのまま用いることができない
  • 移行過程でテーブルのスキーマ変更に対応するため、柔軟なデータ変換を実現する必要がある
  • マイクロサービス化を推進するためにも、移行元であるオンプレDB依存なアーキテクチャにはしたくない
    • CDC(Change Data Capture)のような仕組みは極力採用したくない

採用した戦略

単純にDBやデータをコピーするだけでは、データの正しさとサービス無停止を両立することは難しいと考えられました。そこで、下記の通りアプリケーションレイヤーで工夫することで、上述した課題を全てクリアできると考えました。

  • オンプレDBに書き込みを行う処理を修正し、オンプレとクラウドの両DBにアトミックに書き込みを行うよう実装する
  • データ移行でオンプレDBのデータをクラウドDBにコピー。この時、アプリケーションはオンプレDBのデータを参照している状態
  • オンプレDBのデータを参照している処理を修正し、マイクロサービスAPIからデータを取得するよう実装する

戦略決定の背景

データを移行してからマイクロサービスAPIを使用するアプリケーションの実装をリリースすると、データ抽出の開始時から実装リリースまでの間の時間に発生するデータ変更処理がクラウドDBに反映されません。つまり、サービス停止をせずにデータの正しさを担保するためには、アプリケーション実装リリースの後にデータを移行する必要がありました。

一方で、その場合アプリケーション実装リリース時にはクラウドDBに全くデータが存在しない状態となります。そのため、マイクロサービスのデータを参照せず、DBへの書き込み処理のみをオンプレとクラウド両方のシステムで行われるよう実装することで、サービスにクラウドDBの状態を表出させることなくデータ移行の時間を確保しようと考えました。正しくデータがDBに書き込まれる状態にしてからデータを移行することで、両方のDBに正しく全てのデータが揃っている状態を実現します。最後にクラウドDBのデータを参照する実装をリリースし、データ移行の過程は完了します。

具体的な移行手順

データ移行のために実行した手順をまとめると、下記の通りです。

  1. データ調査と(必要に応じて)データ修正
  2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正
  3. データ移行手順のテスト実行
  4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納
  5. 一時DBに格納されたデータをクラウドDBへ格納
  6. クラウドDBに保存されたデータの検証
  7. データを参照する処理を、マイクロサービスAPIを使用するよう修正

以下、それぞれの過程を具体的に説明します。

1. データ調査と(必要に応じて)データ修正

オンプレDBとクラウドDBのスキーマが異なる場合、もしくはクラウドDBに格納するデータが移行元のオンプレDBと異なる場合は、データを抽出した後にどうやって変換するかを検討する必要があります。オンプレDBの既存データが、マイクロサービスAPIで受け付けられる状態になっているかという観点で調査しました。例えば、マイクロサービスAPIが電話番号や郵便番号に全角文字や数字以外の文字を許容しない場合は、既存のデータにそれらが含まれていないか。enumのような特定の選択肢を期待しているデータに例外がないか、移行先のクラウドDBのカラムサイズを超えるデータがないかなどを確認しました。
データを変換する必要がある場合は、下記のどちらかを検討する必要があります。

  • オンプレDBに存在するデータをクラウドDBのスキーマに合わせて修正する
  • 後述するデータ変換・データ格納のプロセスでデータを変換する

2. DBへの書き込み処理のみ、両方のDBにアトミックに行うよう実装を修正

データを移行する前に、DBに書き込みを行う全ての処理においてオンプレとクラウドの両DBがアトミックに更新されることを担保します。アプリケーションコードにおいて、1つのトランザクション内でオンプレDBへの書き込み処理とマイクロサービスAPIの処理を行い、オンプレDBとクラウドDBの間でデータの整合性を取ります。この、「オンプレDBとクラウドDBに書き込む処理をアトミックに実行する」ことを、本記事では「ダブルライト」と呼びます。

ダブルライトの実装がリリースされた時点での、DB書き込みを伴うユーザー操作の処理を図示すると下記のようになります。

ダブルライトリリース後の処理フロー

ダブルライトの具体的なフローの例として、会員情報を登録する処理は下図のようになります。フロー図はイメージが湧きやすいよう実際にチームで使用している図と近いものを使用していますが、マイクロサービスAPIを叩く際に必ず通過するAPI Gatewayについては、この記事の範疇を超えるため他の図では省略しています。

会員情報登録の際のダブルライト処理の例

注意点として、この時クラウドDBに当たる会員基盤DBにはデータが何もないため、マイクロサービスAPIは、更新・削除APIにレコードが存在しないIDを指定したリクエストが来たとしても、オンプレDBと整合性のとれたデータが保存されるように実装する必要があります(詳細は「工夫した点」として後述します)。

3. データ移行手順のテスト実行

データ移行に失敗するとDBに何度も高い負荷をかけることになるので、本番環境での作業ミスを防止するため、本番環境での移行前に練習としてデータ移行のテスト実行を行いました。弊チームでは、実際にDBにどの程度の負荷がかかるのかを計測するため、より本番環境に近いデータを保持するステージング環境を使用しました。データ修正実行後に本番環境のデータを(個人情報をマスクして)ステージング環境にコピーし、ダブルライトの実装を全ての環境にリリースしたのち、ETL処理をテスト実行しました。

4. オンプレDBからデータを抽出&変換し、クラウド環境に新しいDB(一時DB)を作成して格納

ダブルライトの実装をデータ移行に先立ってリリースすることにより、データが正しくDBに書き込まれることを担保したら、いよいよオンプレDBのデータをクラウドDBに移行するETL処理を実行します。本記事で紹介する手順では、データのETL処理をいくつかのツールを用いて実行します。データ抽出&変換(ET)にEmbulkというツールを使用し、データ格納(L)についてはGo言語でツールを自作しました。それら2つを、KubernetesのJobで実行しました。

データ移行中にもユーザー操作によるDBへの書き込み処理が継続していることを考えると、単純に移行元のオンプレDBのデータを全て抽出して移行先のクラウドDBに挿入するだけでは、要件を満たすことができません。そのため、まずはクラウド環境にデータ移行のための専用のDBを作成し(以降、このDBのことを「一時DB」と表記)、オンプレDBのデータを抽出して保存することにしました。
以降において、「データ抽出」はオンプレDBのデータを全て抽出すること、「データ変換」は抽出したデータをクラウドDB互換のスキーマに変換し、一時DBに保存することを指します。

データ抽出&変換には、社内での使用実績もあるEmbulkを使用しました。Embulkは、移行元のDBからselectしてデータを抽出し、YAMLで書かれた設定ファイルに沿ってテーブルを作成し、データを変換して挿入することが可能です。プラグインを用いて異なる種類のDB間での移行も可能で、弊チームの要件である、SQL ServerからMySQLへのETLに対応したプラグインもありました。Embulkの設定ファイルにデータ格納時のクエリを設定できるため、シンプルなロジックであれば、テーブル構造の差分を解決できました。例えば、移行元のDBにある日本語の文字列を移行先では英語のEnumで扱いたいといった場合には、Embulkが使用するクエリ中にswitch文を記載することでデータを変換してくれます。SQL文での実現が難しい、より複雑なロジックでデータを変換する必要がある場合は、この次に説明するデータ格納の際に使用する自作ツールでデータを変換するロジックを実装し、データ格納時に変換できます。

この時点の状況を整理すると、一時DBのデータはアプリケーションからアクセスされないため、データ抽出開始より後に発生したユーザー操作によるDBへの書き込み処理は反映されません。一方で、クラウドDBには常に書き込み処理が行われている状態です。

データ抽出&変換を行い一時DBが作成された時の構成

5. 一時DBに格納されたデータをクラウドDBへ格納

一時DBからクラウドDBへデータを格納する処理を、本記事では「データ格納」と呼びます。
既にダブルライトの処理が本番環境にリリースされているため、クラウドDBは随時ユーザー操作に伴いデータが更新される状態になっています。つまり、一時DBにはデータ移行プロセス開始以前の状態のデータが更新されない状態で保存されていて、クラウドDBにはデータ移行プロセス開始後の最新のデータだけが保存されている状態です。

データ格納の処理フロー

そこで、すでにクラウドDBに存在する最新のデータは更新せずに、一時DBに保存されているデータをクラウドDBに格納するツールを自作することにしました。

データ格納ツールは、基本的には一時DBのレコードを1件ずつクラウドDBに挿入します。但し、既に最新のデータが存在するDBに更新されていない状態のDBのデータを格納するため、既存のツールではカバーできないやや柔軟な機能が必要でした。データ格納処理において、クラウドDBと一時DBに同じIDのレコードが存在した場合、データ抽出後にデータ更新処理が行われた可能性が高いと考えられます。そのため、アプリケーションからクラウドDBに直接書き込まれたデータは上書きせず、そのまま処理を継続しました。クラウドDBにレコードがすでに存在した場合は、念のためそのレコードがデータ抽出開始より後に作成または更新されていることを確認しました。

データ格納

格納の処理は1,000レコードずつのバッチ処理で行いました。IDが重複した場合にエラーで処理が止まらないよう、複数レコードの同時挿入に失敗した時には重複したIDのレコードを避けて、成功するレコードのみ再度挿入するような実装にしました。

6. クラウドDBに保存されたデータの検証

移行元のDBと移行先のDBのデータに齟齬がないことを確認します。データ格納ツールの実行完了後に、データを検証する自作ツールを実行しました(詳細は「工夫した点」として後述します)。

7. データを参照する処理を、マイクロサービスAPIを使用するよう修正

クラウドDBがオンプレDBと同様のデータを保持した状態であることを確認したら、DBのデータを参照する処理にマイクロサービスAPIを使用する実装をリリースできます。リリース後の処理フローは下図のようになります。

マイクロサービスAPIを使用する実装のリリースが完了した状態

工夫した点

移行元と移行先のDBのレコードのID(プライマリキー)が同じ値になるようにする

データのETL処理が正しく行われていることを検証するため、プライマリキーでデータを識別し、移行元と移行先のDBに保存されているレコードを比較します。そのため、新規にレコードを追加する処理においては、オンプレDBで発行されたIDをマイクロサービスAPIに渡し、両方のDBでプライマリキーを一致させるようにしました。必然的に、いずれオンプレDBへの書き込みをせずにマイクロサービスAPIのみを使用するよう実装する際、レコードを追加するマイクロサービスAPIを修正する必要があります。

データ移行前後でマイクロサービスAPIの実装を変える

クラウド本番DBにデータが存在しない状態でマイクロサービスAPIがリリースされるため、APIはデータ更新もしくは削除処理の際にデータが存在しなくても、404エラーを返さないようにしておく必要があります。つまり、APIの実装としては、更新処理でもレコードが存在しなければ登録する、UPSERTのような処理を行うようにしました。マイクロサービスAPIにおいてDBにデータが存在しない場合にエラーレスポンスを返すようにするためには、データ移行後にハンドリングを入れた実装をリリースする必要があります。

データ格納のロジック実装時に、データの削除方法を考慮する

データ格納ロジックは、データのCRUDを行うアプリケーションロジック、テーブルを跨ぐデータの関係などに影響を受けます。特に注意する必要があるのは、DBへのデータ挿入、更新、削除のうち削除の処理です。データを物理削除するか論理削除するかによって、対応方法が変わります。

  1. 削除パターンが論理削除の場合
    論理削除フラグをもつテーブルの場合は、データ格納において特別なロジックを追加する必要はありません。一方、削除済みデータ用のテーブルを持つタイプのデータの場合は、必要な全てのテーブルについてETL処理を行う必要があり、データ格納用のツールの仕様も複雑になります。メインのデータを保存するテーブルにあるレコードの状態が一時DBのレコードと異なる場合に、削除済みのデータが保存された別のテーブルをチェックするよう実装する必要があります。全てのデータ更新をクラウド環境内で検知できるため、オンプレ環境からの移行を考えている場合には物理削除のパターンよりもこちらの方が比較的実装は楽かもしれません。
  2. 削除パターンが物理削除の場合
    物理削除されるデータの場合は、データ抽出開始後に削除されたデータのIDをDBから確認できません。そのため、一旦一時DBの全てのデータを格納してから、削除されるべきレコードのIDを特定してデータ格納完了後に削除する必要がありました。
    データ移行期間中に発生する削除処理の回数が少ないケースでは、データ抽出開始〜データ格納完了の間に処理した削除リクエストのログからIDを抽出し、クラウドDBに直接SQLを流して削除しました。
    短時間に大量に削除リクエストが発行されるアプリケーションの場合、一時的に削除されたレコードを保存する別テーブルを作成し、APIの削除処理を論理削除を行う実装にした上で、データ移行が完了したら物理削除を行う実装に修正するなどの方法も考えられます。

データ格納後のデータ検証のやり方

弊社では、移行元のオンプレDBへのアクセスを多くのマイクロサービスから行いたくなかったため、オンプレDBとクラウドDBのデータを直接突合させるツールを作成できませんでした。そこで、マイクロサービスが保持する一時DBとクラウドDBのデータに齟齬がないかをまずは確認し、齟齬があった場合はログを出力して後で調査しました。データ抽出開始より後に作成もしくは更新されたと考えられるデータの場合は、スプレッドシートなどを用いてオンプレDBのデータと突合しました。物理削除された可能性があるデータの場合は、一時DBに保存されているIDを出力し、アプリケーションログにて削除処理が行われたことを確認しました。もし検証プログラムが移行元と移行先の両DBにアクセスできるのであれば、それぞれのDBのレコードが全て一致していることを確認するだけでクラウドDBに正しくデータが移行されたことを確認できます。

データ移行結果

結果として、この戦略は要件を全て満たす状態で成功し、ノウハウが社内で共有され複数のチームで採用されるようになりました。会員基盤チーム以外の事例も含めて、移行にかかった時間や手間などについて紹介します。

データ移行にかかった時間

約3千万件のデータ移行で、データ抽出&変換は40分、データ格納は2時間程度で完了し、ETL処理の工程は1日あれば全て完了しました。一方で、イベントを記録する場合などデータ量がより多くなる場合には、数億件のデータを1週間かけて移行した事例もありました。データ移行にかかる時間は、DBのバージョンやスペック、データの複雑さ、レコードの件数と大きさ、移行中にDBにかかる負荷など、複数の要因に影響を受けます。
※ 移行対象のデータの数はデータ移行の対象となったテーブルの行数を示しており、ZOZOTOWNの会員数もしくは会員情報の総数などを表すものではありません。

苦労した点

データ移行の前にダブルライトの処理をリリースすることにより、それぞれの過程で考えることが非常に多くなり、自作ツール開発などの追加作業が発生しました。加えて、マイクロサービスAPIを使用する実装のリリースタイミングを2段階設けたことにより、リリース前に行う必要があるテストを複数回実施する必要があり、これも工数が増えた要因でした。特にダブルライトのテストは画面上からデータを確認できないため、操作時に入力したデータをスプレッドシートに記録しておき、オンプレとクラウドそれぞれのDBのデータと突合する必要がありました。

ノウハウが溜まっていない時期には、考慮漏れにより全ての工程をやり直したこともありました。データ調査を怠ってデータ変換に失敗したり、データ検証の際にダブルライトの実装が漏れていたことに気づいたこともありました。前者のデータ修正の追加対応を行なったケースでは、オンプレDBのデータ調査、データ修正ののち、データ移行の工程を全てやり直し、結局データ移行を開始してから完了するまでに10時間弱かかりました。

また、データ移行のやり方がデータの性質によっても大きく変わってくるため、データの修正など準備にかける手間が非常に大きくなることもありました。例えば、プライマリキーを持たないテーブルに新たにプライマリキーを作成した上でデータを移行し、移行元のDBの修正処理を含めると合計5日ほど時間がかかったこともありました。

まとめ

本記事では、データ移行の前にデータ移行元と移行先の両方のDBにアトミックに書き込む実装をリリースすることで、サービスを停止させずにデータを移行する戦略について説明しました。本記事で紹介した戦略は、単純なデータコピーやデータレプリケーションと比較すると大幅に手間がかかります。しかし、サービス無停止でのデータ移行の成功例ができたことにより、機会損失を出すことなく複数のチームでマイクロサービス化のプロジェクトを進めることができるようになりました。マイクロサービス化の過程でサービス停止を伴わないデータ移行を検討している方の参考になれば幸いです。

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co

カテゴリー