Cayleyでお手軽グラフデータ操作入門

Cayley logo

Cayleyとは

Cayleyは、最近リリースされたばっかりのグラフエンジンです。
グラフエンジンとは、グラフ構造を扱うために特化したNOSQLのDBMSです。グラフデータベースなどと呼ばれたりもします。
Cayleyの他には、neo4jが有名です。
Cayleyの作者のBarak Michener氏はGoogleナレッジチームのエンジニアです。
特徴は以下の通り:

  • Googleナレッジグラフにインスパイアされている
  • Go言語で実装
  • バックエンドストアはMongoDBとLevelDB、エフェメラルな用途でのインメモリに対応
  • Gremlinに似たオブジェクトを備えるJavaScript、MQLに対応

Google Open Source Blogによると、Cayleyが作られた経緯には、Freebaseの膨大なグラフデータを素早くフリーで簡単に使えるようにする事が挙げられています。

本エントリでは、付属のサンプルデータセットを使って基本的な使い方を把握したいと思います。

グラフ構造の基本

Cayleyを使う前に、それが扱うデータ構造についての理解が必要です。
Cayleyが扱うグラフ構造とは、以下のような要素間関係構造をもつデータのことです。

Screen Shot 2014-08-13 at 8.20.56 PM

データは、Vertex(頂点)とEdge(枝)の群で構成されます。
これで表される情報は、例えば友達関係、親子関係、所属、ネットワーク、部品構成などです。
巷で人間関係が「ソーシャルグラフ」と呼ばれるゆえんは、このデータ構造の名称にあります。

Screen Shot 2014-08-13 at 8.21.06 PM

上図は、Steveの親はJohnである事を示すグラフデータです。
このようなエッジに方向性を含むグラフ構造を有向グラフと呼びます。
Cayleyは有向グラフを扱うエンジンです。

インストールと開始

Release Binaryからプラットフォームに合ったバイナリをダウンロードして任意の場所に展開します。

$ wget https://github.com/google/cayley/releases/download/v0.3.1/cayley_0.3.1_linux_amd64.tar.gz
$ cd cayley_0.3.1_linux_amd64

次にサンプルデータのパスを指定して起動します。

$ ./cayley http --dbpath=30kmoviedata.nq.gz
Cayley now listening on 0.0.0.0:64210

起動すると、ポート64210でウェブサーバが立ち上がります。
ブラウザからアクセスすると以下のような画面が表示されます。

Screen Shot 2014-08-13 at 8.45.18 PM

そのほかの起動方法

起動オプションは他にもいろいろあります。詳しくはこちらを参照。

httpの代わりにreplを指定するとREPLプロンプトが立ち上がります。

$ ./cayley repl --dbpath=30kmoviedata.nq.gz
cayley>

基本的なクエリ

クエリはJavaScriptで書けます。
読み込んだサンプルデータはFreebaseの映画データです。
まずは女優の「Uma Thurman」のVertexを探してみます。

graph.Vertex("Uma Thurman").All()

結果:

{
 "result": [
  {
   "id": "Uma Thurman"
  }
 ]
}

このように、graphオブジェクトのメソッドを呼び出してグラフデータにアクセスします。

ここで、Uma Thurmanは名前でありエンティティではありません。
nameUma Thurmanを持つエンティティを取得してみます。

graph.Vertex("Uma Thurman").In("name").All()

結果:

{
 "result": [
  {
   "id": "/en/uma_thurman"
  }
 ]
}

また、graphにはgVertexにはVというエイリアスがあります。
以下の式は等価です:

graph.Vertex("Uma Thurman").In("name").All()
g.V("Uma Thurman").In("name").All()

では、Uma Thurmanのエンティティが取得できたので、更に主演した映画の一覧を取得してみます:

graph.V().Has("name", "Uma Thurman").In("/film/performance/actor").In("/film/film/starring").Out("name").All()

結果:

