まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:cloudant

先日、IBM の NoSQL DBaaS である Cloudant のデータベース単位でのバックアップ/リストアを行うツールを作って公開しました:

本エントリでは、そもそも何故このようなツールを作ったのか、実現においてどのような制約事項があり、どのように制約を回避したか、その背景を技術者視点で紹介します。


【そもそもそんなツールが必要なのか?】
「必要なのか?」と言われると、結構鋭い視点だと思います。Cloudant はいわゆる「マルチマスターレプリケーション」型の DBaaS であり、データは複数のサーバーインスタンスに分散して格納されます。つまり「どこか1つのサーバーが死んだ場合でもデータがどこかに残っている」ことはこの仕組自体の中で提供されています。これで充分、と考える人や用途であれば不要という判断もアリです。

とはいえ、データベースを手軽に丸ごと引越したり、(精神衛生上の問題から)手元にバックアップを残しておきたいと考えたり、そのバックアップファイルをストレージに保存してストレージ代を稼ぎたいクラウドベンダー/インテグレーター側の事情(苦笑)などからバックアップとリストアを行う需要はあると思っています。


【ツールがないとバックアップ/リストアはできないのか?】
これも難しい質問です。まず Cloudant はいわゆる CRUD 操作に REST API が用意されており、基本的にはこの REST API を使って読み書き更新削除検索・・といった操作を行うことになります。この API のリファレンスはこちらを参照ください:
https://console.ng.bluemix.net/docs/services/Cloudant/api/index.html#api-reference

このリファレンスを読むと分かるのですが、データベースに対して「データや添付ファイルも含めた全ドキュメントデータを取得する API」も「複数のドキュメントデータをまとめて書き込む API」も、どちらも存在しています。以下の記述では前者を「データ取得 API」、後者を「データ書込 API」と呼ぶことにします。

これら2つの API を組み合わせればバックアップとリストアは実現できそうに見えます。が、実はいくつかの問題があり(一部は後述します)、その中でも最も大きなものは「データ取得 API で得られるアウトプットデータと、データ書き込み API でインプットするデータのフォーマットが異なる」という点です。より正確に表現すると「データ取得 API で得られるアウトプットデータを、そのままデータ書込み API のインプットデータとして使った場合、書込み処理自体は成功し、全てのデータがインポートされるが、書き込まれた個々のドキュメントデータの JSON データとしてのフォーマット(データ階層)が元のデータのフォーマットとは異なる」という結果が待っているのでした。つまりバックアップ元とリストア先のデータベースの中身(データの JSON のフォーマット)が同じではなくなってしまうのでした。


シンプルに表現すると、バックアップ元のデータベースに以下のようなデータが入っていたとすると、、
{ "_id": "001", "name1": 123, "name2": "value1", "name3": true },
{ "_id": "002", "name1": 456, "name2": "value2", "name3": false },
{ "_id": "003", "name1": 789, "name2": "value3", "name3": true },
  :

上記 API でバックアップ&リストアした先のデータは以下のようになります:
{ "_id": "001", "doc": { "_id": "001", "name1": 123, "name2": "value1", "name3": true } },
{ "_id": "002", "doc": { "_id": "002", "name1": 456, "name2": "value2", "name3": false } },
{ "_id": "003", "doc": { "_id": "003", "name1": 789, "name2": "value3", "name3": true } },
  :

バックアップ元では、例えば "name1" という属性の値を参照しようとすると、(document).name1 でアクセスできた内容が、リストア先だと (document).doc.name1 という方法でアクセスすることになる、という違いが生じます。


ここは考えようで、「元のデータが残っているかどうか」という観点では残っているといえます。ただ JSON データとして見たときの階層情報が異なっていて、全く同じデータではない(特定の属性の値を取得する場合の取得方法は異なる)のです。これを許容できるかどうかの問題とも言えますが、一般的にバックアップ/リストアという言葉からイメージするのはデータフォーマットの変更が許されるものではないと思っています。その意味では上記 API をそのまま使って実現する方法は選択できなくなります。

また細かいことですが、データ書込み API を実行するには事前にデータベースを新規に作成しておく必要があります。わざわざ「データベース作成+文書リストア」という2度手間をかけるのも不便だし、そういった利便性まで含めて考えると1発で実現できるなんらかのツールがあるべきだし必要、という判断になります。


