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

アプリエンジニアからみた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 !

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と同名のディレクトリを作成することによって、ファイルをリプレイスしてくれる

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を組んだ際に、若干はまったところのメモ

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を使ってみた

この記事は、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

jersey + Spring Boot

SpringBootとglassfishのjerseyを連携させてみた

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を挿しておかないと、POJOJSONに変換できない
調べると、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;
	}
}