Continuous Deliveryの時代とJava

かつてバージョン管理やCIなどが存在しなかった頃
人々は手作業で(FTPなどを使い)PHPファイルをアップロードすることで、本番環境へデプロイを行っていた

筆者はJavaプログラマだったので
「本番環境でバグが見つかっても、問題のファイルを修正してアップロードすることで、一瞬で修正できる」
というPHPの利点が非常にうらやましかった
Javaだとjarに固めてアプリケーションサーバにFTPで上げて再起動する、みたいな感じで非常に面倒くさかったのだ

しかし時代は経過し、「ユニットテスト等が行われた後でしか本番環境のファイルが変更されるべきではない」という考え方が常識となってきた
こうなると、「手軽にちょちょいと問題を修正する」というPHPの良さは失われることになる

ウェブアプリケーションを構成するファイル群が全体としてバージョン管理され、テストをパスしたファイル群のみが本番環境に到達できるのだ
まぁそれが正解なのだが、ぶっちゃけ、いざというときの俊敏さは失われることになる
PHPを手作業でアップロードしていた時代、ひどい場合には電話で「なんか問題でてるんだけど」という報告を受けてから数十秒で修正が完了していたケースもあるだろう。

そんなわけで、相対的にJavaでもいいかな、という感じになってきた。


Log4j 2にも採用されたLMAX Disruptorはなぜ狂ったように速いのか?

dis

LMAXという会社はおそらくFX業者で、筆者はLMAXの開発者の講演を、InfoQの動画で何度か見たことがあった。
彼らは非常に特異な集団で、さしずめ「Javaのスピード狂」という感じだ。
印象的なのは、シングルスレッドで仕事を片付けることを強調している点だ。
「Javaならマルチスレッドで並列処理すれば性能が出ると広く思われているが、我々の仕事においてはシングルスレッドが最速だ」というような主張を何度も見た。
ゴールドマンサックスといいLMAXといい、やはり多額の金が動く会社でガチでJavaをやっている連中はカリカリにチューニングするため、技術的には非常に面白い。
彼らがコアのライブラリをOSS化してくれるというのは、金融業界を否定的な目で見る筆者からすると複雑だが、悔しいことに参考になる。

LMAX DisruptorはJavaのライブラリだ。Producer/Consumerパターンであるスレッドから別のスレッドへメッセージを渡す際のパフォーマンスを追求したものである。Log4j2のAsync実装に採用されているようだ。

Disruptorの速さの秘密はいくつかのブログで詳細に解説されているので、じっくり知りたい人はソースコードを読むとともにそちらを参照するとよいだろう。

  • ロックを極力排除し、Producerからのアクセスの一ヶ所でしか使用していない
  • データ構造としてリングバッファを使い、メッセージが格納されている領域のインデックスを各Consumer等がシーケンス番号として管理することで、ロックを減らしている

何とも、非常に頭が良い解決方法である。

シーケンス番号はどんどん上がっていくので、配列を用意すると無限の大きさの配列が必要となってしまう。しかしリングバッファをぐるぐる廻して、全Consumerが処理済みの領域を上書きして使っていくことでこの問題を解決している。

どんなConsumerが存在するのか?が中央で管理されているのが面白い。オブジェクト指向的にはイケていないが、ロックを排除するための工夫ということだろう。

2015/01/06追記
最後の「オブジェクト指向的にはイケていないが…」の部分に対して「別にイケていなくてもいいのでは」的な反応を頂いたので追記しておく。

まさしくその通りで、Disruptorはパフォーマンスを極めるためのものなので、「オブジェクト指向的にイケて」いなくてもOKである。
しかし実は筆者ではなく、LMAXの中の人が、この点について(かなり強く)言及しており、ここは大きなポイントだ。
オブジェクト指向的に綺麗に設計する(関心を分離する)ことにこだわることがパフォーマンス劣化(正確にはcontention)につながっている
と、http://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdfに記述されている。

