定期的にまとめたい、技術系のおはなし。
スマホアプリの実績集計のデータ蓄積にMongoDB×Javaを使用していて、いろいろなことに遭遇しました。備忘録がてら、どのようなことが起きて、対処したかをまとめました。

根本的な解決にならず、一時的な対処方法である方法もあるかもしれません。 時間が経過して対応方法が変わったりしたときは、この記事に追記します。
また英語翻訳はGoogle先生にお任せしています。

MongoDB×Java

対応している諸々のバージョンとか

以前の記事でお話した環境で開発しています。まったくかわっていないのが、情報乗っけておきますね。

  • Java 1.8
  • Spring Framework 4.3.4
  • MyBatis 3.3.0
  • JSONIC 1.3.10
  • MySQL5.7
  • MongoDB 3.4

以降、いろいろ経験していて

MongoDBの接続が断続的になる

アプリを使用してるユーザーが多くなり、MongoDBへの読み書きが多くなってきたころ、アプリロジック上は問題ないはずなのに、MongoDBの接続が断続的になってしまう現象が発生。

Java内で接続している箇所は以下のような実装していました。
※メソッドわけだったり、implementsだったり、いろいろ省略していますが、だいたいこんなかんじ。

public class MovieInfoService {
	/**
	* MongoClient
	*/
	private MongoClient mongoClient;

	public MongoDatabase mongoSample() {

		// Mongoクライアントオブジェクト取得 []内は環境設定
		this.mongoClient = new MongoClient(
		new MongoClientURI("mongodb://" + [hostPort] + "/?replicaSet=" + [replicaSetName]));


		// MongoDBに対する処理

		// Mongoクライアントクローズ
		this.mongoClient.close();
	}
}

原因:MongoDBClientインスタンスを接続するたびにclose()していた

色々調べてみたんだけど、MongoDBClientインスタンスをクローズしていて、インスタンスを失くしてしまうことが悪いっぽい。

Loading…

だったり。

以下、MongoDB JavaDriver 接続方法 の引用。

Typically you only create one MongoClient instance for a given MongoDB deployment (e.g. standalone, replica set, or a sharded cluster) and use it across your application. However, if you do create multiple instances:

All resource usage limits (e.g. max connections, etc.) apply per MongoClient instance.

To dispose of an instance, call MongoClient.close() to clean up resources.

通常は、特定のMongoDBデプロイメント用に1つのMongoClientインスタンス(スタンドアロン、レプリカセット、またはシャードされたクラスタなど)を作成し、アプリケーション全体で使用します。ただし、複数のインスタンスを作成する場合は、

MongoClientインスタンスごとに、すべてのリソース使用制限(最大接続など)が適用されます。

インスタンスを破棄するには、MongoClient.close()を呼び出してリソースをクリーンアップします。

1つのMongoClientインスタンスで使いまわせってめっちゃ書いてるのをね、見落としてたんですよね……。

対策1:MongoClientインスタンス

新しい接続のときはMongoClientインスタンスを作成させて、既にあれば作成しないようにします。
また、むやみにクローズさせないようにします。

実装の方はカンタン。1文追加。close()メソッドは呼び出さないようにします。

public class MovieInfoService {
	/**
	* MongoClient
	*/
	private MongoClient mongoClient;

	public MongoDatabase mongoSample() {

		// Mongoクライアントオブジェクト取得 []内は環境設定
		if(this.mongoClient==null)
		this.mongoClient = new MongoClient(
		new MongoClientURI("mongodb://" + [hostPort] + "/?replicaSet=" + [replicaSetName]));


		// MongoDBに対する処理

		// Mongoクライアントクローズ
		this.mongoClient.close();
	}
}

対策2:接続するmaxPoolSizeの調整

MongoDBに接続する際、接続プール内の最大接続数を設定することができます。アプリケーションによっては調整が必要になってくる箇所だと思う。

public class MovieInfoService {
	/**
	* MongoClient
	*/
	private MongoClient mongoClient;

	public MongoDatabase mongoSample() {

		// Mongoクライアントオブジェクト取得 []内は環境設定
		if(this.mongoClient==null)
		this.mongoClient = new MongoClient(
new MongoClientURI("mongodb://" + hostPort + "/?replicaSet=" + replicaSetName+"&maxPoolSize="+[maxPoolSize]));



		// MongoDBに対する処理

		// Mongoクライアントクローズ
		this.mongoClient.close();
	}
}

こちらは検証した結果、既存のアプリでは結果的にはデフォルトの値(100)で問題ないことになりました。

https://docs.mongodb.com/v3.4/reference/connection-string/

aggregateでMongoCommandExceptionエラー

MongoDBに蓄積しているデータをaggregateを使って集計したりしているのですが、前日まで正常に動いていたのに、MongoCommandExceptionエラーが発生。

MongoCommandExceptionエラーのメッセージが以下のとおり。

Command failed with error 16819: ‘Sort exceeded memory limit of 104857600 bytes, but did not opt in to external sorting. Aborting operation. Pass allowDiskUse:true to opt in.’ on server ip-127-0-0-1.localdomain:27017.
The full response is { “ok” : 0.0, “errmsg” : “Sort exceeded memory limit of
104857600 bytes, but did not opt in to external sorting. Aborting operation.
Pass allowDiskUse:true to opt in.”, “code” : 16819, “codeName” :
“Location16819” }

原因:aggregate処理で取得したデータ量が大きかった

通常のMongoDBへの接続だと一時ファイルへの書き込みをせずaggregate処理を行う。
その取得したデータをソートしているのだが、そのデータの上限が104857600 バイト、つまり約100MBとのこと。

対策:aggregate処理にallowDiskUseオプションを使用する

aggregate処理のとき、一時ファイルへの書き込みを有効にできるallowDiskUseオプションを使用して接続する。

db.sampleCollection.aggregate(
[
{aggregateの処理}
],
{allowDiskUse: true}
);

JavaだとgetCollection().aggregateメソッドを使用するときにallowDiskUseオプション使用することになる。たとえば、下のような感じ。

List pipeline = new ArrayList();

	// pipelineの処理作成
	//MongoDB接続後、allowDiskUseをtrueにして接続する
AggregateIterable iterable = getCollection().aggregate(pipeline).allowDiskUse(true);

	// aggregate処理で取得した値の処理

一時ファイルへの書き込みを許可することになるので、処理を行っているサーバーのHDDの容量には注意が必要です。
HHDの容量があまりにも小さい場合は、容量増加の検討をしてみてください。

https://docs.mongodb.com/v3.4/reference/command/aggregate/

まとめ

参考記事などが英語が多く、ノウハウが落ちていることが少ないMongoDB×Java。
なにかの参考になれば幸いです。

MongoDB×Javaのアプリケーション開発で立ち上げたときに苦労したことだったり、いろいろまとめたのがあるので、そちらもよければどうぞ。

MongoDB×Javaを仕事でつかったのでまとめてみました

参考URL

What's New