ようへいの日々精進XP

よかろうもん

ダイナミック!ダイナモー(DynamoDB tutorial)vol.1

tl;dr

www.youtube.com

ダイナミック!ダ◯クマー。

懐かしい CM を胸に秘め DynamoDB をチュートリアルしてみる。


DynamoDB とは

参考

黒帯先輩の教えを乞う

以下の資料等を写経。

www.slideshare.net

  • 完全マネージド型 NoSQL データベース
  • ハイスケーラブル、低レイテンシー
  • 高可用性(三拠点(AZ)レプリケーション、SPOF 無し)
  • シンプル&パワフル API
  • ストレージの容量制限無し
  • テーブル毎に Read / Write に対してスループットキャパシティを割り当てることが出来る(オンラインで変更可能)
  • Cassandra の先輩(Cassandra は Dynamo 論文にインスパイヤされた)

整合性モデル

  • Write は二つの AZ で書き込みが完了した時点で書き込み完了とする
  • Read はデフォルトでは結果整合性のある読み込み(結果整合性については FAQ にも記載がある)→最新の書き込み結果が反映されないことがある
  • Consistent Read オプションを付けた場合には Read リクエストを受け取る前までの Write を保証、Capacity Unit を 2 倍消費する

なんぼ?

テーブル設計

チュートリアル当初は全く意味不二子(不明)だったテーブル設計だが、黒帯先輩の以下のページでザクッと把握。

http://image.slidesharecdn.com/20150805aws-blackbelt-dynamodb-150805104937-lva1-app6892/95/aws-black-belt-tech-2015-amazon-dynamodb-23-638.jpg?cb=1439875593

