Skip to content

Instantly share code, notes, and snippets.

@nakagami
Last active March 8, 2018 07:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nakagami/504167b7567ee0ee0548577b4d8471d6 to your computer and use it in GitHub Desktop.
Save nakagami/504167b7567ee0ee0548577b4d8471d6 to your computer and use it in GitHub Desktop.
PyConJP2017 にプロポーザルとして提出した資料(落選)

How to write python NoSQL database driver

Python データーベースドライバーの作り方(NoSQL編)

Profile

image

image

- 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

Abstract

PyConJP 2016 において 「Python データーベースドライバーの作り方」というタイトルで RDBMS のドライバーを書いた経験を発表しました。

https://gist.github.com/nakagami/bfbe98d62377f3f4554121ab161ae8c9

その後、RDBMS でないデーターベースのドライバーをいくつか書いたので 今回はそれらについて紹介をしながら、そこでやり取りされるデータの ネットワークプロトコルの話をします。

紹介するドライバーの github リポジトリにスターを付けてくれると私が喜びます。

Message packet format basis

一般的な、データやりとりの形式

  • リクエスト(クライアント→サーバー)とレスポンス(サーバー→クライアント)の繰り返し
  • TLV (Tag Length Value) 形式のメッセージパケット
  • メッセージボディの解釈は Tag の種別によりまちまち
種別 意味
Tag メッセージパケットの種別
Length ヘッダーを含む全体の長さの場合もあれば、メッセージボディの長さを意味する場合もある
Value メッセージボディ。データフォーマットはTag の種類によって色々
  • Tag と Length の順番が逆のものもある
  • 長さは、メッセージパケット全体を意味する場合もあるし、Value の部分だけの長さを意味する場合もある
  • 数値のエンディアンには注意

Redis

Key Value Store。 文字列のほかに、配列、set、ソート済み set、ハッシュなどのデータ保存、操作できる。 文字列については、文字データの文字コードはサーバーで管理されていないので、保存した値をそのまま取得する。

Redis ってスループット高くて、みんな使ってた印象だったけど、 最近は事例報告を聞くことがなくて、 もうみんな使ってないの? それとも Amazon ElastiCache 使うから、運用の話題がないの?

Driver

https://github.com/nakagami/toyredis

  • Python3.5+
  • redis cluster には対応してない
import toyredis
conn = toyredis.connect('servername')
conn.set('foo', 'bar')
assert conn.get('foo') == 'bar'

Protocol and driver implementation

サーバークライアント間でやりとりされるストリームデータはよくある TLV の形式をとらない

  • リクエスト&レスポンス

    CRLF で区切られたテキスト形式のリクエストとレスポンスの繰り返し

  • リクエスト

    コマンド + CRLF + '*' + パラメータの数 + CRLF + *(パラメータCRLF) の形式

  • レスポンス

    1byte のデータ型を表す文字(文字列/エラー/数値/配列)を表し、内容が CRLFで区切られたテキスト形式で続く

  • Bulk Strings

    CRLF や '\0' をデータに含めたい場合や Null を表現したい場合は('$' + 文字数+ CRLF + 値+ CRLF) で表現する

  • toyredis ドライバーは、パラメータを常に Bulk Strings の形式で渡している

Standard driver

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'

MongoDB

ドキュメントデーターベース。一般的には JSON の入れ子構造になったデータを保存、検索できる データーベースの総称。 MongoDB は、JSON にいくつかのデータ型を足した形式をバイナリーにした BSON フォーマットの データを取り扱う。

https://en.wikipedia.org/wiki/BSON

MongoDB は、CRUD のリクエスト、レスポンス、登録・検索結果のドキュメント等が、すべて BSON (≒JSON)形式なので、内部的な処理も人間の理解もシンプルになっていて、うまくできていると思う

Driver

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()

Protocol and driver implementation

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 するところくらい。

Standard driver

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

Neo4j

グラフDB。ノード間をリレーションで繋いだグラフ表現を取り扱うのに適したデーターベース。

O'Reilly のグラフデーターベースの第2版の電子書籍がダウンロードできる。 https://neo4j.com/graph-databases-book/

Driver

https://github.com/nakagami/minibolt

クエリーの結果として、プロトコルドキュメントに記載されているデータは取得できている。これをうまくグラフ形式のデータとして返したいが、まだできていない。

Protocol and driver implementation

以前は、Java 以外のプログラミング言語はREST API を呼ぶようになっていた。 気が付いたら Bolt プロトコルというのが定義され Python のドライバーもできていた。

プロトコルドキュメントは図解入りで、非常に分かりやすい

http://boltprotocol.org/

Standard driver

https://github.com/neo4j/neo4j-python-driver

Cassandra

分散型のカラム型データーベース。

カラム型って、データを行単位でなく列単位で持っているということらしい http://www.publickey1.jp/blog/11/post_175.html

複数のサーバーにデータを分散して(冗長性を持たせて)保存するアーキテクチャー。 マスターのサーバーが存在せず、問い合わせが複数サーバーに分散されるので検索速度が速くて、高可用性が・・・ というような特徴があるらしいが、ドライバーを書く側としては、あまり意識するところではない

Amazon Redshift や Google の BigQuery の仲間? スケールするけど使い方に慣れが必要な SQL っぽいクエリーが書けるデーターベースという印象。

結果セットとしては、フールド名付きの表形式のデータが受け取れるので、 Python のデーターベースドライバー的には、RDBMS のドライバーと同じ PEP-249 に従った API を用意することもできる。

  • 問い合わせ言語(CQL)の構文は SQL に似てる
  • データーベース(Oracle 用語のインスタンス)ではなくて「キースペース」
  • テーブルに主キーの指定が必要
  • Foreign Key 張れない

使いどころによっては凄くよさそうなんだけど、最近の日本での導入事例を全く聞かない。 誰か、現在も稼働している実績があったら教えてください。

Driver

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 だけを使っている。

Protocol and driver implementation

以前は 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 など、データをシリアライズするプロトコルではよく行われているが、 ほかのデーターベースでここまで頑張って転送データを減らそうとしているプロトコルは、あまりないと思う。

Standard driver

https://github.com/datastax/python-driver

Cassandra 性能が引き出せるようなライブラリ設計になっていて、PEP 249 とはずいぶん違うAPIになっている。

http://datastax.github.io/python-driver/getting_started.html

Conclusion

NoSQL databases

  • 2010年頃の NoSQL ブームで痛い目にあったのか、RDBMSの性能が上がったのか日本では、もう NoSQL データベースが使われていない感じがする
  • しかし、プロダクトは着実に進化を遂げて良いものになっている
  • DB-Engines Ranking https://db-engines.com/en/ranking_trend/system/Redis%3BMongoDB%3BNeo4j%3BCassandra
  • 日本での使用事例を聞かなくなってしまったので、事例を持っている人は発表して欲しい

Database drivers

  • OSS のデーターベースは、ネットワークプロトコルのドキュメントがわかりやすく整備されている
  • REST API やThrift を使っていたデーターベースも、今ではネイティブプロトコルで設計、実装されている
  • 慣れれば OSS のデーターベースドライバーは書ける
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment