Python データーベースドライバーの作り方(NoSQL編)
- I am a python programmer.
- I work for a company BeProud Inc.
- https://github.com/nakagami
BeProud Inc. http://www.beproud.jp/
- connpass (http://connpass.com/) .. image:: https://connpass.com/static/img/common/sitelogo_295x100.png - PyQ (https://pyq.jp/) .. image:: https://pyq.jp/static/img/logo_square_small.png
PyConJP 2016 において 「Python データーベースドライバーの作り方」というタイトルで RDBMS のドライバーを書いた経験を発表しました。
https://gist.github.com/nakagami/bfbe98d62377f3f4554121ab161ae8c9
その後、RDBMS でないデーターベースのドライバーをいくつか書いたので 今回はそれらについて紹介をしながら、そこでやり取りされるデータの ネットワークプロトコルの話をします。
- Redis https://github.com/nakagami/toyredis
- MongoDB https://github.com/nakagami/nmongo
- Neo4j https://github.com/nakagami/minibolt
- Cassandra https://github.com/nakagami/minicql
紹介するドライバーの github リポジトリにスターを付けてくれると私が喜びます。
一般的な、データやりとりの形式
- リクエスト(クライアント→サーバー)とレスポンス(サーバー→クライアント)の繰り返し
- TLV (Tag Length Value) 形式のメッセージパケット
- メッセージボディの解釈は Tag の種別によりまちまち
種別 意味 | |
---|---|
Tag | メッセージパケットの種別 |
Length | ヘッダーを含む全体の長さの場合もあれば、メッセージボディの長さを意味する場合もある |
Value | メッセージボディ。データフォーマットはTag の種類によって色々 |
- Tag と Length の順番が逆のものもある
- 長さは、メッセージパケット全体を意味する場合もあるし、Value の部分だけの長さを意味する場合もある
- 数値のエンディアンには注意
Key Value Store。 文字列のほかに、配列、set、ソート済み set、ハッシュなどのデータ保存、操作できる。 文字列については、文字データの文字コードはサーバーで管理されていないので、保存した値をそのまま取得する。
Redis ってスループット高くて、みんな使ってた印象だったけど、 最近は事例報告を聞くことがなくて、 もうみんな使ってないの? それとも Amazon ElastiCache 使うから、運用の話題がないの?
https://github.com/nakagami/toyredis
- Python3.5+
- redis cluster には対応してない
import toyredis
conn = toyredis.connect('servername')
conn.set('foo', 'bar')
assert conn.get('foo') == 'bar'
サーバークライアント間でやりとりされるストリームデータはよくある TLV の形式をとらない
- リクエスト&レスポンス
CRLF で区切られたテキスト形式のリクエストとレスポンスの繰り返し
- リクエスト
コマンド + CRLF + '*' + パラメータの数 + CRLF + *(パラメータCRLF) の形式
- レスポンス
1byte のデータ型を表す文字(文字列/エラー/数値/配列)を表し、内容が CRLFで区切られたテキスト形式で続く
- Bulk Strings
CRLF や '\0' をデータに含めたい場合や Null を表現したい場合は('$' + 文字数+ CRLF + 値+ CRLF) で表現する
- toyredis ドライバーは、パラメータを常に Bulk Strings の形式で渡している
https://pypi.python.org/pypi/redis (redis-py)
- 高機能
- python2.7 でも使える
import redis
r = redis.StrictRedis(host='servername')
r.set('foo', 'bar')
assert r.get('foo') == 'bar'
import redis
pool = redis.ConnectionPool(host='servername')
r = redis.Redis(connection_pool=pool)
r.set('foo', 'bar')
assert r.get('foo') == 'bar'
ドキュメントデーターベース。一般的には JSON の入れ子構造になったデータを保存、検索できる データーベースの総称。 MongoDB は、JSON にいくつかのデータ型を足した形式をバイナリーにした BSON フォーマットの データを取り扱う。
https://en.wikipedia.org/wiki/BSON
MongoDB は、CRUD のリクエスト、レスポンス、登録・検索結果のドキュメント等が、すべて BSON (≒JSON)形式なので、内部的な処理も人間の理解もシンプルになっていて、うまくできていると思う
https://github.com/nakagami/nmongo
- Python3.5+
- MongoDB3.2 以降のみをサポート
import nmongo
# Connect
db = nmongo.connect('servername', 'somewhatdatabase')
# search and fetch one item.
cur = db.fruits.find({'name': 'banana'})
document = cur.fetchone()
# fetch all collection items.
cur = db.fruits.find()
documents = cur.fetchall()
https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/
リクエストもレスポンスも共通のヘッダーを持っていて、 ヘッダーの後に (messageLength - 16) bytes のボディーが続く
https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#standard-message-header :
struct MsgHeader {
int32 messageLength; // total message size, including this
int32 requestID; // identifier for this message
int32 responseTo; // requestID from the original request
// (used in responses from db)
int32 opCode; // request type - see table below
}
opCode の持ちうる値は何種類かある
https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#request-opcodes
3.0より前のバージョンから使える opCode は、 リクエストが CRUD の種類だけあって、そのリクエストに対するレスポンスを OP_RESPONSE メッセージを受け取る形式
- OP_UPDATE
- OP_INSERT
- OP_QUERY
- OP_GET_MORE
- OP_DELETE
- OP_KILL_CURSORS
- OP_RESPONSE
これらのリクエストとレスポンスは、非同期にやり取りされ、どのリクエストに対するレスポンスかは resposeTo の値を見て判断する必要がある。
MongoDB 3.0 から OP_COMMAND と、そのレスポンスの OP_COMMANDREPLY が使えるようになった。
OpCode Name | Value | Comment |
---|---|---|
OP_COMMAND | 2010 | Cluster internal protocol representing a command request. |
OP_COMMANDREPLY | 2011 | Cluster internal protocol representing a reply to an OP_COMMAND. |
Mongo shell の db.runCommand() 相当の機能で、この組み合わせで CRUD の操作が一通りできる。
https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-commandreply
OP_COMMAND:
struct {
MsgHeader header; // standard message header
cstring database; // the name of the database to run the command on
cstring commandName; // the name of the command
document metadata; // a BSON document containing any metadata
document commandArgs; // a BSON document containing the command arguments
inputDocs; // a set of zero or more documents
}
OP_COMMANDREPLY:
struct {
MsgHeader header; // A standard wire protocol header
document metadata; // A BSON document containing any required metadata
document commandReply; // A BSON document containing the command reply
document outputDocs; // A variable number of BSON documents
}
nmongo では、上記の OP_COMMAND, OP_COMMANDREPLY だけを使っている。 これを使うと、実装上難しいのは BSON フォーマットのデータをdecode/encode するところくらい。
https://github.com/mongodb/mongo-python-driver
- 高機能
- gridfs も扱える
- python2.7 でも使える
- MongoDB 2.4, 2.6 でも使える
- Azure DocumentDB に MongoDB API で接続できる
コード例 https://github.com/mongodb/mongo-python-driver#examples
グラフDB。ノード間をリレーションで繋いだグラフ表現を取り扱うのに適したデーターベース。
- http://www.atmarkit.co.jp/ait/articles/1507/28/news015.html
- https://moneyforward.com/engineers_blog/2016/01/13/neo4j/
O'Reilly のグラフデーターベースの第2版の電子書籍がダウンロードできる。 https://neo4j.com/graph-databases-book/
https://github.com/nakagami/minibolt
クエリーの結果として、プロトコルドキュメントに記載されているデータは取得できている。これをうまくグラフ形式のデータとして返したいが、まだできていない。
以前は、Java 以外のプログラミング言語はREST API を呼ぶようになっていた。 気が付いたら Bolt プロトコルというのが定義され Python のドライバーもできていた。
プロトコルドキュメントは図解入りで、非常に分かりやすい
https://github.com/neo4j/neo4j-python-driver
分散型のカラム型データーベース。
カラム型って、データを行単位でなく列単位で持っているということらしい http://www.publickey1.jp/blog/11/post_175.html
複数のサーバーにデータを分散して(冗長性を持たせて)保存するアーキテクチャー。 マスターのサーバーが存在せず、問い合わせが複数サーバーに分散されるので検索速度が速くて、高可用性が・・・ というような特徴があるらしいが、ドライバーを書く側としては、あまり意識するところではない
Amazon Redshift や Google の BigQuery の仲間? スケールするけど使い方に慣れが必要な SQL っぽいクエリーが書けるデーターベースという印象。
結果セットとしては、フールド名付きの表形式のデータが受け取れるので、 Python のデーターベースドライバー的には、RDBMS のドライバーと同じ PEP-249 に従った API を用意することもできる。
- 問い合わせ言語(CQL)の構文は SQL に似てる
- データーベース(Oracle 用語のインスタンス)ではなくて「キースペース」
- テーブルに主キーの指定が必要
- Foreign Key 張れない
使いどころによっては凄くよさそうなんだけど、最近の日本での導入事例を全く聞かない。 誰か、現在も稼働している実績があったら教えてください。
https://github.com/nakagami/minicql
import minicql
conn = minicql.connect('server_name', 'keyspace')
cur = conn.cursor()
cur.execute("select * from test")
for c in cur.fetchall():
print(c)
conn.close()
API は PEP-249 に合わせた。効率、可用性の面からは(おそらく)よろしくない。
opcode でいうと PREPARE, EXEC を使うと効率よさそうだが、 現状は QUERY だけを使っている。
以前は thrift プロトコル(簡易なRPC プロトコルの一種)でしか接続できず python のドライバーも thrift のライブラリを使っていた。
現在は CQL native protocol というプロトコルができていて、それに対応した Python driver がある。
https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v5.spec
Cassandra 4.0 では thrift プロトコルは廃止されるらしい。
プロトコル上は STARTUP で接続のネゴシエーションをするときに、サーバーが対応している 圧縮アルゴリズムが提示され、フレームの body を圧縮してやりとりすることができる(未対応)
body が圧縮されているかどうかは flags の圧縮フラグが立っているかどうかで判断。
1. Overview
The CQL binary protocol is a frame based protocol. Frames are defined as:
0 8 16 24 32 40
+---------+---------+---------+---------+---------+
| version | flags | stream | opcode |
+---------+---------+---------+---------+---------+
| length |
+---------+---------+---------+---------+
| |
. ... body ... .
. .
. .
+----------------------------------------
数値、文字列などは、少ない桁数の数値、短い文字列はできるだけ少ないバイト数に収まるようにしている。
3. Notations
.
.
[int] A 4 bytes integer
[long] A 8 bytes integer
[byte] A 1 byte unsigned integer
[short] A 2 bytes unsigned integer
[string] A [short] n, followed by n bytes representing an UTF-8
string.
[long string] An [int] n, followed by n bytes representing an UTF-8 string.
MessagePack など、データをシリアライズするプロトコルではよく行われているが、 ほかのデーターベースでここまで頑張って転送データを減らそうとしているプロトコルは、あまりないと思う。
https://github.com/datastax/python-driver
Cassandra 性能が引き出せるようなライブラリ設計になっていて、PEP 249 とはずいぶん違うAPIになっている。
http://datastax.github.io/python-driver/getting_started.html
- 2010年頃の NoSQL ブームで痛い目にあったのか、RDBMSの性能が上がったのか日本では、もう NoSQL データベースが使われていない感じがする
- しかし、プロダクトは着実に進化を遂げて良いものになっている
- DB-Engines Ranking https://db-engines.com/en/ranking_trend/system/Redis%3BMongoDB%3BNeo4j%3BCassandra
- 日本での使用事例を聞かなくなってしまったので、事例を持っている人は発表して欲しい
- OSS のデーターベースは、ネットワークプロトコルのドキュメントがわかりやすく整備されている
- REST API やThrift を使っていたデーターベースも、今ではネイティブプロトコルで設計、実装されている
- 慣れれば OSS のデーターベースドライバーは書ける