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

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

タグ:couchdb

ラズベリーパイ(Raspbian OS)に NoSQL 型のデータベースである Apache CouchDB を導入する手順を紹介します:
2019011402


Apache CouchDB (以下、"CouchDB")はオープンソースで提供されている NoSQL データベースです。IBM Cloud の IBM Cloudant はこの CouchDB をベースに提供されているマネージド DBaaS です:
2019011401


なお、IBM Cloudant は分散データベース環境がはじめから提供されていますが、以下で紹介する例では CouchDB を1インスタンスで運用する前提での導入方法とさせていただきます。また導入する CouchDB のバージョンは 2019/01/14 時点での最新版である 2.3.0 を対象としています。

まずは準備作業としてリポジトリをアップデートしておきます:
$ sudo apt-get update -y

$ sudo apt-get dist-upgrade -y

続いて Erlang Solutions のリポジトリを追加し、再度アップデートします:
$ wget http://packages.erlang-solutions.com/debian/erlang_solutions.asc

$ sudo apt-key add erlang_solutions.asc

$ sudo apt-get update

追加したリポジトリを使って、ビルドに必要なライブラリ等を導入します:
$ sudo apt-get --no-install-recommends -y install build-essential pkg-config erlang libicu-dev libmozjs185-dev libcurl4-openssl-dev

またビルドしたバイナリを実行するユーザー(couchdb)と、そのホームディレクトリ(/home/couchdb/)をこの段階で作成しておきます:
$ sudo useradd -d /home/couchdb couchdb

$ sudo mkdir /home/couchdb

$ sudo chown couchdb:couchdb /home/couchdb

ではいよいよ CouchDB のビルドを行います。2.3.0 のソースコードをダウンロード&展開して、ビルドします:
$ cd

$ wget http://mirror.ibcp.fr/pub/apache/couchdb/source/2.3.0/apache-couchdb-2.3.0.tar.gz

$ tar -zxvf apache-couchdb-2.3.0.tar.gz

$ cd apache-couchdb-2.3.0/

$ ./configure

$ make release

ビルドが成功すると、完成したバイナリが ./rel/couchdb/ 以下に作成されています。このフォルダ以下を /home/couchdb/ 以下にコピーし、couchdb ユーザー向けに実行権限を変更します:
$ cd ./rel/couchdb/

$ sudo cp -Rp * /home/couchdb

$ sudo chown -R couchdb:couchdb /home/couchdb

これでビルドは完了です。不要であればソースコードやリポジトリのファイルは削除しても構いません(残しておいても構いません):
$ cd

$ rm -R apache-couchdb-2.3.0/

$ rm apache-couchdb-2.3.0.tar.gz

$ rm erlang_solutoins.asc

この段階で CouchDB を起動することは可能ですが、デフォルト設定のままだと CouchDB はローカルホストからのリクエストのみ受け付ける設定になっています。CouchDB を外部ホストから利用する場合や、管理者向けコンソールに外部ホストからアクセスする場合は外部アクセスを許可するよう、起動前に設定を変更しておく必要があります。

外部ホストからのアクセスを許可するには、/home/couchdb/etc/local.ini ファイルをテキストエディタで開き、
#bind_address = 127.0.0.1

となっている箇所を、以下のように書き換えて保存します:
bind_address = 0.0.0.0

そして couchdb ユーザーの権限で CouchDB を起動します:
$ sudo -i -u couchdb /home/couchdb/bin/couchdb

起動後、別のターミナルから以下の curl コマンドを実行して動作を確認します:
$ curl http://localhost:5984/

{"couchdb":"Welcome","version":"2.3.0","git_sha":"07ea0c7","uuid":"48b039b37b2b29236f33806d9a96c248","features":["pluggable-storage-engines","scheduler"],"vendor":{"name":"The Apache Software Foundation"}}


私自身はラズパイ3とラズパイゼロで動作を確認しましたが、ラズパイゼロでも CouchDB 程度であれば大した負荷にはならずに実行できました。




(参考)
https://www.hackster.io/mehealth-ch/installing-couchdb-on-raspbian-stretch-ccb2a7



