PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

複数の Amazon RDS クラスタを使用してダウンタイムを最小限に抑える設計と実装

こんにちは、SaaS 事業部テックリードの丸山です。

当社の SaaS プロダクトは、基本的には AWS のサービスを利用して構築されています。RDB としては、Amazon RDS (Aurora) を利用することが多いのですが、これを利用する上で避けては通れないのが、AWS によって定期的に実施されるメンテナンスの存在です。

docs.aws.amazon.com

Amazon RDS のメンテナンスは、一時的にクラスタをオフラインにする必要があるため*1、サービスへの影響を伴います。また、メンテナンスの所要時間についても、通常は数分〜数十分程度で終了しますが、一定時間以内に終了することが保証されているわけでもありません。エンドユーザーに対して事前にメンテナンス期間を告知しておくことで、堂々と数時間程度サービスを停止できるのであれば、あまり問題はないかもしれませんが、私たちが取り扱っている SaaS という商品の提供形態上、ひとつのシステムを複数の顧客企業様にご利用いただいており、それぞれの企業様がさまざまな動画配信サービスを運用されています。このような状況において、すべての顧客企業様に「XX 月 XX 日の深夜、最大 2 時間程度、動画が配信できなくなります」といったご連絡をするのは、現実的には困難です(各企業様のサービスに大きな影響を与えることになりますし、各企業様側でエンドユーザー向けの周知や問い合わせ対応を実施いただかなくてはなりません)。そのため、私たちの使命としては、「もしも長時間のメンテナンスや障害等の要因により、長時間にわたりクラスタがオフライン状態になったとしても、動画の配信だけは止めない」ということになります。

上記を実現するために、私たちは複数の Amazon RDS クラスタを使用してフェイルオーバーさせる方式を採用しております。この方式で数年間、実際にシステムの運用を続けてみて、その効果が実感できてきましたので、今回はこのアーキテクチャについて紹介してみたいと思います。

アーキテクチャ設計

とある動画配信システムの基盤部分のアーキテクチャ(一部抜粋)を下図に示します。

図中の左側にあるアプリケーションサーバ(EC2 インスタンス)と、右側にあるデータベースとの間で通信が行われますが、この設計のポイントとなるのは、RDS Aurora Cluster (Primary) と表記されたメインのクラスタ(以下、Primary クラスタと呼びます)のほかに、RDS Aurora Cluster (Backup) と表記されたもうひとつのクラスタ(以下、Backup クラスタと呼びます)が存在することです。

通常、Amazon RDS でクラスタを作成すると、クラスタ内のマスターインスタンスとリードレプリカの間で自動的にデータ同期が行われ、マスターインスタンスに障害が起こった場合は自動的にリードレプリカが昇格し、クラスタとしての機能が維持されるようになっています。そのため、一般的にはクラスタをひとつ作成しさえすれば十分かもしれません(実際に、当社でも多くのシステムがひとつのクラスタで動作しています)。

しかし、先述したように、メンテナンスの実施中はマスターインスタンスとリードレプリカの両方が停止している時間が発生する場合があります。その状況が長時間継続した場合に備えて、特にダウンタイムを許容できない重要なシステムについては、上図のような設計となっています。Backup クラスタにフェイルオーバーしている間、書き込み系のクエリは実行できませんが、読み込み系のクエリは引き続き実行できます(読み込みさえできれば、動画の配信はできるようになっています)。

この設計を採用する場合、新たに考えなければならない 2 つのポイントがあります。

  • Backup クラスタの構築とクラスタ間のデータ同期
  • 自動フェイルオーバーの実装

これらのポイントについて、これから順に解説していきます。

Backup クラスタの構築とクラスタ間のデータ同期

以下の手順に従い、Backup クラスタの構築とデータ同期のための設定を行います。なお、以下ではデータベースエンジンとして Amazon Aurora(MySQL 5.7 互換)を使用することを前提としています。また、ここで紹介する手順は以下の AWS ドキュメントの内容とほぼ同等になります。

docs.aws.amazon.com

まず、バイナリログによるレプリケーションを有効化するため、レプリケーションソース(Primary クラスタ)側の DB パラメータグループに以下の値を設定します。

パラメータ
binlog_format MIXED
enforce_gtid_consistency ON
gtid-mode ON

なお、今回は GTID ベースのレプリケーションを行うため、GTID に関するパラメータを設定しています。GTID ベースのレプリケーションは、従来のレプリケーションとは異なり、レプリケーションの開始時にログファイル名や開始位置を指定する必要がないという特徴があります。

docs.aws.amazon.com

次に、レプリケーションソース(Primary クラスタ)のスナップショットを作成します。AWS マネジメントコンソールで、対象のクラスタを選択して「アクション」メニューから「スナップショットの取得」をクリックすることで、スナップショットを取得できます。

続いて、取得したスナップショットを復元(リストア)して Backup クラスタを作成します。作成するクラスタの設定値については、以下を参考にしてください。

  • インスタンスサイズは、Primary クラスタよりは小さくても良いかもしれません(非常時以外は使用されないため、サイズを抑えておくことでコストが削減できます)。
  • DB パラメータグループで read_only パラメータの値を 1 に設定します(Backup クラスタに書き込みができてしまうと、Primary のデータと不整合が発生するため)。
  • 耐障害性を高めるため、Backup クラスタのインスタンスは、Primary クラスタのどのインスタンスとも異なるアベイラビリティーゾーンに配置することを推奨します。
  • それ以外の設定値については、基本的に Primary クラスタと揃えておきます。

続いて、レプリケーションソース(Primary クラスタ)側にレプリケーション用の MySQL ユーザーを作成します。

