読者です 読者をやめる 読者になる 読者になる

アプリエンジニアからみたMongo shell プログラミング

この記事はMongoDB Advent Calendar 2015 の記事です

qiita.com

業務でMongoDBを利用しているので、少しでも貢献しようと思い参加させていただきました

記事の内容的には、最近感動したMongo shelについて書きたいと思います!

2013に似たような記事を書かれていることに気づいてしまったため

アプリエンジニア視点で、MongoShellの便利さを書いてみようと思います

Mongo shellって?

Mongodbでは検索クエリだけでなく、javascriptを実行できるshellが用意されている

ドキュメント https://docs.mongodb.org/manual/reference/mongo-shell/

なにができるの?

一般的な使い方

データの検索・更新・削除

db.users.find({_id:1})

よくあるやつです

応用編

ここからがmongoシェルのすごいところ

jsのコードが書けます

var users = [];
db.users.forEach(function(user) {
  users.push(user);
});

var ids = users.map(function(user) { return user.userId });
var messages = [];
db.messages.find({from: ids}).forEach(function(message) {
  messages.push(message);
});

さらに、DBをまたぐこともできます

2系で複数DB構成の場合はかなり便利

var hogeDB = db.getMongo().getDB('hoge');
var hugaDB = db.getMongo().getDB('huga');
hogeDB.users.find();
hugaDB.users.find();

本気出せばすべてのコレクションをドロップすることも可能です(やっちゃだめです)

var collectionNames = db.getCollectionNames();
collectionNames.forEach(function(collectionName) {
  db[collectionName].drop();
});

外部jsファイルの読み込みもできます

(--quiteオプションにより余分な出力を消せます)

mongo sample_db --quite sample.js

バッチでの利用

ユーザーID一覧をだして!!

user.js
 db.users.find().forEach(function(user) { print(user.userId) });

mongo sample_db user.js --quite > user_id.csv

yes sir !

今日登録したユーザーの課金ログ出して!!

purchase.js
  db.users.find({registerDate: {$gt: new Date()}}).forEach(function(users) {
    var ids = users.map(function(user) {
      return user.userId;
    });

    purchases.find({userId: {$in: ids}}).forEach(function(purchase) {
       print(purchase.userId + "," + purchase.orderId + "," + purchase.purchaseTime + "," + purchase.amount);
    });
  });


mongo sample_db purchase.js --quite > purchase.csv

yes sir !

なんてことが簡単にできます!

find結果の参照はcursorなので、多少大きいデータを抜いたところで メモリーがあふれるとかそういう心配はないはずです

バッチ処理にはもってこいですね!

並列処理に関しては、課題がありますが...

ちょっとしたTips

  • SpiderMonkey準拠なのでv8ベースのnodejsとは異なる
    • 今後v8へ置き換わる可能性はあり
    • console.logはprint
  • find結果のforEachはカーソルなので、一回しかforEachかけられない。二回目はundefinedになる
  • サンプルコードはこの辺を参考にすると良いです

いままで、簡単なデータ抽出でも、いちいちjsでバッチ書いて、クライアントでつないで・・・

ってやってたのですが

ここまで自由にデータ操作ができれば、いちいちバッチとか書かなくても全然いいですね!!

さぁ、みなさんもlet's try MongoDB !

ホントは怖いMongoDB

とあるMongoDBを利用しているサービスを引き継いだら死にかけていたって話。

mongoose populate事件

まず、最初にサービスにjoinしたころ

mongoが遅い!mongoが遅い!という声が聞かれたので、試しにスロークエリーをとってみることにした

cat /var/log/slow.log

===