(この記事は IBM Cloud アドベントカレンダー 2018 に参加しています。3日目の記事です)

IBM Cloudant (Apache CouchDB) にあまり詳しくない人が他のデータベースと同じ感覚でデータを扱っている時に、特に既存データを更新している時にふと気づくことがあります。例えば以下のような現象を目の当たりにした時、何が起こっているのか正しく理解できるでしょうか?


IBM Cloudant のダッシュボード画面にアクセスし、今回は "testdb" という名称のデータベースを IBM Cloudant 上に新規に作成しました。以下の手順はすべてこのデータベースを対象に行います(CouchDB でも同様の結果になります)。作成したばかりなのでまだドキュメント数はゼロです:
2018100201


testdb データベースを選択した画面です。普通はここで testdb 内のドキュメント一覧が表示されますが、まだ1つも存在していないので "No Documents Found" と表示されています。ここでドキュメントを新規に作成するため "Create Document" ボタンをクリックします:
2018100202


新規に JSON ドキュメントを作成する画面に切り替わります。Cloudant(CouchDB) のドキュメントは "_id" というユニーク ID を含める必要があります(API 経由で _id を含めずに作ると自動的に割り振られます)。自動的に設定された "_id" 以外に "name" というキーを作り、適当な値(下図では "kkimura")を設定して "Create Document" ボタンをクリックします(JSON ドキュメントなので "_id" キーの最後にカンマをつけることを忘れずに):
2018100203


先程のドキュメントが作成され、ドキュメント一覧に1つのドキュメントが表示されるようになりました:
2018100204


ちなみに、この段階でデータベース一覧に戻ると testdb データベースのドキュメント数もゼロから 1 に変わっていることが確認できます:
2018100205


またドキュメント一覧からこのドキュメントを選択するとドキュメントの確認/編集画面になります。"_rev" という先ほど指定しなかったキーと値が追加されていますが、こちらは後で説明します:
2018100206


ここまでは特別におかしな所はないと思います。この文書を編集するあたりから Cloudant 特有のクセというか、「あれ?」と感じる所が出てくるようになってきます。

この画面から JSON ドキュメントを編集してみます。試しに "name" の値を(下図では "Kei Kimura" に)変更し、"Save Changes" ボタンをクリックします:
2018100207


変更内容が保存されて、ドキュメント一覧に戻ります。既存文書を編集して保存したので文書数は変わらずに1つのままです。ではこの文書を選択して開いてみます:
2018100208


"name" の値が "Kei Kimura" になった文書が開きました。が、よく見ると "_rev" の値が先程と異なっています。最初に作った直後は "1-" で始まる値だったのが、 "2-" で始まる値になっています。ここは変更しなかったはずなんですが・・・:
2018100209


また、このタイミングでデータベース一覧の画面に戻ると、testdb の文書数は1のままなんですが、データベースサイズが微妙に増えています。これほどの差がでるような変更をしたつもりはないのですが・・・:
2018100210


更にこの文書を開いて、再度 "name" 値を "kkimura" に変更して(元に戻して)みます。値を変更して "Save Changes" ボタンをクリックします:
2018100211


すると(中を開いて確認してもいいのですが)また "_rev" の値が変わっていることが一覧からもわかります。今度は "3-" で始まる値になっていました:
2018100212


この辺りから「???」と感じることが増えてきました。では最後にこの文書を削除してみます。一覧からチェックをつけてゴミ箱ボタンをクリックします:
2018100213


削除すると一覧からは文書は消えて、元通りの "No Documents Found" が表示されます:
2018100214


しかしデータベース一覧に戻って testdb を見ると、文書数は "0" ですが、横に!マークが付いています。また文書を削除した割にはデータベースサイズがあまり減っていないように見えます:
2018100215


この!マーク部分にマウスカーソルをあわせると、"This database has just 0 docs and 1 deleted docs" と表示されます。このメッセージの意味はいったい・・・:
2018100216