{
 "result": [
  {
   "id": "The Golden Bowl"
  },
  {
   "id": "Les Miserables - The Dream Cast in Concert"
  },
  {
   "id": "Vatel"
  },

...

  {
   "id": "Kill Bill"
  },

...

  {
   "id": "The Life Before Her Eyes"
  },
  {
   "id": "The Avengers"
  }
 ]
}

みなさんご存知「キル・ビル」に主演している事が分かりました!
では逆に、Kill Billの出演者一覧を取得してみます:

g.V().Has("name","Kill Bill").Out("/film/film/starring").Out("/film/performance/actor").Out("name").All()

結果:

{
 "result": [
  {
   "id": "Uma Thurman"
  },
  {
   "id": "David Carradine"
  },
  {
   "id": "Daryl Hannah"
  },

...

  {
   "id": "Sonny Chiba"
  },
  {
   "id": "Julie Dreyfus"
  }
 ]
}

使用したメソッドが In から Out に変化した事にお気づきでしょうか?
これは、起点とするVertexが逆になり、それに伴ってエッジの方向も逆になったためです。

クエリのビジュアライズ

ウェブUIのQuery Shapeを選択して、下記の主演映画一覧のクエリを実行してみます。

graph.V().Has("name", "Uma Thurman").In("/film/performance/actor").In("/film/film/starring").Out("name").All()

すると、下記のような図が表示されます。なんかぐねぐねしてます。

Screen Shot 2014-08-13 at 9.10.40 PM

モーフィズムとタグを使う

graph.Morphism(Alias: graph.M)は、パスオブジェクトを作ります。
以下のようによく使うパスを定義すれば、後から使い回せます。

var shorterPath = graph.Morphism().Out("foo").Out("bar")

以下のようにMorphismを呼び出します:

var p = graph.M().Has("name", "Uma Thurman").In("/film/performance/actor").In("/film/film/starring").Out("name");
graph.V().Follow(p).All();

タグを使うと、辿ったパスの任意の頂点を保存できます。
例えば、Uma Thurmanが主演した全ての映画で、共演した俳優・女優の一覧を取得するには:

costar = g.M().In("/film/performance/actor").In("/film/film/starring").Out("name")
graph.V().Has("name", "Uma Thurman").Follow(costar).As("Movie")
.FollowR(costar).Out("name").As("Costar").All()

結果:

{
 "result": [
  {
   "Costar": "Jeremy Northam",
   "Movie": "The Golden Bowl",
   "id": "Jeremy Northam"
  },
  {
   "Costar": "Peter Eyre",
   "Movie": "The Golden Bowl",
   "id": "Peter Eyre"
  },
  {
   "Costar": "Madeleine Potter",
   "Movie": "The Golden Bowl",
   "id": "Madeleine Potter"
  },
  {
   "Costar": "Nickolas Grace",
   "Movie": "The Golden Bowl",
   "id": "Nickolas Grace"
  },

...

  {
   "Costar": "Uma Thurman",
   "Movie": "The Avengers",
   "id": "Uma Thurman"
  },
  {
   "Costar": "Sean Connery",
   "Movie": "The Avengers",
   "id": "Sean Connery"
  },
  {
   "Costar": "Eddie Izzard",
   "Movie": "The Avengers",
   "id": "Eddie Izzard"
  },
  {
   "Costar": "Fiona Shaw",
   "Movie": "The Avengers",
   "id": "Fiona Shaw"
  },
  {
   "Costar": "Patrick Macnee",
   "Movie": "The Avengers",
   "id": "Patrick Macnee"
  },
  {
   "Costar": "Jim Broadbent",
   "Movie": "The Avengers",
   "id": "Jim Broadbent"
  }
 ]
}

クエリ結果のビジュアライズ

Uma Thurmanと過去に共演した事のある主演者同士の関係を取得します:

costar = g.M().In("/film/performance/actor").In("/film/film/starring")


function getCostars(x) {
  return g.V(x).As("source").In("name")
          .Follow(costar).FollowR(costar)
          .Out("name").As("target")
}