【ツールでの実現方法】
上記の理由により、リストア先でもデータフォーマットを変えずに復元できるよう、API をそのまま使う方法ではなく、処理の途中にフォーマット変換を行うようなバックアップ/リストア処理を行うツールを作ります。

この時点で考えられる実装アルゴリズムには以下のようなものが挙げられます:
(1) バックアップ時には上記のデータ取得 API をそのまま使い、その結果をバックアップファイルとする。リストア時にはバックアップファイルからデータを1つずつ取り出し、リストアしたい部分だけを取り出して1件ずつ書き込む。
(2) バックアップ時には上記のデータ取得 API をそのまま使い、その結果をバックアップファイルとする。リストア時には、まず上記のデータ書込み API で一括書込みした時にデータフォーマットが乱れないような形にバックアップファイルを加工する。その上でデータ書込み API を実行して一括書き込みする
(3) バックアップ時にはまず上記のデータ取得 API をそのまま使い、その結果をデータ書込み API 用に加工した上で保存し、バックアップファイルとする。リストア時にはデータ書き込み API をそのまま実行して一括書き込みする
  :


いずれも理論的には実現できそうなのですが、ここで API とは別の制約が問題になります。上記の (1) の方法で(データを1件ずつ)リストアすることは可能なのですが、その際に「Cloudant のスタンダードプランでは 1 秒間に 10 件しか書込みリクエストを処理できない」という制約事項を意識する必要があります。例えばバックアップファイルに 11 件以上のドキュメントが存在していた場合、データを書き込む API は1秒間に 10 回しかコールできません(どこまで厳密にこの制約が管理されているのかはわかりませんが、私自身がリクエスト上限によるエラー "You've exceeded your current limit of 10 requests per second for write class. Please try later." の発生を確認しています)。このエラーを回避しようとすると、プログラム内で実行タイミングを測り、10回書込みを実行したら、次の書込みリクエストは1秒以上待ってから実行する、という本質的ではない管理が必要になります(そしてこれは Node.js のような非同期処理を基本とする実行環境では非常に面倒な問題になります)。また仮にそこまで作り込んだとしても、多くのドキュメントを持つデータベースのリストアには非常に多くの時間がかかり、その多くは待ち時間となる可能性が高くなります。バックアップ取得にかかる時間に対し、リストアにあまりに多くの時間が取られる、という非生産的な制約ができてしまうので、上記の (1) の方法は断念することにしました。また上記の (1) ~ (3) 以外にも1件ずつ取り出して、1件ずつ書き込んで・・・という方法も考えられますが、同じ理由で非現実な方法となってしまうだけでなく、API の実行回数に比例するコスト上昇という問題も抱えてしまいます。

というわけで、(2) か (3) の方法での(一回の API 実行でまとめて書き込むような方法での)実装が候補として考えられるのですが、今回自分は (3) の方法を取ることにしたのでした。つまりバックアップ時にデータ取得 API を使って一括取得したデータをそのまま保存するのではなく、リストア用(データ書込み API 用)に加工してからバックアップファイルとして保存する、という方法です。そしてリストア時にはまずリストア先のデータベースを作成(既存の場合は削除した上で作成)した上で、取得したバックアップファイルをそのままデータ書込み API に渡して一括書込みを行うことにします。
バックアップツールの処理
  • データ取得 API を使って、バックアップ元データベースから全文書を一括取得
  • 取得した結果をデータ書込み API で一括書込みできるようなフォーマットに変換してから保存
リストアツールの処理
  • リストア先に指定されたデータベースが既に存在していたら削除する
  • リストア先データベースを作成
  • データ書込み API でバックアップファイル内の全文書データを一括書込み

なお、この方法であれば Node.js のような非同期処理の実行環境でも(一括で書込みを行うだけなので)あまり意識せずに実装することができます。

また、この (3) の方法であれば (2) の方法に比べてリストア時にはこのツールを使わなくても、バックアップファイルを指定して(curl などで)データ書込み API を直接実行してもリストアできる、という副産物的なアドバンテージもあります。