ドキュメントに勝手に "_rev"(と "_id")が付与されること、編集して保存すると "_rev" の値が勝手に変更されること、文書を削除してもデータベースサイズが減らないこと、文書を削除した時の謎のメッセージ、・・・ と、この辺りが Cloudant(CouchDB) を始めて使うと戸惑う点でしょうか? 前置きが長くなってしまいましたが、以下にこの謎を解くための説明を記載します。


上記の振る舞いを理解するには、まず自動付与される2つの値 "_id" と "_rev" の意味と役割を正しく理解する必要があります。

"_id" はいわゆる「文書 ID」です。この値はデータベース内でユニークな値をなっており、各文書を一意に取得することができるキー値となっています。正しい ID 値が与えられるだけで(他の絞り込み条件がなくても)データベース内から目的の文書を特定して取得することができます。ID 値については普通のデータベースでも扱うものなので、あまり難しくないと思っています。

一方、もうひとつの "_rev" 、こちらは IBM Cloudant(CouchDB) の特徴的な予約語となっており、「文書のリビジョン」を管理する値となっています。「リビジョン」は「バージョン」と読み替えていただいてもいいです。

上記の例だと、最初に "name" = "kkimura" という値で文書を作成しました。この時点ではこの文書のリビジョン(バージョン)は 1 で、"_rev" 値は "1-" で始まる値になっていました:
2018100204


次に同じ文書を "name" = "Kei Kimura" と変更して保存しました。この時点でこの文書のリビジョンは 2 となり、"_rev" 値も "2-" で始まる値に更新されました:
2018100208


更に同じ文書を "name" = "kkimura" に戻して保存しました。この時点でこの文書のリビジョンは 3 となり、"_rev" 値も "3-" で始まる値に更新されました:
2018100212


つまり "_rev" 値は "_id" 値で決まる文書のバージョンを管理する役割を持って自動的に更新されるシステム値ということになります。ただ Cloudant(CouchDB) でドキュメントが更新される際にはもう1つの特徴があります。

実は Cloudant(CouchDB) ではドキュメントが更新されることはほぼなく、「新しいドキュメントが新しい "_rev" 値を持って新規作成」されます。つまり厳密には同じ "_id" 値を持った複数のドキュメントがデータベース内には存在しているが、その中で最も大きな "_rev" 値を持ったドキュメントだけが有効になります。論理的にドキュメントを更新したつもりでいても、物理的には古いドキュメントは消えずに残っていて、新しいドキュメントが同じ "_id" 値&新しい "_rev" 値で作成されるのでした。なお最新でないリビジョンのドキュメントは _id 値を指定してドキュメントを取得する時に { revs_info: true } というオプションを指定することで取得することができます(このオプションをつけない限り、最新 _rev のものだけで取得できます):
http://docs.couchdb.org/en/stable/api/document/common.html


上記で Cloudant(CouchDB) のドキュメントが更新されることは「ほぼ」ないと書いたのですが、厳密にはあります。それが文書削除時です。Cloudant(CouchDB) の文書削除はいわゆる「ソフトデリート(論理削除)」であって、「ハードデリート(物理削除)」ではありません。文書に削除フラグ( { _deleted: true } )をつけて更新し、最新 "_rev" の文書が削除されているようにすることで、論理的に文書が削除されたことにしています。そしてこの論理削除を行う際には _id 値だけではなく、_rev 値と合わせて指定して、「この ID 値の、このリビジョンの文書を削除する」ことを明示的に指定する必要があります。論理的には _id 値だけで削除できそうな感覚を持ってしまいますが、その場合はまずその _id 値を持ったドキュメントの最新リビジョンを取得し、取得したドキュメントから _rev 値を取り出し、改めて _id 値と _rev 値を指定して論理削除する、という流れになります。


これらの部分を理解していると、文書を更新したり、削除した時にデータベースサイズが増える謎が理解できると思います。要は物理的に書き換えたり、物理的に削除しているわけではなく、新リビジョンのドキュメントを追加したり、削除フラグをつけたりしているだけなので、(別途物理削除するまでは)データベースサイズという観点では減ることがないのでした。








 