function getActorNeighborhood(primary_actor) {
  actors = getCostars(primary_actor).TagArray()
  seen = {}
  for (a in actors) {
    g.Emit(actors[a])
    seen[actors[a].target] = true
  }
  seen[primary_actor] = false
  actor_list = []
  for (actor in seen) {
    if (seen[actor]) {
      actor_list.push(actor)
    }
  }
  getCostars(actor_list).Intersect(g.V(actor_list)).ForEach(function(d) 
{
    if (d.source < d.target) {
      g.Emit(d)
    }
  })
}

getActorNeighborhood("Uma Thurman")

ウェブUIでVisualizeを選択して、上記クエリを実行すると、下図のような画像が得られます:

Screen Shot 2014-08-13 at 9.45.56 PM

おお、かっこいいですね!

参考

fluentd + MongoDB + Elasticsearch + Kibanaでログを可視化する

elasticsearch+kibana

SaaSは利用料が高いのでOSSを使う

サーバのログを可視化するSaaSは沢山あり、手軽にBusiness Intelligenceを施行できます。
DataDogとかKeen IOとかlibratoLogglyなどなど。
とても便利そうですね。でも価格が高い!
なんでもかんでもSaaSに頼ってたら毎月数十万とかになりそうです。貧乏にはつらいです。
だから、OSSで無料でやりましょう。
もちろんインフラ代は最低限かかります。
でも、クラウドリソースはいずれ水みたいになりますから、そこはしばらくの辛抱です。

要件

独自フォーマットのログを扱いたい

今回はApacheのアクセスログみたいな一般的なものではなく、独自のアプリケーションログを集積して可視化します。
ログデータのフォーマットは以下のようなイメージです:

{
    "logid": "Log ID",
    "level": "Log level (DBG/MSG/WRN/ERR)",
    "message": "Message",
    "module": "Name of the application module",
    "hostname": "Hostname sent the log",
    "time": "timestamp of the log",
    <Further data..>
}

このフォーマットは昔に設計したもので、解析するのを意識していなかった事情があります。

アプリケーション特化の情報も一緒に格納したい

上記フォーマットに書いてある<further data..>の部分は、アプリケーション特化の情報です。
エラーログならコールスタックとか、ユーザ登録ならユーザIDとかです。
そういう特別な情報は、可視化までしなくとも個別に見られるようにしておきたい。
また、今回の構成が将来的に変更になっても対応しやすいように、フォーマットをシステム特化で縛りたくない。

グラフ設定を簡単に柔軟に変えられるようにしたい

Graphiteとかすごく良いんですが、細かい調整がやりにくい。
タイムスケールをもうちょっと細かくみたいな、とか、ヒストグラムの分割単位を変えたいとか、そこはパイグラフで見たいとか、トレンドで見たいとか、このシリーズだけ抜き出したいとか。
あと、上記でも挙げた特化データを個別にすぐ表示できると便利です。
GrowthForecastもいいんだけど、イマドキRRDtoolかよ・・JSでグリグリ動くのがいい。

システム構成

上記の要件を満たす構成は、以下のようなイメージです。
タイトルの通り、fluentd + MongoDB + Elasticsearch + Kibanaとなりました。

log_visualization_system

以下、それぞれ概要と用途を説明します。

fluentd

ログ集積のためのツール。
PHP/node.js/Ruby/Python製などのさまざまなプログラムと連携させられます。
syslogやApache/nginxログにも対応。
それらのログを集めて、フィルタリングしたり整形した後、集積先へと転送してくれます。

MongoDB

NOSQLのDBMSです。
構造体のデータを格納できます。
スキーマレスなので、アプリケーション特化の情報をどんどん追加格納できます。
Capped collectionで高速にログを蓄積できます。

今回は、ログの一次格納先として使用します。
のちのち可視化部分のシステム構成が変更になった場合に、元のデータをオリジナルで保持しておけば対応がいくらか容易になるからです。

Elasticsearch

Luceneベースの全文検索サーバ。
MongoDBと同じく、スキーマレスで構造体が扱えます。
MongoDBからデータを流し込んで、可視化用にインデックスを作成してくれます。

