アプリエンジニアからみたMongo shell プログラミング
この記事はMongoDB Advent Calendar 2015 の記事です
業務で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 !
The purchase token was not found.
先月くらいから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と同名のディレクトリを作成することによって、ファイルをリプレイスしてくれる
プロジェクトを作成したときにデフォルトで作られる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の掛け合わせでアプリの挙動が変わる
以前に書いた記事で、GoogleMapのキーがうまく取れなかったのもこのflavorが原因だと思われる
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" } } } } } }
これの、
この設定を
_allからログのindex名に書き換えればエラーが出なくなる
ログ解析に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をあてて、期間ごとにデータを切り出して集計をします。
イメージはこんな感じです
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が用意されていて、サービスの再起動なしにクエリを変更する事が可能です
こんなの
パフォーマンス面では、こちらの記事にあるように、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を配布し、必要に応じて受けたログの再集計を出来るようにしました。
こんな感じです
投げているクエリは、こんな感じにステータスコードを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くらいでした
実際に可視化した図はこんな感じになります
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
jersey + Spring Boot
SpringBootとglassfishのjerseyを連携させてみた
jerseyって
> RestfulなWebApplicationを実現するためのフレームワーク
> JAX-RS — Project Kenaiっていう規格にのっとってる
基本的には役割はSpringMVCと近い感じかと
構築方法
依存関係 (きちんと調べきれてないです)
build.gradle
dependencies { compile "org.springframework.boot:spring-boot-starter-aop:$springbootVersion" compile "org.springframework.boot:spring-boot-starter-web:$springbootVersion" compile "org.springframework:spring-context:$springVersion" // jersey本体 compile "org.glassfish.jersey.core:jersey-server:2.12" // サーブレットのラッパー compile "org.glassfish.jersey.containers:jersey-container-servlet:2.12" // springのアノテーションを使う場合必須 @Autowiredとか compile "org.glassfish.jersey.ext:jersey-spring3:2.12" // media typeとか管理するライブラリ compile "org.glassfish.jersey.media:jersey-media-moxy:2.12" // jsonに変換するライブラリ compile "org.glassfish.jersey.media:jersey-media-json-jackson:2.12" }
SpringBootそのまま
Main.java
@EnableAutoConfiguration @Configuration @ComponentScan(basePackages = "com.hase.sample") public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
自分はかなりハマった部分で(ドキュメント読まなかったから)
ここで、JacksonFeature.clssを挿しておかないと、POJOをJSONに変換できない
調べると、Servletのinit parameterに差し込む方法もあるようだけど、web.xmlは利用していないから使えない
loggerの出力はイマイチ
import org.glassfish.jersey.filter.LoggingFilter; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.glassfish.jersey.servlet.ServletProperties; import org.springframework.boot.context.embedded.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.RequestContextFilter; @Configuration public class JerseyConfiguration extends ResourceConfig { public static class JerseyServletConfig extends ResourceConfig { public JerseyServletConfig() { // filter類はここにいれていく register(JacksonFeature.class); register(RequestContextFilter.class); packages("com.hase.sample"); register(LoggingFilter.class); } } @Bean public ServletRegistrationBean jerseyServlet() { ServletRegistrationBean registration = new ServletRegistrationBean(new ServletContainer(), "/*"); registration.addInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS, JerseyServletConfig.class.getName()); return registration; } }
コントローラー
import org.springframework.stereotype.Component; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @Component @Path("/") public class JerseyController { @GET @Produces(MediaType.APPLICATION_JSON) @Path("/hello/{name}") public String hello(@DefaultParam("taro") @PathParam("name") String name) { return "Hello" + name; } }