Further investigation and a focus on the computer science made us realise that the conflation of concerns inherent in
conventional approaches, (e.g. queues and processing nodes) leads to contention in multi-threaded implementations,
suggesting that there may be a better approach.

同様にこちらのブログの以下の部分も同様。

The ConsumerTrackingProducerBarrier has a list of all the Consumers that are accessing the ring buffer. Now to me this seemed a bit odd – I wouldn’t expect the ProducerBarrier to know anything about the consuming side. But wait, there is a reason. Because we don’t want the “conflation of concerns” a queue has (it has to track the head and tail which are sometimes the same point), our consumers are responsible for knowing which sequence number they’re up to, not the ring buffer. So, if we want to make sure we don’t wrap the buffer, we need to check where the consumers have got to.

筆者の書き方が悪かったのだが、つまり「オブジェクト指向的にきれいな設計を捨てることで性能向上を実現している」という、トレードオフの部分が非常に面白いのだ。(そのため「オブジェクト指向的にはイケていない」と書いたことは文脈的におかしくないし、むしろもっと強調してもよい部分だと考えている。)

ちなみに上記の指摘をされた方からは「アルゴリズム的な話に、オブジェクト指向をもってくるのがナンセンスなんです。」とも言われたのだが、上記PDFの図を見てもらえばわかるようにDisruptorは典型的なオブジェクト指向で書かれたソフトウェアであるので、Disruptorについて言及している本エントリにおいて「オブジェクト指向をもってくるのがナンセンス」と言われても困る。おそらく指摘された方はDisruptorのドキュメントやソース等は読まずに、本エントリだけに脊髄反射されただけだろうと思う。

Disruptorはいわば「守破離」的に、全体としてはオブジェクト指向に従いながら、オブジェクト指向で当たり前とされる「関心の分離」の一部だけをわざと崩すことで最高のパフォーマンスを実現しているところが素晴らしい。


WekaのIBkはデフォルトで正規化してくれる

フリーソフトではじめる機械学習入門という本があります。

フリーソフトではじめる機械学習入門
荒木 雅弘
森北出版
売り上げランキング: 18,141

筆者的にはかなりツボな本で、既に2度ほど読み通しています。
今度は実際に手を動かしながら(Wekaを使ったりJavaのコードを書きながら)もう一度読んでみようかと思っています。

18ページあたりから、KnowledgeFlow Environment(KFE)を使ってWekaに格納されているirisのデータをK近傍法で分類する例が始まります。
28ページ真ん中あたりで正規化(1~10程度の値を取る属性と1~1000程度の属性などが混在する場合に、データ間の距離の計算において、後者の属性の影響が強くならないようにすること)が紹介され、下図のようにNormalize(左から3つめ)のが追加されます。

weka-nfe

筆者はまず、本書の通りにKFEを使ってみました。

次に、KFEのようなGUIのツールではなく、JavaのコードからIBkクラスを使ったirisデータの分類をしたかったので、Googleで調べながら独学で進めてみました。これは、特に難しいところはありませんでしたが、その時点で手元で書いてみたコードでは、データの正規化を行っていませんでした。そのためこのときの結果は不正確だろうと考えました。

影響を確認するために、わざと極端にレンジの広い属性と狭い属性を持つデータを用意して分類させてみたのですが、特におかしな結果になりません。仕方ないのでNormalizeクラスの使い方を学んできちんと正規化してみましたが、結果はNormalizeクラスを使っても使わなくても同じで、正常な結果となりました。

原因を調べてみると、どうやらIBkを利用したK近傍法による分類では、デフォルトで正規化を行ってくれるようです。デフォルトではインスタンス間の距離を計算するのにweka.core.EuclideanDistanceクラスが使われますが、こちらのクラスが計算の際に正規化をしてくれます。-Dオプションを渡すと、正規化を行わないという動作になるようです。

