tl;dr
ダイナミック!ダ◯クマー。
懐かしい CM を胸に秘め DynamoDB をチュートリアルしてみる。
DynamoDB とは
参考
- http://www.slideshare.net/AmazonWebServicesJapan/20150805-aws-blackbeltdynamodb
- http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-amazon-dynamodb
- http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DataModel.html
黒帯先輩の教えを乞う
以下の資料等を写経。
www.slideshare.net
- 完全マネージド型 NoSQL データベース
- ハイスケーラブル、低レイテンシー
- 高可用性(三拠点(AZ)レプリケーション、SPOF 無し)
- シンプル&パワフル API
- ストレージの容量制限無し
- テーブル毎に Read / Write に対してスループットキャパシティを割り当てることが出来る(オンラインで変更可能)
- Cassandra の先輩(Cassandra は Dynamo 論文にインスパイヤされた)
整合性モデル
- Write は二つの AZ で書き込みが完了した時点で書き込み完了とする
- Read はデフォルトでは結果整合性のある読み込み(結果整合性については FAQ にも記載がある)→最新の書き込み結果が反映されないことがある
- Consistent Read オプションを付けた場合には Read リクエストを受け取る前までの Write を保証、Capacity Unit を 2 倍消費する
なんぼ?
テーブル設計
チュートリアル当初は全く意味不二子(不明)だったテーブル設計だが、黒帯先輩の以下のページでザクッと把握。
(出典: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)
概念
以下の図が解りやすかった。
Hash
- Hash キーをプライマリキーとするパターン
- Hash キーとは順序を指定しないハッシュインデックスを構築する為のキー
Hash and Range
- Hash キーと Range キーを合わせてプライマリキーとするパターン
- Range キーとはHash キーの順番を保証する為のキー
パーティション?
- 性能を確保するためにデータはパーティションに分けて保存される
- Hash キーはパーティション間でのデータ分散に利用される
- Range キーはパーティション内でのデータの順序を保証する
- 格納されるデータ量やスループットによってリパーティショニングされる
型
Attribute の型については以下をサポート。
- String
- Number
- Binary
- Boolean
- Null
- 多値データ型
- ドキュメントデータ型(List 型/Map 型)
Hash キー、Range キーの型については String / Number / Binary のいずれかであること。尚、詳細については「DynamoDB データ型」にて。
ここまで 2000 文字強
スループット等についても深堀りする必要があるが、詳細についてはドキュメントや上記の資料にお任せして、とりあえず手を動かしてダイナミック!ダイナモーしてみたいと思う。
ダイナミック!ダイナモー
DynamoDB Local
DynamoDB がどげなもんか知るためにはマネジメントコンソールから弄ってみるのが一番良い(個人的感想)かもしれないが、DynamoDB Local というローカルホストで動作する DynamoDB モドキを使ってみる。
Docker エンジンが動いているところであればどこでも動かせるようにしておくと良いかもしれないということで Dockerfile をこさえた。
- https://github.com/inokappa/oreno-dynamic-dynamo
- https://hub.docker.com/r/inokappa/oreno-dynamic-dynamo/
大気中の汚染物質濃度情報をダイナミック!ダイナモー
以下の教材を使って進める。
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_code
とCHECK_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
が指定された値以上で且つ(AND
)mon_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_code
が 40137010
で且つ CHECK_TIME
が 2015-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_conditions
とquery_filter
の組み合わせだとうまく結果が取得出来なかった(これらのハッシュキーはThis is a legacy parameter, for backward compatibility.
とあるので注意が必要)
とは言え、DynamoDB の入門の入り口の入り口位には立てたつもりで引き続きチュートリアルを続けていきたい。