12億件のレコードがある Heroku Postgres を Amazon RDS にダウンタイム1時間で移行した話

自社サービスのデータベースを Heroku から RDS に移行したのでその経験を紹介します。

## はじめに
家畜の行動モニタリングシステムを自社サービスとして提供しています。
そのデータベースを Heroku Postgres にて運用していましたが、諸事情により Amazon RDS に移行することになりました。

## 移行方法の検討
家畜の行動データなどデータ量が多く、レコード数は約12億件ありました。
ユーザーへの影響を最小限にするため、なるべくダウンタイムを減らしつつ移行を行う方法を検討しました。

### dump → restore
Heroku側でデータベース全体のバックアップを取得し、それをRDSに復元する方法です。
DBパラメータを変更しながら色々試してみましたが、10時間程度かかるためユーザーへの影響が大きく見送りました。

### AWS DMS (Database Migration Service)
AWS が提供するデータベース移行サービスです。 
しかし、Heroku Postgres では DMS が必要とする権限を付与することができなかったため見送りました。

### 段階的な移行
更新頻度の低いテーブルや過去のデータを先に移行し、最後に直近のデータを移行する方法です。 
この方法はダウンタイムを短縮できますが、アプリケーション側の修正や移行手順が複雑になるため見送りました

### Bucardo
最終的に、Bucardo を採用することにしました。
Bucardo は PostgreSQL のレプリケーションツールであり、運用を継続しつつデータ移行できるという利点があります。

## Bucardoとは
Bucardo は、PostgreSQL のレプリケーションを行うためのオープンソースツールです。 
https://bucardo.org/

### Bucardoの仕組み
Bucardo は以下のような仕組みでレプリケーションを実現しています。

**トリガー:** ソースデータベース(Heroku)のテーブルに設定され、データの変更 (INSERT、UPDATE、DELETE) を検知します。
**デルタテーブル:** トリガーによって検知されたデータ変更の内容を記録する専用のテーブルです。Bucardo によってソースデータベース(Heroku)に作成されます。
**デーモン:** デルタテーブルの内容に基づいて、ソースデータベース(Heroku)のからターゲットデータベース(RDS)へデータの同期を行います。

### Bucardo利用時の注意点
**ソースデータベースにリードレプリカは利用できない**: ソースデータベース(Heroku)の負荷を抑えるためにリードレプリカの利用を考えましたが、リードレプリカにはトリガーやデルタテーブルを作成できないため実現できませんでした。
**移行するテーブルにはプライマリーキーの設定が必要**: Bucardo はプライマリーキーを利用してデータの同期を行うため、移行するテーブルにはプライマリーキーの設定が必要です。

## 移行の流れ
移行は以下のような流れで行いました。

1. Bucardo のセットアップ: 移行するDBの登録、Herokuへのトリガー・デルタテーブルの作成などを行う。切り替え前日に行う。
2. 同期開始: Bucardo を起動し、同期を開始する。切り替え前日から開始。
3. アプリケーションの停止: データベースの切り替え前にアプリケーションを停止する。切り替え作業の直前に行う。
4. 同期完了の確認: Herokuへの変更がRDSに反映されるのを待つ。
5. 切り替え: アプリケーションの接続先をHerokuからRDSに変更する。
6. アプリケーションの再開: アプリケーションを起動する。


Bucardo の利用手順についての詳細は以下を参照して下さい。
[Bucardo Tutorial](https://bucardo.org/Bucardo/pgbench_example)


## 移行前の検証と課題
※データベース移行にあたっては様々な検証を行いましたが、ここでは Bucardo を利用した同期の検証について述べます。

ステージング環境で Bucardo を用いた同期が問題なく動作するか検証を行いました。 

- 本番環境と同様のデータ、スペックの環境を構築し、Bucardo実行時の負荷テストを実施しました。
- 特に更新レコードが多い処理を本番と同頻度で実行し、同期が正常に行われることを確認しました。

検証中に以下の問題が発生しましたが、対策を施すことで解決できました。

### 同期に時間がかかる
Bucardo での同期は以下のような流れで行われます。

1. 同期開始のタイミングのソースデータベースのレコードをターゲットテーブルに反映する。(初回コピー)
2. 上記1.が終わったタイミングで上記1.以降の差分を取得し、ターゲットテーブルに反映する。(差分反映)
3. 以降は随時、差分のみを反映する。

検証当初は上記1.の初回コピーだけで13時間近くかかり、その次の1回目の差分反映も3時間近くかかりました。
その間にもさらに差分が溜まるといった具合で、完全に同期するまでに時間がかかる問題が発生しました。


この問題についてはデータベースのサーバースペックを、移行期間のみ以下のように上げることで対処しました。

**Heroku**: Heroku Postgres Standard 5 → Heroku Postgres Standard 7
**RDS**: db.r6g.2xlarge → db.r6g.4xlarge


### 同期が途中で停止する
検証中に Bucardo の同期処理が停止する問題が発生しました。

`bucardo status all` コマンドで確認したところ、`Current state` が `delta_check on db from_db` の状態で長時間処理が止まっていることが分かりました。
差分チェック処理がボトルネックとなっていると考えられたため、以下の Bucardo のパラメータを調整しました。

* **quick_delta_check:** 差分チェックを高速化するためのパラメータです。 デフォルトでは有効になっており、説明では特定のインデックスや最適化されたクエリを利用して高速なチェックを行うものということでした。しかし、大量のトラフィックがあるテーブルでは、この高速化処理自体がボトルネックとなる場合があるようでした。そのため値を `0` に設定することで無効化しました。
* **autokick:** デルタテーブルに変更が発生した際に自動的に同期を開始するかどうかを指定するパラメータです。 デフォルトでは有効になっており、リアルタイムで同期したい場合に適していますが、大量のデータ変更が発生する場合は、同期処理が頻繁に実行され、システム負荷が高まる可能性があります。 そこで、値を `0` に設定することで自動同期を無効化しました。
* **checktime:** Bucardo デーモンがデルタテーブルをチェックする間隔を指定するパラメータです。 デフォルト値は 10 秒です。値を `10m` (10分) に設定することで、チェック頻度を減らし、同期処理の実行頻度を抑制しました。`autokick` を無効化したので代わりに10分おきに同期処理が実行されるように設定しました。

これらのパラメータ調整により、差分チェック処理の負荷が軽減され、同期処理の停止問題が解消されました。

## 本番移行
同期遅延が発生した場合に備え、更新レコードが多いバッチ処理を間引く計画を立てていました。
また、切り替え前の時間帯は、必要最小限の処理のみを実行し、データの差分を最小限に抑える予定でした。

幸いなことに、当日は同期遅延が発生しなかったため、予定していた間引き処理を行うことなく移行を完了できました。 

アプリケーションの停止から同期の完了、データベースに切り替えまでは20分程度で完了して、疎通確認などの確認作業を含めるとダウンタイムは1時間以内で済みました。
もともと当サービスでは月に1度1時間程度のメンテナンス作業を行っており、その時間の中で余裕をもって作業を完了することができたことになります。


## まとめ
約12億件のレコードを持つ Heroku Postgres データベースを、Bucardo を利用して Amazon RDS に移行しました。
データ量が多かったですが、1時間以内のダウンタイムで完了することができました。 


なお、この移行プロジェクトについては アマゾンウェブサービスジャパン合同会社様 と MEGAZONE株式会社様 にアドバイス・ご協力を頂きました。