そのため、本書の28ページから29ページにかけて行われているNormalizeの追加は、省略しても同じ結果になります。


HadoopからMongoDBのデータ(BSON)にアクセスする

米国を中心に、オンライン処理はMongoDB、バッチ処理はHadoopという組み合わせが非常にポピュラーになってきている印象である。従来からMongoDB Connector for Hadoopを使うことでHadoopからMongoDBに直接アクセスすることは可能だったが、つい先日、単なる MongoDBのデータ(BSONフォーマットのファイル)がHadoopから読み込めるようになった(また、HadoopのoutputとしてBSON 形式のファイルを使用することも同時に可能になった)。

これはMongoDBのデータベースファイルではなく、mongodumpを使ってダンプされる純粋なBSONファイルであることに注意が必要だ。つまり、HadoopがBSONファイルを読むときには、MongoDBで設定したインデックス等は使用されない。単にデータの塊があり、そのフォーマットがBSONである場合でもHadoopで処理できますよ、ということだ。そのため、条件を指定したデータのみ処理したい場合には、あらかじめ mongodumpする段階でクエリを実行し、目的のデータのみBSONとして取り出しておくとよいと考えられる。

Hadoopが読み込む際には、大きなBSONファイルは当然複数にsplitされる。これがHadoopのウリであり、分散処理が可能となる。試してみた感じでは、分割に使用される一時ファイルなどが入力BSONファイルと同じディレクトリ上に勝手に生成されるようなので、Hadoopが異常終了した際などにはゴミとならないように注意が必要だと思われる。

今回の機能追加によって、MongoDBそのものを稼働させることなく(つまりCPUとメモリのリソースなしで)、MongoDBに溜めたデータを Hadoopで処理できるようになった。これは個人的に非常に大きな可能性を感じさせる進化だ。

Hadoopを夜間等、バッチで走らせる場合、直接MongoDBにアクセスさせてしまうと、複数のノードからのアクセスがMongoDBに集中することで負荷が急激に上がり、オンラインアプリケーション側に影響が出てしまう可能性があった。また、当然HadoopとMongoDBがネットワーク的に繋がっている必要があった。しかし事前に必要な情報のみmongodumpで取り出し、S3等のHadoop処理を行う環境に置いておくことで、低い管理コストでこの課題が解決できるようになった。Amazon EMRはまさにこの使用方法にぴったりのソリューションである。

また、半構造化されたデータをHadoopの出力として用いたい場合にBSONを選択すれば、これをまたMongoDBに戻し、オンライン処理で使うことができる。

試しにBSONに含まれるデータの単語を数えてみた。ソースコードは以下のとおりで、Hadoop1.0.3でテストした。MongoDBの Javaドライバと、MongoDB Connector for Hadoopの両方のjarファイルをクラスパスに含めておく必要がある。


import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileOutputFormat;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.MapReduceBase;
import org.apache.hadoop.mapred.Mapper;
import org.apache.hadoop.mapred.OutputCollector;
import org.apache.hadoop.mapred.Reducer;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.TextOutputFormat;

import com.mongodb.hadoop.io.BSONWritable;
import com.mongodb.hadoop.mapred.BSONFileInputFormat;

public class WordCount {
	public static class Map extends MapReduceBase implements
			Mapper<NullWritable, BSONWritable, Text, IntWritable> {
		private final static IntWritable one = new IntWritable(1);

		public void map(NullWritable key, BSONWritable value,
				OutputCollector<Text, IntWritable> output, Reporter reporter)
				throws IOException {
			final java.util.Map valueMap = value.toMap();
			final Iterator p = valueMap.keySet().iterator();
			while( p.hasNext() )
				{
				final Object _key = p.next();
				if( _key.equals( "_id" ) )
					{
					continue;
					}
				final String[] array = ( valueMap.get( _key ) + "" ).split( "[^a-zA-Z0-9]+" );
				for( int i = 0; i < array.length; ++i )
					{
					output.collect(  new Text( array[ i ] ), one );				
					}
				}
			}
		}

