GraphDBの「Dgraph」の話 - Goで叩く
こんにちは。
もう、Webの開発を何年もやってきたのになと思うのに、まだまだ理解できてなかったことがあるんだなぁと日々痛感させられてるmasamikiです。
GraphDB
こないだ GoのORM「ent」の話 を書かせてもらったのですが、GraphQLを使う仕事も結構でてきたなと。
さてさて、同じGraphの名を冠するGraphDBとはなんでしょうか。
Graph自体はentでMySQLに対して表現していたように、DBによらず、そのデータ構造のモデルを作ることは難しくありません。
この記事の言葉をかりると Graph DBとは、index-freeな隣接する頂点を取得できるストレージシステムなら、それをGraphDBと呼べるそうです。
もうちょっと細かい定義としては、(といっても翻訳してるだけですが)
- すべての要素(つまり、頂点またはエッジ)には、隣接する要素への直接のポインタがある。
- どの頂点が他のどの頂点に隣接しているかを判断するために、O(logn)インデックスルックアップを行わない。(Bツリー等のインデックスを用いた走査が行われない)
- グラフが接続されている場合、グラフ全体は単一の原子データ構造になる
つまり、ポインタが入ってしまっている構造であれば、こういう隣接点の取り方をできるのですが、
Bツリーなどのインデクシングを使ったDBでのGraph表現はこうなってしまうので、
MySQLでentを使って、Graphな表現ができても、MySQLはGraphDBではないと言える訳ですね。
O(logn)インデックスが、データ量が多くても、それ程遅くならない仕組みであったとしても、データ構造がGraphになることが分かっているなら、隣接点のポインタが直接入ってしまっているGraphDBを使う方がスピード的にも確かに有利そうですね。(記事の55ページ)
どんなのがあるの?
「Neo4j」、「Janus Graph」、「sones」、「OrientDB」、「Info Grid」、「Infinite Graph」といったDBがあり、Neo4jが一番使い易く、広く使われている印象です。
Dgraph
GraphQLを最速で構築するアプリ(←Webサイトの記述を翻訳)だそうです。
まず、デモ遊びたい方はここで遊べます。
(といっても、クエリのこと分からなかったら分かんないっすよね)
get startedの説明だとこんな感じ。
Dgraph is an open-source, scalable, distributed, highly available and fast graph database, designed from the ground up to be run in production.
(Dgraphとは、オープンソースでスケーラブルな分散型の高可用性で高速なグラフデータベースであり、本番環境で実行できるようにゼロから設計されています。)
そして、githubの説明はこんな感じ。
Dgraph is a horizontally scalable and distributed GraphQL database with a graph backend.
(Dgraphは、グラフバックエンドを備えた水平方向にスケーラブルで分散型のGraphQLデータベースです。)
あれ?GraphDBと書かず、よく見るとGraphQL databaseと書いてある……
グラフバックエンドと書いてあるように、確かにGraphDBではあるものは間違いなさそうなものの、
クエリとして使える言語がGraphQL(正確にはGraphQLを基にした言語)だというのをどうやら押したいのではないのかなぁと。
押すのも、そう、GraphQLをネイティブにサポートしてるデータベースって、これがどうやら最初らしんですよね。
ちなみに、↓はgithubにあった、他のDBとの比較表です。(Neo4jでGraphQLを使うためには、Extensionが必要だったりします。)
Features | Dgraph | Neo4j | Janus Graph |
---|---|---|---|
Architecture | Sharded and Distributed | Single server (+ replicas in enterprise) | Layer on top of other distributed DBs |
Replication | Consistent | None in community edition (only available in enterprise) | Via underlying DB |
Data movement for shard rebalancing | Automatic | Not applicable (all data lies on each server) | Via underlying DB |
Language | GraphQL inspired | Cypher, Gremlin | Gremlin |
Protocols | Grpc / HTTP + JSON / RDF | Bolt + Cypher | Websocket / HTTP |
Transactions | Distributed ACID transactions | Single server ACID transactions | Not typically ACID |
Full Text Search | Native support | Native support | Via External Indexing System |
Regular Expressions | Native support | Native support | Via External Indexing System |
Geo Search | Native support | External support only | Via External Indexing System |
License | Apache 2.0 | GPL v3 | Apache 2.0 |
GraphQL(っぽいクエリ)を使えるむっちゃ速いGraphDB、それが「Dgraph」という感じですかね。
Dgraph、シャーディングして分散できるようにもなっているのですが、これまでのグラフデータベースはパフォーマンスが高いものの分散されるような設計ではなかったこともあり、そこもDgraphの特徴のようです。
準備
Docker Composeを使ってDgraphを用意するとしたら、こんな感じになります。
これを実行すると、
Dgraph Alpha、Dgraph Zero、Ratelが起動します。
- Dgraph Zero: Dgraphクラスターを制御し、サーバーをグループに割り当て、サーバーグループ間でデータのバランスを取り直します。
- Dgraph Alpha: predicateとindexをホストします。predicateは、ノードに関連付けられたプロパティ、または2つのノード間の関係を示します。indexは、適切な関数を使用してフィルタリングを有効にするためにpredicateに関連付けることができるトークナイザーです。
- Ratel: クエリ、ミューテーション、スキーマの変更を実行するためのUIを提供します。
version: "3.2"
services:
dgraph:
image: dgraph/standalone:v20.03.0
ports:
- 8080:8080
- 8000:8000
- 9080:9080
volumes:
- ./dgraph:/dgraph
ただし、このstandaloneイメージは、クイックスタート用なので、本番環境での利用は推奨されていません。
本番用にはこんな形で、それぞれのコンテナを立ち上げるような形になります。
version: "3.2"
services:
zero:
image: dgraph/dgraph:latest
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
ports:
- 5080:5080
- 6080:6080
restart: on-failure
command: dgraph zero --my=zero:5080
alpha:
image: dgraph/dgraph:latest
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
ports:
- 8080:8080
- 9080:9080
restart: on-failure
command: dgraph alpha --my=alpha:7080 --lru_mb=2048 --zero=zero:5080 --whitelist 172.18.0.1/8
ratel:
image: dgraph/dgraph:latest
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
ports:
- 8000:8000
command: dgraph-ratel
volumes:
dgraph:
リードレプリカを作りたい時は、nginxを使ってこんな感じ
version: "3.5"
services:
nginx:
image: nginx:1.17.7
depends_on:
# Hostnames referenced in nginx.conf need to be available
# before Nginx starts
- zero1
- zero2
- zero3
- alpha1
- alpha2
- alpha3
ports:
- 80:80
- 8080:8080
- 9080:9080
volumes:
- type: bind
source: ./nginx.conf
target: /etc/nginx/conf.d/dgraph.conf
read_only: true
alpha1:
image: dgraph/dgraph:latest
working_dir: /data/alpha1
hostname: alpha1
ports:
- 8080
- 9080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph alpha --my=alpha1:7080 --lru_mb=1024 --zero=zero1:5080 --logtostderr
-v=2 --idx=1 --whitelist 172.19.0.9/8
alpha2:
image: dgraph/dgraph:latest
working_dir: /data/alpha2
hostname: alpha2
depends_on:
- alpha1
labels:
cluster: test
ports:
- 8080
- 9080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph alpha --my=alpha2:7080 --lru_mb=1024 --zero=zero1:5080 --logtostderr
-v=2 --idx=2 --whitelist 172.19.0.9/8
alpha3:
image: dgraph/dgraph:latest
working_dir: /data/alpha3
hostname: alpha3
ports:
- 8080
- 9080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph alpha --my=alpha3:7080 --lru_mb=1024 --zero=zero1:5080 --logtostderr
-v=2 --idx=3 --whitelist 172.19.0.9/8
zero1:
image: dgraph/dgraph:latest
working_dir: /data/zero1
hostname: zero1
ports:
- 5080
- 6080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph zero --idx=1 --my=zero1:5080 --replicas=3 --logtostderr -v=2
--bindall
zero2:
image: dgraph/dgraph:latest
working_dir: /data/zero2
hostname: zero2
ports:
- 5080
- 6080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph zero --idx=2 --my=zero2:5080 --replicas=3 --logtostderr -v=2
--peer=zero1:5080
zero3:
image: dgraph/dgraph:latest
working_dir: /data/zero3
hostname: zero3
ports:
- 5080
- 6080
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph zero --idx=3 --my=zero3:5080 --replicas=3 --logtostderr -v=2
--peer=zero1:5080
ratel:
image: dgraph/dgraph:latest
working_dir: /data/ratel
ports:
- 8000:8000
volumes:
- type: volume
source: dgraph
target: /dgraph
volume:
nocopy: true
command: dgraph-ratel
volumes:
dgraph:
↓nginx
upstream alpha_http {
server alpha1:8080;
server alpha2:8080;
server alpha3:8080;
}
# $upstream_addr is the ip:port of the Dgraph Alpha defined in the upstream
# Example: 172.25.0.2, 172.25.0.7, 172.25.0.5 are the IP addresses of alpha1, alpha2, and alpha3
# /var/log/nginx/access.log will contain these logs showing "localhost to <upstream address>"
# for the different backends. By default, Nginx load balancing is round robin.
# [15/Jan/2020:00:28:11 +0000] 172.25.0.1 - - - localhost to: 172.25.0.2:9080: POST /api.Dgraph/Query HTTP/2.0 200 upstream_response_time 0.028 msec 1579048091.865 request_time 0.027
# [15/Jan/2020:00:28:11 +0000] 172.25.0.1 - - - localhost to: 172.25.0.7:9080: POST /api.Dgraph/Query HTTP/2.0 200 upstream_response_time 0.032 msec 1579048091.897 request_time 0.031
# [15/Jan/2020:00:28:11 +0000] 172.25.0.1 - - - localhost to: 172.25.0.5:9080: POST /api.Dgraph/Query HTTP/2.0 200 upstream_response_time 0.028 msec 1579048091.926 request_time 0.028
log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name $host to: $upstream_addr: $request $status upstream_response_time $upstream_response_time msec $msec request_time $request_time';
server {
listen 9080 http2;
access_log /var/log/nginx/access.log upstreamlog;
location / {
grpc_pass grpc://alpha_grpc;
}
}
server {
listen 8080;
access_log /var/log/nginx/access.log upstreamlog;
location / {
proxy_pass http://alpha_http;
}
}
あとはdocker-compose up
で立ち上げるだけ。
そして、リードレプリカを使う時は、NginxのgRPCに対するコネクションを増やすためにこんなコマンドを実行。
docker-compose exec alpha1 dgraph increment --alpha nginx:9080 --num=10
lru_mbでalphaが使うRAMの設定ができるのですが、これは利用できるRAMの3分の1程度をしてして上げるのいいとのこと。(なんかJVMみたい)
ちなみに、alphaに対してportが2つ(8080、9080)空けられているのは、8080がhttp用、9080がgrpc用です。
Dockerを立ち上げてはみましたが、Dgraphを搭載したフルマネージドGraphQLバックエンドサービスであるSlashGraphQLというのがあるので、自分で立てなくても大丈夫です。そっちも使えます。
触ってみよう
http://localhost:8000/
でRatelにアクセスすることができます。
(デモと内容は一緒ですね)
Launch Latest(でなくてもいいですが)をクリックすると、Ratelの画面が表示されます。
叩き方
GraphQL押しのGraphDBですが、Dgraphにはいくつかの叩き方があります。
- GraphQL
- RDF(Resource Description Framework): ミューテーションで使えます。
- DQL(Dgraph Query Language):クエリで使えます。
GraphQLは言わずもがなですが、DQLは、名前の通り、Dgraph専用のクエリで、上で説明していたUIのRatelでもDQLを叩いて、取得したりミューテーションしたりします。
Schemaの作成
GraphQLがー、と言っている通り、Schemaが重要。
まず、Schemaを作ります。
ただし、Dgraphは、構造やスキーマを強制せず、すぐにデータの入力を開始し、必要に応じて制約を追加できるので、スキーマ作りは必須ではないです。
ではでは、このグラフのスキーマを作ってみましょか。
GraphQLで作る
type Tweet {
id: ID!
title: String!
content: String! @search(by: [fulltext])
user: User!
}
type User {
name: String! @id
age: Int @search
tweets: [Tweet] @hasInverse(field: user)
}
GraphQLのスキーマをもっと知りたい方はこちらにて
@idや@hasInverseというのはdgraph側で予約されてるdirectiveです。
index of Directiveにもあるように
@idがUNIQUEですよというDirectiveで、@hasInverseが一方向のEdgeだけでなく、双方向のEdgeにするという意味のDirectiveです。
これをschema.grapqlというファイルを作って、その中に書き込み、curlなら
curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql'
こんな形でデータを投げてやればSchemaがつくられます。
こんなレスポンスが返ってきてるのではないでしょうか?
{"data":{"code":"Success","message":"Done"}}
Dgraphは/graphql
のパスでGraphQL APIを実行し、/admin
で管理インターフェースを実行します。 /admin
インターフェースを使用すると、/graphql
で提供されるGraphQLスキーマを追加および更新できます。
GraphQLを使わず作る(RDF?)
定義の書き方はこんな感じです。
まず、Predicatesを記載して、それをtypeに使ってあげるという定義の仕方です。
Tweet.content: string @index(fulltext) .
Tweet.title: string .
Tweet.user: uid .
User.age: int @index(int) .
User.name: string @index(hash) @upsert .
User.tweets: [uid] .
type Tweet {
Tweet.title
Tweet.content
Tweet.user
}
type User {
User.name
User.age
User.tweets
}
また、DirectiveもGraphQLのモノとはことなっており、@index
はインデックスを貼るのに使い、@upsert
は、トランザクションをコミットする際に、インデックスキーのコンフリクトをチェックし、同時アップサートを実行時のユニーク制約を適用させるのに使います。
この定義をこんな形でリクエストすれば、定義が行われます。
curl "localhost:8080/alter" -XPOST -d $'
Tweet.content: string @index(fulltext) .
Tweet.title: string .
Tweet.user: uid .
User.age: int @index(int) .
User.name: string @index(hash) @upsert .
User.tweets: [uid] .
type Tweet {
Tweet.title
Tweet.content
Tweet.user
}
type User {
User.name
User.age
User.tweets
}'
こんなレスポンスが返ってきてるのではないでしょうか。
{"data":{"code":"Success","message":"Done"}}
cliではなく、Ratelのguiでもできます。
RatelのSchemaページを開くと、Dgraphに登録してるSchemaを見ることができます。
右上のBuik Edit
をクリックするとこんなページがでてきます。
defaultで入っているSchemaに追加して、上のSchemaを入れて上げましょう。
こんな感じでSchemaが入ったのがみれるかと思います。
(あ、今日12月24日だからか、メニューの一番上、サンタの帽子被ってますね笑)
Goでやってみる
GoでDgraphを扱うためのpackageがありまして、名をdgoと言います。
import (
"github.com/dgraph-io/dgo/v200"
"github.com/dgraph-io/dgo/v200/protos/api"
"google.golang.org/grpc"
)
最低限必要なpackageはこんな感じです。
grpcを使うので、grpcのpackageを含めておく必要があります。
まず、DgraphのClientを準備しましょう。
自分はfuncで切り出してこんな感じにしてます。
func getDgraphClient() (*dgo.Dgraph, CancelFunc) {
conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
dc := dgo.NewDgraphClient(api.NewDgraphClient(conn))
return dc, func() {
if err := conn.Close(); err != nil {
log.Printf("Error while closing connection:%v", err)
}
}
}
GraphQLで投げたい。とは思っているものの、どうやらdgoはDQLで投げるようで。
Schemaはこんな感じで記載してやります。
op := &api.Operation{Schema: `
Tweet.content: string @index(fulltext) .
Tweet.title: string .
Tweet.user: uid .
User.age: int @index(int) .
User.name: string @index(hash) @upsert .
User.tweets: [uid] .
type Tweet {
Tweet.title
Tweet.content
Tweet.user
}
type User {
User.name
User.age
User.tweets
}
`,}
ここまでをまとめるとこんな感じです。
package main
import (
"context"
"log"
"github.com/dgraph-io/dgo/v200"
"github.com/dgraph-io/dgo/v200/protos/api"
"google.golang.org/grpc"
)
type CancelFunc func()
func main() {
dc, cancel := getDgraphClient()
defer cancel()
ctx := context.Background()
op := &api.Operation{Schema: `
Tweet.content: string .
Tweet.content: string @index(fulltext) .
Tweet.title: string .
Tweet.user: uid .
User.age: int @index(int) .
User.name: string @index(hash) @upsert .
User.tweets: [uid] .
type Tweet {
Tweet.title
Tweet.content
Tweet.user
}
type User {
User.name
User.age
User.tweets
}`,}
err := dc.Alter(ctx, op)
if err != nil {
log.Fatal(err)
}
}
func getDgraphClient() (*dgo.Dgraph, CancelFunc) {
conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
dc := dgo.NewDgraphClient(api.NewDgraphClient(conn))
return dc, func() {
if err := conn.Close(); err != nil {
log.Printf("Error while closing connection:%v", err)
}
}
}
これを実行して、Schemaを作ります。
Mutation(Add)
GraphQLで追加する
Dgraphは、Schema作成時にミューテーションのための、inputとreturnの型をSchemaに自動で追加してくれます。
なので、今回の場合だとこんなmutationを書いてあげればデータの作成ができます。
mutation MyMutation {
addUser(input: [
{ name: "masamiki", age: 99, tweets: [{ content: "Dgraph", title: "GraphQL" }]}
]) {
numUids
user {
name
}
}
これをcurlでリクエストすると
curl -X POST 'localhost:8080/graphql' -H 'Content-Type: application/graphql' -d 'mutation MyMutation {
addUser(input: [
{ name: "masamiki", age: 99, tweets: [{ content: "Dgraph", title: "GraphQL" }]}
]) {
numUids
user {
name
}
}
}'
userの名前が載ったレスポンスが返ってきてるのではないでしょうか。
{"data":{"addUser":{"numUids":2,"user":[{"name":"masamiki"}]}},"extensions":{"touched_uids":5,"tracing":{"version":1,"startTime":"2020-12-24T05:43:26.9452586Z","endTime":"2020-12-24T05:43:26.9797827Z","duration":34519200,"execution":{"resolvers":[{"path":["addUser"],"parentType":"Mutation","fieldName":"addUser","returnType":"AddUserPayload","startOffset":243200,"duration":34227000,"dgraph":[{"label":"mutation","startOffset":426600,"duration":23382300},{"label":"query","startOffset":28743900,"duration":5642100}]}]}}}}
RDFで追加する
RDFだとこんな感でTripleというものを使って表現します。
構造としては<subject> <predicate> <object> .
という感じで、subject
は、有向性Edgeのpredicate
を持つobject
にリンクされることを意味しています。
{
set {
_:t <Tweet.content> "Dgraph" .
_:t <Tweettitle> "GraphQL" .
_:t <dgraph.type> "Tweet" .
_:m <User.name> "masamiki" .
_:m <User.age> "99" .
_:m <User.tweets> _:t .
_:m <dgraph.type> "User" .
}
}
ここではSchemaのどのタイプなのかを<dgraph.type>という形で表現します。
これをcurlで
curl -H "Content-Type: application/rdf" "localhost:8080/mutate?commitNow=true" -XPOST -d $'
{
set {
_:t <Tweet.content> "Dgraph" .
_:t <Tweet.title> "GraphQL" .
_:t <dgraph.type> "Tweet" .
_:m <User.name> "masamiki" .
_:m <User.age> "99" .
_:m <User.tweets> _:t .
_:m <dgraph.type> "User" .
}
}'
そうするとこん..(ry
{"data":{"code":"Success","message":"Done","queries":null,"uids":{"m":"0x2b","t":"0x2a"}},"extensions":{"server_latency":{"parsing_ns":661700,"processing_ns":8785500,"assign_timestamp_ns":1486000,"total_ns":10474300},"txn":{"start_ts":1975,"commit_ts":1976,"preds":["1-Tweet.content","1-Tweet.title","1-User.age","1-User.name","1-User.tweets","1-dgraph.type"]}}}
Goでやってみる
まず、structの定義です。
type Tweet struct {
Uid string `json:"uid,omitempty"`
Title string `json:"Tweet.title,omitempty"`
Content string `json:"Tweet.content,omitempty"`
User User `json:"Tweet.user,omitempty"`
DType []string `json:"dgraph.type,omitempty"`
}
type User struct {
Uid string `json:"User.uid,omitempty"`
Name string `json:"User.name,omitempty"`
Age int `json:"User.age,omitempty"`
Tweets []Tweet `json:"User.tweets,omitempty"`
DType []string `json:"dgraph.type,omitempty"`
}
投げる時は、JSONで投げるので、JSONにマーシャルするためのタグを付けておきます。
structを初期化、JSONにマーシャル、transactionを貼ってミューテーション。
これが、一連の流れです。
package main
import (
"context"
"encoding/json"
"log"
"github.com/dgraph-io/dgo/v200"
"github.com/dgraph-io/dgo/v200/protos/api"
"google.golang.org/grpc"
)
type CancelFunc func()
type Tweet struct {
Uid string `json:"uid,omitempty"`
Title string `json:"Tweet.title,omitempty"`
Content string `json:"Tweet.content,omitempty"`
User User `json:"Tweet.user,omitempty"`
DType []string `json:"dgraph.type,omitempty"`
}
type User struct {
Uid string `json:"User.uid,omitempty"`
Name string `json:"User.name,omitempty"`
Age int `json:"User.age,omitempty"`
Tweets []Tweet `json:"User.tweets,omitempty"`
DType []string `json:"dgraph.type,omitempty"`
}
func main() {
dc, cancel := getDgraphClient()
defer cancel()
ctx := context.Background()
u := User{
Uid: "_:m",
Name: "masamiki",
Age: 99,
DType: []string{"User"},
Tweets: []Tweet{{
Title: "GraphQL",
Content: "Dgraph",
DType: []string{"Tweet"},
}},
}
mu := &api.Mutation{
CommitNow: true,
}
pb, err := json.Marshal(u)
if err != nil {
log.Fatal(err)
}
mu.SetJson = pb
response, err := dc.NewTxn().Mutate(ctx, mu)
if err != nil {
log.Fatal(err)
}
log.Println(response)
}
func getDgraphClient() (*dgo.Dgraph, CancelFunc) {
conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
dc := dgo.NewDgraphClient(api.NewDgraphClient(conn))
return dc, func() {
if err := conn.Close(); err != nil {
log.Printf("Error while closing connection:%v", err)
}
}
}
Query
Queryの話、書こうと思ったのですが、長くなりそうなので、後日、別で書きます。
とりあえず……
DQLで取ってみる
DQLを使って、今回入れたデータをさっとRatelで見てみるだけは、やりましょうか。
とりあえず、userとそれに紐付くtweetを取りたいならこんな感じ。
{
users(func: has(<dgraph.type>)) @filter(eq(<dgraph.type>, "User")) {
expand(_all_) {
expand(_all_)
}
}
}
expand(_all_)
を使うと、すべてのpredicateを表示することができます。
一旦全部取得するって時は、これを使うのがいいですかね。
そうすると、こんな関係性をもったノードが表示されているのではないでしょうか?
以上。
また、時間作って、次の記事書きますmm。
Discussion