IBM CloudantApache CouchDB をベースとしたマネージド NoSQL DB サービスです。IBM Cloud のライトアカウントを利用することで無料枠内で利用することも可能です。

そんな便利な IBM Cloudant ですが、IBM Cloud では(特に無料枠で使った場合)では、どのようなクラスタリング構成で運用されているのか気になりました。もともと NoSQL DB はスケーリングに優れていて、大規模運用向きと言われています。ではこの(特に無料のライトプランで提供されている)IBM Cloudant はどのような運用構成で提供されているのでしょう? 理論上は1サーバーノードで(クラスタリングなしで)提供することも可能だし、無料プランということを加味して、クラスタリング無しだったとしてもまあそうだよね・・とも考えられます。一方で無料プランと有償プランで(わざわざ)差をつけて運用しているのか?という疑問もあります。このあたりをそっと調べてみました。


まず、調べる方法は CouchDB REST API の GET /db を使うことにしました。この REST API を実行すると指定したデータベースの情報を得ることができ、その中には(クラスタリング構成になっていれば)クラスターに関する情報が含まれていることになっています。この方法で自分がローカルで構成した単一構成の CouchDB と、IBM Cloud のライトプランで契約した IBM Cloudant の2つのデータベースに対して実行し、その結果を比較してみることにします。

まず前者の単一構成 CouchDB のデータベースに対してこのコマンドを curl で実行しました(実行結果は青字):
$ curl 'http://localhost:5984/ccdb'

