こんにちは。
2016新卒でLobiのサーバーサイドエンジニアをやっているmorikuniです。
今回は、mdqというGo製のツールを作ったので、その紹介をします。
mdq
は複数のデータベースに並列にクエリを投げて、結果をJSONで出力してくれるというものです。
mdq
に近いコンセプトのツールとして、弊社の荒賀(@ken39arg)が作ったshard_promptがあります(#6「Shardingマジ怖い」tech.kayac.com Advent Calendar 2012)。
こちらはSQLを解析し、複数データベースにまたがる集約関数を一部サポートしていますが、MySQL専用かつ直列にクエリを投げるというものでした。
mdq
では複数データベースにまたがる集約関数はサポートしませんが、PostgreSQLなどMySQL以外のデータベースへのクエリや、並列処理による速度向上などを目指しました。
はじめに
LobiではMySQLへの書き込みをスケールさせるために、一部テーブルの水平分割を行っています。
水平分割によって書き込みには強くなりますが、「ターミナルからクエリを投げてデータを取得したい!」となったときには複数回コマンドを実行しなければいけなかったり、その度にホスト名などを変更しないといけなかったりと、オペレーションの手間が増えてしまうという側面もあります。
また、ログ解析にはRedshiftを使っており、ログのデータを見るときにはpsql
コマンド、サービスのデータを見るときにはmysql
コマンドと、データベースによってコマンドを使い分ける必要があります。
データベースにクエリを投げたいだけで、ホスト名とかコマンドとかをいちいち考えたくないよね、というモチベーションで作ったのがmdq
です。
mdq
mdq
を使えば、設定ファイルに接続先の情報とタグを書いておき、クエリを投げる際にはタグを指定するだけでよくなります。
説明するよりは例を出した方が分かりやすいと思うので、実際にMySQLとPostgreSQLのデータベースにクエリを投げてみます。(実際にMySQLとPostgreSQLに同じクエリを投げたいということはないと思いますが。)
まずはデータベースを用意します。
両方のデータベースにexample
というデータベースとhello
というテーブルを作成し、いくつかデータを挿入しています。
mysql.sql
CREATE DATABASE example; USE example; CREATE TABLE hello ( id int, name text ); INSERT INTO hello values (1, 'foo'), (2, 'bar');
postgres.sql
CREATE DATABASE example; \c example; CREATE TABLE hello ( id int, name text ); INSERT INTO hello values (3, 'hoge'), (4, 'fuga');
$ docker run --name mysql -e MYSQL_ROOT_PASSWORD=root --rm -d -p 33060:3306 mysql $ mysql -u root -h 127.0.0.1 -P 33060 -p < mysql.sql $ docker run --name postgres -e POSTGRES_PASSWORD=postgres --rm -d -p 54320:5432 postgres $ psql -U postgres -h 127.0.0.1 -p 54320 < postgres.sql
次に接続先の情報を書いた設定ファイルを用意します。
PostgreSQLにはpostgres
、MySQLにはmysql
というタグをつけておきました。
$ cat ~/.config/mdq/config.yaml dbs: - name: "mysql_db" driver: "mysql" dsn: "root:root@tcp(127.0.0.1:33060)/example" tags: ["mysql"] - name: "postgres_db" driver: "postgres" dsn: "user=postgres password=postgres host=127.0.0.1 port=54320 sslmode=disable dbname=example" tags: ["postgres"]
ここまでで準備は出来たので、mdq
でクエリを投げてみます。
mysql
とpostgres
の両方にクエリを投げた結果がJSONで出力されています。
$ mdq "select * from hello" | jq . [ { "Database": "postgres_db", "Columns": [ "id", "name" ], "Rows": [ { "id": 3, "name": "hoge" }, { "id": 4, "name": "fuga" } ] }, { "Database": "mysql_db", "Columns": [ "id", "name" ], "Rows": [ { "id": "1", "name": "foo" }, { "id": "2", "name": "bar" } ] } ]
タグを指定すると片方のデータベースにのみクエリを投げることも出来ます。
$ mdq --tag mysql "select * from hello" | jq . [ { "Database": "mysql_db", "Columns": [ "id", "name" ], "Rows": [ { "id": "1", "name": "foo" }, { "id": "2", "name": "bar" } ] } ] $ mdq --tag postgres "select * from hello" | jq . [ { "Database": "postgres_db", "Columns": [ "id", "name" ], "Rows": [ { "id": 3, "name": "hoge" }, { "id": 4, "name": "fuga" } ] } ]
このように設定ファイルを用意しておけば、
- 複数のデータベースにクエリを投げる
- MySQLとPostgreSQLを同じコマンドで扱う
ということができるようになります。
出力形式もJSONなのでjq
を使うなどして、ターミナル上である程度データの加工などもできます。
Goでの実装について
mdq
はGoで作られていますが、どんな値が入っているかわからないデータベース上のデータを静的型付け言語であるGoのデータ構造に変換するには少し工夫が必要でした。
なので実装についても少し紹介しようと思います。
mdq
では標準のdatabase/sql
パッケージによってデータベースを扱っています。
全体の処理の概要としては、各データベースにDB.Queryを使ってクエリを投げ、Rows.Scanで値を読み込み、結果を結合するというものです。
Rows.Scan
ではScannerインターフェースに対応している型を受け取ることができるため、なんでも読み込めるScanner
型を実装することにしました。
Scanner
の定義を見てみると、以下のように、対応すべき型が定義されています。
type Scanner interface { // Scan assigns a value from a database driver. // // The src value will be of one of the following types: // // int64 // float64 // bool // []byte // string // time.Time // nil - for NULL values // // An error should be returned if the value cannot be stored // without loss of information. Scan(src interface{}) error }
[]byte
だけはJSONに変換したときに望ましくない結果になる場合が多いのでstring
に変換しますが、その他の型はそのまま使えそうです。
なので、基本的にはsrc
の値をそのまま使って、[]byte
の場合はstring
に変換するという実装にしました。
以下が定義したAnyValue
型です。
type AnyValue struct { Value interface{} } func (v *AnyValue) Scan(src interface{}) error { v.Value = src if bs, ok := src.([]byte); ok { v.Value = string(bs) } return nil }
AnyValue
型を使うことによって、任意のデータ型をデータベースから読み込めるようになります。
エラー処理は省きますが、使用例も載せておきます。
rows, _ := db.Query("select id, ....") columns, _ := rows.Columns() columnSize := len(columns) var result [][]interface{} for rows.Next() { values := make([]AnyValue, columnSize) valuePtrs := make([]interface{}, columnSize) for i := range columns { valuePtrs[i] = &values[i] } rows.Scan(valuePtrs...) row := make([]interface{}, columnSize) for i, val := range values { row[i] = val.Value } result = append(result, row) }
最終的なresult
には、レコード数×カラム数のデータが読み込まれます。
ポイントとなるのはvaluePtrs
を定義しているところです。
Rows.Scan
の引数は...[]interface
なので、[]*AnyValue
をそのまま渡すことはできません。
カラム数が固定の場合にはrows.Scan(&a, &b, &c)
のように書けますが、そうでない場合は一度[]interface{}
に変換する必要があります。
おわりに
mdq
で使ったdatabase/sql
もそうですが、Goでは標準ライブラリで多くのインターフェースが提供されています。
オープンソースのライブラリもそれに対応している場合が多いので、同じインターフェースで様々なプロダクトを扱うことができます。
クロスビルドやシングルバイナリ生成といった特徴からも、今CLIツールを作るのに最もアツイ言語の1つだと思います。
弊社にはみんなのGo言語の著者のひとりであるfujiwara (id:sfujiwara)もいるので、Go言語に興味がある方はカヤックで働いてみるのはいかがでしょうか?

- 作者: 松木雅幸,mattn,藤原俊一郎,中島大一,牧大輔,鈴木健太,稲葉貴洋
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/09
- メディア: 大型本
- この商品を含むブログ (3件) を見る