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

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

2017/03

先日、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 ライセンスでオープンソース化しているので、何かあったら適当に(笑)対応していただけるとうれしいです。



Google ドライブのファイルシステム(?)を fuse を使って Linux にマウントする、というためのツールを使ってみました。今回は Google Drive ocamlfuse というツールを使って、Ubuntu 14.04 環境にマウントしてみました:
https://github.com/astrada/google-drive-ocamlfuse


なお、今回紹介する手順では途中でウェブブラウザを使った OAuth 認証を行うため、GUI 環境が必要です(コマンドライン環境だけでは最後までマウントできません)。GUI アクセスできる Ubuntu とウェブブラウザをご用意ください。

まずはターミナルを開き、Google Drive ocamlfuse を apt-get でインストールできるようにするため、リポジトリを追加します:
$ sudo add-apt-repository ppa:alessandro-strada/ppa
$ sudo apt-get update

そして apt-get install を実行します:
$ sudo apt-get install google-drive-ocamlfuse

準備の最後に Google Drive をマウントする先のマウントポイントとなるディレクトリ(以下の例では ~/googledrive)を用意しておきます:
$ mkdir ~/googledrive

では Goodle Drive ocamlfuse を使って実際にマウントしてみます。初回のみコマンドラインから引数なしで実行します:
$ google-drive-ocamlfuse

するとウェブブラウザが起動し、Google の OAuth 認証が行われます。Google ドライブを使うためのユーザーおよびパスワードでログインします:
2017032201


オフラインアクセスのための許可が求められるので「許可」をクリック:
2017032202


以下のようなメッセージが表示されれば OAuth 認証完了です:
2017032203


再びターミナル画面に戻り、今度はマウントポイントを指定して google-drive-ocamlfuse を実行します。これで OAuth 認証時に使ったユーザーの Google ドライブが指定ディレクトリにマウントされます:
$ google-drive-ocamlfuse ~/googledrive


この状態で df -h コマンドを実行すると、指定したディレクトリに Google Drive がマウントされていることが確認できます:
2017032204


ls コマンドなどでこのディレクトリ内を確認すると、Google Drive 内のドキュメントが odt 等のフォーマットで存在していることを確認できます:
2017032205


アンマウントする場合は fusermount コマンドを -u オプションを付けて(アンマウントポイントを指定して)実行します:
$ fusermount -u ~/googledrive


Google ドライブがマウントできると Boostnote のドキュメント共有が異なるシステム間でも可能になったりできて、ますます便利です。



(参考)
http://o2t.hatenablog.com/entry/2014/09/08/143621
 

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
 

IBM InterConnect 2017 の期間中に IBM Bluemix Platform にドイツリージョン(eu-de)が追加されました。記念に早速ランタイムアプリを1つデプロイして公開してみました:
http://spen.eu-de.mybluemix.net/


このアプリはスマホ用(正確にはジャイロセンサーを搭載したスマホ用)なので、試しに使ってみる場合は上記 URL にスマホのブラウザでアクセスしてみてください。以下のような画面が表示されます:
IMG_0430


画面の下半分あたりを指でタップし、離さずにそのまま空中でスマホを振って、文字等を一筆書で書いてみてください。書き終わったらタップした指を離してください:
IMG_0430


指でタップしていた間にスマホの先端が動いた軌跡が画面内に表示されます。また画面上部の表にはタップしていた間のスマホの左右(LR)や前後(FB)の動きが数値として表示されます(ちなみに DIR は向いている方向角で、今回の軌跡描画では使っていません):
IMG_0431


これだけのアプリですが、全て1つの HTML だけで実現しています。スマホの動きは JavaScript でジャイロセンサーにアクセスしてその値を取得し、軌跡の描画には HTML5 の Canvas を利用しています。また HTML 自体では Canvas は非表示としており、画面に表示されているのはこの Canvas を描き終えたタイミングで動的に画像データを生成して、その画像を <img> タグで動的に表示しています。

スマホのジャイロデータを JavaScript だけで取得するとか、Canvas データから画像データを生成するとか、それなりに凝った処理を行っているこの HTML を Github で公開しました:
https://github.com/dotnsf/SPen/


MIT ライセンスで公開しています。勉強目的なり、取得データを使って別の処理を行うよう改造するなり、いろんな応用はできると思うので、よかったら遊んでみてください。

このページのトップヘ