と、まあ色々を事情や背景を書き並べましたが、要は「自分はバックアップを手元に置きたい派」で、「自分で手軽にバックアップを取りたくて作った」のです。で、色々調べているうちに Cloudant API の制約事項の勉強にもなったし、一応動くものもできたし、いい勉強にもなりました。


IBM Bluemix からも提供されている NoSQL DBaaS である Cloudant 。このデータベースサービスにはベータベースの全文書を取得したり、複数データをバルクでインサートするような REST API が提供されており、これらを応用することで(curl を併用するなどして)バックアップやリストアを行うこともできることになっています。


ただこの方法にはいくつかの問題点もありました。個人的には以下の2点がちと無視できない制約でした:
(1) バックアップしたデータをバルクインサートすると、元のデータベース内にあった JSON ドキュメントとは異なる階層構造になってしまう
(2) バックアップデータには attachments(添付ファイル)情報が含まれない。そのためバルクインサートでリストアしても添付ファイルは復元されない


これらの問題を解決して API でバックアップ&リストアするための方法を考えていましたが、結論としては「専用ツールを作った方が早くて便利そう」でした。で、実際に作ってみました:
https://github.com/dotnsf/cdbtool


セットアップ方法や使い方は README.md にも書いておきましたが、動作前提に Node.js が必要です。お使いのシステムに併せて Node.js を導入しておいてくださいませ。

で、上記 URL からツール本体をダウンロード&展開するか、git clone します。

展開後のファイル一覧の中に settings.js というファイルがあります。このファイルをテキストエディタで開き、バックアップ&リストアの対象とする Cloudant サービスのユーザー名およびパスワードに該当部分を書き換えて保存します:
exports.cloudant_username = 'ここを Cloudant のユーザー名に書き換える';
exports.cloudant_password = 'ここを Cloundant のパスワードに書き換える';

なお、Cloudant のユーザー名およびパスワードは別途確認しておいてください。IBM Bluemix 環境の場合であればランタイムやサービスの資格情報から確認することができます:
2017032801


最後にこのツールが必要なライブラリをまとめてインストールします。package.json があるディレクトリで以下のコマンドを実行します:
$ npm install

これで準備完了!

ではまずは Cloudant データベースをダンプ(バックアップ)してみます。使うファイルは dump.js で、コマンドラインから以下のように入力します:
$ node dump (dbname) (dumpfilename)

最初の2つ(node dump)は「Node.js で dump.js を実行する」ことを指定しています。残りの2つはいわゆるコマンドラインパラメータです。

最初のコマンドラインパラメータの (dbname) は Cloudant 上のデータベースの名称です。例えば、現在 Cloudant のダッシュボードでデータベース一覧を見た時に以下のようになっているものとします:
2017032801


データベースが5つありますが、この中の一番下にある "spendb" データベース(文書数 53)のバックアップを取得するのであれば、このパラメータには spendb と指定することになります。

最後のパラメータ (dumpfilename) はダンプ結果を保存するファイル名を指定します。今回はここに spendb.dump と指定して、この名前のファイルをダンプファイルとして新たに作成することにします。つまりコマンドラインからは以下のように実行することになります:
$ node dump spendb spendb.dump

このコマンドが成功すると、実行時のディレクトリに spendb.dump という名前のファイルが作成されているはずです。

ではバックアップで作成したファイルを使って、新しいデータベースにリストアしてみましょう。リストア時は以下のようなコマンドを入力します:
$ node restore (newdbname) (dumpfilename)

ここでも後ろの2つがコマンドラインパラメータで、最初の (newdbname) はリストア先のデータベース名です。指定した名前のデータベースが存在していない場合は新たに作成され、存在している場合は一度削除されて新たに作成されます(文書データだけ上書き、ではありません)。 また (dumpfilename) には上記で作成したダンプファイル名を指定します。仮に上記で作成したダンプファイルを使って、newdb という名前のデータベースにリストアするのであれば、以下のように実行することになります:
$ node restore newdb spendb.dump