db.users.find({_id: {$in: [1,2,3,4,5,6,7,8,8,9,................. 
~ コンソールいっぱいのID列(略) ~
.limit(10)  // 律儀にlimitはかかっている
query not recording (too large)

mongodbのログってクエリが長いと省略されるんだーへー

じゃない!

おかしい!

しかし、調べてもソースコード上でin句を使ってるところが見当たらない

結果的には、mongooseのpopulateメソッドが犯人だった

user to user のデータにpopulateを使うべからず

Mongooseにはpopulateという、RDBでいうとこのjoinみたいな機能がある

Mongoose Query Population v4.1.11

KVSでjoinとか革新的じゃん

しかし。。このpopulateを過信して設計を誤ると大変なことになる (ふつうのDBでもuser to userデータにjoin使わないけど。。)

簡単例で、たとえばユーザーデータとして

  • ユーザーのID
  • フォローするユーザーのID

があったとする

実際のデータはこんな感じ

{
  "user_id": 1,
  "follow": [2,3,4] // ここにユーザーIDが入ることはmongooseのModelに定義済とする
},
{
  "user_id": 2,
  "follow": []
},
{
  "user_id": 3,
  "follow": []
},
{
  "user_id": 4,
  "follow": []
}

それに対して、以下のpopulteをなげると

User
  .findOne({_id: 1})
  .populate('follow')
  .exec(function (err, users) {
    // 
});
[
{
  "user_id": 1,
  "follow" : [
     {
        "user_id": 2,
        "follow": []
     }
     {
        "user_id": 3,
        "follow": []
     }
     {
        "user_id": 4,
        "follow": []
     }
  ]
}
]

データがjoinされて返って来る!便利!

この便利なメソッド、内部的にはこんなクエリが発行される

db.users.find({_id:1});
db.users.find({_id: {$in: [2,3,4]}});

ここで注意しないといけないのは、$in句は1件取得なので計算量はO(n)である。

これをそこそこ大きいサービスで運用するとどうなるかというと

計算量はユーザー数に比例してでかくなり、スロークエリが連発する。

そして何より恐ろしいのが

MongoDBのslow queryがin句によりうめつくされ、tailするとコンソールが固まり、更にはログサーバーのディスク容量が枯渇する(経験談)

解決策 データ構造を見直し、indexを利用

一度に全員引く必要が無いなら、サンプルの例はこれのほうがいい

followUsers

{
  user_id: 1,
  follow: 2
},
{
  user_id: 1,
  follow: 3
},
{
  user_id: 1,
  follow: 4
},
{
  user_id: 1,
  follow: 5
},
{
  user_id: 1,
  follow: 6
}

これに、ユニークインデックスを貼っとく

db.followUsers.exsureIndex({user_id: 1, follow:1}, {unique: true});

BtreeのオーダーはO(log n)なので、計算量は膨れ上がらないのと、limit, offsetが効くので元の構造より全然早く取れる

そもそもの話

KVSで複雑なindexが必要な状況なら、RDB使った方がいい 基本的にはタイムラインとか、シーケンシャルにIDで引けるものが一番パフォーマンスを発揮する

インスタンスがやたらつよいけど、スロークエリでまくってる事件

色々とクエリを改修して、とりあえず、障害クラスのスロークエリーがなくなり始めた頃

LAが全然低いのに、パフォーマンスが上がらない。。

という問題にぶち当たった

ちなみに環境は

  • MongoDBのバージョンは2.2
  • サーバー8コア

お気づきの方はいるかと思うが

Mongodbの2系はマルチコアが利用できないため、8コアのうちの1コアしか使えない

というもの。。

また、Databaseが一個しかない構成だった

MongoDBが適さないケース - LinuxとApacheの憂鬱

MongoDB 2.2以前はReaders-Writerロックなので、長いクエリーが走ったその瞬間から、短いクエリーもロックされる。 WebからのPVを捌く様なシステムの場合、一瞬でもロックが走れば、WEBサーバ上の全てワーカースレッドが潰されてサイトがダウンする。

MongoDBの2.2系はデータベースのグローバルロック問題を抱えている

一般的にMongoDBの2系を運用する際には、機能ごとにデータベースを分けて、ロックを分散させるように作るらしい

解決策

リリース直後のMongoの3系へのバージョンアップを決意

Mongo3系の特徴としては 

  • コレクションレベルロック(2.6系から)
  • WiredTiger
  • マルチコアサポート

そして、人柱となる覚悟を決めたのであった

Mongo3.0はいい感じなので、後でまとめたい

Tips

色々と運用してて溜まったTipsたち

MongoDBを選ぶ理由

  • スキーマレス
  • シャーディングによるスケーラビリティの高さ

MongoDBを選ばない理由

サービスの規模が予測可能、データ構造が変わりにくい場合はMySQL使った方が安定・安心

MongoDBを使う上で気をつけないといけないこと

正規化よりも、データの参照性を意識したほうがいい

  • KVS全般に言えることと思うけど、正規化しすぎると参照が苦しくなる

基本的に検索したいっていう要件には向かない

  • KVSは時系列など、並んでいるデータをとるときに真価を発揮する
  • MongoDBの場合、B-Tree Indexを使えるが、RDBに比べてアドバンテージがあるわけではない
  • 全文検索するなら別のミドルウェアを考えたほうがいい (elasticsearch + river + mongoとか)
  • トランザクションがないため、代替してデータを保護する仕組みが必要
  • Lock機構とか

データ設計で注意すること

  • 最初からシャーディングを意識した設計にする
    • シャーディングは基本_idでシャード
    • シャーディング環境のみでエラーになるクエリもあるので注意
  • 1ドキュメントがでかくなりすぎないような設計にする
  • データを重複で管理するとなってもデータの取り回しを優先した方がいい

シャーディング

  • 複数データベースをリストアするときは1データベースずつやる
  • データをロックしてしまい結果的に完了するまでの時間がかかる

The purchase token was not found.

android

先月くらいから1,2件同じ現象が起きてたんだけど

09/28 - 09/29のAndroid課金で、Subscriptionの一部のレシートがおかしい現象が発生

androidから送られてくるレシートをandroid-play-publisher-api経由でpurchase tokenをきくとtokenが無いと言われる

```
{
"code" : 404,
"errors" : [ { "domain" : "global",
"location" : "token",
"locationType" : "parameter",
"message" : "The purchase token was not found.",
"reason" : "purchaseTokenNotFound"
} ],
"message" : "The purchase token was not found."
}
```

Googleの管理コンソールから見るときちんと決済完了している

なんでや

調べたところ、issueも上がってた

The purchase token was not found. · Issue #21 · googlesamples/android-play-publisher-api · GitHub

正しいレシートが取得できないが、流石にクライアント側のデータを信じる訳にはいかないので手詰まり感


現状のところ解決策がないようである。。

10/1追記

Googleに問い合わせた人がいたらしく、近日中には修正されるとのこと
同様の現象が起きたらキャンセルしちゃっていいらしい

I talked with a guy who works at Google yesterday. This problem will be fixed soon.
They were working on the servers yesterday and it affected the billing api.
In the app I work here is working fine again.
For someone who is still getting this error, try to cancel the subscription (in my case) once and buy it again.

ちなみに、その時取得できなかったレシートは今は取得できるようになってました

Android勉強会 flavor

Androidの勉強会をしたのでメモ

環境ごとにファイルを書き換える必要がある場合flavorという機能を利用する

flavorを利用すると、src以下にflavorと同名のディレクトリを作成することによって、ファイルをリプレイスしてくれる

f:id:hase-xpw:20150326171252p:plain


プロジェクトを作成したときにデフォルトで作られるbuildTypesもflavorの一つ

buildTypesにはデフォルトでdebug, releaseが用意されている(debugは省略される)

build.gradle

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
        develop {
            applicationId = "12345"
        }
        staging {
            applicationId = "23456"
        }
        production {
            applicationId = "34567"
        }
    }

このflavorの掛け合わせでアプリの挙動が変わる

f:id:hase-xpw:20150326164850p:plain

以前に書いた記事で、GoogleMapのキーがうまく取れなかったのもこのflavorが原因だと思われる

hase.hateblo.jp

Kibana3 + Elasticsearchでエラーが止まらない

kibana3 Elasticsearch

kibana3 + Elasticsearchを組んだ際に、若干はまったところのメモ

kibanaを導入して、グラフを作った瞬間ElasticsearchにDEBUGレベルで大量にエラーが吐き出された

[2014-12-18 12:51:06,254][DEBUG][action.search.type       ] [Comet] [kibana-int][4], node[xi8KVRzfQmiJqS9txENE8g], [P], s[STARTED]: Failed to execute [org.elasticsearch.action.search.SearchRequest@7d5de0a6] lastShard [true]
org.elasticsearch.search.SearchParseException: [kibana-int][4]: from[-1],size[-1]: Parse Failure [Failed to parse source [{"facets":{"22":{"date_histogram":{"key_field":"@timestamp","value_field":"count","interval":"30s"},"global":true,"facet_filter":{"fquery":{"query":{"filtered":{"query":{"query_string":{"query":"status_code:(>=500 AND <600)"}},"filter":{"bool":{"must":[{"range":{"@timestamp":{"from":1418871064739,"to":1418874664739}}}]}}}}}}}},"size":0}]]
	at org.elasticsearch.search.SearchService.parseSource(SearchService.java:660)
	at org.elasticsearch.search.SearchService.createContext(SearchService.java:516)
	at org.elasticsearch.search.SearchService.createAndPutContext(SearchService.java:488)
	at org.elasticsearch.search.SearchService.executeQueryPhase(SearchService.java:257)
	at org.elasticsearch.search.action.SearchServiceTransportAction$5.call(SearchServiceTransportAction.java:206)
	at org.elasticsearch.search.action.SearchServiceTransportAction$5.call(SearchServiceTransportAction.java:203)
	at org.elasticsearch.search.action.SearchServiceTransportAction$23.run(SearchServiceTransportAction.java:517)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.elasticsearch.search.facet.FacetPhaseExecutionException: Facet [22]: (key) field [@timestamp] not found
	at org.elasticsearch.search.facet.datehistogram.DateHistogramFacetParser.parse(DateHistogramFacetParser.java:169)
	at org.elasticsearch.search.facet.FacetParseElement.parse(FacetParseElement.java:93)
	at org.elasticsearch.search.SearchService.parseSource(SearchService.java:644)
	... 9 more

kibanaの設定をデフォルトにしておくと、ダッシュボードまで検索対象に含めてしまってエラーになってしまうらしい

curl http://localhost/api/_mapping?pretty
{
  "kibana-int" : {
    "mappings" : {
      "dashboard" : {
        "properties" : {
          "dashboard" : {
            "type" : "string"
          },
          "group" : {
            "type" : "string"
          },
          "title" : {
            "type" : "string"
          },
          "user" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

これの、
f:id:hase-xpw:20141218130650p:plain

この設定を
f:id:hase-xpw:20141218130701p:plain

_allからログのindex名に書き換えればエラーが出なくなる

ログ解析にNorikraを使ってみた

Norikra

この記事は、CyberAgent エンジニア Advent Calendar 2014 の 17 日目の記事です。
昨日は@neo6120さんのアドテクスタジオのゼミ制度の紹介と活動報告 でした。
18日目は@sitotkfmさんのSpark StreamingでHyperLogLogを実装してみたです。



弊社で、プラットフォーム機能の一部を作らせて頂いている、@hase_xpwです。

@kakerukaeruさんにやろうぜ!って誘われたので参加してみました
これを機にブログもっと更新したいと思います!

今回のテーマは、Norikraというミドルウェアで、業務でログ解析をする際に使ってみた所かなり便利だったのでNorikraの魅力を少しでもお伝えできればいいなと思いテーマに選びました。

これおかしくね?みたいなのがあったらバシバシ指摘して頂きたいです。

環境構築と使い方は、わかりやすくまとめられている方がいるので割愛させて頂きます

1. Norikraってなに

LINE株式会社の TAGOMORI Satoshi (@tagomoris)さんが開発
norikra/norikra · GitHub
スキーマレスにストリームデータの処理を行ってくれます

fluentdのプラグインが用意されているため、fluentd経由でデータを吸い上げて集計する事が可能です

内部的には、jRubyで書かれていて、バックエンドのエンジンにはEsperというJavaのライブラリが利用されています。
Esperはデータのストリーム処理をSQL Likeに行えるJavaのライブラリで、ストリームのデータに対してwindowをあてて、期間ごとにデータを切り出して集計をします。

イメージはこんな感じです
f:id:hase-xpw:20141216110111p:plain


Norikraの利用例としては、Elasticsearch + kibana + fluentd + Norikraでログのリアルタイム可視化が挙げられます。ログデータをrawデータのまま保持すると、ディスク容量を圧迫し、また、データ量に比例して解析のコストが非常に大きくなるため、潤沢なリソースが必要になってしまいます。このため、Norikraであらかじめデータを集計して無駄なものを省いておくことにより、解析のコストを抑えることができます。

2. なにができるの

Norikraでは、データの集計・検索・結合等、EsperでサポートされているSQLシンタックスのほとんどを利用可能です
ドキュメント

INSERT, UPDATEは非対応ですが、ログ解析とかの利用では全然不要かと
Group By, JOINやサブクエリ周りが利用できるのはかなり夢が広がりますね
パフォーマンス面でも、4コアで2000 event/sくらいさばいても全然余裕なようです

Current Status:
	•	10 queries
	•	2,000 events per seconds
	•	5% usage of 4core CPU

5. なにがすごいの

SQLの知識だけあれば、かなり色々な形でログを集計できます
レイテンシの集計・アクセス数の集計・エラーレートの集計・課金額の集計・イベントのクリア状況の集計 etc...

webUIが用意されていて、サービスの再起動なしにクエリを変更する事が可能です

こんなの
f:id:hase-xpw:20141215194906p:plain


パフォーマンス面では、こちらの記事にあるように、fluentdはデフォルトでシングルプロセスで動くため、マルチコアの恩恵が受けられない のですが(プラグインを利用すればマルチプロセスで動かせます)

一方で、Norikraはjrubyで動くため、マルチプロセスで稼働させる事が可能です

6. 利用事例

弊社のとあるサービスで、ログのリアルタイム解析に利用しています

サービスの特性上、不特定多数のクライアントからAPIが呼び出されるため、各クライアントがどのくらいアクセスしていて、どんなレスポンスを返しているかを確認したいことがあり、毎回ログを集計するのも大変なので、クライアント別にステータスコードを集計し、kibanaを使いリアルタイムに可視化しようと考えました。

利用する候補として、Stormか、fluent-plugin-datacounter(こちらも@tagomorisさん作です)かNorikraを考えていたのですが、Stormはヘビーすぎたためあきらめ、datacounter pluginは正規表現でマッチしたログの数を集計するため、今回のようにクライアントが増える場合、増えるたびに設定を書き換えなければいけないため、Norikraにしました。

fluent-plugin-datacounterの設定ファイル

<match accesslog.baz>
  type datacounter
  count_key status
  pattern1 OK ^2\d\d$
  pattern2 NG ^\d\d\d$
  input_tag_remove_prefix accesslog
  output_per_tag yes
  tag_prefix datacount
  output_messages yes
</match>

構成
・Elasticsearch
・kibana
・fluentd
・Norikra

導入するシステムは200 req/s * 10台 = 2000 req/sくらいのアクセスが想定されていたため、公式に書いてある実績に従うならさばけると思うのですが、今後規模が大きくなったり、複雑なクエリを投げたくなった際に苦しくなる事が予測されるので、各サーバーにNorikraを配布し、必要なデータを集計して送るように構築しました。また、Aggregator側にもNorikraを配布し、必要に応じて受けたログの再集計を出来るようにしました。

こんな感じです
f:id:hase-xpw:20141216191603p:plain

投げているクエリは、こんな感じにステータスコードをclientのid別に集計するクエリです

SELECT
    client_id,
    status_code,
    count (client_id) as count
FROM
    access_api01.win:time_batch (10 sec)
GROUP BY
    client_id,
    status_code

クエリ自体がシンプルなこともありますが、現状リソースを圧迫するような事はなく安定稼働しています
windowは10sに設定していますが、この辺はCPUとメモリーのトレードオフだと思うので環境に合わせてチューニングすればいいかと思います

参考までに、8コアサーバーでのCPU使用率グラフです、メモリは多めにとってますが実際に使われてるのは100 ~ 200MBくらいでした
f:id:hase-xpw:20141215204940p:plain


実際に可視化した図はこんな感じになります
f:id:hase-xpw:20141217102417p:plain

7.まとめ

SQL likeにログを集計できるというのは使ってみた感じすごく便利

ElasticsearchのバックエンドがLuceneだったり、NorikraのバックエンドがEsperだったり、基盤技術がしっかりしているものの方が流す血が少なくてすむ気がする

Norikraを作るまでのアプローチとか読んでみるととても面白い!
fluent-plugin-esper構想概略 - たごもりすメモ

JOIN, subquery周りをもっと検証してみたい。windowのサイズが小さければそこまでjoinにコストはかからなそうな印象

ElasticSearchのお勉強中

elasticsearchを使ってみた

1. データの登録

POST
http://localhost:9200/twitter/tweet


request body

{
        "tweet":"ほげほげ",
        "time": "2014-05-10T00:00:00Z",
        "name": "hase"
}

response

{
    "_index": "twitter",
    "_type": "tweet",
    "_id": "e79Bx6MEQ_u6XhMz4R4sEg",
    "_version": 1,
    "created": true
}


2. 一件取得

GET
http://localhost:9200/twitter/tweet/e79Bx6MEQ_u6XhMz4R4sEg

{
    "_index": "twitter",
    "_type": "tweet",
    "_id": "e79Bx6MEQ_u6XhMz4R4sEg",
    "_version": 1,
    "found": true,
    "_source": {
        "tweet": "ほげほげ",
        "time": "2014-05-10T00:00:00Z",
        "name": "hase"
    }
}


3. 検索

GET
http://localhost:9200/twitter/tweet/_search

o
http://localhost:9200/*/tweet/_search
http://localhost:9200/_all/tweet/_search
http://localhost:9200/twitte*/tweet/_search
http://localhost:9200/twitte*/tweet1,tweet2/_search

x
http://localhost:9200/_all/*/_search

{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
            {
                "_index": "twitter",
                "_type": "tweet",
                "_id": "e79Bx6MEQ_u6XhMz4R4sEg",
                "_score": 1,
                "_source": {
                    "tweet": "ほげほげ",
                    "time": "2014-05-10T00:00:00Z",
                    "name": "hase"
                }
            },
            {
                "_index": "twitter",
                "_type": "tweet",
                "_id": "7plY2ahJTLGkhjF6tG3jOQ",
                "_score": 1,
                "_source": {
                    "tweet": "ほげほげ",
                    "time": "2014-05-10T00:00:00Z",
                    "name": "hase"
                }
            },
            {
                "_index": "twitter",
                "_type": "tweet",
                "_id": "vQNGXh-NR0aA6IkyTDR1Ig",
                "_score": 1,
                "_source": {
                    "tweet": "ほげほげ",
                    "time": "2014-05-10T00:00:00Z",
                    "name": "hase"
                }
            }
        ]
    }
}

query
http://localhost:9200/twitter/tweet1,tweet2/_search

request body

{
  "query": {
    "term": {"name":"hase"}
  }
}


response

{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 0.5945348,
        "hits": [
            {
                "_index": "twitter",
                "_type": "tweet1",
                "_id": "V0wxff00TPOjA-ZkjJp5Nw",
                "_score": 0.5945348,
                "_source": {
                    "tweet": "ほげほげ",
                    "time": "2014-05-10T00:00:00Z",
                    "name": "hase"
                }
            },
            {
                "_index": "twitter",
                "_type": "tweet2",
                "_id": "mp78tH-tRRGD_31cQ7Ij-g",
                "_score": 0.30685282,
                "_source": {
                    "tweet": "ほげほげ",
                    "time": "2014-05-10T00:00:00Z",
                    "name": "hase"
                }
            }
        ]
    }
}

http://localhost:9200/twitter/tweet1,tweet2/_search
from_size

{
  "from": 1, "size":1,
  "query": {
    "term": {"name":"hase"}
  }
}

インデックスをうまく設計するのが重要そう


参考:
https://medium.com/hello-elasticsearch/elasticsearch-api-83760ce1424b