	public static class Reduce extends MapReduceBase implements
			Reducer<Text, IntWritable, Text, IntWritable> {
		public void reduce(Text key, Iterator<IntWritable> values,
				OutputCollector<Text, IntWritable> output, Reporter reporter)
				throws IOException {
			int sum = 0;
			while (values.hasNext()) {
				sum += values.next().get();
			}
			output.collect(key, new IntWritable(sum));
		}
	}

	public static void main(String[] args) throws Exception {
		JobConf conf = new JobConf(WordCount.class);
		conf.setJobName("wordcount");

		conf.setOutputKeyClass(Text.class);
		conf.setOutputValueClass(IntWritable.class);

		conf.setMapperClass(Map.class);
		conf.setReducerClass(Reduce.class);

		conf.setInputFormat( BSONFileInputFormat.class );
		conf.setOutputFormat(TextOutputFormat.class);

		FileInputFormat.setInputPaths(conf, new Path(args[0]));
		FileOutputFormat.setOutputPath(conf, new Path(args[1]));

		JobClient.runJob(conf);
	}
}

Hadoopのセカンダリソートを避け、より高速に値をソートする方法

Scaling out H2 on Hadoop

HadoopのReduceに渡されるのはキーと値のリストだが、このとき値のリストに含まれる各アイテム(値そのもの)はソートされていない。ソートされていて欲しい場合にはセカンダリソートと呼ばれるテクニックを使うのが定石とされているが、これは実装の面でも概念的な面でもバッドノウハウ的な側面がある。Hadoopには「キーをソートする」機能は実装されている。そこで、値をキーに入れてしまい、このHadoopに備わっている「キーをソートする」機能によって、実質的に値をソートしようというわけだ。

Map/Reduceというのはキーごとにデータを分割して処理する方法なので、「キーに値が入ったら分割がおかしくなるんじゃ?」と思うのは当然である。キーに値が入っていても、分割に影響しないよう、Partitioningクラスを自分で拡張し、分割の基準となる値(本来のキー)には、値の影響が出ないようにするのだ。それでいて、ソートの際に使われる比較(つまりComparator)については、「キーに含まれる値」を使うということである。

上記を読んで少しでも意味がわかっただろうか?分からないのが当然で、つまりセカンダリソートはウ○コだということなのである(w

そこで、Java組み込み型のRDBMSであるH2を利用して、値のソートを行うというテクニックを使う。Reduceの処理において、単純にすべての値をH2データベースに格納する。そして、「select … order by …」で取り出すだけで、ソートは終わる。単純明快、コードも短く、そして驚いたことにパフォーマンスはHadoopのセカンダリソートより速くなる。唯一、H2がまるごとデータを扱うことになるため、そのぶんのディスク領域が追加で必要となる点はデメリットだ。

H2にデータをすべて格納することによって、セカンダリソートを避けることが可能になるだけでなく、ロジックは明快になり、またデータに対して複数回の問い合わせを高速に(これはインデックスが使えるためだ)行うことができるようになる。

下記リンクではHadoop上でH2を使うテクニックについて詳細にまとめたので、ぜひご一読を。

http://www.scutum.jp/information/waf_tech_blog/2013/08/waf-blog-025.html


Perlは本当にオワコンなのか調べてみた

Perlは、日本国内では私と同年代(30代後半)の人を中心に、相変わらず人気があると思う。
しかし、私がよく見ているStackOverflow/DZone/InfoQ辺りでは、確かにこの数年はあまりPerlを見かけないという感覚があった。

そこで、Googleで『Perl site:stackoverflow.com』『1年以内の記事』のような感じで、いくつかのプログラミング言語の検索結果の件数を調べ、グラフ化してみた。




(;´Д`)…