Kibana

タイムスタンプがついたデータならなんでも可視化できるというツール。
Elasticsearchと密結合になっています。
UIがよくできていて、柔軟に設定を変えられるので、今回の用途にもってこいです。

Chefを使ったセットアップ手順

基本的にサーバはChefで管理したいので、Cookbookを拾ってきて使います。
以下を前提に進めます

  • MongoDBサーバはすでに立ててある
  • サーバはDebian/Ubuntu
  • インフラはEC2

手順内にsspeと出てきますが、プロジェクトのコードネームです。
ご自身のプロジェクト名と読み替えてください。

fluentdの設定

アプリケーションは、td-agent(fluentd agent)に対してhttp経由でログを投げます。
td-agentは、受け取ったログを指定のmongodbに転送します。
mongodbはreplicasetなので、typeをmongo_replsetとしています。

<source>
  type http
  port 8888
</source>

<match sspe.log.*>
  type mongo_replset
  database fluent
  collection log

  # Following attibutes are optional
  nodes mongodb-server-1:27017,mongodb-server-2:27017

  # Set 'capped' if you want to use capped collection
  capped
  capped_size 1000m

  # Other buffer configurations here
  flush_interval 10s
</match>

ElasticsearchとKibanaのインストール

1つのサーバにElasticsearchとKibanaの両方を入れます。
Elasticsearchには、以下の4つのプラグインもインストールします。

  • elasticsearch-mapper-attachments
    • elasticsearch-river-mongodbから依存
    • GridFSを取り扱うためのもの
  • elasticsearch-river-mongodb
    • MongoDBからElasticsearchにデータを流し込むためのもの
  • elasticsearch-head
    • 簡易管理UI
  • elasticsearch-HQ
    • headよりリッチな管理UI

まずはEC2ノードを立てる

Knifeコマンドでサクっと立てます。

  • OS: Ubuntu Server 14.04 LTS (HVM), SSD Volume Type
$ knife ec2 server create 
    -I ami-a1124fa0 
    -f t2.small 
    --node-name server-name 
    --security-group-ids sg-hogehoge 
    --subnet subnet-111111111 
    --associate-public-ip 
    --ebs-size 50 
    --ssh-key ssh-key-name 
    --identity-file ~/.ssh/id_rsa 
    --ssh-user ubuntu 
    --ssh-gateway ubuntu@hogehuga 
    --verbose

CookbookをChef serverにアップロードする

Berksfileに以下の行を追記します。

cookbook 'elasticsearch'
cookbook 'java'
cookbook 'kibana', git: 'git@github.com:lusis/chef-kibana.git'

なぜかKibanaだけ同名の想定とは別のものが入ってしまったので、repos uriを指定します。
インストールしてアップロードします。

$ berks install
$ berks upload

Chefノードを設定する

ノードの設定を編集します。

$ knife node edit server-name
{
  "name": "server-name",
  "chef_environment": "_default",
  "normal": {
    "tags": [

    ],
    "java": {
      "install_flavor": "openjdk",
      "jdk_version": "7"
    },
    "elasticsearch": {
      "version": "1.2.2",
      "cluster": {
        "name": "sspe"
      },
      "plugins": {
        "elasticsearch/elasticsearch-mapper-attachments": {
          "version": "2.3.0"
        },
        "com.github.richardwilly98.elasticsearch/elasticsearch-river-mongodb": {
          "version": "2.0.1"
        },
        "mobz/elasticsearch-head": {
        },
        "royrusso/elasticsearch-HQ": {
        },
        "elasticsearch/elasticsearch-lang-javascript": {
          "version": "2.1.0"
        }
      }
    },
    "kibana": {
      "config": {
        "elasticsearch": "window.location.protocol+"//"+window.location.hostname+":"+9200",
        "default_route": "/dashboard/file/guided.json"
      },
      "kibana": {
        "web_dir": "/opt/kibana/current"
      }
    },
  },
  "run_list": [
    "recipe[java]",
    "recipe[elasticsearch]",
    "recipe[elasticsearch::plugins]",
    "recipe[kibana::install]"
  ]
}

