AWS
DynamoDB
aws-sdk
DAX

Amazon DynamoDB Accelerator (DAX) を使ってみた

グレンジ Advent Calendar 2017 20日目の記事を担当することになりましたmrt_techです。
グレンジでサーバーエンジニアをしています。
今年の6月から利用できるようになったAmazon DynamoDB Accelerator (DAX) を試してみました。

DAXについて

DAXはDynamoDBのメモリキャッシュサービスで、アプリ開発者がキャッシュのクリアを気にすること無く利用できるとされるサービスです。
ダックスと発音します。
DAXを導入すると読み込み消費キャパシティの削減や読み込みスループット向上が期待できるということなので、導入前・導入後を比較してみました。

DAX導入前後の消費キャパシティ比較

1000件の項目を読み込みループさせたときの読み込みキャパシティのメトリクスです。

GrengeAdventCalendar.png

導入後では消費キャパシティを削減できていることがわかります。
消費キャパシティが0になっているのは、DAXによりキャッシュヒットとなったため一切消費されなくなった状態です。
消費キャパシティがたまに増えていることがありますが、DAXのTTLによりキャッシュ切れとなり、再度元データから読み込まれたことになります。

DAX導入前後の読み込みスループット比較

1000件の項目を読み込みループさせたときのスループット比較です。
1項目は約1.7kB。1分間測定して平均を求めました。
DAX導入後の1分間は全てキャッシュヒットした1分から算出しています。

平均(ms) 最小(ms) 最大(ms)
DAX 導入前 9.72 3.64 135.65
DAX 導入後 1.17 1.06 6.75

結果は明かで、導入後はDAXのキャッシュ機構によりスループット向上を確認できました。

本検証で実行したNodeJSのコード

環境変数DAXの有無で、DAXの利用有無を切り替えられるようにしました。
DocumentClientを利用していれば、既存コードをほぼ変更せず導入できると思います。
原因はわかりませんが、MalformedResultExceptionがエラーレスポンスで返却されることがあるので、リトライさせることで対応しました。
このメトリクスの「失敗したリクエストの数」にもカウントされないので、ハンドリングすることが望ましいです。

get_loop.js
const AWS = require('aws-sdk');
const AmazonDaxClient = require('amazon-dax-client');
const _ = require('lodash');
const async = require('neo-async');
const moment = require('moment');
const sleep = require('sleep');
const util = require('util');

// AWSのconfig設定
AWS.config.update({
  region: 'ap-northeast-1',
  accessKeyId: '<YOUR_ACCESS_KEY_ID>',
  secretAccessKey: '<YOUR_SECRET_ACCESS_KEY>',
});

// 環境変数よりDAX導入を制御
const useDax = (process.env.DAX !== undefined);

// DynamoDBのDocumentClientを初期化
let dynamoClient;
if (useDax) {
  let dax = new AmazonDaxClient({
    endpoints: '<YOUR_ENDPOINT>',
    region: 'ap-northeast-1',
  });
  // DocumentClientはDAXのラッパーになるので既存コードの変更はほぼ無くなります
  dynamoClient = new AWS.DynamoDB.DocumentClient({service: dax});
} else {
  dynamoClient = new AWS.DynamoDB.DocumentClient();
}

// ループ処理用の定数と変数
const minIndex = 1;
const maxIndex = 1000;

let i = minIndex;

// ループ処理
async.doDuring((next) => {
  const id = util.format('%s', i);
  const getParam = {
    'TableName': "user",
    'ReturnConsumedCapacity': "TOTAL",
    'ConsistentRead': false,
    'Key': {
      'id': id
    }
  };

  console.log(i + ',' + moment().format("YYYY-MM-DD HH:mm:ss"));
  console.time(i + ' 処理時間');

  // getItem
  dynamoClient.get(getParam, (err, data) => {
    console.timeEnd(i + ' 処理時間');

    // 正常応答の確認。正常であれば次のidのデータを取得する。
    if (data && data.Item) {
      i++;
      if (i > maxIndex) {
        i = minIndex;
      }
      next();
      return;
    }

    // DAXだと本エラーが発生することがある。
    // ドキュメントより「An exception thrown when the response from the server is invalid.」
    // 同一idのデータ取得をリトライ
    if (err.code === 'MalformedResultException') {
      console.log(i + ' MalformedResultException');
      next();
      return;
    }

    // 他エラーはとりあえずリトライ
    console.log(err);
    next();
  });

}, callback => {
  // 無限ループ
  callback(null, true);
}, err => {
  if (err) {
    console.log(err);
  }
});

まとめ

DAXを導入することで、読み込み消費キャパシティの削減や読み込みスループット向上を確認することができました。
書き込みよりも読み込みの多いテーブルがあれば、プロビジョニング済みキャパシティを減らすことができるので、コスト削減を見込めます。

DAXはクラスター構成で作成されるので、最低2ノードのインスタンスが必要になります(推奨は3ノード以上)。
一番低いインスタンスタイプのdax.r3.largeは 0.322 USD / 1時間。
30日だと、 0.322 USD * 24時間 * 30日 * 2ノード = 463.68 USD。
コスト削減を目的とするのであれば 1ヶ月で約464 USDを削減できるかが導入のポイントになりそうです。

導入後に注意したいのが、アプリの長時間メンテナンスを解除するようなときです。
DAXのTTLによりキャッシュがクリアされ、キャッシュミスが多発することになると思います。
メンテナンスを開けるときはオートスケールによりプロビジョニング済みキャパシティが低くなっていることが考えられ、バーストによりスケールアウトが追いつかずスロットルが発生する恐れがあります。
事前にオートスケールの最小値を上げておくなどのオペレーションが必要になるかもしれません。

補足

AWSドキュメントにあるNodeJSのサンプルですが、一部間違っています。
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.client.run-application-nodejs.03-getitem-test.html
http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.client.run-application-nodejs.04-query-test.html

2つのサンプルとも下記のように置き換えるとOKです。
置き換えないとDAXを経由せず、DynamoDBを直でアクセスするコードです。

aws_sample.js
daxClient = new AWS.DynamoDB.DocumentClient({service: dax });
// ↓
ddbClient = new AWS.DynamoDB.DocumentClient({service: dax });