AWS RDS Aurora Cluster(MySQL互換)でパーティションをプロシージャで定期的に追加しつつ、エラーハンドリングもする

AWS RDS Aurora Cluster(MySQL互換)でパーティションをプロシージャで定期的に追加しつつ、エラーハンドリングもする
目次

AWSのRDS AuroraはOSSのDBミドルウェアと互換性のあるマネージドサービスです。 今回はAuroraのMySQL互換での日付パーティションの作成に関して説明します。 AuroraというよりはMySQLの仕様に関する説明も多いのでご了承ください。

今回やりたかったこと

今回はRDS Aurora(MySQL互換)にてClusterを組み、テーブルは日でパーティショニングすることにしました。 要件はざっくり以下です。

  1. ユーザ操作の都度データが格納されていく(データは常にINSERT)
  2. 一定期間が経過したデータには利用価値が薄い ため退避した後削除して良い

そもそもDynamoDBでよくない?というツッコミがあるかもしれませんが、システムとは別の理由があるため採用していません。

今回は以下のような hoge テーブルを考えます。 hoge テーブルにはユーザ(id)が行なった操作を info カラムに格納します。 create_at はパーティションキーであり、 idcreate_at が PKです。

なお、パーティションキーはPKに含める必要があります。

カラム名備考
idvarchar(255)ユーザのID
infovarchar(255)ユーザが行なった操作の情報
create_attimestampレコードが作成された日時

パーティションを日次で追加する

ここから具体的な手順を説明します。 hoge テーブルには以下のような操作をすることにしました。

  • 最初に一定量のパーティションを作成しておく
  • パーティションの追加はプロシージャで実施する
  • 日次でパーティションの最後に+1日ぶんのパーティションを追加する

一般的に 「パーティションの追加は一定量をまとめて実施した方が良い」 と言われています。

私の場合、システムの運用要件や、格納するデータの想定量を設計に加え、左記を踏まえた負荷試験も実施したところ、オンラインでの日次パーティション追加でもパフォーマンス上問題がなかったため、上記の対応方針にしました。

テーブル作成とパーティションの指定

以下のようなクエリを発行し hoge テーブルを作成します。 加えて、パーティション名のルールは p + yyyyMMdd として、create_at カラムでRANGEパーティション指定をします。

 1-- create hoge table
 2DROP TABLE IF EXISTS hoge;
 3CREATE TABLE hoge (
 4  id varchar(255) NOT NULL,
 5  info varchar(255) NOT NULL,
 6  create_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 7  PRIMARY KEY (id, create_at),
 8  INDEX index_id (id)
 9) ENGINE=InnoDB DEFAULT CHARSET=utf8;
10
11ALTER TABLE hoge PARTITION BY RANGE (UNIX_TIMESTAMP(create_at)) (
12  PARTITION p20180219 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-19 00:00:00'))
13);

パーティション追加用のプロシージャを作成

次にパーティションを追加するためのプロシージャを登録します。 可視性の観点でバリデーション等は省略しています。

 1--
 2-- hogeテーブルにパーティションを追加するストアドプロシージャ
 3-- 引数 from_date: パーティション作成の開始日時 to_date: パーティション作成の終了日時
 4-- パーティションは from_date < to_date - 1日 分まで作成されます。
 5-- 呼び出しサンプル
 6-- CALL add_hoge_partition(str_to_date('2018-01-01', '%Y-%m-%d'), str_to_date('2019-01-01', '%Y-%m-%d'));
 7--
 8DROP PROCEDURE IF EXISTS add_hoge_partition;
 9DELIMITER $$
10CREATE PROCEDURE add_hoge_partition(IN from_date DATE, IN to_date DATE)
11  proc_label:BEGIN
12    DECLARE target_date DATE;
13    DECLARE partition_range DATE;
14    DECLARE p_count INT;
15
16    -- 日次パーティションの作成
17    SET target_date = from_date;
18    WHILE DATEDIFF(to_date, target_date) > 0 DO
19
20      -- パーティションはLESS THANになるので1日追加
21      SET partition_range = DATE_ADD(target_date, INTERVAL 1 DAY);
22      -- パーティションの追加
23      SELECT CONCAT(
24        'ALTER TABLE hoge ADD PARTITION ( PARTITION ',
25        DATE_FORMAT(target_date, 'p%Y%m%d'),
26        ' VALUES LESS THAN (UNIX_TIMESTAMP(', QUOTE(DATE_FORMAT(partition_range, '%Y-%m-%d 00:00:00')), ')))'
27      ) INTO @ddl;
28
29      PREPARE ddl_stmt FROM @ddl;
30      EXECUTE ddl_stmt;
31      DEALLOCATE PREPARE ddl_stmt;
32
33      -- 次の日次のパーティションの設定
34      SET target_date = DATE_ADD(target_date, INTERVAL 1 DAY);
35    END WHILE;
36
37  END$$
38DELIMITER ;