このコマンドが成功すると、Cloudant 上に newdb という名前のデータベースが新たに作成され、その中にドキュメントが(元データベースと同じ 53 文書)ロードされているはずです。文書ID も元のデータベースのものがそのまま使われ、元データベース内に添付ファイル(attachments)が含まれていた場合は添付ファイルも含めてリストアされる仕様です(これを実現したくて、このツールを作りました):
2017032802



今後はダンプファイルのサイズ圧縮とかにも対応しようかなあ。気が向いたら機能追加したりバグ修正したりもしますが、MIT ライセンスでオープンソース化しているので、何かあったら適当に(笑)対応していただけるとうれしいです。



IBM Bluemix からも提供されている、スケーラブルな NoSQL DBaaS である Cloudant 。データの読み書きには REST API が提供されており、ウェブアプリケーションだけでなく、スマホのネイティブアプリなど色々なアプリケーションから利用することができます。
2017032101


この Cloudant はいわゆる「JSON ドキュメント」を格納するデータベースなのですが、バイナリデータ(というかファイル)を扱う機能("attachement")も持っています。

以下にバイナリデータを格納する方法を紹介します。まずは JSON データを無視して「バイナリデータだけを新たに格納する場合」は以下のような JSON データを新規作成します(赤字はコメント):
{
  "_id": "D001",  Cloudant 上でのドキュメントID、省略した場合は作成時に自動的に割り振られる
  "_attachments": { この _attachments オブジェクトがバイナリ保存時の肝
    "A001": {     任意に付ける Attachment 名、取り出し時のURLに指定する
      "content_type": "image/jpeg",  バイナリデータの Content-Type
      "data": (バイナリデータを Base64 エンコードしたテキストデータ)
    }
  }
}

上記のデータを(普通の JSON ドキュメントと同様に)以下の URL に対して POST リクエストすると、このバイナリデータを含むドキュメント(と attachment )が新規に Cloudant 内に作成されます:
https://(Cloudant のホスト名)/(データベース名)


また、作成したバイナリデータを取り出す場合は、以下の URL に対して GET リクエストを実行します:
https://(Cloudant のホスト名)/(データベース名)/(ドキュメント ID)/(Attachment 名)


バイナリデータだけのドキュメントを作成する場合は上記の方法でした。一方、バイナリデータも含む JSON データを保存する場合は、以下のような JSON データを用意して、同様に POST します:
{
  "_id": "D001",
  "myname": "abc",   この2つの値が普通の JSON データとして扱われる部分
  "myvalue": 123,
  "_attachments": {
    "A001": { 
      "content_type": "image/jpeg",  
      "data": (バイナリデータを Base64 エンコードしたテキストデータ)
    }
  }
}

取得時には content_type で指定したデータ型が有効になってレスポンスが返ってきます。なので、例えばバイナリデータとして JPEG 画像データを格納し、その際の cotent_type 値に "image/jpeg" などの正しい型が指定されていれば、取得 URL にブラウザでアクセスすればそのまま画像を表示することができます:
2017032102

(↑ Cloudant 上に格納した画像データを直接 URL 指定で表示している様子)


一般的にはファイルなどのバイナリデータをネット上のストレージに格納する場合は Object Storage などを使うことが多いと思っています。が、Content-Type を意識して取り出したり、「(添付ファイルなどの)JSON ドキュメントに紐付いたバイナリデータ」として利用する場合に便利な機能だと思っています。


なお、Cloudant の Attachment 関連 API についてはこちらを参照ください:
https://docs.cloudant.com/attachments.html
 

以前に Cloudant データベースにインデックスを作成する方法を紹介しました:
Cloudant 内のデータをインデックスして検索する


上記エントリの (2) で紹介した方法は単独のフィールド(cputemp)にインデックスを作成して、数値が一定値よりも大きいレコードを探す、というクエリーができるようにしたものでした。

では例えば、複数フィールドで検索条件を指定するようなクエリーを実行したい場合はどのようにすればいいでしょうか? 例えば以下のようなレコードが Cloudant のデータベース db に入っているものとします:
IDNAMEAGE
1鈴木20
2鈴木41
3佐藤20
4田中35
5田中15
: : :

(↑ RDB っぽく書いてますが、実際には JSON フォーマットで {"ID":1, "NAME":"鈴木", "AGE":20} のように格納されているとします)


