NewsPicksこんにちは、ソーシャル経済メディア「NewsPicks」の高山周太郎です。昨日はakaneyoshiさんによる『BEMの記述メソッドを参考にデザインファイルのクラス名を定義する』でした!NewsPicksではデータストアの一部にDynamoDBを使用しています。先日、DynamoDBの約5000万件データが入ったテーブルAを、新規のテーブルBとCに分割する移行作業を実施したので、その事例紹介をします。今回の移行では以下のような要件を満たす必要がありました。テーブルA(旧テーブル)に存在する既存データは、移行後テーブルBかC(新テーブル)どちらかに存在する必要がある。移行後しばらくは新旧両テーブルに同じデータが書き込まれる。参照系のコードを一気に修正せず、徐々に本番適用していくため。サービスを停止しない移行時点では新テーブルを参照するコードは存在せず、旧テーブルへの変更も加えないので比較的容易に達成できるはず。前述の要件を満たすため、以下のような3段階での移行を計画しました。JSONでの一括importDynamoDBにはS3バケットへJSON形式でデータをexportする/S3バケットのJSONデータからDynamoDBテーブルをimportする、という機能があるためこれを利用しました。GlueなどのETLツールを利用するしかありませんでした。S3を使ったデータ移行が可能になったことでかなり簡単にできるようになっただけでなく、この方法はテーブルの書き込み容量を消費しないためコスト面でもメリットが大きいです。S3からのimportは新テーブルでしか実行できず、すでに存在するテーブルはサポートされていません。明確に書かれていないのですが、この方法ではローカルセカンダリインデックス(LSI)が作成できないという制約があります。今回はLSIの代わりにグローバルセカンダリインデックス(GSI)を代用することにしました。exportしたJSONデータを適切な形に変形する必要がありますが、移行元のカラムの状態でどちらのテーブルに移行するか仕分けるだけなので、簡単なスクリプトを用意して変形しました。今回はDenoを使ったTypeScriptでデータ変形スクリプトを用意しました。Deno+TypeScriptを選定したのは以下の理由です。扱うデータ形式がJSONなため、JS/TSだと楽。1~2ヶ月ほどの中期に渡って実験・調整していくことが予想されるため、型チェックの恩恵が受けられると嬉しい。Denoだと実行時に依存関係解決ができたり、デフォルトでTypeScript対応しているため、環境構築が楽。大量のファイルIOが発生するため、適度に非同期処理をいれて高速化したい。Denoの非同期処理はデフォルトでPromiseが返されるため、async/awaitを使った非同期処理が書きやすい。Denoは去年からnpmパッケージに対応したので、AWS-SDKが問題なく使用できました。そのため、変形スクリプトだけでなくS3へのアップロードなどの周辺スクリプトもDenoで簡単に用意できました。(変形スクリプト自体はJSONの操作のみなのでAWS-SDKを使用していません。)DynamoDBのJSONについてDynamoDBからJSON形式でexportしたとき、各ItemがJSONとして表現されますが、その形式が通常のJSONと異なります。{"Item":{"id":{"N":"1"},"text":{"S":"sample"},"flag":{"BOOL":"true"}}}{"Item":{"id":{"N":"2"},"text":{"S":"sample2"},"flag":{"BOOL":"false"}}}...まず、ファイル全体ではJSONの正しい形式なっていません。ファイルの1行が一つのJSON形式の文字列になっており、これがDynamoDBの一つのアイテムと対応しています。これは、プログラムで扱うときにファイル全体を読み込まずとも、1行ずつ読み込んでJSON文字列として扱えるメリットがあります。フィールド名をつけることで指定します。13時間41分52秒importが圧倒的に時間がかかることがわかりました。この時点では新テーブルは参照されないため時間がかかっても構わないのですが、ここが長引けばPhase2で処理すべきデータが増えるため、なるべく短いほうが嬉しいです。読んで、いくつかの高速化処理を施しました。各ファイルは並列してインポートされるため、分割が推奨されています。1ファイル内のアイテム数は400万件程度が目安とされています。今回のデータは全体で5000万件程度あるので、最低でも10~20個のファイルに分割したほうがよいでしょう。実験後もデータ数は増えていくので、余裕を持って100個へ分割しました。ファイル数も5000個を上限とすることが推奨されているようなので、あまり多すぎない意味で100個のファイルにしています。書き込み先のファイルをアイテム1件ごとにローテーションする1件目のアイテムはファイル1に、2件目のアイテムはファイル2に...といった要領ですアイテムのソート順がランダム化されていたほうがインポートの効率がよいらしいのですが、データ量が膨大で完全にランダムにするコストが大きかったので、連続するアイテムの書き込み先ファイルを分散することで擬似的にランダム化しています。これがどの程度効果があるのかは不明ですが、実装コストが軽いためこの方法をとりました。これらの処理の結果、約14時間かかっていた処理が4時間に短縮できました。ファイル数が増えたためスクリプトの処理時間が微増したのですが、非同期戦略を調整することでほとんど同じ時間で実行できました。)を有効化し、新規データも新テーブルに書き込まれるようになります。Phase1の実行開始~新旧同期処理の有効化までの約6時間分のデータが新テーブルに書き込まれていません。Phase2では、Phase1と新旧同期有効化の間に書き込まれたデータを新テーブルに書き込むことを考えます。以下のようなスクリプトを用意しました。実装はPhase1と同じ理由でdreno+TypeScriptです。Streamを使い書き込みログを5分ごとに吐き出す仕組みがすでに存在していました。スクリプト1はそこから変更があったキーを抽出・重複排除する形で実装しました。旧テーブルを確認し、新テーブルが適切な形になるように修正します。残差分の定点観測Phase2までにほとんどのデータの移行が完了しているはずですが、DynamoDBにはトランザクションの機能的な制約が大きく、以下のような状況で新旧のデータが一致しないことが考えられます。何らかの理由で新テーブルのみ書き込みに失敗し、旧テーブルのみ書き込みに成功した場合トランザクションで回避可能ですが、実装コストやコードの移行が完全に完了すれば不要になることを鑑みて、今回はトランザクションを使っていません。NewsPicksでは分析のためDynamoDBのデータを定期的にRedshiftに連携しており、今回のテーブルもその対象となっています。Redshift上で比較用のSQLを実行することで、新旧テーブル間で差分がないか定期的に確認します。差分があったアイテムについては、Phase2のスクリプト2を実行することで差分を解消します。まとめDynamoDBではimport/exportの制約があり、RDSと比較して整合性が弱いですが、不整合を丁寧に検知しながら進めることで移行することができました。各種の制約が許容できるのであれば、S3へのimport/exportは扱いやすくコスト的にもメリットが大きいです。大量のデータや頻繁な書き込みがある場合、不整合の発生頻度が高まるため、より丁寧な移行計画が求められると思います。エンジニアを募集中です!ご興味のある方はこちらまで。アドベントカレンダーの他の記事も見てみてくださいね。明日はmorinotaさんが書いてくれます。reserved.引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。