初期パーティションを作成する

登録したプロシージャ add_hoge_partition を使用して、初期パーティションを追加します。

とりあえず、現在日付から365日ぶんのパーティションを追加しましょう。

1CALL add_hoge_partition(CURDATE(), DATE_ADD(CURDATE(), INTERVAL 365 DAY));

なお、パーティションは 追加しかできない ことに注意してください。

プロシージャ実行をEVENT登録し定期実行する

MySQLには CREATE EVENT 構文にてイベントをスケジュール実行する仕組みがあります。 これを利用することにしましょう。

まず、 RDSのDBのパラメータグループにて event_schedulerON に指定します。(デフォルトでは OFF ) その際、event_scheduler パラメータグループの変更に伴うDBの再起動は不要です。

その後以下のようなクエリを発行し、日次でパーティションを追加できるようにしましょう。 少々見づらいですが、INFORMATION_SCHEMA から既に存在するパーティションの情報を取得し、それに +1日してパーティションを追加しています。

1CREATE EVENT add_hoge_partition
2ON SCHEDULE EVERY 1 DAY STARTS '2018-02-19 00:00:00'
3COMMENT 'hogeテーブルに対して1日毎に1日分のパーティションを追加します'
4DO CALL
5    add_hoge_partition(
6        (select from_unixtime(max(PARTITION_DESCRIPTION)) from INFORMATION_SCHEMA.PARTITIONS where TABLE_NAME = 'hoge', DATE_ADD(
7            (select from_unixtime(max(PARTITION_DESCRIPTION)) from INFORMATION_SCHEMA.PARTITIONS where TABLE_NAME = 'hoge'),INTERVAL 1 DAY)
8        );

これで、毎日0時にパーティション追加のプロシージャが実行されるようになりました。

RDS Aurora Clusterの場合を考える

ここで、Aurora Clusterの場合を考えます。 Auroraでクラスタを組んだ場合、Master/Slaveの構成ではなく、Writer/Readerの構成になります。 詳細は割愛しますが、Auroraに関してはBalckBeltの資料を参照してください。

EVENTはWriterのみで実行される

先程、CREATE EVENT 構文にてプロシージャを日次で実行するように登録しました。 Clusterを組んだ場合においては、「WriterとReaderの両方で実行されてしまうのでは?」と思い、以下のクエリを実行してみたところ、プロシージャはWriterで1回だけ呼ばれている ことが確認できました。

  • WriterでEVENTを確認
1select * from INFORMATION_SCHEMA.PROCESSLIST where USER = 'event_scheduler' limit 10;
2> 1	event_scheduler	localhost		Daemon	40803	Waiting for next activation
  • ReaderでEVENTを確認
1select * from INFORMATION_SCHEMA.PROCESSLIST where USER = 'event_scheduler' limit 10;
2> Empty set (0.01 sec)

Failoverさせた状態でも、翌日にもパーティションが作成されていることも確認できたため、Writerのみが実行できているといえます。

エラーが発生したらどうするか

今回の「定期的にプロシージャを実行」という方法は Auroraインスタンスの中に閉じた処理 になります。 仮に実行時エラーやバリデーションエラー等が発生した場合に、開発者がそれを拾えなければサイレント障害になりかねません。 そのため、エラーが発生した場合に検知する機構 が必要になってきます。

プロシージャ実行時のエラーはlambdaで検知する

今回はAurora(MySQL互換)に標準で組み込まれたプロシージャ mysql.lambda_async を使用して問題発生を検知するようにします。 mysql.lambda_async は、RDSから直接AWS Lambdaを実行することができ、その際にメッセージを指定できるのです。 以降ではその手順を説明します。

Aurora ClusterにIAM Roleを追加する

Auroraからlambdaを実行するために、別途権限を付与する必要があります。 IAMのメニューから専用のIAM Roleを作成しましょう(今回は rdsToLambdaRole という名前にします)。 rdsToLambdaRole には

1"Action": [
2  "lambda:InvokeFunction"
3]

が許可されていれば良いので、AWSから提供されている AWSLambdaRole ポリシーを適用すればOKです。

作成した rdsToLambdaRole のARNを クラスタのパラメータグループaws_default_lambda_roleに指定しましょう。

こちらのパラメータも dynamic のためクラスタ再起動は不要です。

rds_to_lambda_role

RDSインスタンスのあるsubnetのルーティング設定をする

RDSからのoutbound通信を許可してあげる必要があるので、ルーティングの設定を行います。 一般的にDBサーバはprivate subnetに置くケースが大半だと思いますので、private subnetから外に出られるように設定してあげましょう。

プロシージャ側のエラーハンドリングを追加する

プロシージャ内でエラーが発生した場合に、lambdaを実行する HANDLER を先程のプロシージャに追加しましょう。 lambda関数 rds_monitormysql.lambda_async プロシージャで呼び出します。

追記箇所は以下のようになります。

 1    -- 失敗したらlambdaで通知する
 2    DECLARE EXIT HANDLER FOR SQLEXCEPTION
 3      BEGIN
 4        GET DIAGNOSTICS CONDITION 1 @p1 = RETURNED_SQLSTATE, @p2 = MESSAGE_TEXT;
 5        CALL mysql.lambda_async(
 6          'arn:aws:lambda:ap-northeast-1:${account_id}:function:rds_monitor',
 7          CONCAT('{"message":"', @p2, '",',
 8                  '"state":"', @p1, '"}')
 9        );
10      END;

先程のプロシージャに組み込んだ場合は以下のようになります。

 1--
 2-- hogeテーブルにパーティションを追加するストアドプロシージャ
 3-- 引数 from_date: パーティション作成の開始日時 to_date: パーティション作成の終了日時
 4-- パーティションは from_date < to_date - 1日 分まで作成されます。
 5-- 呼び出しサンプル
 6-- CALL add_hoge_partition(str_to_date('2018-01-01', '%Y-%m-%d'), str_to_date('2019-01-01', '%Y-%m-%d'));
 7--
 8DROP PROCEDURE IF EXISTS add_hoge_partition;
 9DELIMITER $$
10CREATE PROCEDURE add_hoge_partition(IN from_date DATE, IN to_date DATE)
11  proc_label:BEGIN
12    DECLARE target_date DATE;
13    DECLARE partition_range DATE;
14    DECLARE p_count INT;
15    -- 失敗したらlambdaで通知する
16    DECLARE EXIT HANDLER FOR SQLEXCEPTION
17      BEGIN
18        GET DIAGNOSTICS CONDITION 1 @p1 = RETURNED_SQLSTATE, @p2 = MESSAGE_TEXT;
19        CALL mysql.lambda_async(
20          'arn:aws:lambda:ap-northeast-1:${account_id}:function:rds_monitor',
21          CONCAT('{"message":"', @p2, '",',
22                  '"state":"', @p1, '"}')
23        );
24      END;
25
26    -- 日次パーティションの作成
27    SET target_date = from_date;
28    WHILE DATEDIFF(to_date, target_date) > 0 DO
29
30      -- パーティションはLESS THANになるので1日追加
31      SET partition_range = DATE_ADD(target_date, INTERVAL 1 DAY);
32      -- パーティションの追加
33      SELECT CONCAT(
34        'ALTER TABLE hoge ADD PARTITION ( PARTITION ',
35        DATE_FORMAT(target_date, 'p%Y%m%d'),
36        ' VALUES LESS THAN (UNIX_TIMESTAMP(', QUOTE(DATE_FORMAT(partition_range, '%Y-%m-%d 00:00:00')), ')))'
37      ) INTO @ddl;
38
39      PREPARE ddl_stmt FROM @ddl;
40      EXECUTE ddl_stmt;
41      DEALLOCATE PREPARE ddl_stmt;
42
43      -- 次の日次のパーティションの設定
44      SET target_date = DATE_ADD(target_date, INTERVAL 1 DAY);
45    END WHILE;
46
47  END$$
48DELIMITER ;

mysql.lambda_asyncの引数にはlambdaのARN指定が必要という点が注意ポイントです。

DBのマイグレーションツールを使用して複数のAWSアカウントを運用されている方は、AWSのアカウント番号が異なるため、チェックサムが変わってしまいます。マイグレーションツールがテンプレート変数をサポートしているか確認すると良いでしょう。

lambda関数を作成する

Auroraから渡されたメッセージを処理して、監視システムやチャットツールに通知するためのlambda関数を実装します。 私の場合は監視用SaaSである datadog に通知していました。

[備考]RDSのエラーログが出力されない?

コンソール上からRDSインスタンスのエラーログを参照しようと error/mysql-error-running.log を選択しても、 以下のように END OF LOG しか表示されない場合があります。 コンソール上部には 38.2kB とログの容量が記載されているにも関わらずに、です。

rds error log is empty

実はこれは、「AWS側で使用するログを上記のログファイルに出力しているらしく、我々AWSのユーザ側には表示されない」という仕様 によるものです。少しわかりにくいですが、このログファイルの容量はAWS用のログの容量も含まれて表示されている、ということですね。

まとめ

今回はAurora Clusterからプロシージャを定期実行することで、日付パーティションを定期追加する方法をまとめました。 マネージドサービスであるRDSのプロシージャ内部の処理は隠蔽されて見通しが悪くなるため、エラーハンドリングをきちんと定義してあげることが肝要です。今回はRDSからlambdaを実行しましたが、CloudwatchLogsへ連携することもできるそうなので、機会があれば試してみたいと思います。

参考にさせていただいたサイト