この時に「30歳以上の鈴木さん」とか「20歳の田中さん(実際には見つからない)」という条件でレコードを検索したい場合、どのようなインデックスを作って、どのように検索すればいいでしょうか?


まず、NAME と AGE の2つのフィールドにまたがるインデックスを用意する必要があります。Cloudant の管理画面から目的のデータベースを選び、"Design Documents" の右にある + をクリックして、"Query Indexes" を選択してください:
2016101001


Index の定義画面が表示されたら、テキストフィールド内に以下の値を記述します:
{
  "index": {
    "fields": [
      {
        "name": "NAME",
        "type": "string"
      },
      {
        "name": "AGE",
        "type": "number"
      }
    ]
  },
  "type": "text",
  "name": "db-index"
}

string 型の NAME と、number 型の AGE を指定して db-index という名前のインデックスを作ります。入力後に "Create Index" ボタンをクリックすると実際にインデックスが作成されます:

2016101002


インデックス作成後、実際にクエリーを実行してみましょう。例えば「30歳以上の鈴木さん」を検索するには以下のような POST リクエストを発行します(curl で実行する場合の例):
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"鈴木"},"AGE":{"$ge":30}}}'
(Cloudant のサーバーが XXX-bluemix.cloudant.com 、ユーザー名が username 、パスワードが password であるものと仮定しています)

同様にして「20歳の田中さん」を検索する場合はこのようなリクエストを発行することになります:
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"田中"},"AGE":{"$eq":20}}}'

それぞれ _find にセレクター(selector) を指定して POST リクエストを発行しています。そしてそのセレクターの中で存在するインデックスを使った検索条件を指定して、クエリーを実行しています。その際にあらかじめ作成しておいたインデックス(db-index)が使われて、想定したレコードを検索することができるようになる、というものです。

この「想定するクエリーを実行するためのインデックスをあらかじめ用意しておく」というのが RDB に比べてちと面倒で、クエリーの柔軟性にかける部分ではありますが、Cloudant はこういった方法でクエリーに対応しています。


IBM Bluemix からも提供されている NoSQL 型データベースの DBaaS "Cloudant" の開発者向けエディションが Docker イメージでの無料提供が開始されました:
https://hub.docker.com/r/ibmcom/cloudant-developer/

2016091004


1インスタンスでスケーリングなし&サポートもなし、という条件は付きますが、IBM Bluemix などのアプリケーション開発者がローカル環境だけでシングルテナントの Cloudant の環境を使うことができるようになります。以下、その手順を紹介します。

まず利用にあたり、docker 1.9 以上が必要です。RHEL/CentOS の 6.x の場合は docker 1.7 までしかサポートされていないため、バージョン7以上を用意する必要があります。

docker 環境が用意できたら、以下のコマンドで開発者向け Cloudant のイメージを pull します:
# docker pull ibmcom/cloudant-developer

そして以下のコマンドでローカル環境上にコンテナを作り、起動します:
# docker run --privileged --detach --volume cloudant:/srv --name cloudant-developer --publish 8080:80 --hostname cloudant.dev ibmcom/cloudant-developer

初回起動時のみ、ライセンス規約に同意する必要があります。以下のコマンドを実行してライセンス画面を表示します:
# docker exec -ti cloudant-developer cast license --console

すると以下の様なライセンス内容がコンソールに表示されます。同意する場合は "1" を入力してください:

2016091001


また、これも最初の1回だけ利用前にデータベースを初期化する必要があります。初期化は以下のコマンドを実行します:
# docker exec cloudant-developer cast database init -v -y -p pass

これで準備は完了です、簡単ですね。実際にローカル環境の Cloudant を利用するにはウェブブラウザで以下のアドレスにアクセスします:
http://(docker が動いているマシンのIPアドレス):8080/dashboard.html

すると以下の様なログインが画面が現れます。デフォルトでは
 Username = admin
 Password = pass
という ID & パスワードがセットされているので、この値を入力してログインしてください:

2016091002


ログインが成功すると、見慣れた Cloudant のダッシュボード画面が表示されます。後はいつもと一緒です。これでいつでもローカルで手軽に使える Cloudant 環境が用意できました:

2016091003

このページのトップヘ