設定出来たら、chef-clientを実行します。

動作確認

以下のように結果が返ってきたら成功です。

$ curl "http://localhost:9200/_cluster/health?pretty"
{
  "cluster_name" : "sspe",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 0,
  "active_shards" : 0,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0
}

Elasticsearchの設定

インデックスの作成

Elasticsearchのインデックスとは、RDBMSでいうデータベースみたいなもんです。
下記コマンドで作成します。

curl -XPUT 'http://localhost:9200/sspe/'

マッピングの作成

マッピングとは、RDBMSにおけるスキーマ定義みたいなもんです。
明示しなくても自動で作成させる事もできますが、今回は勉強も兼ねて指定します。

$ curl -XPUT 'http://localhost:9200/sspe/log/_mapping' -d '
{
    "log" : {
        "properties" : {
            "logid" : {"type" : "string" },
            "level" : {"type" : "string" },
            "message" : {"type" : "string", "store" : true },
            "module" : {"type" : "string" },
            "hostname" : {"type" : "string" },
            "time" : {"type" : "date" },
            "userinfo": { "type": "object", "enabled": false, "include_in_all": true }
        }
    }
}
'

MongoDB riverの設定

riverとは、Elasticsearchにデータを流し込む経路設定のことです。
elasticsearch-river-mongodbをインストールしてあるので、これを使う設定をします。

$ curl -XPUT "localhost:9200/_river/mongodb/_meta" -d '
{
  "type": "mongodb",
  "mongodb": { 
    "servers":
    [
      { "host": "mongodb-server-1", "port": 27017 },
      { "host": "mongodb-server-2", "port": 27017 }
    ],
    "options": { 
      "secondary_read_preference" : true
    },
    "db": "fluent", 
    "collection": "log"
  }, 
  "index": { 
    "name": "sspe", 
    "type": "log"
  }
}'

実行すると、データの抽出が開始されるはずです。

データの流入確認

下記URLから、headプラグインの管理画面を開きます。

http://<elasticsearch-server>:9200/_plugin/head/

Screen Shot 2014-08-05 at 3.43.45 AM

ドキュメント数が増えていれば成功です。

トラブルシューティング

Cannot start river mongodb. Current status is INITIAL_IMPORT_FAILED

もし設定がうまく動かなくて、Elasticsearchを再起動してもログに上記が吐かれて止まってしまう場合。
Riverのステータスをリセットしてやれば再稼働します。

$ curl -XDELETE 'http://localhost:9200/_river/mongodb/_riverstatus'

実行後、Elasticsearchを再起動します。

Kibanaの設定

ElasticSearchとの連動確認

ESとKibanaをインストールしたサーバにブラウザからアクセスします。

http://<elasticsearch-server>/#/dashboard/file/guided.json

すると、下記のような画面が表示されるはずです。

Screen Shot 2014-08-05 at 3.40.39 AM

自分用のダッシュボードを設定する

以下にアクセスすると、空っぽのダッシュボードにアクセスできます。

http://<elasticsearch-server>/#/dashboard/file/blank.json

ADD A ROWというボタンをクリックして、グラフを表示する領域の行を追加します。

Screen Shot 2014-08-05 at 3.46.27 AM

Rowの名前を入力後、Create Rowします。

Screen Shot 2014-08-05 at 3.47.23 AM

次に、パネルを追加します。

Screen Shot 2014-08-05 at 3.48.53 AM

Screen_Shot_2014-08-05_at_3.51.10_AM

  1. パネルタイプをhistogramにします
  2. 幅を12に設定
  3. タイムフィールドにtimeを指定
  4. Auto-intervalのチェックを外す
  5. Saveボタンをクリック

すると・・・

Screen Shot 2014-08-05 at 3.51.19 AM

やった!出ましたね!
エラーログのみを表示したい場合は、QUERYの欄に level:ERR と打ちます。
クエリ構文はLuceneと基本同じものが使えるようです。以下が参考になります。

参考リンク