-- repl-user ユーザーを作成
CREATE USER 'repl-user'@'172.31.%' IDENTIFIED BY 'Passw0rd!';

-- レプリケーションに必要な権限を repl-user ユーザーに付与
GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'repl-user'@'172.31.%';

最後に、レプリケーションターゲット(Backup クラスタ)側で以下のストアドプロシージャを実行し、レプリケーションを開始します。

-- レプリケーションソース(Primary クラスタ)の接続情報を設定
CALL mysql.rds_set_external_master_with_auto_position('<Primaryクラスタのクラスタエンドポイント>', 3306, 'repl-user', 'Passw0rd!', 0, 0);

-- レプリケーションを開始
CALL mysql.rds_start_replication;

レプリケーションが正常に行われていることを確認するには、レプリケーションターゲット(Backup クラスタ)側で SHOW SLAVE STATUS を実行し、Seconds Behind Master の値が 0 であることを確認します。

自動フェイルオーバーの実装

Amazon RDS では通常、読み込みと書き込みが可能なクラスタエンドポイントと、読み込みのみ可能なリーダーエンドポイントが、クラスタごとに提供されます。マスターインスタンスに障害が発生してリードレプリカがマスターに昇格した際には、各エンドポイントの向き先となるインスタンスが自動的に入れ替わるため、アプリケーションからは常に同じエンドポイントを見ているだけで済みます。

しかし、異なるクラスタ間でフェイルオーバーする仕組みについては AWS 側では提供されていません。そのため、今回のアーキテクチャを採用するにあたっては、各クラスタの状態を監視し、その状態によって接続先を動的に切り替える仕組みをアプリケーション側に自前で実装しなければなりません。

この目的のために、アプリケーションサーバで HAProxy を動作させています。アプリケーションからは、書き込み系のクエリは直接 Primary クラスタのクラスタエンドポイントに投げますが、読み込み系のクエリはサーバ内(localhost)で動作する HAProxy に向かって投げます(書き込み/読み込みによって投げ先を変えるのは、ORM の機能により実現しています)。そして、HAProxy の機能により、クエリが適切な DB のエンドポイントに振り分けられるようになっています。

以下が HAProxy の設定ファイルとなります。

global
    daemon
    maxconn 4000

defaults
    mode tcp
    retries 3
    timeout connect 10s
    timeout client 1m
    timeout server 1m
    timeout check 10s

listen mysql
    bind 127.0.0.1:3306

    # mysql 監視を有効化("haproxy" ユーザとして mysql に接続)
    option mysql-check user haproxy

    # 負荷分散方式("first" = 最初の server を優先的に使用)
    balance first

    # 接続先候補(負荷分散方式が first であるため、平常時は一番上が使用される)
    server reader <Primaryクラスタのリーダーエンドポイント>:3306 check
    server writer <Primaryクラスタのクラスタエンドポイント>:3306 check
    server backup <Backupクラスタのクラスタエンドポイント>:3306  check

option mysql-check オプションを設定することで、HAProxy がバックエンドのデータベースの監視を行ってくれます。監視に使用する MySQL ユーザー名を指定できます。当然ながら、指定した名前のユーザーをあらかじめデータベース側に作成しておく必要があります。

-- 監視用のユーザーをデータベース側に用意
CREATE USER 'haproxy'@'172.31.%';

balance オプションでは、負荷分散方式を設定できます。今回は、Primary クラスタが使用できないときだけ Backup クラスタが使用されるようにするため、負荷分散方式に first を指定するとともに、server オプションを優先的に使用したいエンドポイントの順に上から記述しています。

実際にメンテナンスを実施した結果

先日、実際に Aurora MySQL のマイナーバージョンアップグレードを実施しました。今回紹介したアーキテクチャを採用している本番環境と、採用していない開発環境とでは、サービス影響に明確な差が見られました。

今回紹介したアーキテクチャを採用していない開発環境では、取得系の API で 37 秒間のダウンタイム(HTTP 500 エラー)が発生しました。一方、本番環境では、ダウンタイムをわずか 12 秒間に抑えることができました*2。データベースがオフラインになっている時間の長さは変わらないのですが、本番環境では HAProxy による Backup クラスタへのフェイルオーバーが発生したことで、速やかにサービスが自動的に復旧しました。また、メンテナンスの終了後に自動的に Backup クラスタから Primary クラスタにフェイルバックしたことも確認しています。

まだ改善できる点

  • HAProxy のヘルスチェックの閾値を調整することで、Backup クラスタへのフェイルオーバーをより高速に実行させ、ダウンタイムを短縮することができると考えられます。
  • 今回紹介したアーキテクチャでは、Backup クラスタを Primary クラスタと同じリージョン(東京リージョン)に配置していますが、より高い耐障害性を実現するためには、Backup クラスタを別リージョンに配置したほうが良いでしょう。このシステムを構築した当時はまだありませんでしたが、今なら大阪リージョンに Backup クラスタを配置するのが良いかもしれません。

今回は、Amazon RDS を使用して可用性の高いアプリケーションを構築するための設計についてご紹介しました。どこかで誰かのお役に立ちましたら幸いです。

*1:特定の条件を満たせば、Zero-downtime Patching (ZDP) が適用可能な場合もありますが、ベストエフォートであるため、必ず適用されるものではありません。

*2:EC2 上の設定ファイルを直接編集し、事前にアプリケーションからの接続先を Backup クラスタに書き換えておけば、ダウンタイムはほぼゼロにできますが、複数台の EC2 にログインして作業するのにオペミスの可能性があったことや、自動フェイルオーバーが 10 秒程度で完了することが事前にわかっていたなどの理由から、今回は自動フェイルオーバーで実施することにしました。