(出典:AWS Black Belt Tech シリーズ 2015 - Amazon DynamoDB

テーブル作成に際しては以下の情報が必要になる。

  • テーブル名
  • プライマリキーのタイプ(Hash or Hash and Range)を選択→二つのテーブルタイプが存在する
  • プライマリキーに指定する Attribute
  • (オプション)インデックス(Global Secondary Index)に指定する Attribute(※プライマリキータイプが Hash and Range の場合には Local Secondary Index も選択可)
  • スループットキャパシティ

二つのテーブルタイプ(Hash or Hash and Range)

概念

以下の図が解りやすかった。

http://image.slidesharecdn.com/20140521aws-blackbelt-dynamodb-public-140521192912-phpapp02/95/aws-black-belt-tech-amazon-dynamodb-44-638.jpg?cb=1426464768

Hash

  • Hash キーをプライマリキーとするパターン
  • Hash キーとは順序を指定しないハッシュインデックスを構築する為のキー

Hash and Range

  • Hash キーと Range キーを合わせてプライマリキーとするパターン
  • Range キーとはHash キーの順番を保証する為のキー

パーティション

Attribute の型については以下をサポート。

  • String
  • Number
  • Binary
  • Boolean
  • Null
  • 多値データ型
  • ドキュメントデータ型(List 型/Map 型)

Hash キー、Range キーの型については String / Number / Binary のいずれかであること。尚、詳細については「DynamoDB データ型」にて。

ここまで 2000 文字強

スループット等についても深堀りする必要があるが、詳細についてはドキュメントや上記の資料にお任せして、とりあえず手を動かしてダイナミック!ダイナモーしてみたいと思う。


ダイナミック!ダイナモ

DynamoDB Local

DynamoDB がどげなもんか知るためにはマネジメントコンソールから弄ってみるのが一番良い(個人的感想)かもしれないが、DynamoDB Local というローカルホストで動作する DynamoDB モドキを使ってみる。

docs.aws.amazon.com

Docker エンジンが動いているところであればどこでも動かせるようにしておくと良いかもしれないということで Dockerfile をこさえた。

大気中の汚染物質濃度情報をダイナミック!ダイナモ

以下の教材を使って進める。

github.com

AWS SDK for Ruby V2 で環境省大気汚染物質広域監視システム(そらまめ君)で提供されているデータを解析して DynamoDB に格納、検索をやってみる。

DynamoDB Local を起動

% docker run -p 7777:7777 -d inokappa/oreno-dynamic-dynamo -inMemory -port 7777

起動オプションは以下の通り。

  • -inMemory オプションではデータがメモリ上に保存される為、コンテナを停止するとデータがダイナミックに吹っ飛んでしまうので注意する
  • -port 7777 オプションでポート 7777 番で Listen する

HTTP で Listen しているので curl でアクセスしてみると以下のように出力される。

% curl http://127.0.0.1:7777
{"__type":"com.amazonaws.dynamodb.v20120810#MissingAuthenticationToken","message":"Request must contain either a valid (registered) AWS access key ID or X.509 certificate."}% 

テーブルの作成

以下のようにテーブルを作成する。

  • プライマリキータイプは Hash and Range
  • Hash キーには観測地点コードが格納されている mon_st_code を指定
  • Range キーには観測時刻が格納されている CHECK_TIME を指定
  • mon_st_codeCHECK_TIME の Attribute 属性は String を指定
  • スループットは Write / Read ともに 1 で

テーブルを作成するコードは以下の通り。尚、テーブルの作成は create メソッドを利用する。

def create_table(table_name)
  table = $client.create_table({
    table_name: table_name ,
    key_schema:[
      {
        attribute_name: "mon_st_code",
        key_type: "HASH"
      },
      {
        attribute_name: "CHECK_TIME",
        key_type: "RANGE"
      },
    ],
    attribute_definitions: [
      {
        attribute_name: "CHECK_TIME",
        attribute_type: "S",
      },
      {
        attribute_name: "mon_st_code",
        attribute_type: "S",
      },
    ],
    provisioned_throughput: {
      read_capacity_units: 1,
      write_capacity_units: 1,
    },
  })
end

そらまめ君データを DynamoDB Local に放り込む

そらまめ君データは API 提供等はされていないので HTML を解析して整形して DynamoDB Local に放り込む。

% put-record.rb
{"CHECK_TIME"=>"2015-09-12 20:00:00", "mon_st_code"=>"40101010", "town_name"=>"北九州市門司区", "mon_st_name"=>"門司観測局", "SO2"=>"0.001", "NO"=>"0", "NO2"=>"0.005", "NOX"=>"0.005", "CO"=>"NANA", "OX"=>"0.059", "NMHC"=>"NANA", "CH4"=>"NANA", "THC"=>"NANA", "SPM"=>"0.006", "PM2.5"=>"NANA", "SP"=>"NANA", "WD"=>"南西", "WS"=>"0.9", "TEMP"=>"NANA", "HUM"=>"NANA", "mon_st_kind"=>"一般局"}
#<Seahorse::Client::Response:0x007fdc99c7e3e0>
{"CHECK_TIME"=>"2015-09-12 20:00:00", "mon_st_code"=>"40101020", "town_name"=>"北九州市門司区", "mon_st_name"=>"松ヶ江観測局", "SO2"=>"0.001", "NO"=>"0", "NO2"=>"0.004", "NOX"=>"0.004", "CO"=>"NANA", "OX"=>"0.057", "NMHC"=>"NANA", "CH4"=>"NANA", "THC"=>"NANA", "SPM"=>"0.014", "PM2.5"=>"10", "SP"=>"NANA", "WD"=>"西", "WS"=>"2.9", "TEMP"=>"NANA", "HUM"=>"NANA", "mon_st_kind"=>"一般局"}
#<Seahorse::Client::Response:0x007fdc99217e38>

(snip)

{"CHECK_TIME"=>"2015-09-12 20:00:00", "mon_st_code"=>"46466010", "town_name"=>"志布志市", "mon_st_name"=>"志布志", "SO2"=>"0.016", "NO"=>"0", "NO2"=>"0.006", "NOX"=>"0.006", "CO"=>"NANA", "OX"=>"0.043", "NMHC"=>"0.12", "CH4"=>"1.96", "THC"=>"2.08", "SPM"=>"0.025", "PM2.5"=>"NANA", "SP"=>"NANA", "WD"=>"西南西", "WS"=>"1.4", "TEMP"=>"NANA", "HUM"=>"NANA", "mon_st_kind"=>"一般局"}
#<Seahorse::Client::Response:0x007fdc9949cff0>{"CHECK_TIME"=>"2015-09-12 20:00:00", "mon_st_code"=>"46482010", "town_name"=>"肝属郡東串良町", "mon_st_name"=>"東串良", "SO2"=>"0", "NO"=>"0", "NO2"=>"0.002", "NOX"=>"0.002", "CO"=>"NANA", "OX"=>"0.045", "NMHC"=>"0.16", "CH4"=>"1.96", "THC"=>"2.12", "SPM"=>"0.008", "PM2.5"=>"NANA", "SP"=>"NANA", "WD"=>"西南西", "WS"=>"4.4", "TEMP"=>"NANA", "HUM"=>"NANA", "mon_st_kind"=>"一般局"}
#<Seahorse::Client::Response:0x007fdc99474348>

データを放り込む場合には put_item メソッド又は batch_write_item を利用する。今回は put_item を利用している。

データを全件取得

とりあえず全件取得するには scan_item メソッドを利用する。

def scan_item(table_name)
  result = $client.scan(
    table_name: table_name,
    select: "ALL_ATTRIBUTES",
  )
 
  puts "Records: " + "#{result.items.count}"
end

レスポンス(上記例では result)の items.count メソッドを利用すれば件数を取得することが可能。

% ./scan-item.rb
Records: 205

205 件のレコードが登録されている。

クエリを投げる

登録されているテーブルに対して Hash キーと Range キーを指定してクエリを投げて結果を得たい場合には query メソッドを利用する。

def query_item(table_name, mon_st_code, check_time, limit_num=1)
  result = $client.query({
    table_name: table_name,
    select: "ALL_ATTRIBUTES",
    key_condition_expression: "mon_st_code = :v_mon_st_code and CHECK_TIME >= :v_check_time",
    expression_attribute_values: {
      ":v_mon_st_code" => mon_st_code,
      ":v_check_time" => check_time,
    },
  })

  result.items.each do |item|
    puts "-------------"
    item.each do |key, value|
      puts "#{key}: #{value}"
    end
  end
end

上記のメソッドは以下のような処理を行っている。

  • それぞれのキーの条件を key_condition_expression で指定している
  • 上記例では CHECK_TIME が指定された値以上で且つ(ANDmon_st_code が指定された値であることが条件となる
  • 条件の指定にあたっては :v_mon_st_code:v_check_time 等のプレースホルダを利用する
  • expression_attribute_values のハッシュキーにてこれらのプレースホルダとして利用している

詳細、制限事項等については「DynamoDB でのクエリおよびスキャンオペレーション」に明記されている。

実際に以下のような条件でクエリを投げてみる。

  • mon_st_code : 40137010
  • CHECK_TIME : 2015-03-12

以下のように出力される。

% ./query-item.rb
-------------
NO: 0
HUM: 93
CHECK_TIME: 2015-09-12 20:00:00
mon_st_kind: 一般局
OX: 0.049
mon_st_name: 祖原
CO: NANA
WD: 西南西
CH4: 1.93
THC: 2.01
mon_st_code: 40137010
NO2: 0.006
SPM: 0.015
TEMP: 21
town_name: 福岡市早良区
NOX: 0.006
SO2: 0
PM2.5: NANA
WS: 1.4
SP: NANA
NMHC: 0.08

mon_st_code40137010 で且つ CHECK_TIME2015-03-12 以上のレコードのみが抽出された。

テーブルの削除

テーブルの削除については delete_table メソッドを利用する。

def delete_table(table_name)
  resp = $client.delete_table({
    table_name: table_name
  })
end

サクッとテーブルが消える。


ざっと(ここまで 9000 文字超)

ダイナミック!ダイナモーしてみたが、たかだか 9000 文字程度で語れる位 DynamoDB は甘くは無いことを痛感した。特に以下の二点...

  • テーブルのプライマリキータイプについての理解が乏しくてテーブルそのものが作れない(今は Hash と Hash and Range テーブルの二種類があるところまでは理解できている...一応)
  • query メソッド使ってクエリ投げる場合に条件指定が key_conditionsquery_filter の組み合わせだとうまく結果が取得出来なかった(これらのハッシュキーは This is a legacy parameter, for backward compatibility. とあるので注意が必要)

とは言え、DynamoDB の入門の入り口の入り口位には立てたつもりで引き続きチュートリアルを続けていきたい。