{"db_name":"ccdb","update_seq":"6-

g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuRAYeCJAUgmWSPX40DSE08WA0TLjUJIDX1eM3JYwGSDA1ACqhsPiF1C

yDq9uO2E6LuAETdfULmPYCoA_khCwCKxmL8","sizes":{"file":58598,"external":615,"active":1730},"purge_seq":0,"other":

{"data_size":615},"doc_del_count":2,"doc_count":2,"disk_size":58598,"disk_format_version":6,"data_size":1730,"com

pact_running":false,"instance_start_time":"0"}

指定したデータベース(上例では ccdb)の現在の状態が表示されています。各項目の意味は上述の GET /db API のリンク先で説明されているのでそちらを参照いただきたいのですが、この実行結果にはクラスタリング情報が含まれていません。実際クラスタリング構成ではなく単一構成で動いているので、この実行結果もその運用状態を正しく表しています。

次に同じコマンドを IBM Cloudant のライトプランで作成したデータベースに対して実行しました。その結果がこちらです:
{"status":true,"info":{"update_seq":"20-

g1AAAAQneJzLYWBgEMhgTmHQTElKzi9KdUhJMtFLytVNTtYtLdYtzi8tydA1NNBLzskvTUnMK9HLSy3JAWphSmRI4v___39WIgNIsxZcs6EhMbqTB

IBkkjzYAGZU24nTrwDSr49NvzlR-g1A-u0RHiDR90kOIP3-

CPtJDoAAkAHx2BxAnP4EkP58bPqJC4ACkP56sgMgjwVIMjQAKaAR_VmJTOQEAsSQCRBD5pMXEBAzFkDMWE9eYEDM2AAxYz_UM2QFyAGIGeezEhnJD

5ALEEPuUxIgDyBmvCcve0DM-AAxA5TEswCIaVtl","db_name":"statedb","sizes":

{"file":11774513,"external":10511286,"active":10543509},"purge_seq":0,"other":

{"data_size":10511286},"doc_del_count":7,"doc_count":2,"disk_size":11774513,"disk_format_version":6,"data_size":1

0543509,"compact_running":false,"cluster":{"q":16,"n":3,"w":2,"r":2},"instance_start_time":"0"}}

↑特に赤字部分に注目してほしいのですが、先程の実行結果には存在しなかったクラスタリングに関する情報が含まれています。そしてこの結果を見ると、このデータベースは
 ・シャード数: 16(!)
 ・1つのドキュメントの分散数: 3
 ・書き込みコマンドを実行した場合、2つ以上に書き込めたら書き込み成功とする
 ・読み取りコマンドを実行した場合、2つ以上から結果が返ってきたら読み取り成功とする

という条件でクラスタリングが構成されていることがわかります。無料のライトプランでも結構な好条件でクラスタリングされていたんですね、へぇ~。


 

以前に PouchDB を使った IBM Cloudant(Apache CouchDB) との同期について紹介しました:
IBM Cloudant と PouchDB で同期をとる

multiuser-couch-pouch


PouchDB は JavaScript 上で動作する CouchDB 互換の NoSQL データベースで、最大の特徴の1つが CouchDB との同期機能です(だと思ってます)。JavaScript で動作するということはブラウザ内のローカル DB で同期することもできて、PWA(Progressive Web Application) を作る際にも非常に有用な機能だと思っています。上記エントリではブラウザ内の PouchDB データベースと、サーバー上の CouchDB データベース間で双方向に全同期したり、双方向に部分同期する方法を紹介しました。

実際の PouchDB の使いみちを考えると、単方向にのみ同期させたいこともあると思います。変更作業はローカルのみで行って、その変更した内容のみをサーバー側に反映させたいとか、逆にサーバーで変更された内容をローカルに取り込みたいとかいったケースです。この実現方法を紹介します。

まずローカル側とサーバー側、両方でデータベースを指定します:
<script src="//cdn.jsdelivr.net/pouchdb/5.4.5/pouchdb.min.js"></script>

   :
   :

<script>
var local_db = new PouchDB( 'localdb' );
var remote_db = new PouchDB( 'https://remoteserver/remotedb' );


この2つのデータベース間で全文書を対象に双方向同期を行うのであれば前回紹介した方法を使って、以下のように実行することで実現できました:
local_db.sync( remote_db, {
  live: true,
  retry: true
});

ここを「ローカルでの変更をサーバー側にのみ反映させる(サーバー側の変更はローカルには反映させない)」場合は以下のように指定します:
local_db.replicate.to( remote_db, {
  live: true,
  retry: true
});

または

remote_db.replicate.from( local_db, {
  live: true,
  retry: true
});

逆に「サーバー側での変更をローカル側にのみ反映させる(ローカル側での変更はサーバーには反映させない)」場合は以下のように指定します:
local_db.replicate.from( remote_db, {
  live: true,
  retry: true
});

または

remote_db.replicate.to( local_db, {
  live: true,
  retry: true
});

全文書を対象とするのではなく、一部の文書を対象とする場合は、前回紹介した方法で doc_ids を指定することで同様に実現できます:
local_db.replicate.from( remote_db, {
  doc_ids: [ 001, 002, 003, ... ],
  live: true,
  retry: true
});

または

remote_db.replicate.to( local_db, {
  doc_ids: [ 001, 002, 003, ... ],
  live: true,
  retry: true
});

参考:
https://pouchdb.com/api.html#replication



 

IBM Cloud から提供されている IBM CloudantApache CouchDB をベースとしたマネージドな NoSQL データベースのサービスです:
2018090300


ベース製品が同じなので、例えば REST API レベルでは互換性があります。注意が必要な点として自分が気づいた限りでは IBM Cloudant は標準で Apache Lucene ベースの検索機能が有効になっており、インデックスとなる Design Document を用意することでテキスト検索が可能になる、ということが挙げられますが、それ以外に大きな差はありません。 一方で IBM Cloud から提供されているライトアカウント(無料プラン)でも IBM Cloudant を利用することができるので、わざわざ Apache CouchDB を用意しなくても気軽に使うことができる DBaaS としてとても手軽で便利だと思っています:
2018090301


さて、自分は業務のプログラミングでは主に Node.js を使うのですが、Node.js のパッケージライブラリには IBM Cloudant 用のものと、Apache CouchDB 用のもの、両方が存在しています:
(IBM Cloudant)
2018090302

(Apache CouchDB)
2018090303


仮に対象となるデータベースが IBM Cloudant であれば前者の方が簡単に使えるという印象を持っています。ただし IBM Cloudant 用ライブラリは IBM Cloud 上の IBM Cloudant を想定していることもあり、例えばオンプレミス上の Apache CouchDB に対して使えるものではありません。

一方、Apache CouchDB 用ライブラリはローカルや社内サーバー、クラウド上にある Apache CouchDB データベース全般に対して利用することが可能です。この対象はクラウド上の IBM Cloudant であっても構いません。要するにこちらのライブラリを使えば Apache CouchDB だけでなく IBM Cloudant にも接続できる、ということです。


実際にこういった需要がどれだけあるのかわからないのですが、例えばあるシステムを作る際に、そのデータストアとして、
(1) 試しに動かす場合は IBM Cloud 上の IBM Cloudant を使って気軽に開発/テストを行い、
(2) 本番運用ではオンプレミスな Apache CouchDB を利用する(IBM Cloudant の独自機能は使わない想定)

といったことが接続先の切り替えだけでできると便利です。ただこれを実現するためには IBM Cloudant 用の便利なライブラリを使って開発しまうと (2) の本番の時に問題が起こってしまいます。以下では IBM Cloudant に対しても Apache CouchDB 用ライブラリ(以下 node-couchdb)を使ってアクセスするように実装してみたコードを紹介します。ベースが同じ製品なのでできることはできるんですが、そのための手順と注意点を含めて紹介します。


【準備】
まず Node.js のコードを記述する前に上述の node-couchdb を npm install しておきます:
$ npm install node-couchdb


【データベース接続】
node-couchdb を使って IBM Cloudant に接続します。こんな感じのコードを記述します:
var dblib = require( 'node-couchdb' );

var option = {
  auth: {
    user: 'username',
    pass: 'password'
  },
  'host': 'username.cloudant.com',
  'protocol': 'https',
  'port': 443
};
var db = dblib( option );

usernamepassword の部分にはそれぞれ IBM Cloudant の username と password を指定します(localhost の Apache CouchDB に接続する場合は option = {} で接続できます)。これで IBM Cloudant との接続ができました。ここで取得した db を使って、以下の主要な操作を行うことができます。


【主要な操作】

ドキュメント追加
insert() メソッドにデータベース名(以下の例では 'testdb')を指定して、ドキュメントを追加します。取得前に db.uniqid() でユニーク ID を取得し、_id に設定している点に注意してください:
var doc = { name: 'dotnsf', height: 170.0 };  //. 追加するドキュメント
db.uniqid().then( function( id ){
  doc._id = id[0];
  db.insert( 'testdb', doc ).then( function( body, headers, status ){
    console.log( body );
  }).catch( function( err ){
    console.log( err );
  });
});

ドキュメント読み取り
同様に get() メソッドにデータベース名と id を指定してドキュメントを取得します:
db.get( 'testdb', id ).then( function( doc, headers, status ){
  console.log( doc );
}).catch( function( err ){
  console.log( err );
});

ドキュメント削除
del() メソッドにデータベース名と id と rev を指定して、データベースからドキュメントを削除します。以下の例では一度 get() メソッドを実行して id 値から rev 値を取り出してから del() を実行しています :
db.get( 'testdb', id ).then( function( doc, headers, status ){
  db.del( 'testdb', doc.data._rev ).then( function( data, headers, status ){
    console.log( data );
  }).catch( function( err ){
    console.log( err );
  });
}).catch( function( err ){
  console.log( err );
});

ビューを指定してドキュメント一覧取得
あらかじめ作成したビューを指定して、そのビューに含まれるドキュメントの一覧を取得します。以下の例ではデザイン名 : library, ビュー名 : byname というビューを指定して文書一覧を取得しています :
db.get( 'testdb', '_design/library/_view/byname', {} ).then( function( data, headers, status ){
  if( data && data.data ){
    var docs = data.data.rows;
    console.log( docs );
  }
}).catch( function( err ){
  console.log( err );
});


IBM Cloudant の npm だと最初にデータベース名を指定してそのデータベースのオブジェクトを取得した上で各種操作を行う、、という流れなんですが、Apache CouchDB 版だと毎回データベース名と一緒に各種操作を行う、、という点が大きな違いだと思いました。ただその辺りさえ理解していればまあ大丈夫かな。。


 

このページのトップヘ